diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000000..7dff644e5f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,59 @@ +name: Bug report +description: A title, homebrew or test stops, crashes or renders wrongly. +labels: [bug] +body: + - type: markdown + attributes: + value: | + Search open and closed issues and pull requests for the error text first, and comment there if it is already reported. + Game bring-up progress belongs in [Discussions](https://github.com/boykopovar/AnyPS5/discussions/categories/game-bring-up), one thread per title. + A slow scene or a change in speed goes into the [Performance report](https://github.com/boykopovar/AnyPS5/issues/new?template=performance.yml) form, which asks for a [performance trace](https://github.com/boykopovar/AnyPS5/blob/main/docs/dev/BUILD.md#performance-trace) and its `tools/trace_digest.py` digest. + - type: input + id: summary + attributes: + label: What happens + description: One sentence, with the exact error text if there is one. + validations: + required: true + - type: input + id: title + attributes: + label: Title, test or build step + description: Title ID and content version (for example PPSAxxxxx 01.000.000), a homebrew with its source, the ctest name, or the build step that fails. + validations: + required: true + - type: input + id: commit + attributes: + label: AnyPS5 commit + description: The `main` commit that was built, or the release that was used, and any pull requests merged on top. + validations: + required: true + - type: input + id: system + attributes: + label: System + description: OS, GPU and driver version, and the compiler if you built it yourself. + placeholder: Arch Linux, GCC 16.2.1, RTX 5080 (NVIDIA 615.78.08) + validations: + required: true + - type: textarea + id: steps + attributes: + label: Steps to reproduce + description: The commands that were run, environment variables included, and how long it takes to stop. + validations: + required: true + - type: textarea + id: log + attributes: + label: Log + description: The lines around the stop. + render: text + validations: + required: true + - type: textarea + id: notes + attributes: + label: What was checked + description: Other devices or commits tried, and whether it is the same every run. Attach the full log and screenshots here. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..789cbc7d74 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: true +contact_links: + - name: Game bring-up + url: https://github.com/boykopovar/AnyPS5/discussions/categories/game-bring-up + about: Progress of one title, one thread per title ID + - name: Question + url: https://github.com/boykopovar/AnyPS5/discussions/categories/q-a + about: Building, converting and running diff --git a/.github/workflows/windows-msys2.yml b/.github/workflows/windows-msys2.yml new file mode 100644 index 0000000000..def52370d2 --- /dev/null +++ b/.github/workflows/windows-msys2.yml @@ -0,0 +1,117 @@ +name: Windows MSYS2 + +on: + schedule: + - cron: '23 8 * * *' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: windows-msys2-${{ github.ref }} + cancel-in-progress: true + +env: + CONFIGURE: >- + -G Ninja -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=ON + -DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++ + -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache + +jobs: + windows-msys2: + runs-on: windows-latest + timeout-minutes: 45 + defaults: + run: + shell: bash + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ github.sha }} + persist-credentials: false + - id: submodules + name: Key the submodules on their commits + run: | + echo "key=$(git ls-files -s | awk '$1 == "160000" { print $2, $4 }' | cat - .gitmodules | sha256sum | cut -c1-16)" >> "$GITHUB_OUTPUT" + echo "ffmpeg=$(git rev-parse HEAD:3rdparty/ffmpeg-core)" >> "$GITHUB_OUTPUT" + - id: submodule-cache + uses: actions/cache/restore@v6 + with: + path: ${{ runner.temp }}/submodule-gitdirs.tar + key: submodule-gitdirs-${{ runner.os }}-${{ steps.submodules.outputs.key }} + - name: Check out the submodules + run: | + if [ -f "$RUNNER_TEMP/submodule-gitdirs.tar" ]; then tar --force-local -xf "$RUNNER_TEMP/submodule-gitdirs.tar"; fi + git submodule update --init --recursive --depth 1 --jobs 8 + - if: steps.submodule-cache.outputs.cache-hit != 'true' + run: tar --force-local -cf "$RUNNER_TEMP/submodule-gitdirs.tar" .git/modules + - if: steps.submodule-cache.outputs.cache-hit != 'true' + uses: actions/cache/save@v6 + continue-on-error: true + with: + path: ${{ runner.temp }}/submodule-gitdirs.tar + key: submodule-gitdirs-${{ runner.os }}-${{ steps.submodules.outputs.key }} + - name: Use the CA bundle of Git for Windows + run: | + for prefix in ucrt64 mingw64; do + bundle="C:/Program Files/Git/$prefix/etc/ssl/certs/ca-bundle.crt" + if [ -f "$bundle" ]; then echo "SSL_CERT_FILE=$bundle" >> "$GITHUB_ENV"; exit 0; fi + done + echo "::error::No CA bundle found in Git for Windows"; exit 1 + - id: msys2 + uses: msys2/setup-msys2@v2 + with: + msystem: UCRT64 + update: true + install: >- + mingw-w64-ucrt-x86_64-gcc + mingw-w64-ucrt-x86_64-cmake + mingw-w64-ucrt-x86_64-ninja + - shell: pwsh + env: + MSYS2_LOCATION: ${{ steps.msys2.outputs.msys2-location }} + run: Add-Content -LiteralPath $env:GITHUB_PATH -Value "$env:MSYS2_LOCATION\ucrt64\bin" + - uses: hendrikmuhs/ccache-action@v1.2.24 + with: + key: windows-msys2 + save: ${{ github.ref == 'refs/heads/main' }} + evict-old-files: job + - id: ffmpeg-cache + uses: actions/cache/restore@v6 + with: + path: build/externals + key: ffmpeg-${{ runner.os }}-${{ steps.submodules.outputs.ffmpeg }} + - shell: cmd + run: cmake -S . -B build %CONFIGURE% "-DANYPS5_AGC_EXECUTION_TESTS=F16ToF32Unpack;F64FmaSubnormals;RegistrationPrepareShutdown" + - if: steps.ffmpeg-cache.outputs.cache-hit != 'true' + uses: actions/cache/save@v6 + continue-on-error: true + with: + path: build/externals + key: ${{ steps.ffmpeg-cache.outputs.cache-primary-key }} + - name: Check that every test executable is registered with ctest + shell: cmd + run: python tools/check_test_registration.py build + - shell: cmd + run: cmake --build build + - shell: cmd + run: cmake --build build --target libs + - shell: cmd + working-directory: build + run: ctest --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 + - name: Report tests slower than 30 s + if: ${{ !cancelled() }} + run: | + [ -f build/ctest.log ] || exit 0 + awk '/Test +#[0-9]+: / && $NF == "sec" { for (i = 1; i <= NF; i++) if ($i ~ /^#[0-9]+:$/) name = $(i + 1); t = $(NF - 1) + 0; if (t > max[name]) max[name] = t } + END { for (name in max) if (max[name] > 30) printf "::warning title=Slow test::%s took %.1f s on %s; tests must finish in 30 s (CONTRIBUTING.md)\n", name, max[name], ENVIRON["RUNNER_OS"] }' build/ctest.log + - uses: actions/upload-artifact@v7 + if: ${{ !cancelled() }} + with: + name: windows-msys2-tests + path: | + build/ctest.log + build/ctest.xml + if-no-files-found: ignore diff --git a/.gitmodules b/.gitmodules index 64a98a2f94..215534a440 100644 --- a/.gitmodules +++ b/.gitmodules @@ -52,3 +52,9 @@ [submodule "3rdparty/vgmstream"] path = 3rdparty/vgmstream url = https://github.com/vgmstream/vgmstream.git +[submodule "3rdparty/ogg"] + path = 3rdparty/ogg + url = https://github.com/xiph/ogg.git +[submodule "3rdparty/vorbis"] + path = 3rdparty/vorbis + url = https://github.com/xiph/vorbis.git diff --git a/3rdparty/ogg b/3rdparty/ogg new file mode 160000 index 0000000000..be05b13e98 --- /dev/null +++ b/3rdparty/ogg @@ -0,0 +1 @@ +Subproject commit be05b13e98b048f0b5a0f5fa8ce514d56db5f822 diff --git a/3rdparty/vorbis b/3rdparty/vorbis new file mode 160000 index 0000000000..0657aee69d --- /dev/null +++ b/3rdparty/vorbis @@ -0,0 +1 @@ +Subproject commit 0657aee69dec8508a0011f47f3b69d7538e9d262 diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index f84fa93f40..bf75035afa 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -39,9 +39,16 @@ if(MINGW) check_cxx_source_compiles("#ifdef __USING_SJLJ_EXCEPTIONS__\n#error\n#endif\nint main() { return 0; }" ANYPS5_MINGW_DWARF_EXCEPTIONS) unset(CMAKE_REQUIRED_FLAGS) if(NOT ANYPS5_MINGW_DWARF_EXCEPTIONS) - message(FATAL_ERROR "${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSION} selects SjLj exceptions with -fno-asynchronous-unwind-tables; the libraries need DWARF unwinding. Use the MinGW-w64 toolchain listed in docs/dev/BUILD.md.") + check_cxx_source_compiles("#if !defined(__SEH__) || defined(__USING_SJLJ_EXCEPTIONS__)\n#error\n#endif\n[[gnu::sysv_abi, gnu::noinline]] void Throw() { throw 1; }\n[[gnu::sysv_abi, gnu::noinline]] int Catch() { try { Throw(); } catch (int value) { return value; } return 0; }\nint main() { return Catch() != 1; }" ANYPS5_MINGW_SYSV_SEH_EXCEPTIONS) + if(NOT ANYPS5_MINGW_SYSV_SEH_EXCEPTIONS) + message(FATAL_ERROR "The Windows libraries require DWARF exceptions or GCC with System V ABI SEH support. Use the toolchains listed in docs/dev/BUILD.md.") + endif() + add_library(windows_seh_personalities OBJECT ${CMAKE_CURRENT_SOURCE_DIR}/prx/libc/src/specifics/windows/SehPersonalities.cpp) + target_link_options(windows_seh_personalities INTERFACE LINKER:--undefined=LibcSehPersonalities_nid_no_patch) + link_libraries(windows_seh_personalities) + else() + add_compile_options(-fno-asynchronous-unwind-tables) endif() - add_compile_options(-fno-asynchronous-unwind-tables) endif() function(configure_windows_unwind target) @@ -50,7 +57,7 @@ function(configure_windows_unwind target) message(FATAL_ERROR "Windows DWARF unwinding requires objcopy") endif() add_custom_command(TARGET ${target} POST_BUILD - COMMAND "${CMAKE_OBJCOPY}" --rename-section .eh_frame=.ehfram "$" + COMMAND "${CMAKE_OBJCOPY}" --rename-section .eh_frame=.ehfram --rename-section .eh_fram=.ehfram "$" VERBATIM ) endif() @@ -211,7 +218,7 @@ if(TARGET mimalloc) target_compile_features(pe_import_first PRIVATE cxx_std_20) if(MINGW) target_compile_options(pe_import_first PRIVATE -fasynchronous-unwind-tables) - target_link_options(pe_import_first PRIVATE -static -static-libgcc -static-libstdc++) + target_link_options(pe_import_first PRIVATE -municode -static -static-libgcc -static-libstdc++) endif() add_custom_command( OUTPUT ${PATCHED_DIR}/mimalloc.dll @@ -302,12 +309,18 @@ if(BUILD_TESTING) target_link_libraries(guest_kernel_sys_tests PRIVATE libkernel_sys libkernel libc) configure_windows_unwind(guest_kernel_sys_tests) add_test(NAME guest_kernel_sys COMMAND guest_kernel_sys_tests) + add_test_executable(guest_kernel_sys_thread_defaults_tests tests/GuestKernelSysThreadDefaults.cpp) + target_include_directories(guest_kernel_sys_thread_defaults_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_kernel_sys_thread_defaults_tests PRIVATE libkernel_sys libkernel libc) + configure_windows_unwind(guest_kernel_sys_thread_defaults_tests) + add_test(NAME guest_kernel_sys_thread_defaults COMMAND guest_kernel_sys_thread_defaults_tests) + set_tests_properties(guest_kernel_sys_thread_defaults PROPERTIES TIMEOUT 20) if(Python3_Interpreter_FOUND) add_test(NAME nid_verneed COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_verneed.py $) endif() if(WIN32) - set_tests_properties(guest_kernel_sys PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_kernel_sys guest_kernel_sys_thread_defaults PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() if(APPLE) @@ -327,6 +340,9 @@ if(BUILD_TESTING) if(WIN32) add_test_executable(undeliverable_host_exception_tests tests/UndeliverableHostException.cpp) + if(MINGW) + target_compile_options(undeliverable_host_exception_tests PRIVATE -fno-asynchronous-unwind-tables -fno-exceptions) + endif() add_dependencies(undeliverable_host_exception_tests libc) add_test(NAME undeliverable_host_exception COMMAND undeliverable_host_exception_tests $) set_tests_properties(undeliverable_host_exception PROPERTIES @@ -362,6 +378,16 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_test_executable(guest_gnm_neo_retail_tests tests/GuestGnmNeoRetail.cpp) + target_include_directories(guest_gnm_neo_retail_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_gnm_neo_retail_tests PRIVATE libSceGnmDriverForNeoMode libc) + configure_windows_unwind(guest_gnm_neo_retail_tests) + add_test(NAME guest_gnm_neo_retail COMMAND guest_gnm_neo_retail_tests) + if(WIN32) + set_tests_properties(guest_gnm_neo_retail PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_move_tests tests/GuestMove.cpp) target_include_directories(guest_move_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_move_tests PRIVATE libSceMove libc) @@ -595,6 +621,16 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_test_executable(guest_np_cpp_web_api_signed_out_tests tests/GuestNpCppWebApiSignedOut.cpp) + target_include_directories(guest_np_cpp_web_api_signed_out_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_np_cpp_web_api_signed_out_tests PRIVATE libSceNpCppWebApi libc) + configure_windows_unwind(guest_np_cpp_web_api_signed_out_tests) + add_test(NAME guest_np_cpp_web_api_signed_out COMMAND guest_np_cpp_web_api_signed_out_tests) + if(WIN32) + set_tests_properties(guest_np_cpp_web_api_signed_out PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(savedata_core_write_replacement_tests tests/SaveDataWriteReplacement.cpp) target_include_directories(savedata_core_write_replacement_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(savedata_core_write_replacement_tests PRIVATE libSceSaveData) @@ -687,9 +723,11 @@ if(BUILD_TESTING) add_test_executable(guest_vorbis_dec_tests tests/GuestVorbisDec.cpp) target_include_directories(guest_vorbis_dec_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) - target_link_libraries(guest_vorbis_dec_tests PRIVATE libSceVorbisDec libc) + target_compile_definitions(guest_vorbis_dec_tests PRIVATE OV_EXCLUDE_STATIC_CALLBACKS) + target_link_libraries(guest_vorbis_dec_tests PRIVATE libSceVorbisDec vorbisenc vorbisfile vorbis ogg libc) configure_windows_unwind(guest_vorbis_dec_tests) add_test(NAME guest_vorbis_dec COMMAND guest_vorbis_dec_tests) + set_tests_properties(guest_vorbis_dec PROPERTIES TIMEOUT 30) if(WIN32) set_tests_properties(guest_vorbis_dec PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") diff --git a/core/libs/allocator/PeImportFirst.cpp b/core/libs/allocator/PeImportFirst.cpp index 8e1ee93454..aa4b040ba2 100644 --- a/core/libs/allocator/PeImportFirst.cpp +++ b/core/libs/allocator/PeImportFirst.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -78,16 +79,16 @@ void MoveImportFirst(std::vector& bytes, const std::string& dll) for (std::size_t index = 0; index < descriptors.size(); ++index) std::memcpy(bytes.data() + directory + index * DescriptorBytes, descriptors[index].data(), DescriptorBytes); } -void MoveImportFirstInFile(const std::string& path, const std::string& dll) { +void MoveImportFirstInFile(const std::filesystem::path& path, const std::string& dll) { std::vector bytes; { std::ifstream input(path, std::ios::binary); - if (!input) throw std::runtime_error("cannot open " + path); + if (!input) throw std::runtime_error("cannot open " + path.string()); bytes.assign(std::istreambuf_iterator(input), std::istreambuf_iterator()); } MoveImportFirst(bytes, dll); std::ofstream output(path, std::ios::binary | std::ios::trunc); - if (!output || !output.write(reinterpret_cast(bytes.data()), static_cast(bytes.size()))) throw std::runtime_error("cannot write " + path); + if (!output || !output.write(reinterpret_cast(bytes.data()), static_cast(bytes.size()))) throw std::runtime_error("cannot write " + path.string()); } } diff --git a/core/libs/allocator/PeImportFirst.hpp b/core/libs/allocator/PeImportFirst.hpp index 30aa18cf7d..54db5101fc 100644 --- a/core/libs/allocator/PeImportFirst.hpp +++ b/core/libs/allocator/PeImportFirst.hpp @@ -1,13 +1,14 @@ #ifndef CORE_LIBS_ALLOCATOR_PEIMPORTFIRST_HPP #define CORE_LIBS_ALLOCATOR_PEIMPORTFIRST_HPP +#include #include #include namespace Allocator { void MoveImportFirst(std::vector& image, const std::string& dll); -void MoveImportFirstInFile(const std::string& path, const std::string& dll); +void MoveImportFirstInFile(const std::filesystem::path& path, const std::string& dll); } diff --git a/core/libs/allocator/PeImportFirstMain.cpp b/core/libs/allocator/PeImportFirstMain.cpp index 02ace8e22e..359c357674 100644 --- a/core/libs/allocator/PeImportFirstMain.cpp +++ b/core/libs/allocator/PeImportFirstMain.cpp @@ -2,17 +2,37 @@ #include #include +#include +#include -int main(int argc, char** argv) { - if (argc != 3) { - std::fprintf(stderr, "usage: pe_import_first \n"); - return 2; - } +namespace { + +int Usage() { + std::fprintf(stderr, "usage: pe_import_first \n"); + return 2; +} + +int Run(const std::filesystem::path& file, const std::string& dll) { try { - Allocator::MoveImportFirstInFile(argv[1], argv[2]); + Allocator::MoveImportFirstInFile(file, dll); } catch (const std::exception& error) { - std::fprintf(stderr, "pe_import_first: %s: %s\n", argv[1], error.what()); + std::fprintf(stderr, "pe_import_first: %s: %s\n", file.string().c_str(), error.what()); return 1; } return 0; } + +} + +#ifdef _WIN32 +int wmain(int argc, wchar_t** argv) { + if (argc != 3) return Usage(); + const auto dll = std::filesystem::path(argv[2]).u8string(); + return Run(argv[1], std::string(dll.begin(), dll.end())); +} +#else +int main(int argc, char** argv) { + if (argc != 3) return Usage(); + return Run(argv[1], argv[2]); +} +#endif diff --git a/core/libs/allocator/tests/PeImportFirstTests.cpp b/core/libs/allocator/tests/PeImportFirstTests.cpp index 4df8aceed5..5d40857cdc 100644 --- a/core/libs/allocator/tests/PeImportFirstTests.cpp +++ b/core/libs/allocator/tests/PeImportFirstTests.cpp @@ -113,7 +113,7 @@ void movesTheNamedImportFirst() { void movesTheImportInAFile(const std::filesystem::path& directory) { const auto path = directory / "moved.dll"; writeFile(path, image({"kernel32.dll", "mimalloc-redirect.dll"})); - Allocator::MoveImportFirstInFile(path.string(), "mimalloc-redirect.dll"); + Allocator::MoveImportFirstInFile(path, "mimalloc-redirect.dll"); const auto moved = readFile(path); require(importName(moved, 0) == "mimalloc-redirect.dll" && importName(moved, 1) == "kernel32.dll", "the file's import did not move first"); } @@ -122,7 +122,7 @@ void failsWithoutTouchingTheFile(const std::filesystem::path& directory, const s const auto path = directory / (what + ".dll"); writeFile(path, bytes); try { - Allocator::MoveImportFirstInFile(path.string(), "mimalloc-redirect.dll"); + Allocator::MoveImportFirstInFile(path, "mimalloc-redirect.dll"); } catch (const std::exception& error) { require(std::string(error.what()).find(expected) != std::string::npos, what + ": failed with another error: " + error.what()); require(readFile(path) == bytes, what + ": the file changed although the move failed"); @@ -134,7 +134,8 @@ void failsWithoutTouchingTheFile(const std::filesystem::path& directory, const s } int main() { - const auto directory = std::filesystem::temp_directory_path() / ("aps5-pe-import-first-" + std::to_string(std::random_device{}())); + auto directory = std::filesystem::temp_directory_path() / std::filesystem::path(u8"aps5-pe-import-first-a\u00e7\u00e3o-\u65e5\u672c-"); + directory += std::to_string(std::random_device{}()); try { std::filesystem::create_directories(directory); movesTheNamedImportFirst(); diff --git a/core/libs/prx/libSceAgc/CMakeLists.txt b/core/libs/prx/libSceAgc/CMakeLists.txt index faf498b571..afff1e8a73 100644 --- a/core/libs/prx/libSceAgc/CMakeLists.txt +++ b/core/libs/prx/libSceAgc/CMakeLists.txt @@ -267,6 +267,19 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(agc_instrumentation_query_size_tests EXCLUDE_FROM_ALL tests/InstrumentationQuerySize.cpp) +target_include_directories(agc_instrumentation_query_size_tests PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(agc_instrumentation_query_size_tests PRIVATE libSceAgc libc) +configure_windows_unwind(agc_instrumentation_query_size_tests) +if(BUILD_TESTING) + set_target_properties(agc_instrumentation_query_size_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) + add_test(NAME agc_instrumentation_query_size_tests COMMAND agc_instrumentation_query_size_tests) + if(WIN32) + set_tests_properties(agc_instrumentation_query_size_tests PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(agc_context_state_size_tests EXCLUDE_FROM_ALL tests/ContextStateSize.cpp) target_include_directories(agc_context_state_size_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(agc_context_state_size_tests PRIVATE libSceAgc libc) diff --git a/core/libs/prx/libSceAgc/DcbDraw/src/Stats.cpp b/core/libs/prx/libSceAgc/DcbDraw/src/Stats.cpp index 2681834e0b..5a8ebdd44d 100644 --- a/core/libs/prx/libSceAgc/DcbDraw/src/Stats.cpp +++ b/core/libs/prx/libSceAgc/DcbDraw/src/Stats.cpp @@ -22,13 +22,12 @@ std::uint32_t* APS5_VABI sceAgcDcbGetLodStats(CommandBuffer* buf, std::uint8_t c } std::uint32_t APS5_VABI sceAgcDcbBeginOcclusionQueryGetSize(std::int32_t queryType) { - NotImplemented_nid_no_patch(__func__); - return 0; + if (queryType == 1) return 0x120u; + return queryType == 0 ? 0x10u : 0u; } std::uint32_t APS5_VABI sceAgcDcbEndOcclusionQueryGetSize() { - NotImplemented_nid_no_patch(__func__); - return 0; + return 0x10u; } } diff --git a/core/libs/prx/libSceAgc/Misc/src/Debug.cpp b/core/libs/prx/libSceAgc/Misc/src/Debug.cpp index 8a321110ab..a211959181 100644 --- a/core/libs/prx/libSceAgc/Misc/src/Debug.cpp +++ b/core/libs/prx/libSceAgc/Misc/src/Debug.cpp @@ -1,9 +1,16 @@ #include "prx/libSceAgc/Misc/include/Debug.hpp" +#include #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +namespace { + +std::atomic shaderInstrumentation{0}; + +} + extern "C" { int APS5_VABI sceAgcDebugRaiseException(std::uint32_t exceptionId) { @@ -11,4 +18,13 @@ int APS5_VABI sceAgcDebugRaiseException(std::uint32_t exceptionId) { return 0; } +int APS5_VABI sceAgcSetShaderInstrumentation(std::uint32_t flags) { + shaderInstrumentation.store(flags, std::memory_order_relaxed); + return 0; +} + +std::uint32_t APS5_VABI sceAgcGetShaderInstrumentation() { + return shaderInstrumentation.load(std::memory_order_relaxed); +} + } diff --git a/core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp b/core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp index 8e56d4c0c8..65b337d335 100644 --- a/core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp +++ b/core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp @@ -53,12 +53,9 @@ std::uint32_t CreateInterpolantValueSplitF16(std::uint32_t psWord, const ShaderS : CreateInterpolantValue(psWord, gsSemantic); } -std::uint32_t UpdateInterpolantValue(std::uint32_t psWord, const ShaderSemantic* gsSemantic) { - const std::uint32_t f16 = (psWord >> 20u) & 0x3u; - if (f16 != 0 && f16 != InterpolantF16HiOnly && ((psWord >> 28u) & 0x3u) != ((psWord >> 30u) & 0x3u)) { - throw std::runtime_error(std::string(__func__) + ": DEFAULT_VAL of an f16 input with different low and high defaults is unknown"); - } - return CreateInterpolantValueSplitF16(psWord, gsSemantic); +std::uint32_t CreateInterpolantValueFlatDefault(std::uint32_t psWord, const ShaderSemantic* gsSemantic) { + const std::uint32_t value = CreateInterpolantValue(psWord, gsSemantic); + return ((psWord & 0x00700000u) == 0x00400000u) ? (value | 0x00000020u) : value; } int CreateInterpolantMapping(const char* fn, ShaderRegister* regs, const Shader* gs, const Shader* ps, CreateInterpolantValueFn createValue, bool fillIdentityTail) { @@ -132,7 +129,15 @@ int APS5_VABI sceAgcCreateInterpolantMapping_10000(ShaderRegister* regs, const S } int APS5_VABI sceAgcUpdateInterpolantMapping_10000(ShaderRegister* regs, const Shader* gs, const Shader* ps) { - return CreateInterpolantMapping(__func__, regs, gs, ps, UpdateInterpolantValue, false); + return CreateInterpolantMapping(__func__, regs, gs, ps, CreateInterpolantValueSplitF16, false); +} + +int APS5_VABI sceAgcCreateInterpolantMapping_nid_postfix(ShaderRegister* regs, const Shader* gs, const Shader* ps) { + return CreateInterpolantMapping(__func__, regs, gs, ps, CreateInterpolantValueFlatDefault, true); +} + +int APS5_VABI sceAgcUpdateInterpolantMapping_nid_postfix(ShaderRegister* regs, const Shader* gs, const Shader* ps) { + return CreateInterpolantMapping(__func__, regs, gs, ps, CreateInterpolantValueFlatDefault, false); } } diff --git a/core/libs/prx/libSceAgc/Unimplemented.cpp b/core/libs/prx/libSceAgc/Unimplemented.cpp index 02dcc73aca..d7751bec51 100644 --- a/core/libs/prx/libSceAgc/Unimplemented.cpp +++ b/core/libs/prx/libSceAgc/Unimplemented.cpp @@ -40,14 +40,4 @@ std::uint32_t* APS5_VABI sceAgcCbMemsetExclusive(CommandBuffer* buffer, void* de return 0; } -int APS5_VABI sceAgcSetShaderInstrumentation(std::uint32_t flags) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAgcGetShaderInstrumentation() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - } diff --git a/core/libs/prx/libSceAgc/tests/InstrumentationQuerySize.cpp b/core/libs/prx/libSceAgc/tests/InstrumentationQuerySize.cpp new file mode 100644 index 0000000000..7f75026d39 --- /dev/null +++ b/core/libs/prx/libSceAgc/tests/InstrumentationQuerySize.cpp @@ -0,0 +1,48 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/Shutdown.hpp" + +#include +#include +#include + +extern "C" { +int APS5_VABI sceAgcSetShaderInstrumentation(std::uint32_t); +std::uint32_t APS5_VABI sceAgcGetShaderInstrumentation(); +std::uint32_t APS5_VABI sceAgcDcbBeginOcclusionQueryGetSize(std::int32_t); +std::uint32_t APS5_VABI sceAgcDcbEndOcclusionQueryGetSize(); +} + +namespace { + +void check(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +void testShaderInstrumentation() { + check(sceAgcGetShaderInstrumentation() == 0, "shader instrumentation starts cleared"); + check(sceAgcSetShaderInstrumentation(0x80000005u) == 0, "setting shader instrumentation failed"); + check(sceAgcGetShaderInstrumentation() == 0x80000005u, "shader instrumentation flags were not kept"); + check(sceAgcSetShaderInstrumentation(0) == 0 && sceAgcGetShaderInstrumentation() == 0, "shader instrumentation was not cleared"); +} + +void testOcclusionQuerySizes() { + check(sceAgcDcbBeginOcclusionQueryGetSize(0) == 0x10u, "begin occlusion query size for type 0"); + check(sceAgcDcbBeginOcclusionQueryGetSize(1) == 0x120u, "begin occlusion query size for type 1"); + check(sceAgcDcbBeginOcclusionQueryGetSize(2) == 0, "begin occlusion query size for an unknown type"); + check(sceAgcDcbEndOcclusionQueryGetSize() == 0x10u, "end occlusion query size"); +} + +} + +int main() { + try { + testShaderInstrumentation(); + testOcclusionQuerySizes(); + LibcRunShutdown_nid_postfix(); + std::puts("AGC shader instrumentation and occlusion query size tests passed"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/libs/prx/libSceAgc/tests/InterpolantMapping.cpp b/core/libs/prx/libSceAgc/tests/InterpolantMapping.cpp index 652eacfea7..1b38f7df08 100644 --- a/core/libs/prx/libSceAgc/tests/InterpolantMapping.cpp +++ b/core/libs/prx/libSceAgc/tests/InterpolantMapping.cpp @@ -14,6 +14,8 @@ extern "C" int APS5_VABI sceAgcCreateInterpolantMapping_0100(ShaderRegister* regs, const Shader* gs, const Shader* ps); extern "C" int APS5_VABI sceAgcCreateInterpolantMapping_10000(ShaderRegister* regs, const Shader* gs, const Shader* ps); extern "C" int APS5_VABI sceAgcUpdateInterpolantMapping_10000(ShaderRegister* regs, const Shader* gs, const Shader* ps); +extern "C" int APS5_VABI sceAgcCreateInterpolantMapping_nid_postfix(ShaderRegister* regs, const Shader* gs, const Shader* ps); +extern "C" int APS5_VABI sceAgcUpdateInterpolantMapping_nid_postfix(ShaderRegister* regs, const Shader* gs, const Shader* ps); namespace { @@ -158,13 +160,46 @@ void testUpdateMapping() { check(std::memcmp(create.data(), update.data(), written) == 0, "update differs from the split f16 mapping"); checkIdentity(create, static_cast(inputs.size()), "unused interpolants are not the identity"); - for (const std::uint32_t index : {1u, 3u}) { - inputs[index].default_value = 0; - update = filled(); - expectFailure([&] { sceAgcUpdateInterpolantMapping_10000(update.data(), &gs, &ps); }); - check(std::memcmp(update.data(), untouched.data(), sizeof(update)) == 0, "rejected update wrote registers"); - inputs[index].default_value = inputs[index].default_value_hi; + inputs[1].default_value = 0; + inputs[3].default_value = 0; + update = filled(); + check(sceAgcUpdateInterpolantMapping_10000(update.data(), &gs, &ps) == 0, "update with split f16 defaults failed"); + check(update[1].value == 0x01380007u && update[3].value == 0x03680409u, "f16 DEFAULT_VAL does not come from the low default"); +} + +void testUnversionedMapping() { + std::array inputs{ + semantic(1, 0, 2), semantic(2, 0, 2), semantic(5, 0, 2), semantic(3, 0, 0), semantic(4, 0, 3)}; + inputs[0].is_flat_shaded = 1; + inputs[0].default_value = 1; + inputs[0].default_value_hi = 3; + inputs[1].default_value_hi = 1; + inputs[2].is_flat_shaded = 1; + inputs[2].default_value_hi = 2; + inputs[3].is_flat_shaded = 1; + inputs[3].default_value = 2; + std::array outputs{semantic(1, 7, 3), semantic(2, 4, 1), semantic(3, 9, 0), semantic(4, 2, 1)}; + Shader gs{}; + gs.output_semantics = outputs.data(); + gs.num_output_semantics = static_cast(outputs.size()); + Shader ps{}; + ps.input_semantics = inputs.data(); + ps.num_input_semantics = static_cast(inputs.size()); + const std::array flatDefault{0x02680527u, 0x02380024u, 0x02580020u, 0x00000629u, 0x03180002u}; + const std::size_t written = inputs.size() * sizeof(ShaderRegister); + const auto untouched = filled(); + + auto create = filled(); + check(sceAgcCreateInterpolantMapping_nid_postfix(create.data(), &gs, &ps) == 0, "unversioned mapping failed"); + for (std::uint32_t i = 0; i < inputs.size(); ++i) { + check(create[i].offset == ShaderRegs::SPI_PS_INPUT_CNTL_0 + i && create[i].value == flatDefault[i], "unversioned mapping changed"); } + checkIdentity(create, static_cast(inputs.size()), "unused interpolants are not the identity"); + + auto update = filled(); + check(sceAgcUpdateInterpolantMapping_nid_postfix(update.data(), &gs, &ps) == 0, "unversioned update failed"); + check(std::memcmp(update.data(), create.data(), written) == 0, "unversioned update differs from its mapping"); + check(std::memcmp(update.data() + inputs.size(), untouched.data(), sizeof(update) - written) == 0, "unversioned update wrote past its inputs"); } void testRejections() { @@ -208,8 +243,9 @@ int main() { testIdentity(); testMapping(); testUpdateMapping(); + testUnversionedMapping(); testRejections(); - check(mappings == 7u && links == 5u, "mapping preparation call count changed"); + check(mappings == 10u && links == 8u, "mapping preparation call count changed"); LibcRunShutdown_nid_postfix(); std::puts("AGC interpolant mapping tests passed"); return 0; diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 4a6440642d..7902207afd 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -433,6 +433,16 @@ if(MINGW OR CMAKE_SYSTEM_NAME STREQUAL "Linux") endif() if(BUILD_TESTING) + add_test_executable(agc_driver_unsupported_diagnostics_tests tests/UnsupportedDiagnostics.cpp) + target_include_directories(agc_driver_unsupported_diagnostics_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(agc_driver_unsupported_diagnostics_tests PRIVATE libSceAgcDriver) + configure_windows_unwind(agc_driver_unsupported_diagnostics_tests) + add_test(NAME agc_driver_unsupported_diagnostics COMMAND agc_driver_unsupported_diagnostics_tests) + if(WIN32) + set_tests_properties(agc_driver_unsupported_diagnostics PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(agc_driver_draw_program_trace_tests tests/DrawProgramTrace.cpp Execution/src/Driver/Draw/DrawProgramTrace.cpp @@ -459,6 +469,16 @@ if(WIN32) endif() if(BUILD_TESTING) + add_test_executable(agc_driver_nop_data_packet_tests tests/NopDataWritePacket.cpp) + target_include_directories(agc_driver_nop_data_packet_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(agc_driver_nop_data_packet_tests PRIVATE libSceAgcDriver) + configure_windows_unwind(agc_driver_nop_data_packet_tests) + add_test(NAME agc_driver_nop_data_packet COMMAND agc_driver_nop_data_packet_tests) + if(WIN32) + set_tests_properties(agc_driver_nop_data_packet PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(agc_frame_dump_io_tests tests/FrameDumpIo.cpp) target_include_directories(agc_frame_dump_io_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(agc_frame_dump_io_tests PRIVATE libc) @@ -1653,9 +1673,17 @@ if(BUILD_TESTING) ENVIRONMENT "APS5_PIN_WAIT_MS=2000" SKIP_RETURN_CODE 77 TIMEOUT 30) set_tests_properties(agc_driver_unit_shadow_budget PROPERTIES ENVIRONMENT "APS5_PIN_WAIT_MS=2000;APS5_UNIT_SHADOW_MIB=8" SKIP_RETURN_CODE 77 TIMEOUT 30) + add_test(NAME agc_recorder_completion_labels COMMAND agc_driver_recorder_tests --completion-labels-only) + set_tests_properties(agc_recorder_completion_labels PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) + add_test(NAME agc_recorder_cmask COMMAND agc_driver_recorder_tests --cmask-only) + set_tests_properties(agc_recorder_cmask PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) + add_test(NAME agc_recorder_cube COMMAND agc_driver_recorder_tests --cube-only) + set_tests_properties(agc_recorder_cube PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) if(WIN32) set_tests_properties(agc_driver_host_import_lifetime agc_driver_host_import_mutation_race agc_driver_unit_shadow agc_driver_unit_shadow_budget PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(agc_recorder_completion_labels agc_recorder_cmask agc_recorder_cube PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") endif() set_property(TEST agc_driver_host_import_lifetime APPEND PROPERTY ENVIRONMENT_MODIFICATION "APS5_RELEASE_UNDER_LOCK=unset:" "APS5_RELEASE_ON_UNLOCK=unset:") @@ -2043,13 +2071,23 @@ if(BUILD_TESTING) add_test(NAME agc_driver_store_pending_identity COMMAND agc_driver_store_over_untracked_tests --pending-identity) set_tests_properties(agc_driver_store_pending_identity PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + add_test(NAME agc_driver_store_idle_results_wake COMMAND agc_driver_store_idle_results_tests --idle-epoch-wake) + set_tests_properties(agc_driver_store_idle_results_wake PROPERTIES + ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(WIN32) set_tests_properties(agc_driver_store_pending_identity PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(agc_driver_store_idle_results_wake PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") endif() endif() if(BUILD_TESTING) + add_test(NAME agc_driver_native_fmask COMMAND agc_driver_sample_multisample_target_tests --native-fmask) + set_tests_properties(agc_driver_native_fmask PROPERTIES TIMEOUT 30 ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(agc_driver_native_fmask PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() add_test(NAME agc_driver_native_msaa_vertex_input COMMAND agc_driver_sample_multisample_target_tests --native-vertex-input) set_tests_properties(agc_driver_native_msaa_vertex_input PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" TIMEOUT 30 SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") @@ -2827,15 +2865,16 @@ if(BUILD_TESTING) endif() endforeach() add_test(NAME agc_driver_shutdown_threads_joined COMMAND agc_driver_shutdown_threads_tests --joined) + add_test(NAME agc_driver_shutdown_threads_queued_prebuilds COMMAND agc_driver_shutdown_threads_tests --queued-prebuilds) add_test(NAME agc_driver_shutdown_threads_compare_helpers COMMAND agc_driver_shutdown_threads_tests --compare-helpers) add_test(NAME agc_driver_shutdown_threads_draw_committer COMMAND agc_driver_shutdown_threads_tests --draw-committer) add_test(NAME agc_driver_shutdown_threads_shader_cache_writer COMMAND agc_driver_shutdown_threads_tests --shader-cache-writer) add_test(NAME agc_driver_shutdown_threads_late_shader_cache COMMAND agc_driver_shutdown_threads_tests --late-shader-cache) add_test(NAME agc_driver_shutdown_threads_async_log_held COMMAND agc_driver_shutdown_threads_tests --async-log-held) add_test(NAME agc_driver_shutdown_threads_exit_after_start COMMAND agc_driver_shutdown_threads_tests --exit-after-start) - set_tests_properties(agc_driver_shutdown_threads_joined agc_driver_shutdown_threads_compare_helpers agc_driver_shutdown_threads_draw_committer agc_driver_shutdown_threads_shader_cache_writer agc_driver_shutdown_threads_late_shader_cache agc_driver_shutdown_threads_async_log_held agc_driver_shutdown_threads_exit_after_start PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30) + set_tests_properties(agc_driver_shutdown_threads_joined agc_driver_shutdown_threads_queued_prebuilds agc_driver_shutdown_threads_compare_helpers agc_driver_shutdown_threads_draw_committer agc_driver_shutdown_threads_shader_cache_writer agc_driver_shutdown_threads_late_shader_cache agc_driver_shutdown_threads_async_log_held agc_driver_shutdown_threads_exit_after_start PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30) if(WIN32) - set_tests_properties(agc_driver_shutdown_threads_joined agc_driver_shutdown_threads_compare_helpers agc_driver_shutdown_threads_draw_committer agc_driver_shutdown_threads_shader_cache_writer agc_driver_shutdown_threads_late_shader_cache agc_driver_shutdown_threads_async_log_held agc_driver_shutdown_threads_exit_after_start PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(agc_driver_shutdown_threads_joined agc_driver_shutdown_threads_queued_prebuilds agc_driver_shutdown_threads_compare_helpers agc_driver_shutdown_threads_draw_committer agc_driver_shutdown_threads_shader_cache_writer agc_driver_shutdown_threads_late_shader_cache agc_driver_shutdown_threads_async_log_held agc_driver_shutdown_threads_exit_after_start PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test(NAME agc_driver_compute_user_data_calls_registered COMMAND agc_driver_compute_user_data_calls_tests --registered) set_tests_properties(agc_driver_compute_user_data_calls_registered PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") @@ -3365,6 +3404,19 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(agc_gcn_decode_tests EXCLUDE_FROM_ALL tests/GcnDecode.cpp) +target_link_libraries(agc_gcn_decode_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_gcn_decode_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_gcn_decode_tests) +if(BUILD_TESTING) + set_target_properties(agc_gcn_decode_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_gcn_decode COMMAND agc_gcn_decode_tests) + if(WIN32) + set_tests_properties(agc_gcn_decode PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(agc_pixel_position_export_tests EXCLUDE_FROM_ALL tests/PixelPositionExport.cpp) target_link_libraries(agc_pixel_position_export_tests PRIVATE shader_recompiler_agc_driver libc) set_target_properties(agc_pixel_position_export_tests PROPERTIES CXX_EXTENSIONS OFF) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp index 3fb9f473b3..0e96bcc002 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp @@ -86,6 +86,7 @@ private: friend class SampledReadScope; friend struct PendingView; friend struct PacketTimer; + friend struct DriverTestAccess; void stop(); Driver(); void rethrowFailure() const; diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp index efa0e9d031..24e0fc7968 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp @@ -84,6 +84,7 @@ bool UnchangedCommittedSince(std::span& store); std::uint64_t StoreOwnBytes(std::uint64_t address, std::span source); bool StoredOver(std::uint64_t address, std::size_t bytes, std::uint64_t generation); diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp index 6562238c9e..52303ede78 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp @@ -17,6 +17,7 @@ #include #include #include +#include namespace AgcDriver { @@ -273,7 +274,7 @@ public: // `recipe`, when given, receives the Recipe a successful call built for its dispatch-cache // variant (design_cpu_final M4): only when the resource cache served or took the object // (reusable, cacheable) and recipes are on (APS5_NO_DISPATCH_RECIPE=1 builds none); else null. - void Dispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::span snapshots = {}, std::uint64_t programAddress = 0, std::shared_ptr prepared = nullptr, std::shared_ptr* recipe = nullptr); + void Dispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::span snapshots = {}, std::uint64_t programAddress = 0, std::shared_ptr prepared = nullptr, std::shared_ptr* recipe = nullptr, std::optional> htileClear = std::nullopt); struct IndirectOutcome { int cpuReason; double argumentReadMs; @@ -357,7 +358,7 @@ private: ShaderRecompiler::SpirvTarget buildTarget() const; Graphics::Context buildContext() const; // Body of Dispatch and DispatchIndirect: `arguments` 0 dispatches x, y, z groups. - IndirectOutcome dispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::span snapshots, std::uint64_t programAddress, std::shared_ptr prepared, std::shared_ptr* recipe, const std::array& threadGroup = {}); + IndirectOutcome dispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::span snapshots, std::uint64_t programAddress, std::shared_ptr prepared, std::shared_ptr* recipe, const std::array& threadGroup = {}, std::optional> htileClear = std::nullopt); // The stage-A pre-sync over `surfaces` (see PrepareDispatch): the serial waited for, 0 none. std::uint64_t presync(std::span surfaces); std::shared_ptr computePipeline(const Graphics::Context& context, const ShaderRecompiler::RecompileResult& shader, VkDescriptorSetLayout setLayout, std::span layoutKey, VkShaderStageFlags pushStages, std::uint64_t programAddress); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp index ccdc67b4aa..808000d470 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp @@ -16,7 +16,9 @@ #include "Optimization/ResourceProgram.hpp" #include #include +#include #include +#include namespace AgcDriver::DriverDetail { @@ -119,6 +121,7 @@ void Driver::dispatch(QueueState& queue, std::span packet, } const bool pipelineCandidate = !resolving && !fromAhead && DispatchItemsEnabled() && DrawPipeline::Active() && indirectArguments == 0 && deferredLabels().labels.empty() && !matchesCopyKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute) && !MatchesStreamCopyKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute); const bool partialLastGroup = partialGroups != std::array{}; + std::optional> htileClearCompletion; if (resolving) { if (indirectArguments == 0 && Graphics::MatchMultisampledSingleClear(std::span(snapshot.code).subspan(codeOffset), userData, compute.numThreads)) return; if (!partialLastGroup && matchesFillKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute)) return; @@ -139,7 +142,8 @@ void Driver::dispatch(QueueState& queue, std::span packet, if (const auto htile = Graphics::HtileDepthClearAddress(std::span(snapshot.code).subspan(codeOffset), userData, compute.numThreads); htile != 0) { const auto threads = (packet[4] & 0x20u) != 0 ? static_cast(packet[1]) : static_cast(packet[1]) * compute.numThreads[0]; const auto nativeBytes = Graphics::HtileDepthClearBytes_nid_no_patch(std::span(snapshot.code).subspan(codeOffset), userData, compute, packet, indirectArguments); - Graphics::NoteHtileDepthClear(htile, static_cast(threads * 16u), nativeBytes); + const auto bytes = static_cast(threads * 16u); + if (Graphics::NoteHtileDepthClear(htile, bytes, nativeBytes)) htileClearCompletion = std::pair{htile, bytes}; } if (!partialLastGroup && indirectArguments == 0 && copyBuffer(queue, submission.queue, packet, std::span(snapshot.code).subspan(codeOffset), userData, compute, localDevice, address)) { pendingDispatchPhases().outcome = DispatchOutcome::CopyHle; @@ -462,18 +466,18 @@ void Driver::dispatch(QueueState& queue, std::span packet, const auto epoch = DrawPipeline::EpochToken().load(std::memory_order_relaxed); const auto queueId = submission.queue; itemCommits.fetch_add(1, std::memory_order_relaxed); - DrawPipeline::Queue0().Enqueue([this, localDevice = std::move(localDevice), result = compiledResult, snapshots = std::move(snapshots), liveWords = std::move(liveWords), groups, address, queueId, recipe, keepVariant, dataHit, epoch] { + DrawPipeline::Queue0().Enqueue([this, localDevice = std::move(localDevice), result = compiledResult, snapshots = std::move(snapshots), liveWords = std::move(liveWords), groups, address, queueId, recipe, keepVariant, dataHit, epoch, htileClearCompletion] { DrawPipeline::FollowEpoch(epoch); GuestMemory::SetCurrentPacket(0x15, queueId); const auto& compiledCommit = *result; const bool noteWrites = writeEvidenceEnabled() || traceCapSync(); try { - std::shared_ptr recipeHit = localDevice->PrepareRecipe(recipe, false); + std::shared_ptr recipeHit = htileClearCompletion.has_value() ? nullptr : localDevice->PrepareRecipe(recipe, false); std::shared_ptr builtRecipe; bool writersNoted = false; for (;;) { std::shared_ptr prepared; - if (recipeHit == nullptr || VulkanDevice::VerifyRecipes()) prepared = localDevice->PrepareDispatch(compiledCommit, snapshots); + if (recipeHit == nullptr || VulkanDevice::VerifyRecipes() || htileClearCompletion.has_value()) prepared = localDevice->PrepareDispatch(compiledCommit, snapshots); GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Dispatch); std::lock_guard gpuLock(GuestMemory::GpuMutex()); recordLabelsForPacket(localDevice.get(), queueId); @@ -491,7 +495,7 @@ void Driver::dispatch(QueueState& queue, std::span packet, continue; } } else { - localDevice->Dispatch(compiledCommit, groups[0], groups[1], groups[2], snapshots, address, std::move(prepared), &builtRecipe); + localDevice->Dispatch(compiledCommit, groups[0], groups[1], groups[2], snapshots, address, std::move(prepared), htileClearCompletion.has_value() ? nullptr : &builtRecipe, htileClearCompletion); } break; } @@ -517,7 +521,7 @@ void Driver::dispatch(QueueState& queue, std::span packet, std::shared_ptr recipeHit; if (cached && keepVariant != nullptr && !stampValidate()) { - recipeHit = localDevice->PrepareRecipe(keepVariant->recipe.load(std::memory_order_acquire), indirectArguments != 0); + recipeHit = htileClearCompletion.has_value() ? nullptr : localDevice->PrepareRecipe(keepVariant->recipe.load(std::memory_order_acquire), indirectArguments != 0); phaseTiming.Phase(PhaseRecipePrecheck); } @@ -595,7 +599,7 @@ void Driver::dispatch(QueueState& queue, std::span packet, const auto outcome = localDevice->DispatchIndirect(compiled, indirectArguments, snapshots, address, std::move(prepared), attachTo != nullptr ? &builtRecipe : nullptr, threadGroup); countIndirect(outcome.cpuReason, outcome.argumentReadMs); } else { - localDevice->Dispatch(compiled, groups[0], groups[1], groups[2], snapshots, address, std::move(prepared), attachTo != nullptr ? &builtRecipe : nullptr); + localDevice->Dispatch(compiled, groups[0], groups[1], groups[2], snapshots, address, std::move(prepared), htileClearCompletion.has_value() ? nullptr : (attachTo != nullptr ? &builtRecipe : nullptr), htileClearCompletion); } } catch (const std::exception& error) { if (rethrow(error)) return; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp index dd0231145e..566f251089 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp @@ -487,7 +487,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe results.push_back(ShaderRecompiler::BuildBarycentricGeometryShader(*programResults[0], *programResults[1], localDevice->Target(), geometryLimits)); stages.insert(stages.begin() + 1, Graphics::CompiledShader{Stage::Geometry, &results.back(), 0}); } else { - require(graphics.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST && !graphics.primitiveRestart, "a pixel shader that reads barycentrics without VK_KHR_fragment_shader_barycentric or geometry shaders needs a triangle list without primitive restart"); + require((graphics.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST || graphics.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP) && !graphics.primitiveRestart, "a pixel shader that reads barycentrics without VK_KHR_fragment_shader_barycentric or geometry shaders needs a triangle list or strip without primitive restart"); auto patches = ShaderRecompiler::BuildBarycentricTessellationShaders(*programResults[0], *programResults[1], localDevice->Target(), localDevice->MaxFragmentInputComponents(), graphics.provokingVertexMode == VK_PROVOKING_VERTEX_MODE_LAST_VERTEX_EXT ? 2u : 0u); const auto first = results.size(); results.push_back(std::move(patches.control)); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp index dd66e54ebf..74fd6ea2c1 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp @@ -263,7 +263,7 @@ bool Driver::sameDecode(const DrawDecode& a, const DrawDecode& b) { if (x.binary.stage != y.binary.stage || x.binary.codeAddress != y.binary.codeAddress || x.userDataBase != y.userDataBase || x.firstUserSgpr != y.firstUserSgpr || x.userData != y.userData || x.snapshot != y.snapshot || x.codeOffset != y.codeOffset || x.tailContinuation != y.tailContinuation) return false; } if (p.targetExportPacking != q.targetExportPacking || p.dualSourceBlend != q.dualSourceBlend || p.reversedBlend != q.reversedBlend || p.alphaToCoverage != q.alphaToCoverage || p.rasterPrimitive != q.rasterPrimitive || p.ditheredAlphaToCoverage != q.ditheredAlphaToCoverage || p.quadPixelMask != q.quadPixelMask || p.quadSampleMask != q.quadSampleMask || p.iteratesSamples != q.iteratesSamples || p.positionLocation != q.positionLocation || p.sampleLocations != q.sampleLocations || p.sampleOrder != q.sampleOrder) return false; - if (s.samples != t.samples || s.mergedCoverage != t.mergedCoverage || s.quadSampleMask != t.quadSampleMask || s.pixelEarlyZ != t.pixelEarlyZ) return false; + if (s.samples != t.samples || s.mergedCoverage != t.mergedCoverage || s.truncatedCoverage != t.truncatedCoverage || s.quadSampleMask != t.quadSampleMask || s.pixelEarlyZ != t.pixelEarlyZ) return false; return true; } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp index abad85855e..0557a7af52 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp @@ -224,13 +224,17 @@ Driver::Driver() { void Driver::WaitIdle() { require(!onWorkerThread(), "worker cannot wait for itself"); - std::unique_lock lock(mutex); - const auto target = accepted; - ++idleWaiters; - changed.wait(lock, [&] { return failure != nullptr || stopping || completed >= target; }); - --idleWaiters; - rethrowFailure(); - checkStopping(); + { + std::unique_lock lock(mutex); + const auto target = accepted; + ++idleWaiters; + changed.wait(lock, [&] { return failure != nullptr || stopping || completed >= target; }); + --idleWaiters; + rethrowFailure(); + checkStopping(); + } + std::lock_guard gpuLock(GuestMemory::GpuMutex()); + if (const auto localDevice = device.Load()) localDevice->WaitIdle(); } void Driver::CheckFailure() { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp index b69bccd8fe..8b9b39afe0 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -1146,9 +1146,8 @@ public: Closing() = true; pools = {Existing(), ExistingPrebuild()}; } - for (auto* pool : pools) { - if (pool != nullptr) pool->Close(); - } + if (pools[0] != nullptr) pools[0]->Close(false); + if (pools[1] != nullptr) pools[1]->Close(true); } private: @@ -1204,11 +1203,17 @@ public: idle.wait(lock, [&] { return unfinished.empty() || *unfinished.begin() > ticket; }); } - void Close() { + void Close(bool dropQueued) { std::vector joined; + std::deque dropped; { std::unique_lock lock(mutex); closed = true; + if (dropQueued) { + dropped.swap(tasks); + for (const auto& task : dropped) unfinished.erase(task.sequence); + idle.notify_all(); + } idle.wait(lock, [&] { return tasks.empty() && running == 0; }); joined.swap(workers); } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp index cf5b1a1676..313498d4ca 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp @@ -971,8 +971,10 @@ struct WriteTracker { struct DriverPieces { std::uint32_t whole = 0; std::uint32_t dropped = 0; - std::uint32_t next = 0; std::array pieces{}; + std::uint32_t possibleWhole = 0; + std::uint32_t possibleDropped = 0; + std::array possiblePieces{}; bool covers(std::uint32_t begin, std::uint32_t end, std::uint64_t generation) const { if (generation == 0 || begin >= end || dropped > generation) return false; @@ -1025,7 +1027,7 @@ struct WriteTracker { #endif } - void stampWatchedPart(std::uint64_t address, std::uint64_t end, std::uint32_t stampGeneration) { + void stampWatchedPart(std::uint64_t address, std::uint64_t end, std::uint32_t stampGeneration, bool complete) { #ifdef _WIN32 address = std::max(address, base); end = std::min(end, base + size); @@ -1039,24 +1041,45 @@ struct WriteTracker { const auto stop = std::min(end, blockBegin(block) + WriteBlockBytes); if (!blockPartWatched(first, stop)) continue; stamp(block, stampGeneration, StampKind::Driver); - noteDriverStore(block, address, end, stampGeneration); + noteDriverStore(block, address, end, stampGeneration, complete); } } - void noteDriverStore(std::uint64_t block, std::uint64_t address, std::uint64_t end, std::uint32_t stampGeneration) { + void noteDriverStore(std::uint64_t block, std::uint64_t address, std::uint64_t end, std::uint32_t stampGeneration, bool complete) { const auto begin = blockBegin(block); const auto from = static_cast(std::max(address, begin) - begin); const auto to = static_cast(std::min(end, begin + WriteBlockBytes) - begin); if (from == 0 && to == WriteBlockBytes) { auto& entry = driverPieces[block]; - entry = DriverPieces{}; - entry.whole = stampGeneration; + if (complete) { + entry = DriverPieces{}; + entry.whole = stampGeneration; + } else { + entry.possibleWhole = stampGeneration; + entry.possibleDropped = 0; + entry.possiblePieces = {}; + } return; } auto& entry = driverPieces[block]; - auto& slot = entry.pieces[entry.next++ % entry.pieces.size()]; - entry.dropped = std::max(entry.dropped, slot.generation); - slot = {stampGeneration, from, to}; + auto& pieces = complete ? entry.pieces : entry.possiblePieces; + auto& dropped = complete ? entry.dropped : entry.possibleDropped; + DriverPiece* contained = nullptr; + for (auto& piece : pieces) { + if (piece.generation == 0 || piece.begin < from || piece.end > to) continue; + if (contained == nullptr) contained = &piece; + else piece = {}; + } + if (contained != nullptr) { + *contained = {stampGeneration, from, to}; + return; + } + auto* oldest = &pieces.front(); + for (auto& piece : pieces) { + if (piece.generation < oldest->generation) oldest = &piece; + } + dropped = std::max(dropped, oldest->generation); + *oldest = {stampGeneration, from, to}; } Leaf& leafFor(std::uint64_t block) { @@ -1167,10 +1190,13 @@ struct WriteTracker { continue; } const auto& entry = found->second; - if (entry.whole > generation || entry.dropped > generation) return true; + if (entry.whole > generation || entry.dropped > generation || entry.possibleWhole > generation || entry.possibleDropped > generation) return true; for (const auto& piece : entry.pieces) { if (piece.generation > generation && piece.begin < to && from < piece.end) return true; } + for (const auto& piece : entry.possiblePieces) { + if (piece.generation > generation && piece.begin < to && from < piece.end) return true; + } } return false; } @@ -1671,12 +1697,12 @@ std::uint64_t storeOwn(std::uint64_t address, std::size_t bytes, const std::func ++tracker.generation; if (!tracker.watched || !tracker.covers(stored.first, static_cast(stored.second - stored.first))) { tracker.noteUnstampedStore(stored.first, stored.second); - if (tracker.watched) tracker.stampWatchedPart(stored.first, stored.second, tracker.generation); + if (tracker.watched) tracker.stampWatchedPart(stored.first, stored.second, tracker.generation, true); return 0; } for (auto block = tracker.blockOf(stored.first); block <= tracker.blockOf(stored.second - 1); ++block) { tracker.stamp(block, tracker.generation, StampKind::Driver); - tracker.noteDriverStore(block, stored.first, stored.second, tracker.generation); + tracker.noteDriverStore(block, stored.first, stored.second, tracker.generation, true); } return tracker.generation; }; @@ -1709,7 +1735,7 @@ std::uint64_t StoreOwnBytes(std::uint64_t address, std::span so }); } -std::uint64_t MarkWritten(std::uint64_t address, std::size_t bytes) { +static std::uint64_t markWritten(std::uint64_t address, std::size_t bytes, bool complete) { auto& tracker = Tracker(); auto lock = lockTracker(tracker); tracker.initialize(); @@ -1717,7 +1743,7 @@ std::uint64_t MarkWritten(std::uint64_t address, std::size_t bytes) { ++tracker.generation; if (!tracker.watched || !tracker.covers(address, bytes)) { tracker.noteUnstampedStore(address, address + bytes); - if (tracker.watched) tracker.stampWatchedPart(address, address + bytes, tracker.generation); + if (tracker.watched) tracker.stampWatchedPart(address, address + bytes, tracker.generation, complete); return 0; } { @@ -1725,11 +1751,11 @@ std::uint64_t MarkWritten(std::uint64_t address, std::size_t bytes) { const auto blockStop = (address + bytes + WriteBlockBytes - 1) & ~(std::uint64_t{WriteBlockBytes} - 1); StampRuns runs{tracker, StampKind::Fresh}; #ifdef _WIN32 - const bool complete = collectWindowsWrites(runs, blockFirst, blockStop, false); + const bool collected = collectWindowsWrites(runs, blockFirst, blockStop, false); #else - const bool complete = GuestWriteWatch::GuestWriteWatchCollectFresh_nid_postfix(static_cast(blockFirst), static_cast(blockStop - blockFirst), &stampWrittenRun, &runs); + const bool collected = GuestWriteWatch::GuestWriteWatchCollectFresh_nid_postfix(static_cast(blockFirst), static_cast(blockStop - blockFirst), &stampWrittenRun, &runs); #endif - if (!complete) { + if (!collected) { lock.unlock(); beforeUnwatch(blockFirst, static_cast(blockStop - blockFirst)); lock.lock(); @@ -1743,11 +1769,19 @@ std::uint64_t MarkWritten(std::uint64_t address, std::size_t bytes) { const auto last = tracker.blockOf(address + bytes - 1); for (auto block = first; block <= last; ++block) { tracker.stamp(block, tracker.generation, StampKind::Driver); - tracker.noteDriverStore(block, address, address + bytes, tracker.generation); + tracker.noteDriverStore(block, address, address + bytes, tracker.generation, complete); } return tracker.generation; } +std::uint64_t MarkWritten(std::uint64_t address, std::size_t bytes) { + return markWritten(address, bytes, true); +} + +std::uint64_t MarkPossiblyWritten(std::uint64_t address, std::size_t bytes) { + return markWritten(address, bytes, false); +} + bool StoredOver(std::uint64_t address, std::size_t bytes, std::uint64_t generation) { auto& tracker = Tracker(); const auto lock = lockTracker(tracker); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Screenshot.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Screenshot.cpp index 6a930d4e30..9e60cbac8c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Screenshot.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Screenshot.cpp @@ -16,8 +16,12 @@ namespace { std::atomic requested{false}; std::filesystem::path directory() { +#ifdef _WIN32 + const wchar_t* configured = _wgetenv(L"ANYPS5_SCREENSHOTS"); +#else const char* configured = std::getenv("ANYPS5_SCREENSHOTS"); - if (configured != nullptr && configured[0] != '\0') return configured; +#endif + if (configured != nullptr && configured[0] != 0) return configured; const std::unique_ptr basePath(SDL_GetBasePath(), SDL_free); if (!basePath) throw std::runtime_error(std::string("Screenshot: cannot locate the executable: ") + SDL_GetError()); return std::filesystem::path(reinterpret_cast(basePath.get())) / "anyps5-screenshots"; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 45a1354a4b..c1e000a91c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -156,20 +156,22 @@ public: std::lock_guard lock(mutex); stopping = true; } - changed.notify_one(); + changed.notify_all(); 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) { - std::lock_guard lock(mutex); + std::unique_lock lock(mutex); require(!stopping, "frame dump writer has stopped"); - require(pixels.size() <= maxBytes - queuedBytes, "frame dump queue exceeded 64 MiB"); if (!screenshot && !frameDumpOutput.Enabled_nid_no_patch()) return; + require(pixels.size() <= maxBytes, "a frame dump exceeds the 64 MiB queue"); + changed.wait(lock, [&] { return stopping || pixels.size() <= maxBytes - queuedBytes; }); + 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)}); queuedBytes += bytes; - changed.notify_one(); + changed.notify_all(); } bool Idle() { @@ -202,6 +204,7 @@ private: else WriteFrameBmp(frame.index, frame.width, frame.height, frame.pixels, frame.scale); std::lock_guard lock(mutex); queuedBytes -= frame.pixels.size(); + changed.notify_all(); } } } catch (const std::exception& error) { @@ -342,6 +345,7 @@ struct VulkanDevice::State { bool imageInt64Atomics = false; bool bufferInt64Atomics = false; bool viewportIndexLayer = false; + bool imageFormatList = false; bool primitiveListRestart = false; bool depthClipControl = false; bool graphicsPipelineLibrary = false; @@ -382,6 +386,8 @@ struct VulkanDevice::State { bool samplerFilterMinmax = false; bool nonSeamlessCubeMap = false; VkSampleCountFlags mergedCoverageSampleCounts = 0; + VkSampleCountFlags truncatedCoverageSampleCounts = 0; + bool mixedAttachmentSamples = false; bool fragmentShaderPixelInterlock = false; bool conservativeRasterization = false; bool provokingVertexLast = false; @@ -1313,6 +1319,10 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : state->spirvExtensions.push_back("SPV_EXT_shader_viewport_index_layer"); state->viewportIndexLayer = true; } + if (hasExtension(VK_KHR_IMAGE_FORMAT_LIST_EXTENSION_NAME)) { + deviceExtensions.push_back(VK_KHR_IMAGE_FORMAT_LIST_EXTENSION_NAME); + state->imageFormatList = true; + } if (state->demoteToHelperInvocation) { deviceExtensions.push_back(VK_EXT_SHADER_DEMOTE_TO_HELPER_INVOCATION_EXTENSION_NAME); state->capabilities.push_back(spv::CapabilityDemoteToHelperInvocation); @@ -1507,12 +1517,17 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : check(combinations(selected, &count, supported.data()), "vkGetPhysicalDeviceSupportedFramebufferMixedSamplesCombinationsNV"); for (const auto& combination : supported) { if (combination.coverageReductionMode == VK_COVERAGE_REDUCTION_MODE_MERGE_NV && combination.colorSamples == VK_SAMPLE_COUNT_1_BIT && combination.depthStencilSamples == 0) state->mergedCoverageSampleCounts |= combination.rasterizationSamples; + if (combination.coverageReductionMode == VK_COVERAGE_REDUCTION_MODE_TRUNCATE_NV && combination.colorSamples == VK_SAMPLE_COUNT_1_BIT && combination.depthStencilSamples == combination.rasterizationSamples) state->truncatedCoverageSampleCounts |= combination.rasterizationSamples; } } - if (state->mergedCoverageSampleCounts != 0) { + if (state->mergedCoverageSampleCounts != 0 || state->truncatedCoverageSampleCounts != 0) { deviceExtensions.push_back(VK_NV_FRAMEBUFFER_MIXED_SAMPLES_EXTENSION_NAME); deviceExtensions.push_back(VK_NV_COVERAGE_REDUCTION_MODE_EXTENSION_NAME); } + } else if (hasExtension(VK_AMD_MIXED_ATTACHMENT_SAMPLES_EXTENSION_NAME)) { + state->mixedAttachmentSamples = true; + state->truncatedCoverageSampleCounts = state->properties.limits.framebufferDepthSampleCounts & state->properties.limits.framebufferColorSampleCounts & ~static_cast(VK_SAMPLE_COUNT_1_BIT); + deviceExtensions.push_back(VK_AMD_MIXED_ATTACHMENT_SAMPLES_EXTENSION_NAME); } coverageReductionFeatures = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_COVERAGE_REDUCTION_MODE_FEATURES_NV}; coverageReductionFeatures.coverageReductionMode = VK_TRUE; @@ -1803,7 +1818,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : nonSeamlessCubeFeatures.pNext = const_cast(deviceInfo.pNext); deviceInfo.pNext = &nonSeamlessCubeFeatures; } - if (state->mergedCoverageSampleCounts != 0) { + if (!state->mixedAttachmentSamples && (state->mergedCoverageSampleCounts != 0 || state->truncatedCoverageSampleCounts != 0)) { coverageReductionFeatures.pNext = const_cast(deviceInfo.pNext); deviceInfo.pNext = &coverageReductionFeatures; } @@ -3806,6 +3821,8 @@ Graphics::Context VulkanDevice::buildContext() const { context.samplerFilterMinmax = state->samplerFilterMinmax; context.nonSeamlessCubeMap = state->nonSeamlessCubeMap; context.mergedCoverageSampleCounts = state->mergedCoverageSampleCounts; + context.truncatedCoverageSampleCounts = state->truncatedCoverageSampleCounts; + context.mixedAttachmentSamples = state->mixedAttachmentSamples; context.conservativeRasterization = state->conservativeRasterization; context.meshWave32 = state->meshWave32; context.provokingVertexLast = state->provokingVertexLast; @@ -3842,6 +3859,7 @@ Graphics::Context VulkanDevice::buildContext() const { context.bufferInt64Atomics = state->bufferInt64Atomics; context.placeholderImages = state->placeholderImages.get(); context.viewportIndexLayer = state->viewportIndexLayer; + context.imageFormatList = state->imageFormatList; context.demoteToHelperInvocation = state->demoteToHelperInvocation; context.earlyFragmentSampleMaskTestBeforeSampleCounting = state->earlyFragmentSampleMaskTestBeforeSampleCounting; context.primitiveListRestart = state->primitiveListRestart; @@ -4403,6 +4421,7 @@ struct RecordedDispatch { // A template hit's revalidate time, charged to the [rescache] refresh accounting. double revalidateMs; DispatchTimer* timer; + std::optional> htileClear; // Whether the data refresh recorded anything. bool refreshed = false; bool resourceKeyMatched = false; @@ -4663,8 +4682,8 @@ std::shared_ptr VulkanDevice::PrepareRecipe(const std::shared_ptr snapshots, std::uint64_t programAddress, std::shared_ptr prepared, std::shared_ptr* recipe) { - static_cast(dispatch(shader, x, y, z, 0, snapshots, programAddress, std::move(prepared), recipe)); +void VulkanDevice::Dispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::span snapshots, std::uint64_t programAddress, std::shared_ptr prepared, std::shared_ptr* recipe, std::optional> htileClear) { + static_cast(dispatch(shader, x, y, z, 0, snapshots, programAddress, std::move(prepared), recipe, {}, htileClear)); } VulkanDevice::IndirectOutcome VulkanDevice::DispatchIndirect(const ShaderRecompiler::RecompileResult& shader, std::uint64_t arguments, std::span snapshots, std::uint64_t programAddress, std::shared_ptr prepared, std::shared_ptr* recipe, const std::array& threadGroup) { @@ -4925,21 +4944,31 @@ void VulkanDevice::recordDispatch(RecordedDispatch& record) { recorder.EndGpuTiming(trailingTiming); recorder.MarkCovered(dispatchedAccess); recordStep(PhaseRecordBind); + if (record.htileClear.has_value()) Graphics::ArmHtileDepthClear(record.htileClear->first, record.htileClear->second); resources.MarkGpuWrites(recorder); + if (record.htileClear.has_value()) { + Graphics::QueueHtileDepthClear(record.htileClear->first, record.htileClear->second); + recorder.NotePendingWrite(record.htileClear->first, record.htileClear->second); + } recordStep(PhaseRecordMarks); // Only copied written buffers (and BDA fault checks) need work once the GPU is done; without them // the batch can signal its labels from the GPU. - if (resources.NeedsCompletion() && resources.Reusable()) { + const bool completionNeeded = resources.NeedsCompletion() || record.htileClear.has_value(); + if (completionNeeded && resources.Reusable()) { auto kept = record.resources; - recorder.OnComplete([kept] { kept->CheckFaults(); }); - } else if (resources.NeedsCompletion()) { + recorder.OnComplete([kept, htileClear = record.htileClear] { + kept->CheckFaults(); + if (htileClear.has_value()) Graphics::CompleteHtileDepthClear(htileClear->first, htileClear->second); + }); + } else if (completionNeeded) { // Listed for DispatchIndirect until the write-back ran (a non-reusable object, so once). auto writers = state->copiedWriters; auto kept = record.resources; - recorder.OnComplete([kept, writers] { + recorder.OnComplete([kept, writers, htileClear = record.htileClear] { // Delisted before the write-back: one that fails must not keep indirect dispatches on the CPU. writers->erase(std::remove(writers->begin(), writers->end(), kept), writers->end()); kept->WriteBackBuffers(); + if (htileClear.has_value()) Graphics::CompleteHtileDepthClear(htileClear->first, htileClear->second); }); // Listed after the registration: a throw there leaves nothing that would pin the CPU path forever. if (resources.HasCopiedWrites()) writers->push_back(kept); @@ -5070,7 +5099,7 @@ std::shared_ptr VulkanDevice::computePipeline(const Grap return finish(std::move(objects), nullptr); } -VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::span snapshots, std::uint64_t programAddress, std::shared_ptr prepared, std::shared_ptr* recipeOut, const std::array& threadGroup) { +VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::span snapshots, std::uint64_t programAddress, std::shared_ptr prepared, std::shared_ptr* recipeOut, const std::array& threadGroup, std::optional> htileClear) { startJournalReplay(); for (const auto& binding : shader.bindings) { if (binding.role != ShaderRecompiler::DescriptorRole::GuestBuffers) continue; @@ -5322,7 +5351,7 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec // works; the batch is waited for where the guest expects results (labels, flips, CPU reads). auto& recorder = *state->recorder; IndirectOutcome outcome{0, 0}; - RecordedDispatch record{&context, &shaders[0], resources, objects, pushStages, &pushBytes, x, y, z, arguments, nullptr, programAddress, fromCache && TemplateDataRefresh() ? RecordedDispatch::DataRefresh::Words : RecordedDispatch::DataRefresh::None, 0, revalidateMs, &timer}; + RecordedDispatch record{&context, &shaders[0], resources, objects, pushStages, &pushBytes, x, y, z, arguments, nullptr, programAddress, fromCache && TemplateDataRefresh() ? RecordedDispatch::DataRefresh::Words : RecordedDispatch::DataRefresh::None, 0, revalidateMs, &timer, htileClear}; record.resourceKeyMatched = fromCache; record.bindings = overlay; record.threadGroup = threadGroup; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp index 1ac783d39e..2e9a481930 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp @@ -213,6 +213,8 @@ struct Context { bool samplerFilterMinmax = false; bool nonSeamlessCubeMap = false; VkSampleCountFlags mergedCoverageSampleCounts = 0; + VkSampleCountFlags truncatedCoverageSampleCounts = 0; + bool mixedAttachmentSamples = false; bool conservativeRasterization = false; bool meshWave32 = false; VkBuffer emptyBuffer = VK_NULL_HANDLE; @@ -242,6 +244,7 @@ struct Context { bool maintenance5 = false; bool storageImageMultisample = false; bool storageImageWriteWithoutFormat = false; + bool imageFormatList = false; bool shaderClipDistance = false; bool shaderCullDistance = false; bool nullDescriptors = false; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp index f507ddd3a1..835b80f59b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp @@ -29,6 +29,7 @@ struct DepthPlaneStoreStatistics { std::uint64_t bytes = 0; }; DepthPlaneStoreStatistics DepthPlaneStoreCounts(); +void NoteDepthSurfaceWrite(const Context& context, const DepthTarget& target); bool SampleLocationsCompatibleDepth(const Context& context, VkFormat format, std::uint32_t samples); std::uint64_t DepthSurfacesSerial(); std::uint64_t DepthSurfacesReleased(); @@ -54,7 +55,10 @@ bool DepthSurfaceOverlaps(VkDevice device, std::uint64_t address, std::uint64_t bool DepthSurfaceAt(const Context& context, std::span words, const GuestTextureResource& resource); std::uint64_t HtileDepthClearAddress(std::span code, std::span userData, const std::array& numThreads); std::size_t HtileDepthClearBytes_nid_no_patch(std::span code, std::span userData, const ShaderRecompiler::ShaderComputeStageInfo& compute, std::span packet, std::uint64_t indirectArguments); -void NoteHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes, std::size_t nativeBytes = 0u); +bool NoteHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes, std::size_t nativeBytes = 0u); +void ArmHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes); +void QueueHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes); +void CompleteHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes); struct MemoryValueFill { std::uint64_t address; std::size_t bytes; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp index 6f40129b56..89e151fb24 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp @@ -90,6 +90,7 @@ struct GuestTextureResource { std::uint32_t minLod = 0; std::uint32_t allocatedMipCount = 0; std::uint32_t samples = 1; + std::uint64_t fmaskCmaskAddress = 0; }; float EffectiveMinLod(const GuestTextureResource& resource); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp index c8a8800703..b9c18eb7d1 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp @@ -76,6 +76,7 @@ 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); void NoteColorMetadataFill(std::uint64_t address, std::size_t bytes, std::uint32_t pattern); +std::shared_ptr MultisampleTargetFmaskTexture_nid_no_patch(const Context& context, const GuestTextureResource& resource, VkComponentMapping components); void ClearMultisampleTargets(VkDevice device); void InstallResidentImageReadGuard_nid_no_patch(); void UpdateResidentImageUnmapObserver_nid_no_patch(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index f8b9d16afe..c934bd4e04 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -68,6 +68,8 @@ struct ColorTarget { std::uint32_t pipeBankXor = 0; std::uint32_t samples = 1; std::uint32_t dccControl = 0; + std::uint64_t fmaskAddress = 0; + std::uint32_t fmaskPipeBankXor = 0; }; struct DepthTarget { @@ -140,6 +142,7 @@ struct State { bool ditheredAlphaToCoverage = false; bool perSampleShading = false; bool mergedCoverage = false; + bool truncatedCoverage = false; VkConservativeRasterizationModeEXT conservativeRasterization = VK_CONSERVATIVE_RASTERIZATION_MODE_DISABLED_EXT; VkRect2D scissor; VkCullModeFlags cullMode; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp index 1da043f87f..03865c6a92 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp @@ -347,7 +347,7 @@ void BdaResources::markWrittenPages() const { Require(false, message.str()); } } - GuestMemory::MarkWritten(begin, bytes); + GuestMemory::MarkPossiblyWritten(begin, bytes); }), "invalid BDA written-page record"); for (std::uint32_t claim = 0; claim < count; ++claim) { const auto slot = words[Abi::WrittenListWord + claim]; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index 9090b5952b..11f2c659ac 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -58,6 +58,7 @@ struct DepthGuard { std::size_t bytes; std::shared_ptr> active; std::vector live; + std::shared_ptr> hostWritten; }; std::shared_mutex& depthGuardMutex() { @@ -338,6 +339,7 @@ public: const auto clearLease = clearGeneration == 0u ? GuestAllocations::Lease{} : GuestAllocations::GuestAllocationsAcquireAll_nid_postfix(); Require(active->load(std::memory_order_acquire), "the native multisample depth owner was released"); Require(sampling.refusal.empty(), sampling.refusal); + Require(!clearPending, "native multisample depth clear has not been recorded"); for (const auto& range : sampling.initial) { Require(GuestMemory::Accessible(reinterpret_cast(range.address), range.bytes) && GuestMemory::Watched(range.address, range.bytes), "native multisample depth requires watched accessible guest memory"); if (const auto* recorder = RecorderFor(context)) Require(!recorder->PendingWriteOverlaps(range.address, range.bytes), "external GPU writes over native multisample depth are not implemented"); @@ -491,6 +493,7 @@ public: void CopyAspectFrom(const DepthSurface& source, VkImageAspectFlagBits aspect) { ForgetClear(aspect); + NoteHostWrite(); auto* recorder = RecorderFor(context); std::unique_ptr batch; if (recorder == nullptr) batch = std::make_unique(context); @@ -540,7 +543,12 @@ public: return std::any_of(textures.begin(), textures.end(), [](const auto& entry) { return entry.second.use_count() > 1; }); } + void NoteHostWrite() { + hostWritten->store(true, std::memory_order_release); + } + std::uint64_t written = ++depthWrites; + std::shared_ptr> hostWritten = std::make_shared>(false); bool retired = false; bool Overlaps(std::uint64_t address, std::uint64_t bytes) const { @@ -690,6 +698,7 @@ public: if (plane.storage.get() == &candidate) { if (stencil || !StoreOverwrite(candidate)) copyPlane(plane, plane.storage, stencil, false); written = ++depthWrites; + NoteHostWrite(); return true; } if (std::none_of(plane.replaced.begin(), plane.replaced.end(), [&](const auto& old) { return old.lock().get() == &candidate; })) continue; @@ -707,6 +716,7 @@ public: const auto source = std::const_pointer_cast(candidate.shared_from_this()); copyPlane(plane, source, stencil, false); written = ++depthWrites; + NoteHostWrite(); return true; } return false; @@ -910,7 +920,9 @@ public: bool sampleable = true; bool samplingActive = false; DepthSamplingState sampling; + bool clearPending = false; std::uint64_t clearGeneration = 0u; + bool clearCompletionQueued = false; std::shared_ptr owned; std::shared_ptr sampleShadow; bool useSampleShadow = false; @@ -1527,6 +1539,21 @@ DepthSurface* planeSurface(VkDevice device, std::uint64_t address) { return found == list.rend() ? nullptr : found->get(); } +void settlePendingHtileClear(const Context& context, std::uint64_t address) { + if (Recorder::InCompletion()) return; + auto* recorder = RecorderFor(context); + if (recorder == nullptr) return; + std::pair metadata; + { + std::lock_guard lock(surfacesMutex()); + const auto* surface = planeSurface(context.device, address); + if (surface == nullptr || !surface->samplingActive || surface->clearGeneration == 0u || !surface->clearCompletionQueued || !surface->sampling.refusal.empty()) return; + const auto& range = surface->sampling.initial[1]; + metadata = {range.address, range.bytes}; + } + if (recorder->PendingWriteOverlaps(metadata.first, metadata.second)) recorder->SyncThrough(metadata.first, metadata.second); +} + std::map& clearedHtiles() { static auto* cleared = new std::map(); return *cleared; @@ -1826,8 +1853,28 @@ std::uint64_t MultisampledDepthBytes_nid_no_patch(VkExtent2D extent, std::uint32 void RequireNoResidentDepthRead_nid_no_patch(std::uint64_t address, std::size_t bytes) { if (bytes == 0u || !anyDepthGuard().load(std::memory_order_acquire)) return; Require(address <= std::numeric_limits::max() - bytes, "native depth read range overflows"); - std::shared_lock lock(depthGuardMutex()); - for (const auto& range : depthGuards()) Require(address >= range.address + range.bytes || range.address >= address + bytes, "sample-preserving guest publication of native multisample depth is not implemented"); + const auto overlaps = [&](const DepthGuard& range) { return address < range.address + range.bytes && range.address < address + bytes; }; + { + std::shared_lock lock(depthGuardMutex()); + if (std::ranges::none_of(depthGuards(), overlaps)) return; + } + std::unique_lock lock(depthGuardMutex()); + for (const auto& range : depthGuards()) { + if (overlaps(range)) Require(!range.hostWritten->load(std::memory_order_acquire), "sample-preserving guest publication of native multisample depth is not implemented"); + } + for (const auto& range : depthGuards()) { + if (overlaps(range)) range.active->store(false, std::memory_order_release); + } + std::erase_if(depthGuards(), [](const auto& range) { return !range.active->load(std::memory_order_acquire); }); + anyDepthGuard().store(!depthGuards().empty(), std::memory_order_release); +} + +void NoteDepthSurfaceWrite(const Context& context, const DepthTarget& target) { + if (target.samples == 1u || !anyDepthGuard().load(std::memory_order_acquire)) return; + std::lock_guard lock(surfacesMutex()); + for (const auto& surface : surfaces()) { + if (surface->active->load(std::memory_order_acquire) && surface->context.device == context.device && sameSurface(surface->target, target)) surface->NoteHostWrite(); + } } bool HasResidentDepthGuards_nid_no_patch() { @@ -1900,6 +1947,7 @@ float HostDepthBiasConstant(const Context& context, VkFormat format, float const VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target, bool depthWritten) { const auto htileCleared = RoutesToNativeDepthOwner(target) ? VkImageAspectFlags{0} : storeBackClearedHtile(context, target); std::lock_guard gpu(GuestMemory::GpuMutex()); + settlePendingHtileClear(context, target.address); std::unique_lock lock(surfacesMutex()); const bool cleared = takeClearedHtile(target); DepthSurface* surface = nullptr; @@ -1936,7 +1984,7 @@ VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target, auto& fresh = *surfaces().back(); if (target.samples != 1u && fresh.sampleable && fresh.sampling.refusal.empty()) { std::unique_lock guardLock(depthGuardMutex()); - for (const auto& range : fresh.sampling.initial) depthGuards().push_back({context.device, range.address, range.bytes, fresh.active, {{range.address, range.bytes, 0u}}}); + for (const auto& range : fresh.sampling.initial) depthGuards().push_back({context.device, range.address, range.bytes, fresh.active, {{range.address, range.bytes, 0u}}, fresh.hostWritten}); anyDepthGuard().store(true, std::memory_order_release); InstallResidentImageReadGuard_nid_no_patch(); fresh.samplingActive = true; @@ -2184,17 +2232,22 @@ std::size_t HtileDepthClearBytes_nid_no_patch(std::span cod return address <= std::numeric_limits::max() - bytes ? static_cast(bytes) : 0u; } -void NoteHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes, std::size_t nativeBytes) { +bool NoteHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes, std::size_t nativeBytes) { std::lock_guard lock(surfacesMutex()); forgetHtileReads(htileAddress, bytes); bool existing = false; + bool completionCandidate = false; for (const auto& surface : surfaces()) { if (!surface->active->load(std::memory_order_acquire)) continue; if (surface->samplingActive && surface->target.htileAddress == htileAddress) { const auto& metadata = surface->sampling.initial[1]; if (nativeBytes != bytes || nativeBytes < metadata.bytes || !GuestMemory::Accessible(reinterpret_cast(metadata.address), metadata.bytes) || !GuestMemory::Watched(metadata.address, metadata.bytes)) surface->sampling.Refuse("native multisample depth incomplete or unproven fast-clear transition is not implemented"); - else if (surface->clearGeneration != 0u) surface->sampling.Refuse("overlapping native multisample depth clear transitions are not implemented"); - else surface->clearGeneration = GuestMemory::CollectWritesUncached(metadata.address, metadata.bytes); + else if (surface->clearPending || surface->clearGeneration != 0u) surface->sampling.Refuse("overlapping native multisample depth clear transitions are not implemented"); + else { + surface->clearPending = true; + surface->clearCompletionQueued = false; + completionCandidate = true; + } existing = true; continue; } @@ -2204,6 +2257,39 @@ void NoteHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes, std::siz existing = true; } if (!existing) clearedHtiles()[htileAddress] = bytes; + return completionCandidate; +} + +void ArmHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes) { + std::lock_guard lock(surfacesMutex()); + for (const auto& surface : surfaces()) { + if (!surface->active->load(std::memory_order_acquire) || !surface->samplingActive || surface->target.htileAddress != htileAddress || !surface->clearPending) continue; + const auto& metadata = surface->sampling.initial[1]; + if (bytes < metadata.bytes) continue; + surface->clearGeneration = GuestMemory::CollectWritesUncached(metadata.address, metadata.bytes); + surface->clearPending = false; + if (surface->clearGeneration == 0u) surface->sampling.Refuse("native multisample depth clear requires tracked HTILE generations"); + } +} + +void QueueHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes) { + std::lock_guard lock(surfacesMutex()); + for (const auto& surface : surfaces()) { + if (!surface->active->load(std::memory_order_acquire) || !surface->samplingActive || surface->target.htileAddress != htileAddress || surface->clearGeneration == 0u || !surface->sampling.refusal.empty()) continue; + if (bytes >= surface->sampling.initial[1].bytes) surface->clearCompletionQueued = true; + } +} + +void CompleteHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes) { + std::lock_guard lock(surfacesMutex()); + for (const auto& surface : surfaces()) { + if (!surface->active->load(std::memory_order_acquire) || !surface->samplingActive || surface->target.htileAddress != htileAddress || surface->clearGeneration == 0u) continue; + const auto& metadata = surface->sampling.initial[1]; + if (bytes < metadata.bytes) continue; + Require(GuestMemory::UnchangedSinceCollected(metadata.address, metadata.bytes, surface->clearGeneration), "CPU writes raced native multisample depth clear"); + GuestMemory::MarkWritten(metadata.address, metadata.bytes); + surface->clearCompletionQueued = false; + } } void ClearDepthSurfaces(VkDevice device) { @@ -2231,6 +2317,7 @@ std::shared_ptr DepthSurfaceTexture(const Context& context, std::spanHolds(texture)) return false; @@ -2454,7 +2542,10 @@ bool DepthSurfaceStoreBack(const Context& context, const StorageTexture& storage if (std::get<0>(key) != context.device || entry->storage.get() != &storage) continue; const auto slices = layeredSlices(context, *entry); entry->Copy(slices, false); - for (const auto& slice : slices) slice.surface->written = ++depthWrites; + for (const auto& slice : slices) { + slice.surface->written = ++depthWrites; + slice.surface->NoteHostWrite(); + } return true; } for (const auto& surface : surfaces()) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 90c38e18c1..7926925ff3 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -55,6 +55,7 @@ std::uint64_t DrawRenderPassKey(const Context& context, const State& state, std: mix(state.renderExtent.height); mix(state.samples); mix(state.mergedCoverage); + mix(state.truncatedCoverage); mix(state.customSampleLocations); if (state.customSampleLocations) { mix(state.sampleLocationGrid); @@ -81,6 +82,10 @@ GuestTextureResource ArraySurfaceForTarget(const ColorTarget& color) { namespace { +bool writesDepthTarget(const State& state) { + return state.depthWrite || (state.stencilTest && (state.stencilFront.writeMask != 0u || state.stencilBack.writeMask != 0u)); +} + std::size_t colorKeyCount(const ColorTarget& color, std::size_t bytes) { if (color.mipCount != 1 || color.depth != 1 || !color.dccPipeAligned) return DccKeyBytes(bytes); return DccKeyCount(ColorTextureTileMode(color.tileMode), color.elementBytes, color.extent.width, color.extent.height, bytes); @@ -1043,9 +1048,14 @@ struct DrawInputs { VkPipelineStageFlags shaderStages = 0; std::uint32_t meshGroups = 0; std::vector restartTable; + std::uint32_t expandedIndexCount = 0; const VertexInputLayout& VertexLayout() const { return retainedVertexInput != nullptr ? *retainedVertexInput : vertexInput; } }; +bool StripPatches(const State& state, std::span shaders) { + return state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP && BarycentricPatches(state, shaders); +} + std::shared_ptr zeroVertexBuffer(const Context& context, const ShaderRecompiler::VertexAttribute& attribute) { auto zero = std::make_shared(context, DecodeVertexFormat(attribute).bytes, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT); std::fill(zero->Bytes().begin(), zero->Bytes().end(), std::byte{0}); @@ -1129,19 +1139,58 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P ValidateSampleMask(context, state, shaders); timer.phase(PhaseValidate); inputs.maxIndex = draw.indexed ? 0u : draw.firstVertex + draw.indexCount - 1u; - if (state.quadList) { - Require(!draw.indirect, "indirect quad-list draw is unsupported"); - const std::uint32_t quadCount = draw.indexCount / 4u; - if (quadCount == 0u || draw.instanceCount == 0u) { + if (state.quadList || StripPatches(state, shaders)) { + Require(!draw.indirect, state.quadList ? "indirect quad-list draw is unsupported" : "an indirect triangle strip through emulated barycentric patches is unsupported"); + const std::uint32_t primitives = state.quadList ? draw.indexCount / 4u : draw.indexCount >= 3u ? draw.indexCount - 2u : 0u; + if (primitives == 0u || draw.instanceCount == 0u) { inputs.nothing = true; return inputs; } - const std::uint32_t outputIndexCount = quadCount * 6u; - const std::size_t quadIndexBytes = static_cast(outputIndexCount) * sizeof(std::uint32_t); - auto indexCopy = std::make_shared(context, quadIndexBytes, VK_BUFFER_USAGE_INDEX_BUFFER_BIT); + const std::uint32_t corners = state.quadList ? 6u : 3u; + const std::uint32_t outputIndexCount = primitives * corners; + const std::size_t expandedIndexBytes = static_cast(outputIndexCount) * sizeof(std::uint32_t); + auto indexCopy = std::make_shared(context, expandedIndexBytes, VK_BUFFER_USAGE_INDEX_BUFFER_BIT); auto* outIndices = reinterpret_cast(indexCopy->Bytes().data()); std::uint32_t maxIdx = 0; const bool lastProvoking = state.provokingVertexMode == VK_PROVOKING_VERTEX_MODE_LAST_VERTEX_EXT; + const auto expand = [&](const auto& source) { + for (std::uint32_t primitive = 0; primitive < primitives; ++primitive) { + auto* out = outIndices + static_cast(primitive) * corners; + if (state.quadList) { + const auto i0 = source(primitive * 4u + 0u); + const auto i1 = source(primitive * 4u + 1u); + const auto i2 = source(primitive * 4u + 2u); + const auto i3 = source(primitive * 4u + 3u); + if (lastProvoking) { + out[0] = i0; + out[1] = i1; + out[2] = i3; + out[3] = i1; + out[4] = i2; + out[5] = i3; + } else { + out[0] = i0; + out[1] = i1; + out[2] = i2; + out[3] = i0; + out[4] = i2; + out[5] = i3; + } + } else { + const std::uint32_t odd = primitive & 1u; + if (lastProvoking) { + out[0] = source(primitive + odd); + out[1] = source(primitive + 1u - odd); + out[2] = source(primitive + 2u); + } else { + out[0] = source(primitive); + out[1] = source(primitive + 1u + odd); + out[2] = source(primitive + 2u - odd); + } + } + for (std::uint32_t corner = 0; corner < corners; ++corner) maxIdx = std::max(maxIdx, out[corner]); + } + }; if (draw.indexed) { GuestMemory::FlushGpuWrites(draw.indexAddress, static_cast(indexBytes)); GuestMemory::CheckRange(reinterpret_cast(draw.indexAddress), static_cast(indexBytes), draw.indexSize); @@ -1156,59 +1205,18 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P return v; } }; - for (std::uint32_t q = 0; q < quadCount; ++q) { - const auto i0 = readIndex(q * 4u + 0u); - const auto i1 = readIndex(q * 4u + 1u); - const auto i2 = readIndex(q * 4u + 2u); - const auto i3 = readIndex(q * 4u + 3u); - maxIdx = std::max({maxIdx, i0, i1, i2, i3}); - if (lastProvoking) { - outIndices[q * 6u + 0u] = i0; - outIndices[q * 6u + 1u] = i1; - outIndices[q * 6u + 2u] = i3; - outIndices[q * 6u + 3u] = i1; - outIndices[q * 6u + 4u] = i2; - outIndices[q * 6u + 5u] = i3; - } else { - outIndices[q * 6u + 0u] = i0; - outIndices[q * 6u + 1u] = i1; - outIndices[q * 6u + 2u] = i2; - outIndices[q * 6u + 3u] = i0; - outIndices[q * 6u + 4u] = i2; - outIndices[q * 6u + 5u] = i3; - } - } + expand(readIndex); Require(draw.firstVertex <= std::numeric_limits::max() - maxIdx, "indexed draw vertex range overflow"); - inputs.maxIndex = maxIdx + draw.firstVertex; } else { - maxIdx = quadCount * 4u - 1u; - Require(draw.firstVertex <= std::numeric_limits::max() - maxIdx, "auto draw vertex range overflow"); - for (std::uint32_t q = 0; q < quadCount; ++q) { - const auto i0 = q * 4u + 0u; - const auto i1 = q * 4u + 1u; - const auto i2 = q * 4u + 2u; - const auto i3 = q * 4u + 3u; - if (lastProvoking) { - outIndices[q * 6u + 0u] = i0; - outIndices[q * 6u + 1u] = i1; - outIndices[q * 6u + 2u] = i3; - outIndices[q * 6u + 3u] = i1; - outIndices[q * 6u + 4u] = i2; - outIndices[q * 6u + 5u] = i3; - } else { - outIndices[q * 6u + 0u] = i0; - outIndices[q * 6u + 1u] = i1; - outIndices[q * 6u + 2u] = i2; - outIndices[q * 6u + 3u] = i0; - outIndices[q * 6u + 4u] = i2; - outIndices[q * 6u + 5u] = i3; - } - } - inputs.maxIndex = maxIdx + draw.firstVertex; + const std::uint32_t lastVertex = state.quadList ? primitives * 4u - 1u : primitives + 1u; + Require(draw.firstVertex <= std::numeric_limits::max() - lastVertex, "auto draw vertex range overflow"); + expand([](std::uint32_t idx) { return idx; }); } + inputs.maxIndex = maxIdx + draw.firstVertex; Require(inputs.maxIndex <= context.limits.maxDrawIndexedIndexValue, "index exceeds the device's indexed draw limit"); inputs.indexHandle = indexCopy->Handle(); inputs.indexOffset = 0; + inputs.expandedIndexCount = outputIndexCount; inputs.indices = std::move(indexCopy); } else if (draw.indexed) { const bool listTopology = state.topology == VK_PRIMITIVE_TOPOLOGY_POINT_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_LINE_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; @@ -1491,9 +1499,9 @@ void recordDrawCommands(const Context& context, VkCommandBuffer commands, const return; } if (!inputs.vertexHandles.empty()) context.Resolved(&DeviceFunctions::cmdBindVertexBuffers, "vkCmdBindVertexBuffers")(commands, 0, static_cast(inputs.vertexHandles.size()), inputs.vertexHandles.data(), inputs.vertexOffsets.data()); - if (state.quadList) { + if (inputs.expandedIndexCount != 0u) { context.Resolved(&DeviceFunctions::cmdBindIndexBuffer, "vkCmdBindIndexBuffer")(commands, inputs.indexHandle, inputs.indexOffset, VK_INDEX_TYPE_UINT32); - context.Resolved(&DeviceFunctions::cmdDrawIndexed, "vkCmdDrawIndexed")(commands, (draw.indexCount / 4u) * 6u, draw.instanceCount, 0, static_cast(draw.firstVertex), draw.firstInstance); + context.Resolved(&DeviceFunctions::cmdDrawIndexed, "vkCmdDrawIndexed")(commands, inputs.expandedIndexCount, draw.instanceCount, 0, static_cast(draw.firstVertex), draw.firstInstance); return; } if (draw.indexed) context.Resolved(&DeviceFunctions::cmdBindIndexBuffer, "vkCmdBindIndexBuffer")(commands, inputs.indexHandle, inputs.indexOffset, draw.indexSize == 2 ? VK_INDEX_TYPE_UINT16 : VK_INDEX_TYPE_UINT32); @@ -2148,8 +2156,66 @@ std::optional KnownValidationFailure(const Context& context, std::s return std::nullopt; } -void Draw(const Context& context, const State& state, const Pm4::DrawParameters& draw, std::span shaders, std::span snapshots, std::shared_ptr* recipeOut) { +namespace { + +struct TargetBinding { + ColorTarget color; + bool gpuTiling = false; + std::shared_ptr resident; + bool proxied = false; + std::unique_ptr transfer; + std::unique_ptr tiled; + std::unique_ptr tiledDevice; + std::unique_ptr linearDevice; + std::vector original; + TileMipLayout mip{}; + std::unique_ptr target; + bool multisampled = false; + VkImage multisampleImage = VK_NULL_HANDLE; + std::shared_ptr multisampleOwner; + std::unique_ptr dump; +}; + +struct PreparedDraw { + explicit PreparedDraw(bool profile) : timer(profile) {} + PreparedDraw(const PreparedDraw&) = delete; + PreparedDraw& operator=(const PreparedDraw&) = delete; + std::shared_ptr* recipeOut = nullptr; std::vector stencilReferences; + std::optional timing; + DrawTimer timer; + DrawOutcome outcome; + const ShaderResources::BuildTiming* built = nullptr; + double waitedBefore = 0; + double ownWaitedMs = 0; + DrawInputs inputs; + const Pm4::DrawParameters::IndirectDraw* args = nullptr; + int dumpLimit = 0; + std::vector targets; + std::vector targetViews; + Recorder* recorder = nullptr; + ResolvedResources resolved; + IndirectRecord indirect; + std::vector records; + bool completion = false; + bool listed = false; + bool recorded = false; + bool lean = false; + bool multisampled = false; + std::optional masked; + std::vector>> stencilReplay; + std::shared_ptr pipeline; + std::vector> owners; + std::vector> framebufferOwners; + std::shared_ptr framebuffer; + std::vector reordered; + SampleOrderPass reorderPass; + std::optional depthLocations; +}; + +bool prepareDraw(const Context& context, const State& state, const Pm4::DrawParameters& draw, std::span shaders, std::span snapshots, PreparedDraw& prepared) { + auto*& recipeOut = prepared.recipeOut; + auto& stencilReferences = prepared.stencilReferences; for (const auto& shader : shaders) { if (shader.program != nullptr && !shader.program->stencilReferences.empty()) { Require(shader.stage == ShaderRecompiler::ShaderStage::Fragment && stencilReferences.empty(), "stencil replay requires exactly one fragment stage"); @@ -2172,60 +2238,37 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& Require(state.samples == 1u && (!state.depth || HasStencil(state.depth->format)), "stencil replay requires single-sample rendering and an eight-bit stencil format"); recipeOut = nullptr; } - if (state.quadList) recipeOut = nullptr; + if (state.quadList || StripPatches(state, shaders)) recipeOut = nullptr; if (context.recorder != nullptr) context.recorder->BoundKeptBytes(); - PerformanceTimer timing("Graphics.Draw"); + auto& timing = prepared.timing.emplace("Graphics.Draw"); // APS5_PROFILE_DRAW prints the time of each phase of the draw (microseconds) and the [draws] totals. static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; - DrawTimer timer(profile); + auto& timer = prepared.timer; + timer = DrawTimer(profile); // APS5_TRACE_DRAWS (together with APS5_PROFILE_DRAW; alone it only enables Driver.cpp's own // [draw] lines) additionally prints one line per draw with its phases and, for synchronous // draws, their inputs: per-draw string building at ~1400 draws per 10 s is itself a cost the // totals must not carry. static const bool traceDraws = std::getenv("APS5_TRACE_DRAWS") != nullptr; - DrawOutcome outcome; - const ShaderResources::BuildTiming* built = nullptr; + auto& outcome = prepared.outcome; + auto& built = prepared.built; // The thread's GPU waits so far: the difference at the report, less the draw's own syncs // (`ownWaitedMs`, sampled around them), is what this draw waited for through the flush hook // (nested in the draw's hold, see DrawOutcome::hookWaitUs). - const auto waitedBefore = profile ? Recorder::ThreadWaitedMs() : 0.0; - double ownWaitedMs = 0; + const auto waitedBefore = prepared.waitedBefore = profile ? Recorder::ThreadWaitedMs() : 0.0; + auto& ownWaitedMs = prepared.ownWaitedMs; const auto report = [&](const char* suffix) { reportDrawEnd(state, timer, built, outcome, waitedBefore, ownWaitedMs, suffix); }; - auto inputs = prepareDrawInputs(context, state, draw, shaders, outcome, timer, nullptr); - if (inputs.nothing) return; - const auto* args = draw.indirect ? &*draw.indirect : nullptr; + auto& inputs = prepared.inputs; + inputs = prepareDrawInputs(context, state, draw, shaders, outcome, timer, nullptr); + if (inputs.nothing) return false; + const auto* args = prepared.args = draw.indirect ? &*draw.indirect : nullptr; const auto indexBytes = inputs.indexBytes; timing.Mark("validate"); timing.Mark("index_upload"); - // One binding per color attachment. Tiled targets are detiled and retiled on the GPU in video memory; - // only changed guest blocks are stored back. - struct TargetBinding { - ColorTarget color; - bool gpuTiling = false; - // Resident target: the surface's cached storage image is attached directly and marked dirty - // afterwards, so nothing is copied in or out per draw. - std::shared_ptr resident; - bool proxied = false; - // Linear pixels converted on the CPU, for targets the GPU detiler does not handle. - std::unique_ptr transfer; - // Guest bytes in host memory, and their device-local copies for the detiler. - std::unique_ptr tiled; - std::unique_ptr tiledDevice; - std::unique_ptr linearDevice; - std::vector original; - TileMipLayout mip{}; - std::unique_ptr target; - bool multisampled = false; - VkImage multisampleImage = VK_NULL_HANDLE; - std::shared_ptr multisampleOwner; - // APS5_DUMP_TARGETS: the rendered linear pixels, read back for target_
_.raw. - std::unique_ptr dump; - }; - const int dumpLimit = DumpTargetLimit(); - static std::mutex dumpMutex; - static std::map dumped; - std::vector targets(state.colors.size()); - std::vector targetViews; + const int dumpLimit = prepared.dumpLimit = DumpTargetLimit(); + auto& targets = prepared.targets; + targets.resize(state.colors.size()); + auto& targetViews = prepared.targetViews; for (std::size_t index = 0; index < state.colors.size(); ++index) { auto& binding = targets[index]; binding.color = state.colors[index]; @@ -2317,12 +2360,16 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& binding.target = std::make_unique(context, color, state.blends.at(color.exportIndex).blendEnable != 0); targetViews.push_back(binding.target->View()); } - if (state.depth) targetViews.push_back(DepthSurfaceView(context, *state.depth, state.depthWrite)); + if (state.depth) { + targetViews.push_back(DepthSurfaceView(context, *state.depth, state.depthWrite)); + if (writesDepthTarget(state)) NoteDepthSurfaceWrite(context, *state.depth); + } timer.phase(PhasePrepare); const bool recordDraws = RecordDraws(); - auto* recorder = Recorder::Active(); + auto* recorder = prepared.recorder = Recorder::Active(); bool recordable = recordDraws && recorder != nullptr && dumpLimit == 0 && std::all_of(targets.begin(), targets.end(), [](const TargetBinding& binding) { return binding.resident != nullptr || binding.multisampled; }); - auto resolved = resolveDrawResources(context, state, draw, shaders, snapshots, indexBytes, recordable, outcome, timer); + auto& resolved = prepared.resolved; + resolved = resolveDrawResources(context, state, draw, shaders, snapshots, indexBytes, recordable, outcome, timer); auto& resources = resolved.resources; if (!stencilReferences.empty() && resources->ChecksFaults()) recordable = false; if (!stencilReferences.empty()) { @@ -2362,9 +2409,9 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& // and records plain draws with the same per-dimension rules. The import pointer stays valid: // refreshImports runs only under GuestMemory::GpuMutex, held here, and a retired import's // buffer is kept while the recorder is busy. - IndirectRecord indirect; + auto& indirect = prepared.indirect; indirect.args = args; - std::vector records; + auto& records = prepared.records; if (args != nullptr) { static const bool gpuIndirectDraws = std::getenv("APS5_NO_GPU_INDIRECT_DRAW") == nullptr; const auto decide = [&](std::uint64_t address, std::size_t bytes, const HostImport*& import) { @@ -2385,7 +2432,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& const auto rangeBytes = args->RangeBytes(); if (rangeBytes == 0) { report(" indirect draw without records"); - return; + return false; } Require(rangeBytes <= std::numeric_limits::max(), "indirect draw record range overflow"); if (!gpuIndirectDraws) indirect.path = IndirectDrawPath::Disabled; @@ -2406,7 +2453,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& if (std::none_of(records.begin(), records.end(), [](const Pm4::DrawArguments& record) { return record.count != 0 && record.instances != 0; })) { CountIndirectDraw(indirect.path, indirect.readMs); report(" indirect draw with empty records"); - return; + return false; } } } @@ -2431,14 +2478,14 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& static const bool syncCompletionDraws = std::getenv("APS5_SYNC_COMPLETION_DRAWS") != nullptr; static const bool recorderSyncDraws = std::getenv("APS5_NO_RECORDER_SYNC_DRAWS") == nullptr; static const bool recordCopiedDraws = std::getenv("APS5_NO_RECORD_COPIED_DRAWS") == nullptr; - const bool completion = resources->NeedsCompletion(); + const bool completion = prepared.completion = resources->NeedsCompletion(); const bool copiedWrites = completion && resources->HasCopiedWrites(); // A lease only forces the wait when deferred release is off. const bool lease = resources->HoldsLease() && SyncLeaseWork(); outcome.completion = completion; outcome.recorded = recordable; // Whether the copied write-back is listed in DrawCopiedWriters instead of waited for. - bool listed = false; + auto& listed = prepared.listed; if (!recordable) { outcome.reason = !recordDraws || dumpLimit != 0 ? SyncDisabled : recorder == nullptr ? SyncNoRecorder : SyncNotResident; } else if (completion && (syncCompletionDraws || copiedWrites || lease)) { @@ -2448,7 +2495,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& else if (recorderSyncDraws) outcome.waited = true; else outcome.recorded = false; } - const bool recorded = outcome.recorded; + const bool recorded = prepared.recorded = outcome.recorded; // A build this recorded draw can share with later identical ones goes into the cache (a cache // hit is reusable by construction, so `recorded` holds for it; one with completion work is // never reusable). @@ -2457,8 +2504,8 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& // aid: APS5_DRAW_TRANSITIONS=1 keeps the per-draw upload and download barriers, the layout // transitions of every target and one pass per draw, as before. static const bool drawTransitions = std::getenv("APS5_DRAW_TRANSITIONS") != nullptr; - const bool lean = recorded && !drawTransitions; - const bool multisampled = std::any_of(targets.begin(), targets.end(), [](const TargetBinding& binding) { return binding.multisampled; }); + const bool lean = prepared.lean = recorded && !drawTransitions; + const bool multisampled = prepared.multisampled = std::any_of(targets.begin(), targets.end(), [](const TargetBinding& binding) { return binding.multisampled; }); Require(lean || !multisampled, "multisampled draws are only rendered as recorded draws"); for (const auto& binding : targets) { if (binding.multisampled) Require(std::find(resources->MultisampleSources().begin(), resources->MultisampleSources().end(), binding.color.surfaceAddress) == resources->MultisampleSources().end(), "sampling a multisampled color target the draw renders to is not implemented"); @@ -2470,7 +2517,8 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& if (state.depth) RequireNoNativeDepthFeedback_nid_no_patch(context, *state.depth, resources->SampledTextures()); APS5_LOG_CHARS_OUT_DEBUG("Creating Pipeline"); // The state is copied only when a mask must change. - auto masked = maskedState(state, inputs.fragmentOutputs); + auto& masked = prepared.masked; + masked = maskedState(state, inputs.fragmentOutputs); if (std::any_of(targets.begin(), targets.end(), [](const TargetBinding& binding) { return binding.proxied; })) recipeOut = nullptr; for (std::size_t index = 0; index < targets.size(); ++index) { if (!targets[index].proxied) continue; @@ -2485,7 +2533,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& const auto built = resources->SetLayouts(); Require(described.layouts.main == built.main && described.layouts.fragment == built.fragment && described.layouts.split == built.split && std::ranges::equal(described.mainKey, built.mainKey) && std::ranges::equal(described.fragmentKey, built.fragmentKey), "set layouts described from the shaders differ from the draw's"); } - std::vector>> stencilReplay; + auto& stencilReplay = prepared.stencilReplay; for (const auto reference : stencilReferences) { auto selected = pipelineState; selected.stencilReplayReference = reference; @@ -2495,20 +2543,22 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& Require(!selectedPipeline->SplitsFaces() && !selectedPipeline->SplitsSecondChannel(), "stencil replay refuses pipeline face or channel splits"); stencilReplay.emplace_back(reference, std::move(selectedPipeline)); } - auto pipeline = stencilReplay.empty() ? CachedPipeline(context, pipelineState, inputs.VertexLayout(), *resources, shaders, lean ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL) : stencilReplay.front().second; + auto& pipeline = prepared.pipeline; + pipeline = stencilReplay.empty() ? CachedPipeline(context, pipelineState, inputs.VertexLayout(), *resources, shaders, lean ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL) : stencilReplay.front().second; // Resident targets' views are stable while their storage image lives, so the framebuffer is // reused with the pipeline; a per-draw RenderTarget gets a framebuffer of its own. - std::vector> owners; + auto& owners = prepared.owners; owners.reserve(targets.size()); - std::vector> framebufferOwners; + auto& framebufferOwners = prepared.framebufferOwners; framebufferOwners.reserve(targets.size()); for (const auto& binding : targets) { owners.push_back(binding.resident); framebufferOwners.push_back(binding.multisampled ? binding.multisampleOwner : binding.resident); } - auto framebuffer = pipeline->AcquireFramebuffer(targetViews, framebufferOwners, state.renderExtent, state.renderLayers); - std::vector reordered; - SampleOrderPass reorderPass; + auto& framebuffer = prepared.framebuffer; + framebuffer = pipeline->AcquireFramebuffer(targetViews, framebufferOwners, state.renderExtent, state.renderLayers); + auto& reordered = prepared.reordered; + auto& reorderPass = prepared.reorderPass; if (pipeline->ReordersSamples()) { reorderPass = SampleOrderPassFor(context, pipelineState, pipeline->HostSampleOrder()); for (const auto& step : PlanSampleOrder(context, state, pipeline->HostSampleOrder())) { @@ -2517,9 +2567,50 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& Require(reordered.back().method == step.method, "a reordered attachment's surface disagrees with the planned way of putting its samples in order"); } } - const auto depthLocations = passDepthLocations(context, pipelineState, *pipeline); + prepared.depthLocations = passDepthLocations(context, pipelineState, *pipeline); timer.phase(PhasePipeline); APS5_LOG_CHARS_OUT_DEBUG("Pipeline created"); + return true; +} + +void recordPreparedDraw(const Context& context, const State& state, const Pm4::DrawParameters& draw, std::span shaders, PreparedDraw& prepared) { + static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; + static const bool traceDraws = std::getenv("APS5_TRACE_DRAWS") != nullptr; + static std::mutex dumpMutex; + static std::map dumped; + auto& timing = *prepared.timing; + auto& timer = prepared.timer; + auto& outcome = prepared.outcome; + const auto* built = prepared.built; + const auto waitedBefore = prepared.waitedBefore; + auto& ownWaitedMs = prepared.ownWaitedMs; + const auto report = [&](const char* suffix) { reportDrawEnd(state, timer, built, outcome, waitedBefore, ownWaitedMs, suffix); }; + auto* recipeOut = prepared.recipeOut; + const auto& stencilReferences = prepared.stencilReferences; + auto& inputs = prepared.inputs; + const auto* args = prepared.args; + const int dumpLimit = prepared.dumpLimit; + auto& targets = prepared.targets; + const auto& targetViews = prepared.targetViews; + auto* recorder = prepared.recorder; + auto& resolved = prepared.resolved; + auto& resources = resolved.resources; + const auto& contentKey = resolved.contentKey; + const bool cacheable = resolved.cacheable; + auto& indirect = prepared.indirect; + const bool completion = prepared.completion; + const bool listed = prepared.listed; + const bool recorded = prepared.recorded; + const bool lean = prepared.lean; + const bool multisampled = prepared.multisampled; + const auto& masked = prepared.masked; + const auto& stencilReplay = prepared.stencilReplay; + const auto& pipeline = prepared.pipeline; + const auto& owners = prepared.owners; + const auto& framebuffer = prepared.framebuffer; + const auto& reordered = prepared.reordered; + const auto& reorderPass = prepared.reorderPass; + const auto& depthLocations = prepared.depthLocations; if (lean) { RecordedDraw record; record.recorder = recorder; @@ -2844,6 +2935,15 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& APS5_LOG_CHARS_OUT_DEBUG("Draw finished"); } +} + +void Draw(const Context& context, const State& state, const Pm4::DrawParameters& draw, std::span shaders, std::span snapshots, std::shared_ptr* recipeOut) { + static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; + PreparedDraw prepared(profile); + prepared.recipeOut = recipeOut; + if (prepareDraw(context, state, draw, shaders, snapshots, prepared)) recordPreparedDraw(context, state, draw, shaders, prepared); +} + DrawRecipeOutcome DrawWithRecipe(const Context& context, const State& state, const Pm4::DrawParameters& draw, std::span shaders, std::span snapshots, const DrawRecipe& recipe) { if (context.recorder != nullptr) context.recorder->BoundKeptBytes(); static_cast(snapshots); @@ -2866,7 +2966,7 @@ DrawRecipeOutcome DrawWithRecipe(const Context& context, const State& state, con }; Require(!draw.indirect, "a draw recipe covers direct draws only"); auto* recorder = Recorder::Active(); - if (state.quadList || !RecordDraws() || recorder == nullptr || DumpTargetLimit() != 0) return miss(DrawRecipeMiss::NotRecordable); + if (state.quadList || StripPatches(state, shaders) || !RecordDraws() || recorder == nullptr || DumpTargetLimit() != 0) return miss(DrawRecipeMiss::NotRecordable); if (recipe.hasDepth != state.depth.has_value()) return miss(DrawRecipeMiss::TargetGone); const std::size_t expectedViews = state.colors.size() + (recipe.hasDepth ? 1u : 0u); Require(recipe.targets.size() == state.colors.size() && recipe.targetViews.size() == expectedViews, "draw recipe targets do not match the state"); @@ -2899,6 +2999,7 @@ DrawRecipeOutcome DrawWithRecipe(const Context& context, const State& state, con if (recipe.hasDepth) { timer.phase(PhaseSetup); const auto depthView = DepthSurfaceView(context, *state.depth, state.depthWrite); + if (writesDepthTarget(state)) NoteDepthSurfaceWrite(context, *state.depth); timer.phase(PhaseReadTarget); if (recipe.targetViews.back() != depthView) return miss(DrawRecipeMiss::TargetGone); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 074681e607..c46494982e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -4679,7 +4679,7 @@ void GuestBufferMemory::UploadFinish(bool addressable) { void GuestBufferMemory::copyRegion(Region& region, bool addressable) { static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; const auto bytes = region.end - region.begin; - const auto usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | (addressable ? VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT : 0u); + const auto usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | (addressable ? VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT : 0u); std::uint64_t readBytes = 0; bool inImport = false; if (profile && context.hostImportAlignment != 0) { @@ -5173,7 +5173,7 @@ void GuestBufferMemory::MarkDirectWrites() const { const auto* found = owner(begin, end); if (found == nullptr) continue; const auto& region = *found; - if (region.direct != nullptr || (region.gpuCopy && region.copiedBack)) GuestMemory::MarkWritten(begin, static_cast(end - begin)); + if (region.direct != nullptr || (region.gpuCopy && region.copiedBack)) GuestMemory::MarkPossiblyWritten(begin, static_cast(end - begin)); } } @@ -5261,7 +5261,7 @@ void GuestBufferMemory::WriteBack() { if (region.direct == nullptr && !(region.gpuCopy && (region.copiedBack || region.deviceLocal)) && !(region.mirror != nullptr && !region.mirror->writable)) Recorder::NoteWrittenBack(begin, static_cast(end - begin)); if (region.direct != nullptr) { // The GPU wrote imported guest memory directly; page write watching did not see it. - GuestMemory::MarkWritten(begin, static_cast(end - begin)); + GuestMemory::MarkPossiblyWritten(begin, static_cast(end - begin)); continue; } if (region.mirror != nullptr) { @@ -5291,7 +5291,7 @@ void GuestBufferMemory::WriteBack() { if (region.copiedBack) { // The GPU copied the written sub-range back into the import; page write watching // did not see it. - GuestMemory::MarkWritten(begin, static_cast(end - begin)); + GuestMemory::MarkPossiblyWritten(begin, static_cast(end - begin)); continue; } static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp index 3c81c9c97e..1249e08037 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp @@ -5,6 +5,8 @@ #include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" #include "RdnaDecoder/RdnaDescriptorFormat.hpp" #include +#include +#include #include #include @@ -50,6 +52,45 @@ TextureDimension resolveDimension(std::uint32_t raw) { } } +bool ZeroTail(std::span words) { + return words[4] == 0u && words[5] == 0u && words[6] == 0u && words[7] == 0u; +} + +bool SameLevels(const TileMipLayout& a, const TileMipLayout& b) { + return a.tiledOffset == b.tiledOffset && a.tiledSize == b.tiledSize && a.width == b.width && a.height == b.height && a.blocksPerRow == b.blocksPerRow && a.pitchBytes == b.pitchBytes && a.tail == b.tail && a.tailX == b.tailX && a.tailY == b.tailY; +} + +std::optional> R128ResidentLevelWords(std::span words) { + const auto baseLevel = (words[3] >> 12u) & 0xfu; + const auto lastLevel = (words[3] >> 16u) & 0xfu; + const auto typeRaw = (words[3] >> 28u) & 0xfu; + if (baseLevel == 0u || baseLevel > lastLevel || (typeRaw != 8u && typeRaw != 9u)) return std::nullopt; + const std::array chainWords{words[0], words[1], words[2], words[3], 0u, lastLevel << 4u, 0u, 0u}; + const auto width = (((words[1] >> 30u) & 0x3u) | ((words[2] & 0xfffu) << 2u)) + 1u; + const auto height = ((words[2] >> 14u) & 0x3fffu) + 1u; + const auto minLod = (words[1] >> 8u) & 0xfffu; + const auto widthMinus1 = std::max(width >> baseLevel, 1u) - 1u; + const auto heightMinus1 = std::max(height >> baseLevel, 1u) - 1u; + const auto rebasedMinLod = minLod > baseLevel * 256u ? minLod - baseLevel * 256u : 0u; + const auto levels = lastLevel - baseLevel; + auto rebased = chainWords; + rebased[1] = (words[1] & ~((0xfffu << 8u) | (0x3u << 30u))) | (rebasedMinLod << 8u) | ((widthMinus1 & 0x3u) << 30u); + rebased[2] = (words[2] & ~(0xfffu | (0x3fffu << 14u))) | ((widthMinus1 >> 2u) & 0xfffu) | ((heightMinus1 & 0x3fffu) << 14u); + rebased[3] = (words[3] & ~(0xffu << 12u)) | (levels << 16u); + rebased[5] = levels << 4u; + try { + const auto chain = DescribeSurface(DecodeTextureResource(chainWords)); + const auto view = DescribeSurface(DecodeTextureResource(rebased)); + if (chain.mips.size() != lastLevel + 1u || view.mips.size() != levels + 1u || chain.layers != view.layers) return std::nullopt; + for (std::uint32_t level = baseLevel; level <= lastLevel; ++level) { + if (!SameLevels(chain.mips[level], view.mips[level - baseLevel])) return std::nullopt; + } + } catch (const std::exception&) { + return std::nullopt; + } + return rebased; +} + } bool IsNullTextureResource(std::span words) { @@ -140,7 +181,8 @@ GuestTextureResource DecodeTextureResource(std::span words) const bool fmask = ShaderRecompiler::IsFmaskTextureFormat(static_cast(format)); if (fmask) { Require(format == static_cast(ShaderRecompiler::IrBufferFormat::Fmask32_S8_F8), "guest FMASK format is not implemented"); - Require(!metaCompress && !writeCompress, "FMASK CMASK compression is not implemented"); + Require(!writeCompress && (!metaCompress || metaAddr != 0u), "FMASK CMASK compression is not implemented without readable metadata"); + Require(!metaCompress || metaPipeAligned, "FMASK CMASK metadata must be pipe aligned"); Require((typeRaw == 9u || typeRaw == 13u) && tileModeRaw == 24u && baseLevel == 0u && lastLevel == 0u && maxMip == 0u, "FMASK reads require a single-mip 2D or 2D-array Z_64KB_X surface"); for (const auto selector : {dstSelX, dstSelY, dstSelZ, dstSelW}) Require(selector == 0u || selector == 1u || selector == 4u, "FMASK destination channel selector is not implemented"); } @@ -229,14 +271,19 @@ GuestTextureResource DecodeTextureResource(std::span words) result.dstSelZ = static_cast(dstSelZ); result.dstSelW = static_cast(dstSelW); result.bcSwizzle = static_cast(bcSwizzle); - result.dccAddress = metaCompress ? metaAddr << 8u : 0u; + result.dccAddress = metaCompress && !fmask ? metaAddr << 8u : 0u; + result.fmaskCmaskAddress = metaCompress && fmask ? metaAddr << 8u : 0u; result.dccAlphaOnMsb = dccAlphaPos; result.dccPipeAligned = metaPipeAligned; result.iterate256 = iterate256; result.minLod = minLod; result.samples = samples; if (baseLevel > maxMip) { - Require(LevelsFitAllocation(result, lastLevel + 1u), "guest texture descriptor starts past the surface's last mip level at levels that would move the surface's own" + describe()); + const bool fits = LevelsFitAllocation(result, lastLevel + 1u); + if (!fits && ZeroTail(words)) { + if (const auto resident = R128ResidentLevelWords(words)) return DecodeTextureResource(*resident); + } + Require(fits, "guest texture descriptor starts past the surface's last mip level at levels that would move the surface's own" + describe()); result.mipCount = lastLevel + 1u; } return result; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp index 8911499a06..809e680d42 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -9,6 +9,8 @@ #include "prx/libSceAgcDriver/Graphics/include/SampleOrder.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" +#include "IntermediateRepresentation/IrMetadata/BufferFormat.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libc/include/GuestAllocations.hpp" #include @@ -45,10 +47,57 @@ std::uint64_t collectBlocks(std::uint64_t address, std::size_t bytes) { return generation != 0 ? generation : GuestMemory::CollectWritesUncached(address, bytes); } +GuestTextureResource fmaskSnapshotResource(const GuestTextureResource& resource) { + Require(resource.format == static_cast(ShaderRecompiler::IrBufferFormat::Format32UInt) && resource.fmaskCmaskAddress != 0u, "FMASK image format read semantics are not implemented"); + Require(resource.dimension == TextureDimension::k2D || resource.dimension == TextureDimension::k2DArray, "FMASK image dimension is not implemented"); + Require(resource.tileMode == TextureTileMode::kZ64KBX && (resource.pipeBankXor & ~0xcf00u) == 0u, "FMASK image swizzle or pipe/bank XOR is not implemented"); + Require(resource.width != 0u && resource.width <= 16384u && resource.height != 0u && resource.height <= 16384u && resource.depthOrLastArray < 8192u && resource.baseArray <= resource.depthOrLastArray, "invalid FMASK image extent or layer range"); + Require(resource.dimension == TextureDimension::k2DArray || (resource.depthOrLastArray == 0u && resource.baseArray == 0u), "FMASK 2D image has an array range"); + Require(resource.mipCount == 1u && resource.allocatedMipCount == 1u && resource.baseLevel == 0u && resource.lastLevel == 0u && resource.minLod == 0u, "FMASK image mip fields are not implemented"); + Require(resource.fmaskCmaskAddress == 0u || resource.dccPipeAligned, "FMASK image CMASK must be pipe aligned"); + for (const auto selector : {resource.dstSelX, resource.dstSelY, resource.dstSelZ, resource.dstSelW}) Require(selector == 0u || selector == 1u || selector == 4u, "FMASK image selects an unverified channel"); + auto snapshot = resource; + snapshot.format = static_cast(ShaderRecompiler::IrBufferFormat::Format32UInt); + snapshot.tileMode = TextureTileMode::kLinear; + snapshot.pipeBankXor = 0u; + snapshot.dccAddress = 0u; + snapshot.fmaskCmaskAddress = 0u; + snapshot.dccAlphaOnMsb = false; + snapshot.dccPipeAligned = false; + return snapshot; +} + +GuestTextureResource fmaskResource(const ColorTarget& target) { + GuestTextureResource resource{}; + resource.baseAddress = target.fmaskAddress; + resource.width = target.extent.width; + resource.height = target.extent.height; + resource.mipCount = 1u; + resource.allocatedMipCount = 1u; + resource.tileMode = TextureTileMode::kZ64KBX; + resource.dimension = TextureDimension::k2D; + resource.format = static_cast(ShaderRecompiler::IrBufferFormat::Format32UInt); + resource.dstSelX = 4u; + resource.pipeBankXor = target.fmaskPipeBankXor; + resource.fmaskCmaskAddress = target.cmaskAddress; + resource.dccPipeAligned = true; + return resource; +} + bool sameTarget(const ColorTarget& a, const ColorTarget& b) { return a.surfaceAddress == b.surfaceAddress && a.format == b.format && a.extent.width == b.extent.width && a.extent.height == b.extent.height && a.samples == b.samples; } +constexpr VkImageUsageFlags twinViewUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + +VkFormat colorTwinFormat(const Context& context, VkFormat format) { + if (const auto srgb = SrgbTwinFormat(context, format); srgb != VK_FORMAT_UNDEFINED) return srgb; + for (const auto unorm : {VK_FORMAT_R8_UNORM, VK_FORMAT_R8G8_UNORM, VK_FORMAT_R8G8B8A8_UNORM, VK_FORMAT_B8G8R8A8_UNORM, VK_FORMAT_A8B8G8R8_UNORM_PACK32}) { + if (SrgbTwinFormat(context, unorm) == format) return unorm; + } + return VK_FORMAT_UNDEFINED; +} + bool recreated(const ColorTarget& resident, const ColorTarget& use) { return use.dccAddress != 0 && use.pipeBankXor != resident.pipeBankXor && use.address == resident.address && use.dccAddress == resident.dccAddress && use.dccControl == resident.dccControl && use.dccAlphaOnMsb == resident.dccAlphaOnMsb && use.dccPipeAligned == resident.dccPipeAligned && use.cmaskAddress == resident.cmaskAddress && use.cmaskBytes == resident.cmaskBytes && use.tileMode == resident.tileMode && use.elementBytes == resident.elementBytes; } @@ -101,6 +150,13 @@ public: 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; + twinFormat = colorTwinFormat(context, target.format); + if (twinFormat != VK_FORMAT_UNDEFINED) { + VkFormatProperties twinProperties{}; + context.formatProperties(context.physical, twinFormat, &twinProperties); + const VkImageCreateFlags twinFlags = orderFlags | VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT; + if ((twinProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT) == 0 || !storageSupports(target.format, usage, twinFlags) || !storageSupports(twinFormat, twinViewUsage, twinFlags)) twinFormat = VK_FORMAT_UNDEFINED; + } try { const auto created = allocate(usage, orderFlags); image = created.image; @@ -135,6 +191,16 @@ public: try { VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; info.flags = flags; + const std::array viewFormats{target.format, twinFormat}; + VkImageFormatListCreateInfo formatList{VK_STRUCTURE_TYPE_IMAGE_FORMAT_LIST_CREATE_INFO}; + if (twinFormat != VK_FORMAT_UNDEFINED) { + if ((flags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) == 0 && context.imageFormatList) { + formatList.viewFormatCount = static_cast(viewFormats.size()); + formatList.pViewFormats = viewFormats.data(); + info.pNext = &formatList; + } + info.flags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT; + } info.imageType = VK_IMAGE_TYPE_2D; info.format = target.format; info.extent = {target.extent.width, target.extent.height, 1}; @@ -203,6 +269,7 @@ public: Recorder::CountBarriers(Recorder::CommandClass::Draw, 3); for (auto& [key, texture] : textures) retiredTextures.push_back(std::move(texture)); textures.clear(); + retired.push_back({VK_NULL_HANDLE, VK_NULL_HANDLE, std::exchange(twinView, VK_NULL_HANDLE)}); retired.push_back({image, memory, view}); image = storable.image; memory = storable.memory; @@ -212,6 +279,28 @@ public: recorder.Keep(shared_from_this(), static_cast(storable.bytes)); } + std::shared_ptr FmaskSampled(const GuestTextureResource& resource, VkComponentMapping components) { + const auto snapshot = fmaskSnapshotResource(resource); + Require(target.samples == 8u && target.dccAddress != 0u && fmaskRange.has_value(), "native FMASK sampling requires a tracked eight-fragment DCC target"); + Require(target.tileMode == ColorTileMode::RenderTarget && target.depth == 1u && target.depthSlice == 0u && target.layers == 1u && target.arrayLastSlice == 0u && target.arraySlice == 0u && target.mip == 0u && target.mipCount == 1u, "native FMASK target layout is not implemented"); + Require(target.fmaskAddress == resource.baseAddress && target.fmaskPipeBankXor == resource.pipeBankXor && target.cmaskAddress == resource.fmaskCmaskAddress, "native FMASK descriptor does not match its resident target"); + Require(target.extent.width == resource.width && target.extent.height == resource.height && resource.baseArray == 0u && resource.depthOrLastArray == 0u, "native FMASK extent or subresource does not match its resident target"); + auto* recorder = Recorder::Active(); + Require(recorder != nullptr && recorder->Device() == context.device && !Recorder::InCompletion(), "native FMASK sampling requires its device's active recorder outside completion"); + ApplyFastClear(target); + Keep(); + const std::array key{static_cast(components.r), static_cast(components.g), static_cast(components.b), static_cast(components.a), static_cast(resource.dimension)}; + auto& texture = fmaskTextures[key]; + if (texture == nullptr) { + const auto geometry = DescribeSurface(snapshot); + std::vector bytes(geometry.guestBytes); + constexpr std::uint32_t expanded = 0x76543210u; + for (std::size_t offset = 0; offset < bytes.size(); offset += sizeof(expanded)) std::memcpy(bytes.data() + offset, &expanded, sizeof(expanded)); + texture = std::make_unique(context, *context.detiler, snapshot, components, bytes); + } + return {shared_from_this(), texture.get()}; + } + void Keep() { if (auto* recorder = Recorder::Active(); recorder != nullptr && recorder->Device() == context.device) { recorder->Keep(shared_from_this()); @@ -241,6 +330,7 @@ public: colorFill = MultisampledColorFill(use); fromTexture = false; dccRanges.clear(); + fmaskRange.reset(); metadataValid = std::make_shared>(true); const auto initial = initialDccClear(use); recordCommands(context, [&](VkCommandBuffer commands, Recorder* recorder) { @@ -336,14 +426,39 @@ public: return {shared_from_this(), texture.get()}; } - VkRenderPass ResolvePass() { + VkImageView View(VkFormat format) { + if (format == target.format) return view; + Require(twinFormat != VK_FORMAT_UNDEFINED && format == twinFormat, "a multisampled color target is only viewed in its own format or its sRGB twin"); + if (twinView == VK_NULL_HANDLE) { + VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + viewInfo.image = image; + viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; + viewInfo.format = twinFormat; + viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + VkImageViewUsageCreateInfo viewUsage{VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO}; + viewUsage.usage = twinViewUsage; + viewInfo.pNext = &viewUsage; + Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &twinView), "vkCreateImageView multisampled target twin"); + } + return twinView; + } + + bool Serves(const ColorTarget& use) const { + if (use.format == target.format) return sameTarget(target, use); + auto twin = use; + twin.format = target.format; + return twinFormat != VK_FORMAT_UNDEFINED && use.format == twinFormat && sameTarget(target, twin); + } + + VkRenderPass ResolvePass(VkFormat format) { + auto& resolvePass = resolvePasses[format]; if (resolvePass != VK_NULL_HANDLE) return resolvePass; std::array attachments{}; - attachments[0].format = target.format; + attachments[0].format = format; attachments[0].samples = static_cast(target.samples); attachments[0].loadOp = VK_ATTACHMENT_LOAD_OP_LOAD; attachments[0].storeOp = VK_ATTACHMENT_STORE_OP_STORE; - attachments[1].format = target.format; + attachments[1].format = format; attachments[1].samples = VK_SAMPLE_COUNT_1_BIT; attachments[1].loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; attachments[1].storeOp = VK_ATTACHMENT_STORE_OP_STORE; @@ -376,7 +491,7 @@ public: VkImage image = VK_NULL_HANDLE; VkDeviceMemory memory = VK_NULL_HANDLE; VkImageView view = VK_NULL_HANDLE; - VkRenderPass resolvePass = VK_NULL_HANDLE; + std::map resolvePasses; MultisampledCmask cmask; MultisampledColorFill colorFill; bool fromTexture = false; @@ -391,10 +506,14 @@ private: VkImageUsageFlags usage = 0; VkFormat storageFormat = VK_FORMAT_UNDEFINED; VkImageCreateFlags storageFlags = 0; + VkFormat twinFormat = VK_FORMAT_UNDEFINED; + VkImageView twinView = VK_NULL_HANDLE; std::vector retired; VkImageView storageView = VK_NULL_HANDLE; bool storageWritten = false; VkDeviceSize allocationBytes = 0; + std::optional fmaskRange; + std::map, std::unique_ptr> fmaskTextures; std::map, std::shared_ptr> textures; std::vector> retiredTextures; std::vector dccRanges; @@ -450,18 +569,29 @@ private: Require(a.address >= b.address + b.bytes || b.address >= a.address + a.bytes, "overlapping multisampled DCC color or metadata ranges are not implemented"); } } + if (use.samples == 8u && use.fmaskAddress != 0u && use.cmaskAddress != 0u) { + const auto resource = fmaskResource(use); + static_cast(fmaskSnapshotResource(resource)); + fmaskRange = GuestMemory::UnchangedQuery{use.fmaskAddress, static_cast(DescribeSurface(resource).guestBytes), 0}; + auto& range = *fmaskRange; + GuestMemory::CheckRange(reinterpret_cast(range.address), range.bytes, 65536u); + Require(GuestMemory::Watched(range.address, range.bytes), "native FMASK contents require watched guest memory"); + range.generation = GuestMemory::CollectWritesUncached(range.address, range.bytes); + Require(range.generation != 0, "native FMASK contents have no write generation"); + for (const auto& other : dccRanges) Require(range.address >= other.address + other.bytes || other.address >= range.address + range.bytes, "native FMASK overlaps its color or metadata planes"); + } std::vector dcc(dccRanges[1].bytes); GuestMemory::Read(use.dccAddress, dcc); std::vector cmask(use.cmaskBytes); if (!cmask.empty()) GuestMemory::Read(use.cmaskAddress, cmask); const auto clear = MultisampledDccClear(use, dcc, cmask); - Require(GuestMemory::UnchangedSinceAll(dccRanges), "initial multisampled DCC contents changed during import"); + Require(GuestMemory::UnchangedSinceAll(dccRanges) && (!fmaskRange || GuestMemory::UnchangedSinceAll(std::span(&*fmaskRange, 1u))), "initial multisampled DCC contents changed during import"); return clear; } void requireDccRange(bool valid, const GuestMemory::UnchangedQuery& range, const char* reason) const { if (valid) return; - const auto* plane = range.address == target.address ? "color" : range.address == target.dccAddress ? "DCC" : "CMASK"; + const auto* plane = range.address == target.address ? "color" : range.address == target.dccAddress ? "DCC" : range.address == target.fmaskAddress ? "FMASK" : "CMASK"; char text[384]; std::snprintf(text, sizeof(text), "%s (%s range 0x%llx+0x%zx, %u samples)", reason, plane, static_cast(range.address), range.bytes, target.samples); Require(false, text); @@ -482,13 +612,23 @@ private: requireDccRange(GuestMemory::UnchangedSince(range.address, range.bytes, range.generation) || !GuestMemory::StoredOver(range.address, range.bytes, range.generation), range, "writes to a resident multisampled DCC target or its metadata are not implemented"); if (auto* recorder = Recorder::Active(); recorder != nullptr) requireDccRange(!recorder->PendingWriteOverlaps(range.address, range.bytes), range, "pending writes to a resident multisampled DCC target or its metadata are not implemented"); } + if (fmaskRange) { + Require(use.fmaskAddress == target.fmaskAddress && use.fmaskPipeBankXor == target.fmaskPipeBankXor, "native FMASK target layout changed"); + const auto& range = *fmaskRange; + requireDccRange(GuestMemory::Watched(range.address, range.bytes), range, "native FMASK sampling requires watched guest memory"); + GuestMemory::CollectWritesUncached(range.address, range.bytes); + requireDccRange(GuestMemory::UnchangedSince(range.address, range.bytes, range.generation) || !GuestMemory::StoredOver(range.address, range.bytes, range.generation), range, "writes to a resident native FMASK are not implemented"); + if (auto* recorder = Recorder::Active(); recorder != nullptr) requireDccRange(!recorder->PendingWriteOverlaps(range.address, range.bytes), range, "pending writes to a resident native FMASK are not implemented"); + } } void release() noexcept { + fmaskTextures.clear(); textures.clear(); retiredTextures.clear(); - if (resolvePass) context.Function("vkDestroyRenderPass")(context.device, resolvePass, nullptr); + for (const auto& [format, resolvePass] : resolvePasses) context.Function("vkDestroyRenderPass")(context.device, resolvePass, nullptr); 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); destroy({image, memory, view}); for (const auto& allocation : retired) destroy(allocation); @@ -591,6 +731,7 @@ void guard(const Context& context, const ColorTarget& target, const std::shared_ if (target.dccAddress != 0) { guarded().push_back({context.device, target.dccAddress, MultisampledDccBytes(target), target.samples, active, metadataValid}); if (target.cmaskAddress != 0) guarded().push_back({context.device, target.cmaskAddress, target.cmaskBytes, target.samples, active, metadataValid}); + if (target.samples == 8u && target.fmaskAddress != 0u && target.cmaskAddress != 0u) guarded().push_back({context.device, target.fmaskAddress, DescribeSurface(fmaskResource(target)).guestBytes, target.samples, active, metadataValid}); } anyGuarded().store(true, std::memory_order_release); InstallResidentImageReadGuard_nid_no_patch(); @@ -610,11 +751,13 @@ void discardRetiredTargets() { std::erase_if(targets(), [](const auto& target) { return !target->active->load(std::memory_order_acquire); }); } -MultisampleTarget& acquire(const Context& context, const ColorTarget& use) { +MultisampleTarget& acquire(const Context& context, const ColorTarget& requested) { discardRetiredTargets(); for (const auto& target : targets()) { if (!target->active->load(std::memory_order_acquire)) continue; - if (target->context.device != context.device || !sameTarget(target->target, use)) continue; + if (target->context.device != context.device || !target->Serves(requested)) continue; + auto use = requested; + use.format = target->target.format; if (recreated(target->target, use)) { { std::unique_lock lock(guardedMutex()); @@ -636,6 +779,7 @@ MultisampleTarget& acquire(const Context& context, const ColorTarget& use) { target->ApplyFastClear(use); return *target; } + const auto& use = requested; auto target = std::make_shared(context, use); target->Keep(); targets().push_back(std::move(target)); @@ -956,9 +1100,9 @@ SampleOrderTarget MultisampleTargetOrderTarget(const Context& context, const Col VkImageView MultisampleTargetView(const Context& context, const ColorTarget& target, VkImage* image) { std::lock_guard lock(targetsMutex()); - const auto& acquired = acquire(context, target); + auto& acquired = acquire(context, target); if (image != nullptr) *image = acquired.image; - return acquired.view; + return acquired.View(target.format); } VkImageView MultisampleTargetView(const Context& context, const ColorTarget& target, std::shared_ptr& owner, VkImage* image) { @@ -966,7 +1110,7 @@ VkImageView MultisampleTargetView(const Context& context, const ColorTarget& tar auto& acquired = acquire(context, target); owner = acquired.shared_from_this(); if (image != nullptr) *image = acquired.image; - return acquired.view; + return acquired.View(target.format); } void KeepMultisampleTarget_nid_no_patch(const std::shared_ptr& owner) { @@ -1052,12 +1196,12 @@ void ResolveMultisampleTarget(const Context& context, const ColorTarget& source, std::lock_guard lock(targetsMutex()); auto& target = acquire(context, source); target.Keep(); - const std::array views{target.view, destinationView}; - auto framebuffer = std::make_shared(context, target.ResolvePass(), views, source.extent); + const std::array views{target.View(source.format), destinationView}; + auto framebuffer = std::make_shared(context, target.ResolvePass(source.format), views, source.extent); recordCommands(context, [&](VkCommandBuffer commands, Recorder* recorder) { RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT); VkRenderPassBeginInfo begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO}; - begin.renderPass = target.ResolvePass(); + begin.renderPass = target.ResolvePass(source.format); begin.framebuffer = framebuffer->Handle(); begin.renderArea = {{0, 0}, source.extent}; context.Resolved(&DeviceFunctions::cmdBeginRenderPass, "vkCmdBeginRenderPass")(commands, &begin, VK_SUBPASS_CONTENTS_INLINE); @@ -1107,6 +1251,20 @@ void NoteColorMetadataFill(std::uint64_t address, std::size_t bytes, std::uint32 } } +std::shared_ptr MultisampleTargetFmaskTexture_nid_no_patch(const Context& context, const GuestTextureResource& resource, VkComponentMapping components) { + std::lock_guard lock(targetsMutex()); + discardRetiredTargets(); + std::shared_ptr owner; + for (const auto& candidate : targets()) { + if (!candidate->active->load(std::memory_order_acquire) || candidate->context.device != context.device) continue; + const auto& target = candidate->target; + if (target.fmaskAddress != resource.baseAddress && (resource.fmaskCmaskAddress == 0u || target.cmaskAddress != resource.fmaskCmaskAddress)) continue; + Require(owner == nullptr, "FMASK descriptor refers to more than one resident target"); + owner = candidate; + } + return owner != nullptr ? owner->FmaskSampled(resource, components) : nullptr; +} + void ClearMultisampleTargets(VkDevice device) { std::lock_guard lock(targetsMutex()); std::erase_if(targets(), [&](const auto& target) { return target->context.device == device; }); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index faae8726e5..2eb908e307 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -112,6 +112,10 @@ void ValidateProvokingVertex(const Context& context, const State& state, std::sp } return; } + for (const auto& shader : shaders) { + if (shader.stage != ShaderRecompiler::ShaderStage::Fragment || shader.program == nullptr) continue; + Require(std::none_of(shader.program->fragmentParameters.begin(), shader.program->fragmentParameters.end(), [](const ShaderRecompiler::FragmentParameter& parameter) { return parameter.flatP20; }), "v_interp_mov p20 of a flat input with the last provoking vertex is not measured"); + } if (BarycentricPatches(state, shaders)) { for (const auto& shader : shaders) { if (shader.stage != ShaderRecompiler::ShaderStage::Fragment || shader.program == nullptr) continue; @@ -259,6 +263,8 @@ bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State& appendKey(keys.fragmentOutput, state.perSampleShading); appendKey(keys.fragmentShader, state.mergedCoverage); appendKey(keys.fragmentOutput, state.mergedCoverage); + appendKey(keys.fragmentShader, state.truncatedCoverage); + appendKey(keys.fragmentOutput, state.truncatedCoverage); const auto sampleLocationsEnabled = ValidateSampleLocations(context, state); appendKey(keys.fragmentShader, sampleLocationsEnabled); appendKey(keys.fragmentOutput, sampleLocationsEnabled); @@ -341,11 +347,12 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput Require(!state.negativeOneToOne || context.depthClipControl, "negative-one-to-one depth clipping requires VK_EXT_depth_clip_control with depthClipControl enabled"); Require(!state.perSampleShading || context.sampleRateShading, "per-sample pixel shading requires the sampleRateShading device feature"); Require(!state.mergedCoverage || (context.mergedCoverageSampleCounts & state.samples) != 0, "an overrasterized draw (a multisampled rasterizer into one-sample color targets) requires VK_NV_framebuffer_mixed_samples with merged coverage reduction"); + Require(!state.truncatedCoverage || (context.truncatedCoverageSampleCounts & state.samples) != 0, "one-sample color targets beside a multisampled depth target require VK_AMD_mixed_attachment_samples or VK_NV_framebuffer_mixed_samples with truncated coverage reduction"); Require(state.conservativeRasterization == VK_CONSERVATIVE_RASTERIZATION_MODE_DISABLED_EXT || context.conservativeRasterization, "conservative rasterization requires VK_EXT_conservative_rasterization with at most 1/256 pixel of overestimation and degenerate triangles rasterized"); ValidateProvokingVertex(context, state, shaders); if (state.rectList) Require(context.tessellationShader && context.limits.maxTessellationPatchSize >= 4, "rect-list requires tessellation with four output control points"); const bool barycentricPatches = BarycentricPatches(state, shaders); - if (barycentricPatches) Require(context.tessellationShader && state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST && !state.primitiveRestart, "emulated barycentrics through tessellation require a triangle list without primitive restart"); + if (barycentricPatches) Require(context.tessellationShader && (state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP) && !state.primitiveRestart, "emulated barycentrics through tessellation require a triangle list or strip without primitive restart"); if (std::any_of(shaders.begin(), shaders.end(), [](const CompiledShader& shader) { return shader.stage == ShaderRecompiler::ShaderStage::Geometry; })) Require(context.geometryShader, "device does not support geometry shaders"); if (state.stages.tessellation) { Require(context.tessellationShader, "device does not support tessellation shaders"); @@ -403,7 +410,7 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput for (std::uint32_t index = 0; index < state.colors.size(); ++index) { VkAttachmentDescription color{}; color.format = state.colors[index].format; - color.samples = state.mergedCoverage ? VK_SAMPLE_COUNT_1_BIT : static_cast(state.samples); + color.samples = state.mergedCoverage || state.truncatedCoverage ? VK_SAMPLE_COUNT_1_BIT : static_cast(state.samples); color.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD; color.storeOp = VK_ATTACHMENT_STORE_OP_STORE; color.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; @@ -440,7 +447,8 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput VkAttachmentSampleCountInfoNV attachmentSamples{VK_STRUCTURE_TYPE_ATTACHMENT_SAMPLE_COUNT_INFO_NV}; attachmentSamples.colorAttachmentCount = static_cast(mergedColorSamples.size()); attachmentSamples.pColorAttachmentSamples = mergedColorSamples.empty() ? nullptr : mergedColorSamples.data(); - if (state.mergedCoverage) rendering.pNext = &attachmentSamples; + if (state.truncatedCoverage) attachmentSamples.depthStencilAttachmentSamples = static_cast(state.samples); + if (state.mergedCoverage || state.truncatedCoverage) rendering.pNext = &attachmentSamples; if (state.depth) { rendering.depthAttachmentFormat = HostDepthFormat(context, state.depth->format); if (HasStencil(state.depth->format)) rendering.stencilAttachmentFormat = rendering.depthAttachmentFormat; @@ -514,8 +522,8 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput sampleLocations.sampleLocationsInfo = {VK_STRUCTURE_TYPE_SAMPLE_LOCATIONS_INFO_EXT, nullptr, samples.rasterizationSamples, grid, state.samples * grid.width * grid.height, locations.data()}; if (sampleLocationsEnabled) samples.pNext = &sampleLocations; VkPipelineCoverageReductionStateCreateInfoNV coverageReduction{VK_STRUCTURE_TYPE_PIPELINE_COVERAGE_REDUCTION_STATE_CREATE_INFO_NV}; - coverageReduction.coverageReductionMode = VK_COVERAGE_REDUCTION_MODE_MERGE_NV; - if (state.mergedCoverage) { + coverageReduction.coverageReductionMode = state.truncatedCoverage ? VK_COVERAGE_REDUCTION_MODE_TRUNCATE_NV : VK_COVERAGE_REDUCTION_MODE_MERGE_NV; + if (state.mergedCoverage || (state.truncatedCoverage && !context.mixedAttachmentSamples)) { coverageReduction.pNext = samples.pNext; samples.pNext = &coverageReduction; } @@ -893,6 +901,7 @@ void pipelineKey(std::vector& key, const Context& context, const Stat append(key, state.samples); append(key, state.perSampleShading); append(key, state.mergedCoverage); + append(key, state.truncatedCoverage); append(key, state.customSampleLocations); if (state.customSampleLocations) { append(key, state.sampleLocationGrid); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp index d0669ed238..82d330d68d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp @@ -35,7 +35,7 @@ static_assert(sizeof(VkStencilOpState) == 28, "VkStencilOpState changed: update static_assert(sizeof(VkVertexInputBindingDescription) == 12, "VkVertexInputBindingDescription changed: update the recipe codec"); static_assert(sizeof(VkVertexInputAttributeDescription) == 16, "VkVertexInputAttributeDescription changed: update the recipe codec"); #if defined(__linux__) && defined(__x86_64__) && defined(__GLIBCXX__) -static_assert(sizeof(State) == 784, "State changed: update CanonicalPipelineState and the recipe codec"); +static_assert(sizeof(State) == 808, "State changed: update CanonicalPipelineState and the recipe codec"); static_assert(sizeof(ShaderStages) == 112, "ShaderStages changed: update the recipe codec"); static_assert(sizeof(ShaderRecompiler::MeshConfiguration) == 40, "MeshConfiguration changed: update the recipe codec"); static_assert(sizeof(ShaderRecompiler::TessellationConfiguration) == 40, "TessellationConfiguration changed: update the recipe codec"); @@ -44,7 +44,7 @@ static_assert(sizeof(ShaderRecompiler::TessellationConfiguration) == 40, "Tessel constexpr std::uint32_t FileMagic = 0x4c4a5041u; // Bumped when the recipe codec, CanonicalPipelineState or the store key change in a way the // layout guards above do not catch (a field renumbered or reinterpreted at the same size). -constexpr std::uint32_t FileFormat = 6u; +constexpr std::uint32_t FileFormat = 7u; constexpr std::uint32_t ModuleRecord = 1u; constexpr std::uint32_t GraphicsRecord = 2u; constexpr std::uint32_t ComputeRecord = 3u; @@ -217,6 +217,7 @@ void encodeState(Writer& writer, const State& state) { writer.Bool(state.sampleLocationGrid); writer.Bool(state.perSampleShading); writer.Bool(state.mergedCoverage); + writer.Bool(state.truncatedCoverage); for (const auto mask : state.quadSampleMask) writer.Raw(mask); writer.U32(state.depthRemap.mode); } @@ -278,6 +279,7 @@ void decodeState(Reader& reader, State& state) { state.sampleLocationGrid = reader.Bool(); state.perSampleShading = reader.Bool(); state.mergedCoverage = reader.Bool(); + state.truncatedCoverage = reader.Bool(); for (auto& mask : state.quadSampleMask) mask = reader.Raw(); state.depthRemap.mode = reader.U32(); } @@ -1051,6 +1053,7 @@ State CanonicalPipelineState(const State& state) { canonical.sampleLocationGrid = state.sampleLocationGrid; canonical.perSampleShading = state.perSampleShading; canonical.mergedCoverage = state.mergedCoverage; + canonical.truncatedCoverage = state.truncatedCoverage; canonical.quadSampleMask = state.quadSampleMask; canonical.depthRemap.mode = state.depthRemap.mode; return canonical; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp index 7670cde694..77119254d5 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp @@ -255,10 +255,24 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip } if (partsOnly) return VK_NULL_HANDLE; PerformanceTimer timing("Vulkan.GraphicsPipelineLink"); + std::optional, VkSampleCountFlagBits>> sampleCounts; + for (auto* next = static_cast(rendering.pNext); next != nullptr; next = next->pNext) { + if (next->sType != VK_STRUCTURE_TYPE_ATTACHMENT_SAMPLE_COUNT_INFO_NV) continue; + const auto& counts = *reinterpret_cast(next); + sampleCounts.emplace(std::vector(counts.pColorAttachmentSamples, counts.pColorAttachmentSamples + counts.colorAttachmentCount), counts.depthStencilAttachmentSamples); + } + const auto attachmentSamples = [](const std::pair, VkSampleCountFlagBits>& counts) { + return VkAttachmentSampleCountInfoNV{VK_STRUCTURE_TYPE_ATTACHMENT_SAMPLE_COUNT_INFO_NV, nullptr, static_cast(counts.first.size()), counts.first.empty() ? nullptr : counts.first.data(), counts.second}; + }; + auto linkedSamples = sampleCounts ? attachmentSamples(*sampleCounts) : VkAttachmentSampleCountInfoNV{}; VkPipelineLibraryCreateInfoKHR linked{VK_STRUCTURE_TYPE_PIPELINE_LIBRARY_CREATE_INFO_KHR}; linked.libraryCount = static_cast(libraries.size()); linked.pLibraries = libraries.data(); VkGraphicsPipelineCreateInfo create{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO, &linked}; + if (sampleCounts) { + linkedSamples.pNext = create.pNext; + create.pNext = &linkedSamples; + } create.layout = info.layout; VkPipeline pipeline = VK_NULL_HANDLE; lock.unlock(); @@ -277,12 +291,17 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip const auto cache = context.pipelineCache; const auto cacheSynchronization = context.pipelineCacheSynchronization; const auto libraryLayout = fullLayout; - std::function relink = [slot, createPipelines, cache, cacheSynchronization, libraryLayout, libraries, &store, devicePointer = &device] { + std::function relink = [slot, createPipelines, cache, cacheSynchronization, libraryLayout, libraries, sampleCounts, attachmentSamples, &store, devicePointer = &device] { if (slot->released.load()) return; + auto linkedSamples = sampleCounts ? attachmentSamples(*sampleCounts) : VkAttachmentSampleCountInfoNV{}; VkPipelineLibraryCreateInfoKHR linked{VK_STRUCTURE_TYPE_PIPELINE_LIBRARY_CREATE_INFO_KHR}; linked.libraryCount = static_cast(libraries.size()); linked.pLibraries = libraries.data(); VkGraphicsPipelineCreateInfo create{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO, &linked}; + if (sampleCounts) { + linkedSamples.pNext = create.pNext; + create.pNext = &linkedSamples; + } create.flags = VK_PIPELINE_CREATE_LINK_TIME_OPTIMIZATION_BIT_EXT; create.layout = libraryLayout; VkPipeline result = VK_NULL_HANDLE; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index d6d84cd4aa..d8a6109735 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -4,6 +4,7 @@ #include "ThreadOwned.hpp" #include "prx/libc/include/HostThreadLocal.hpp" #include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" @@ -682,6 +683,14 @@ std::shared_ptr cachedTexture(const Context& context, std::span(resource.baseAddress), resource.width, resource.height, static_cast(resource.dccAddress)); RequireNoMultisampleTarget(resource.baseAddress, guestBytes != 0 ? static_cast(guestBytes) : 1u, "a texture read"); if (auto* recorder = Recorder::Active(); recorder != nullptr) recorder->BoundKeptBytes(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp index f9912af9f9..eca8c4d9e9 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp @@ -659,7 +659,7 @@ std::set ValidateShaders(std::span shaders, const bool geometry = state.stages.path == ShaderPath::Vertex && !state.rectList && shaders.size() == 3u; const bool patches = BarycentricPatches(state, shaders) && shaders.back().program != nullptr && shaders.back().program->barycentricEmulation.active; Require(geometry || shaders.size() == (tessellation || state.rectList || patches ? 4u : 2u), "incorrect graphics stage count"); - if (patches) Require(state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, "emulated barycentrics through tessellation require a triangle list"); + if (patches) Require(state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP, "emulated barycentrics through tessellation require a triangle list or strip"); if (geometry) { Require(shaders.back().program != nullptr && shaders.back().program->barycentricEmulation.active, "a geometry stage is generated only for emulated barycentrics"); Require(state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN, "emulated barycentrics require triangle primitives"); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 2f6864ccc1..f6faccd0e9 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -355,13 +355,14 @@ void decodeDepth(const Registers& cx, std::uint32_t depthControl, bool testValEx const bool opValReference = opValExport && StencilOperationValueReplaces(cx); zero(cx, 0x000, 0x0000101cu, "depth copy, resummarize or decompress draws (DB_RENDER_CONTROL)"); const bool depthClear = (read(cx, 0x000) & 1u) != 0; - const bool stencilClear = (read(cx, 0x000) & 2u) != 0; const auto view = read(cx, 0x002); - zero(cx, 0x002, 0x3c000000u, "depth mips (DB_DEPTH_VIEW MIP_LEVEL)"); - zero(cx, 0x010, 0x000f1000u, "partially resident or mipmapped depth (DB_Z_INFO)"); + zero(cx, 0x010, 0x00001000u, "partially resident depth (DB_Z_INFO)"); + const auto viewMip = (view >> 26u) & 0xfu; + const auto maxMip = (read(cx, 0x010) >> 16u) & 0xfu; zero(cx, 0x011, 0x00001000u, "partially resident stencil (DB_STENCIL_INFO)"); const auto zFormat = read(cx, 0x010) & 3u; const bool stencil = (read(cx, 0x011) & 1u) != 0; + const bool stencilClear = stencil && (read(cx, 0x000) & 2u) != 0; Require(zFormat != 2, "Z_24 depth is unsupported"); if (zFormat == 0) depthControl &= ~6u; if (!stencil) depthControl &= ~1u; @@ -371,7 +372,7 @@ void decodeDepth(const Registers& cx, std::uint32_t depthControl, bool testValEx }; const bool depthReadOnly = (view & 0x01000000u) != 0; const bool stencilReadOnly = (view & 0x02000000u) != 0; - Require(!stencilClear || (stencil && !stencilReadOnly), "stencil clear requires a writable stencil plane"); + Require(!stencilClear || !stencilReadOnly, "stencil clear requires a writable stencil plane"); Require(!stencilClear || !testValExport, "stencil clear with a pixel shader that exports the stencil test value is unsupported"); Require(!depthClear || (zFormat != 0 && !depthReadOnly), "depth clear requires a writable depth plane"); DepthTarget depth{}; @@ -396,7 +397,27 @@ void decodeDepth(const Registers& cx, std::uint32_t depthControl, bool testValEx } const auto size = read(cx, 0x007); depth.extent = {(size & 0x3fffu) + 1u, ((size >> 16u) & 0x3fffu) + 1u}; - if (const auto slice = view & 0x1fffu; slice != 0) { + if (maxMip == 0 && viewMip != 0) Require(false, "depth view mip " + std::to_string(viewMip) + " on a surface without mips"); + if (maxMip != 0) { + char message[320]; + std::snprintf(message, sizeof(message), "depth view mip %u of %u at %ux%u (DB_Z_INFO 0x%08x, DB_DEPTH_VIEW 0x%08x)", viewMip, maxMip + 1u, depth.extent.width, depth.extent.height, read(cx, 0x010), view); + const std::string where(message); + Require(viewMip <= maxMip, "the " + where + " is beyond the surface's mips"); + Require(depth.samples == 1u, "multisampled " + where + " is unsupported"); + Require(depth.depthSwizzle == XorSwizzleMode(TextureTileMode::kZ64KBX), "the " + where + " uses a swizzle other than 64 KiB Z_X"); + const auto depthMips = ComputeElementMipLayout(TextureTileMode::kZ64KBX, zFormat == 1 ? 2u : 4u, depth.extent.width, depth.extent.height, maxMip + 1u); + const auto stencilMips = ComputeElementMipLayout(TextureTileMode::kZ64KBX, 1u, depth.extent.width, depth.extent.height, maxMip + 1u); + Require(zFormat == 0 || !depthMips.at(viewMip).tail, "rendering the " + where + " into the packed mip tail is unsupported"); + Require(!stencil || !stencilMips.at(viewMip).tail, "rendering the stencil of the " + where + " into the packed mip tail is unsupported"); + Require((read(cx, 0x010) & (1u << 29u)) == 0, "HTILE for the " + where + " is unsupported"); + if (const auto slice = view & 0x1fffu; slice != 0) { + if (depth.address != 0) depth.address += static_cast(slice) * ComputeSurfaceSize(depthMips, 1u); + if (depth.stencilAddress != 0) depth.stencilAddress += static_cast(slice) * ComputeSurfaceSize(stencilMips, 1u); + } + if (depth.address != 0) depth.address += depthMips.at(viewMip).tiledOffset; + if (depth.stencilAddress != 0) depth.stencilAddress += stencilMips.at(viewMip).tiledOffset; + depth.extent = {depthMips.at(viewMip).width, depthMips.at(viewMip).height}; + } else if (const auto slice = view & 0x1fffu; slice != 0) { if (depth.address != 0) depth.address += static_cast(slice) * DepthSliceBytes(depth.extent, zFormat == 1 ? 2u : 4u); if (depth.stencilAddress != 0) depth.stencilAddress += static_cast(slice) * DepthSliceBytes(depth.extent, 1u); } @@ -908,8 +929,8 @@ DecodedColorFormat DecodeColorFormat(std::uint32_t format, std::uint32_t number, if (number == unorm) return {VK_FORMAT_R32_UINT, 4, 0xe4u, ShaderRecompiler::ColorExportPacking::Unorm10_11_11}; return {VK_FORMAT_B10G11R11_UFLOAT_PACK32, 4}; case 7: - if (swap != 2 || number != floating) return fail(); - return {VK_FORMAT_R32_UINT, 4, 0xc6u, ShaderRecompiler::ColorExportPacking::Float11_11_10}; + if ((swap != 0 && swap != 2) || number != floating) return fail(); + return {VK_FORMAT_R32_UINT, 4, static_cast(swap == 2 ? 0xc6u : 0xe4u), ShaderRecompiler::ColorExportPacking::Float11_11_10}; case 9: // COLOR_2_10_10_10 keeps red in the low bits, the Vulkan A2B10G10R10 packing. if (number != unorm) return fail(); @@ -1313,8 +1334,7 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { result.sampleLocationGrid = false; } if (result.samples > 1 && (read(cx, 0x292) & 1u) == 0) { - const bool filled = result.topology != VK_PRIMITIVE_TOPOLOGY_POINT_LIST && result.topology != VK_PRIMITIVE_TOPOLOGY_LINE_LIST && result.topology != VK_PRIMITIVE_TOPOLOGY_LINE_STRIP; - Require(filled && !result.alphaToCoverage, "multisampled targets rasterized without MSAA_ENABLE are unsupported for points, lines or alpha-to-coverage"); + Require(RasterPrimitiveOf(result) != ShaderRecompiler::RasterPrimitive::Point && !result.alphaToCoverage, "multisampled targets rasterized without MSAA_ENABLE are unsupported for points or alpha-to-coverage"); if (const auto eqaa = find(cx, 0x201); result.depth && eqaa != cx.end() && (eqaa->second & 0x0f0c0000u) != 0) throw std::runtime_error(zeroMessage(0x201, eqaa->second, "a multisampled depth target rasterized without MSAA_ENABLE with Z interpolation or overrasterization (DB_EQAA)")); if (const auto control = find(cx, 0x203); result.depth && control != cx.end() && (control->second & 0x100u) != 0) throw std::runtime_error(zeroMessage(0x203, control->second, "a multisampled depth target rasterized without MSAA_ENABLE with a sample-mask export")); result.customSampleLocations = true; @@ -1415,8 +1435,10 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { } const bool merged = color.samples == 1u && !result.depth && !result.perSampleShading && (result.colors.empty() || result.mergedCoverage) && overrasterized(cx, result.samples); if (!merged && color.samples == 1 && result.colors.empty() && !result.depth && rasterizesSampleZeroOnly(cx, result)) rasterizeSampleZero(result); - Require((color.samples == result.samples && !result.mergedCoverage) || merged, "a color target's sample count differs from the rasterizer's"); + const bool truncated = color.samples == 1u && result.samples > 1u && result.depth && result.depth->samples == result.samples && (result.colors.empty() || result.truncatedCoverage) && !overrasterized(cx, result.samples); + Require((color.samples == result.samples && !result.mergedCoverage && !result.truncatedCoverage) || merged || truncated, "a color target's sample count differs from the rasterizer's"); result.mergedCoverage = merged; + result.truncatedCoverage = truncated; APS5_LOG_OUT_DEBUG("Color %u address=0x%llx extent=%ux%u bytes=%llu VkFormat=%u", slot, static_cast(color.address), color.extent.width, color.extent.height, static_cast(color.bytes), static_cast(color.format)); if (result.colors.empty()) { result.renderExtent = color.extent; @@ -1856,6 +1878,14 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot, bool allo if (color.samples == 1) GuestMemory::CheckRange(reinterpret_cast(cmask), totalCmaskBytes, CmaskLayout::Alignment, true); } color.cmaskFastClear = (info & 0x2000u) != 0; + if (const auto fmask = find(cx, 0x321 + stride); color.samples > 1u && fmask != cx.end()) { + const auto high = find(cx, 0x3a0 + slot); + const auto fmaskHigh = high == cx.end() ? 0u : high->second; + Require((fmaskHigh & ~0xffu) == 0u, "invalid FMASK address extension"); + const auto address = (static_cast(fmaskHigh) << 40u) | (static_cast(fmask->second) << 8u); + color.fmaskPipeBankXor = static_cast(address & 0xffffu); + color.fmaskAddress = address - color.fmaskPipeBankXor; + } } if ((info & 0x10000000u) != 0) { if (maxMip == 0) { @@ -2028,10 +2058,11 @@ std::string DrawRejection(const QueueState& queue, bool indexed) { if (value(cx, 0x200, word)) { const auto renderControl = find(cx, 0x000); const bool depthClear = renderControl != cx.end() && (renderControl->second & 1u) != 0; - const bool stencilClear = renderControl != cx.end() && (renderControl->second & 2u) != 0; + std::uint32_t stencilInfo = 0; + const bool stencilClear = renderControl != cx.end() && (renderControl->second & 2u) != 0 && !(value(cx, 0x011, stencilInfo) && (stencilInfo & 1u) == 0); if (stencilClear && !depthPlanesAbsent(cx)) { - std::uint32_t stencil = 0, view = 0; - if ((value(cx, 0x011, stencil) && (stencil & 1u) == 0) || (value(cx, 0x002, view) && (view & 0x02000000u) != 0)) return require(false, "stencil clear requires a writable stencil plane"); + std::uint32_t view = 0; + if (value(cx, 0x002, view) && (view & 0x02000000u) != 0) return require(false, "stencil clear requires a writable stencil plane"); } if (depthClear && !depthPlanesAbsent(cx)) { std::uint32_t z = 0, view = 0; diff --git a/core/libs/prx/libSceAgcDriver/State/src/Status.cpp b/core/libs/prx/libSceAgcDriver/State/src/Status.cpp index ae1a8543f7..825a9e9001 100644 --- a/core/libs/prx/libSceAgcDriver/State/src/Status.cpp +++ b/core/libs/prx/libSceAgcDriver/State/src/Status.cpp @@ -5,6 +5,12 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +namespace { + +constexpr int CAPTURE_UNAVAILABLE = static_cast(0x8A6C1000); + +} + extern "C" { bool APS5_VABI sceAgcDriverIsCaptureInProgress(void) { @@ -21,18 +27,15 @@ bool APS5_VABI sceAgcDriverIsSubmitValidationEnabled(void) { int APS5_VABI sceAgcDriverRequestCaptureStart(const char* path) { (void)path; - NotImplemented_nid_no_patch(__func__); - return 0; + return CAPTURE_UNAVAILABLE; } int APS5_VABI sceAgcDriverRequestCaptureStop() { - NotImplemented_nid_no_patch(__func__); - return 0; + return CAPTURE_UNAVAILABLE; } int APS5_VABI sceAgcDriverTriggerCapture() { - NotImplemented_nid_no_patch(__func__); - return 0; + return CAPTURE_UNAVAILABLE; } } diff --git a/core/libs/prx/libSceAgcDriver/Unimplemented.cpp b/core/libs/prx/libSceAgcDriver/Unimplemented.cpp index be639faf70..dd330964bc 100644 --- a/core/libs/prx/libSceAgcDriver/Unimplemented.cpp +++ b/core/libs/prx/libSceAgcDriver/Unimplemented.cpp @@ -57,6 +57,17 @@ int APS5_VABI sceAgcDriverGetHsOffchipParam(uint16_t* first, uint16_t* second) { return 0; } +int APS5_VABI sceAgcDriverNopDataWritePacket(void* destination, uint32_t address, const void* data, uint32_t bytes) { + const auto alignedBytes = (static_cast(bytes) + 3) & ~std::uint64_t{3}; + const auto totalBytes = alignedBytes + 8; + std::memset(destination, 0, static_cast(totalBytes)); + const auto header = (static_cast(totalBytes) << 14) + 0x3ffe0000U | 0xc0001000U; + std::memcpy(destination, &header, sizeof(header)); + std::memcpy(static_cast(destination) + 4, &address, sizeof(address)); + if (data != nullptr && bytes != 0) std::memcpy(static_cast(destination) + 8, data, bytes); + return data == nullptr || bytes == 0 ? 2 : static_cast(totalBytes / 4); +} + int APS5_VABI sceAgcDriverSetValidationDisabled() { return DEBUG_UNAVAILABLE; @@ -154,9 +165,8 @@ int APS5_VABI sceAgcDriverAcquireHDRScopesACQ() { return 0; } -int APS5_VABI sceAgcDriverGetCoredumpAddress() { - NotImplemented_nid_no_patch(__func__); - return 0; +void* APS5_VABI sceAgcDriverGetCoredumpAddress() { + return nullptr; } int APS5_VABI sceAgcDriverHp3dLockEnd() { @@ -174,19 +184,13 @@ int APS5_VABI sceAgcDriverIsEsvmProcess() { return 0; } -int APS5_VABI sceAgcDriverNopDataWritePacket() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceAgcDriverReleaseHDRScopesACQ() { NotImplemented_nid_no_patch(__func__); return 0; } int APS5_VABI sceAgcDriverSetTargetRingForDiag() { - NotImplemented_nid_no_patch(__func__); - return 0; + return DRIVER_FEATURE_UNAVAILABLE; } int APS5_VABI sceAgcDriverSetValueToSharedGds() { diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index e4ea60da83..860ba51020 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -2813,7 +2813,7 @@ bool RunSparseBufferHoleTests(const Context& context, const BdaTestAccess& acces std::memset(reservation.data + 2 * unit, 0x72, unit); VkBuffer seeded = VK_NULL_HANDLE; { - Buffer seed(context, copied, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + Buffer seed(context, copied, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT); std::fill(seed.Bytes().begin(), seed.Bytes().end(), std::byte{0xa5}); seeded = seed.Handle(); } diff --git a/core/libs/prx/libSceAgcDriver/tests/GcnDecode.cpp b/core/libs/prx/libSceAgcDriver/tests/GcnDecode.cpp new file mode 100644 index 0000000000..855c0aecdb --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/GcnDecode.cpp @@ -0,0 +1,45 @@ +#include "RdnaDecoder/RdnaInstructionDecoder.hpp" +#include +#include +#include +#include +#include + +using namespace ShaderRecompiler; + +namespace { + +int failures = 0; + +void Check(bool condition, const char* what) { + if (condition) return; + std::fprintf(stderr, "%s\n", what); + ++failures; +} + +RdnaInstruction Decode(std::span words) { + return DecodeGcnInstruction(0u, words, 0u); +} + +void SetregImmKeepsItsLiteral() { + const std::array setreg{0xba80f801u, 0x12345678u, 0xbf810000u}; + const auto instruction = Decode(setreg); + Check(instruction.op == RdnaOpcode::SSetregImm32B32, "s_setreg_imm32_b32 did not decode as its RDNA opcode"); + Check(instruction.wordCount == 2u, "a GCN s_setreg_imm32_b32 is not two words"); + Check(instruction.source0.value == 0x12345678u, "s_setreg_imm32_b32 lost its literal"); + const std::array movk{0xb0001234u, 0xbf810000u}; + Check(Decode(movk).wordCount == 1u, "s_movk_i32 took a literal word"); +} + +} + +int main() { + try { + SetregImmKeepsItsLiteral(); + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } + if (failures == 0) std::puts("GCN decode tests passed"); + return failures == 0 ? 0 : 1; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 428c1ce75e..4dae15015e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -19,6 +19,7 @@ #include "prx/libSceAgcDriver/Graphics/include/ParallelCompare.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" #include "SceShaders.hpp" #include "prx/libSceAgc/Shader/include/ShaderConstants.hpp" #include "ControlFlow/RequestSerializer.hpp" @@ -765,10 +766,12 @@ void msaaEnableClearOneSampleTargetTests() { expectFailure([&] { DecodeState(queue); }, refused); queue = drawC(); queue.context[0x2dc] = 0x0000aa01u; - expectFailure([&] { DecodeState(queue); }, "without MSAA_ENABLE are unsupported for points, lines"); - queue = drawC(); - queue.userConfig[0x242] = 2; - expectFailure([&] { DecodeState(queue); }, "without MSAA_ENABLE are unsupported for points, lines"); + expectFailure([&] { DecodeState(queue); }, "without MSAA_ENABLE are unsupported for points or alpha-to-coverage"); + for (const auto primitive : {2u, 3u}) { + queue = drawC(); + queue.userConfig[0x242] = primitive; + requireOneSample(queue, "a line list or strip into PPSA18216's draw C target"); + } const auto twoTargets = [&](std::uint32_t first, std::uint32_t second) { auto pair = drawC(); for (const auto offset : {0x318u, 0x31bu, 0x31cu}) pair.context[offset + 0xfu] = pair.context.at(offset); @@ -853,9 +856,11 @@ void msaaEnableClearOneSampleTargetTests() { queue = depthTarget(color); queue.context[0x2dc] = 0x0000aa01u; expectFailure([&] { DecodeState(queue); }, depthRefused); - queue = depthTarget(color); - queue.userConfig[0x242] = 2; - expectFailure([&] { DecodeState(queue); }, depthRefused); + for (const auto primitive : {2u, 3u}) { + queue = depthTarget(color); + queue.userConfig[0x242] = primitive; + expectFailure([&] { DecodeState(queue); }, depthRefused); + } for (const auto eqaa : {0x00050002u, 0x00090002u, 0x01010002u, 0x08010002u}) { queue = depthTarget(color); queue.context[0x201] = eqaa; @@ -1729,7 +1734,39 @@ void DisabledColorTests() { queue.context[0x002] = 3; const auto thirdSlice = AgcDriver::Graphics::DecodeState(queue); Require(thirdSlice.depth && thirdSlice.depth->firstSlice == 3 && thirdSlice.depth->address == depthOnly.depth->address + 3u * AgcDriver::Graphics::DepthSliceBytes(depthOnly.depth->extent, 4u), "a depth view of slice 3 lost its slice or its Z offset"); + queue.context[0x002] = 1u << 26u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "on a surface without mips"); queue.context[0x002] = 0; + { + using AgcDriver::Graphics::ComputeElementMipLayout; + using AgcDriver::Graphics::TextureTileMode; + auto mipped = queue; + mipped.context[0x007] = 0x03ff03ff; + mipped.context[0x010] = 0x80000181u | (2u << 16u); + const auto mips = ComputeElementMipLayout(TextureTileMode::kZ64KBX, 2u, 1024u, 1024u, 3u); + Require(!mips.at(1).tail && mips.at(1).width == 512u && mips.at(1).height == 512u, "the 1024x1024 Z_X layout does not give a 512x512 second mip outside the tail"); + const auto first = AgcDriver::Graphics::DecodeState(mipped); + Require(first.depth && first.depth->address == depthOnly.depth->address + mips.at(0).tiledOffset && first.depth->extent.width == 1024u && first.depth->extent.height == 1024u, "view mip 0 of a mip chain is not its first level"); + mipped.context[0x002] = 1u << 26u; + const auto second = AgcDriver::Graphics::DecodeState(mipped); + Require(second.depth && second.depth->address == depthOnly.depth->address + mips.at(1).tiledOffset, "view mip 1 is not at the second level's offset in the Z_X mip chain"); + Require(second.depth->extent.width == 512u && second.depth->extent.height == 512u, "view mip 1 does not render at the second level's extent"); + mipped.context[0x002] = (1u << 26u) | 1u | (1u << 13u); + const auto sliced = AgcDriver::Graphics::DecodeState(mipped); + Require(sliced.depth && sliced.depth->address == second.depth->address + AgcDriver::Graphics::ComputeSurfaceSize(mips, 1u), "slice 1 of a mip chain does not step by the whole chain"); + mipped.context[0x002] = 3u << 26u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(mipped); }, "is beyond the surface's mips"); + mipped.context[0x002] = 2u << 26u; + mipped.context[0x007] = 0x003f003f; + expectFailure([&] { AgcDriver::Graphics::DecodeState(mipped); }, "into the packed mip tail is unsupported"); + mipped.context[0x007] = 0x03ff03ff; + mipped.context[0x002] = 1u << 26u; + mipped.context[0x010] |= 1u << 29u; + mipped.context[0x005] = 0x300; + expectFailure([&] { AgcDriver::Graphics::DecodeState(mipped); }, "HTILE for the depth view mip"); + mipped.context[0x010] = (0x80000181u | (2u << 16u)) & ~(31u << 4u); + expectFailure([&] { AgcDriver::Graphics::DecodeState(mipped); }, "uses a swizzle other than 64 KiB Z_X"); + } for (const auto colorControl : {0x0u, 0xcc0000u}) { queue = makeState(); queue.context[0x202] = colorControl; @@ -2363,10 +2400,12 @@ void DepthStencilTests() { queue.context[0x000] = 0x22u | control; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "DB_RENDER_CONTROL"); } - queue.context[0x000] = 0x22; + queue.context[0x000] = 0x23; queue.context[0x011] = 0; - expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "writable stencil plane"); - Require(AgcDriver::Graphics::DrawRejection(queue, false).find("writable stencil plane") != std::string::npos, "precheck accepted clear without a stencil plane"); + state = AgcDriver::Graphics::DecodeState(queue); + Require(state.depthWrite && state.depthCompare == VK_COMPARE_OP_ALWAYS && !state.stencilTest && state.colors.size() == 2, "a depth and stencil clear without a stencil plane does not clear depth alone"); + Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "precheck rejected a stencil clear without a stencil plane"); + queue.context[0x000] = 0x22; queue.context[0x010] = 0; state = AgcDriver::Graphics::DecodeState(queue); Require(!state.depth && !state.depthTest && !state.depthWrite && !state.stencilTest && state.colors.size() == 2, "clear flags without depth/stencil planes changed color-only rendering"); @@ -3316,6 +3355,41 @@ void msaaEnableClearDepthTests() { state = DecodeState(queue); Require(state.colors.empty(), "a depth-only 4x draw without MSAA_ENABLE decoded a color target"); requireCentered(state, "a 4x depth target rasterized without MSAA_ENABLE"); + for (const auto primitive : {2u, 3u}) { + queue = drawB(); + queue.userConfig[0x242] = primitive; + state = DecodeState(queue); + Require(state.topology == (primitive == 2u ? VK_PRIMITIVE_TOPOLOGY_LINE_LIST : VK_PRIMITIVE_TOPOLOGY_LINE_STRIP) && state.colors.size() == 1 && state.colors[0].samples == 4, "PPSA18216's line list or strip into its 4x depth and color pair lost its topology or color target"); + requireCentered(state, "a line list or strip into PPSA18216's 4x depth and color pair without MSAA_ENABLE"); + Require(!state.sampleLocationGrid && !state.perSampleShading, "a line list or strip without MSAA_ENABLE kept a sample location grid or sample shading"); + Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck refused a line list or strip into PPSA18216's 4x depth and color pair"); + } + queue = drawB(); + queue.userConfig[0x242] = 1; + queue.context[0x2d5] = 0x2030; + queue.userConfig[0x25b] = 0x6633; + queue.context[0x1ff] = 255; + queue.context[0x2ce] = 5; + queue.context[0x29b] = 2; + queue.context[0x2ab] = 4; + queue.shader[0x8a] = 3u << 29u; + queue.shader[0x8b] = 3u << 16u; + state = DecodeState(queue); + Require(state.topology == VK_PRIMITIVE_TOPOLOGY_POINT_LIST && state.stages.mesh && state.stages.mesh->outputPrimitiveSize == 3u && AgcDriver::Graphics::RasterPrimitiveOf(state) == ShaderRecompiler::RasterPrimitive::Triangle && state.colors.size() == 1 && state.colors[0].samples == 4, "a point list into a geometry program that outputs triangle strips into PPSA18216's 4x depth and color pair did not decode as triangles"); + requireCentered(state, "triangles a geometry program makes from a point list into PPSA18216's 4x depth and color pair without MSAA_ENABLE"); + for (const auto primitive : {4u, 2u, 1u}) { + queue = drawB(); + queue.userConfig[0x242] = primitive; + queue.context[0x2d5] = 0x2030; + queue.userConfig[0x25b] = 0x6633; + queue.context[0x1ff] = 255; + queue.context[0x2ce] = 5; + queue.context[0x29b] = 0; + queue.context[0x2ab] = 4; + queue.shader[0x8a] = 3u << 29u; + queue.shader[0x8b] = 3u << 16u; + expectFailure([&] { DecodeState(queue); }, "multisampled targets rasterized without MSAA_ENABLE are unsupported for points or alpha-to-coverage"); + } for (const auto eqaa : {0x00050002u, 0x00090002u, 0x01010002u, 0x08010002u}) { queue = drawB(); queue.context[0x201] = eqaa; @@ -3327,7 +3401,7 @@ void msaaEnableClearDepthTests() { expectFailure([&] { DecodeState(queue); }, "a multisampled depth target rasterized without MSAA_ENABLE with a sample-mask export"); queue = drawB(); queue.context[0x2dc] = 0x0000aa01u; - expectFailure([&] { DecodeState(queue); }, "multisampled targets rasterized without MSAA_ENABLE are unsupported for points, lines or alpha-to-coverage"); + expectFailure([&] { DecodeState(queue); }, "multisampled targets rasterized without MSAA_ENABLE are unsupported for points or alpha-to-coverage"); queue = drawB(); queue.context[0x010] = 0xa2900987; expectFailure([&] { DecodeState(queue); }, "the depth target's sample count differs from the rasterizer's"); @@ -3634,7 +3708,7 @@ void multisampleTests() { const auto aliased = AgcDriver::Graphics::SampleLocations(state); for (std::uint32_t sample = 0; sample < state.samples; ++sample) Require(aliased[sample].x == 0.5f && aliased[sample].y == 0.5f, "a 2x target rasterized without MSAA_ENABLE did not place every sample at the pixel center"); queue.context[0x2dc] = 0x0000aa01u; - expectFailure([&] { DecodeState(queue); }, "without MSAA_ENABLE are unsupported for points, lines or alpha-to-coverage"); + expectFailure([&] { DecodeState(queue); }, "without MSAA_ENABLE are unsupported for points or alpha-to-coverage"); queue = multisampled(); queue.context[0x292] = 2; queue.context[0x302] = 0x44cc; @@ -4077,6 +4151,29 @@ void multisampledDepthTests() { Require(!DecodeState(queue).perSampleShading, "PS_ITER_SAMPLE requested sample shading for a single-sample scan"); queue = singleSampleScan(); queue.context[0x31d] = 0; + state = DecodeState(queue); + Require(state.truncatedCoverage && !state.mergedCoverage && state.colors.size() == 1 && state.colors[0].samples == 1, "a single-sample scan into a 4x depth target and a one-sample color target did not take the color from sample 0"); + requireCentered(state, "a single-sample scan into a 4x depth target and a one-sample color target"); + const auto truncatedTwoSample = [&] { + auto queue = singleSampleScan(); + queue.context[0x2f8] = 0x00100001; + queue.context[0x292] = 3; + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = 0xcc44; + queue.context[0x010] = 0x22900987; + queue.context[0x31d] = 0; + return queue; + }; + queue = truncatedTwoSample(); + state = DecodeState(queue); + Require(state.samples == 2 && state.depth && state.depth->samples == 2 && state.colors.size() == 1 && state.colors[0].samples == 1 && state.truncatedCoverage && !state.mergedCoverage && !state.perSampleShading, "a one-sample color target beside a 2x depth target under a 2x rasterizer with MSAA_ENABLE (PPSA02225's registers) did not take the color from sample 0"); + Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck refused a one-sample color target beside a 2x depth target"); + queue = truncatedTwoSample(); + queue.context[0x293] = 0x10000u; + queue.context[0x1b3] = queue.context[0x1b4] = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PerspectiveSample) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::FrontFace); + state = DecodeState(queue); + Require(state.truncatedCoverage && state.perSampleShading && state.samples == 2 && state.colors[0].samples == 1, "PS_ITER_SAMPLE into a one-sample color target beside a 2x depth target (PPSA02225's registers) did not run per sample with sample 0's color"); + queue = truncatedTwoSample(); + queue.context[0x31d] = 0x12000; expectFailure([&] { DecodeState(queue); }, "a color target's sample count differs from the rasterizer's"); queue = singleSampleScan(); queue.context[0x010] = 0x22900983; @@ -7894,9 +7991,11 @@ void barycentricEmulationTests() { Require(BarycentricPatches(state, patchShaders) && !BarycentricPatches(state, shaders), "generated tessellation stages were not recognised as barycentric patches"); state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; ValidateShaders(patchShaders, state, subgroup, false); - for (const auto topology : {VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN, VK_PRIMITIVE_TOPOLOGY_POINT_LIST}) { + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP; + ValidateShaders(patchShaders, state, subgroup, false); + for (const auto topology : {VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN, VK_PRIMITIVE_TOPOLOGY_POINT_LIST}) { state.topology = topology; - expectFailure([&] { ValidateShaders(patchShaders, state, subgroup, false); }, "require a triangle list"); + expectFailure([&] { ValidateShaders(patchShaders, state, subgroup, false); }, "require a triangle list or strip"); } state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; const std::array plainPatchShaders{{{ShaderStage::Vertex, &vertex, 0}, {ShaderStage::TessellationControl, &patches.control, 0}, {ShaderStage::TessellationEvaluation, &patches.evaluation, 0}, {ShaderStage::Fragment, &plain, 0}}}; @@ -9136,6 +9235,14 @@ void ProvokingVertexTests() { const std::array flatP10Shaders{{{ShaderRecompiler::ShaderStage::Fragment, &flatP10Pixel, 0}}}; ValidateProvokingVertex(supported, last, flatP10Shaders); expectFailure([&] { ValidateProvokingVertex(supported, first, flatP10Shaders); }, "v_interp_mov p10 of a flat input with the first provoking vertex is not measured"); + const std::array flatP20Color{0xc8120001u, 0xc8160101u, 0xc81a0201u, 0xc81e0301u, 0xf800180fu, 0x07060504u, 0xbf810000u}; + const auto flatP20Pixel = recompilePixel({0x400u}, flatP20Color); + const auto p20Inputs = locatedInputs(flatP20Pixel.spirv.Words()); + Require(p20Inputs.size() == 1 && p20Inputs[0].location == 0 && p20Inputs[0].flat && !p20Inputs[0].perVertex, "flat v_interp_mov P20 did not compile to a flat input"); + Require(flatP20Pixel.fragmentParameters.size() == 1 && flatP20Pixel.fragmentParameters[0].flatP20 && !flatP20Pixel.fragmentParameters[0].flatP10 && !flatP20Pixel.fragmentParameters[0].perVertex && !flatP10Pixel.fragmentParameters[0].flatP20, "flat v_interp_mov P20 was not recorded apart from P10"); + const std::array flatP20Shaders{{{ShaderRecompiler::ShaderStage::Fragment, &flatP20Pixel, 0}}}; + ValidateProvokingVertex(supported, first, flatP20Shaders); + expectFailure([&] { ValidateProvokingVertex(supported, last, flatP20Shaders); }, "v_interp_mov p20 of a flat input with the last provoking vertex is not measured"); auto lastStrip = last; lastStrip.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP; auto lastList = last; @@ -9500,7 +9607,12 @@ void float11_11_10TargetTests() { auto changed = queue; changed.context[0x31c] &= ~0x40000u; expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "rounding (ROUND_MODE 0) 11_11_10 float color targets"); - for (const auto swap : {0u, 1u, 3u}) { + changed = queue; + changed.context[0x31c] &= ~0x1800u; + const auto standard = AgcDriver::Graphics::DecodeState(changed); + Require(standard.color.format == VK_FORMAT_R32_UINT && standard.color.componentMapping == 0xe4u && standard.color.packing == ColorExportPacking::Float11_11_10, "an 11_11_10 float target with the standard component order did not decode"); + Require(AgcDriver::Graphics::ExportMappings(standard)[0] == 0xe4u, "export 0 reordered its components into the standard-order 11_11_10 float target"); + for (const auto swap : {1u, 3u}) { changed = queue; changed.context[0x31c] = (changed.context[0x31c] & ~0x1800u) | (swap << 11u); expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "unsupported color format 7 number type 7 component swap " + std::to_string(swap)); @@ -10686,6 +10798,39 @@ void pipelineJournalTests(bool longPath = false, bool extended = false) { const PipelineSetLayouts decodedLayouts{layouts.layouts.main, layouts.layouts.fragment, decoded.splitSets, decoded.mainLayoutKey, decoded.fragmentLayoutKey}; BuildPipelineStoreKey(replayed, context, decoded.state, decoded.vertexInput, decodedLayouts, shaders, decoded.attachmentLayout); Require(!original.empty() && original == canonical && original == replayed, "the canonical or the decoded state keys another pipeline"); + auto fmaskState = state; + fmaskState.samples = 8; + fmaskState.color.samples = 8; + fmaskState.color.fmaskAddress = 0x30000; + fmaskState.color.fmaskPipeBankXor = 0x1200; + for (auto& target : fmaskState.colors) { + target.samples = 8; + target.fmaskAddress = 0x40000; + target.fmaskPipeBankXor = 0x2300; + } + const auto fmaskRecipe = MakeGraphicsPipelineRecipe(fmaskState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL); + Require(fmaskRecipe.state.color.fmaskAddress == 0 && fmaskRecipe.state.color.fmaskPipeBankXor == 0, "the canonical state kept the primary target's FMASK location"); + for (const auto& canonicalColor : fmaskRecipe.state.colors) { + Require(canonicalColor.fmaskAddress == 0 && canonicalColor.fmaskPipeBankXor == 0, "the canonical state kept an attachment's FMASK location"); + } + const auto fmaskBytes = EncodeGraphicsPipelineRecipe(fmaskRecipe); + GraphicsPipelineRecipe fmaskDecoded; + Require(DecodeGraphicsPipelineRecipe(fmaskBytes, fmaskDecoded) && EncodeGraphicsPipelineRecipe(fmaskDecoded) == fmaskBytes, "the FMASK pipeline recipe did not round-trip byte for byte"); + std::vector fmaskKey, fmaskReplayedKey; + BuildPipelineStoreKey(fmaskKey, context, fmaskState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL); + BuildPipelineStoreKey(fmaskReplayedKey, context, fmaskDecoded.state, fmaskDecoded.vertexInput, layouts.View(), shaders, fmaskDecoded.attachmentLayout); + Require(fmaskKey != original && fmaskKey == fmaskReplayedKey, "the decoded FMASK state keys another pipeline"); + auto relocatedFmask = fmaskState; + relocatedFmask.color.fmaskAddress += 0x10000; + relocatedFmask.color.fmaskPipeBankXor ^= 0x100; + for (auto& target : relocatedFmask.colors) { + target.fmaskAddress += 0x20000; + target.fmaskPipeBankXor ^= 0x200; + } + std::vector relocatedFmaskKey; + BuildPipelineStoreKey(relocatedFmaskKey, context, relocatedFmask, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL); + Require(relocatedFmaskKey == fmaskKey, "the FMASK resource location changed the pipeline key"); + Require(EncodeGraphicsPipelineRecipe(MakeGraphicsPipelineRecipe(relocatedFmask, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL)) == fmaskBytes, "the FMASK resource location changed the journal recipe"); auto gridState = state; gridState.samples = 2; gridState.customSampleLocations = true; diff --git a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp index db38d183f5..0cc0c41ed7 100644 --- a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp @@ -95,6 +95,52 @@ void rejectFields(const Fields& f, std::string_view reason) { } +static void RunR128TextureTests() { + constexpr std::array streamed{0xa0c18800u, 0xcad00000u, 0x01ffc1ffu, 0x905b2facu, 0u, 0u, 0u, 0u}; + const auto view = DecodeTextureResource(streamed); + Require(view.baseAddress == 0xa0c1880000ull && view.format == 173u && view.tileMode == TextureTileMode::kStandard4KB && view.width == 512u && view.height == 512u && view.baseLevel == 0u && view.lastLevel == 9u && view.mipCount == 10u && view.allocatedMipCount == 10u && view.minLod == 0u, + "a 128-bit BC3 T# (2048x2048, levels 2..11, words 4-7 zero) must read as the 512x512 chain of its 10 resident levels, got " + std::to_string(view.width) + "x" + std::to_string(view.height) + " levels " + std::to_string(view.baseLevel) + ".." + std::to_string(view.lastLevel) + " of " + std::to_string(view.mipCount)); + auto chainWords = streamed; + chainWords[3] &= ~(0xfu << 12u); + chainWords[5] = 11u << 4u; + const auto full = DecodeTextureResource(chainWords); + Require(full.width == 2048u && full.mipCount == 12u, "the same surface with MAX_MIP 11 must be the 12-level 2048x2048 chain"); + const auto chain = DescribeSurface(full); + const auto resident = DescribeSurface(view); + Require(chain.mips.size() == 12u && resident.mips.size() == 10u, "the chain and the resident levels must have 12 and 10 levels"); + for (std::uint32_t level = 0; level < 10u; ++level) { + const auto& a = chain.mips[level + 2u]; + const auto& b = resident.mips[level]; + Require(a.tiledOffset == b.tiledOffset && a.tiledSize == b.tiledSize && a.width == b.width && a.height == b.height && a.blocksPerRow == b.blocksPerRow && a.tail == b.tail && a.tailX == b.tailX && a.tailY == b.tailY, "resident level " + std::to_string(level) + " must be level " + std::to_string(level + 2u) + " of the 12-level chain"); + } + Require(resident.guestBytes <= chain.mips[1].tiledOffset && chain.mips[0].tiledOffset > chain.mips[1].tiledOffset, "the resident levels must end before the chain's levels 1 and 0, which come last in a GFX10 swizzled chain"); + auto levelZero = streamed; + levelZero[3] &= ~(0xfu << 12u); + const auto single = DecodeTextureResource(levelZero); + Require(single.mipCount == 1u && single.lastLevel == 0u, "a T# with words 4-7 zero that starts at level 0 must stay one level"); + auto tailed = streamed; + tailed[5] = 4u << 20u; + reject([&] { DecodeTextureResource(tailed); }, "would move the surface's own"); + + constexpr std::array tall{0xa0c1de00u, 0xcad00000u, 0x01ffc0ffu, 0x905b4facu, 0u, 0u, 0u, 0u}; + const auto tallView = DecodeTextureResource(tall); + Require(tallView.width == 64u && tallView.height == 128u && tallView.lastLevel == 7u && tallView.mipCount == 8u, "a 128-bit 1024x2048 T# of levels 4..11 must read as its 64x128 chain of 8 levels"); + + Fields clamped; + clamped.base40 = 0x55ea000ull; + clamped.format = 20; + clamped.width = 1024; + clamped.height = 1024; + clamped.tileModeRaw = 0x1b; + clamped.baseLevel = 2; + clamped.lastLevel = 10; + clamped.minLod = 2u * 256u + 64u; + const auto clampedView = DecodeTextureResource(pack(clamped)); + Require(clampedView.width == 256u && clampedView.mipCount == 9u && clampedView.minLod == 64u, "reading a 128-bit T# from its resident levels must move its MIN_LOD clamp with the base level"); + clamped.minLod = 256u; + Require(DecodeTextureResource(pack(clamped)).minLod == 0u, "a MIN_LOD clamp below the base level must vanish when read from the resident levels"); +} + void RunGuestTextureResourceTests() { Fields base; const auto baseWords = pack(base); @@ -123,6 +169,15 @@ void RunGuestTextureResourceTests() { fmask.dstSelY = fmask.dstSelZ = fmask.dstSelW = 0u; const auto fmaskResource = DecodeTextureResource(pack(fmask)); Require(fmaskResource.format == 20u && fmaskResource.dimension == TextureDimension::k2D && fmaskResource.tileMode == TextureTileMode::kZ64KBX, "uncompressed 32-bit FMASK did not use an integer Z surface"); + auto compressedFmask = fmask; + compressedFmask.metaCompress = compressedFmask.metaPipeAligned = true; + compressedFmask.metaAddr = 0x123456789aull; + const auto compressedResource = DecodeTextureResource(pack(compressedFmask)); + Require(compressedResource.fmaskCmaskAddress == compressedFmask.metaAddr << 8u && compressedResource.dccAddress == 0u && compressedResource.dccPipeAligned, "compressed FMASK CMASK was interpreted as color DCC"); + compressedFmask.metaPipeAligned = false; + rejectFields(compressedFmask, "FMASK CMASK metadata must be pipe aligned"); + compressedFmask.metaPipeAligned = compressedFmask.writeCompress = true; + rejectFields(compressedFmask, "FMASK CMASK compression is not implemented"); Fields fmaskArray = fmask; fmaskArray.typeRaw = 13u; fmaskArray.depth = 2u; @@ -620,6 +675,7 @@ void RunGuestTextureResourceTests() { pastUntailed.maxMip = 0; pastUntailed.baseLevel = 1; pastUntailed.lastLevel = 1; + pastUntailed.perfMod = 4; rejectFields(pastUntailed, "would move the surface's own"); auto untailedChain = tailView; untailedChain.mipCount = 1; @@ -627,6 +683,8 @@ void RunGuestTextureResourceTests() { untailedChain.mipCount = 2; Require(untailedAllocated == 0xff0000u && DescribeSurface(untailedChain).guestBytes == 0x1470000u, "a second level must grow a 1920x1080 64 bpp SW_64KB_R_X surface from addrlib's 0xff0000 to 0x1470000 bytes"); + RunR128TextureTests(); + std::array shortWords{}; reject([&] { DecodeTextureResource(shortWords); }, "8 dwords"); diff --git a/core/libs/prx/libSceAgcDriver/tests/NopDataWritePacket.cpp b/core/libs/prx/libSceAgcDriver/tests/NopDataWritePacket.cpp new file mode 100644 index 0000000000..587bc082bc --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/NopDataWritePacket.cpp @@ -0,0 +1,38 @@ +#include "SceTypes.hpp" +#include +#include +#include +#include +#include + +extern "C" int APS5_VABI sceAgcDriverNopDataWritePacket(void* destination, uint32_t address, const void* data, uint32_t bytes); + +int main() { + std::array storage; + storage.fill(std::byte{0xaa}); + const std::array data{std::byte{0x11}, std::byte{0x22}, std::byte{0x33}, std::byte{0x44}, std::byte{0x55}}; + const auto words = sceAgcDriverNopDataWritePacket(storage.data() + 4, 0x12345678, data.data(), static_cast(data.size())); + std::uint32_t header = 0; + std::uint32_t address = 0; + std::memcpy(&header, storage.data() + 4, sizeof(header)); + std::memcpy(&address, storage.data() + 8, sizeof(address)); + if (words != 4 || header != 0xc0021000U || address != 0x12345678U) return 1; + if (!std::equal(data.begin(), data.end(), storage.begin() + 12)) return 2; + if (storage[17] != std::byte{0} || storage[18] != std::byte{0} || storage[19] != std::byte{0}) return 3; + if (storage[3] != std::byte{0xaa} || storage[20] != std::byte{0xaa}) return 4; + + storage.fill(std::byte{0xaa}); + const auto emptyWords = sceAgcDriverNopDataWritePacket(storage.data() + 4, 0x87654321, nullptr, 5); + std::memcpy(&header, storage.data() + 4, sizeof(header)); + std::memcpy(&address, storage.data() + 8, sizeof(address)); + if (emptyWords != 2 || header != 0xc0021000U || address != 0x87654321U) return 5; + if (storage[12] != std::byte{0} || storage[19] != std::byte{0} || storage[20] != std::byte{0xaa}) return 6; + + storage.fill(std::byte{0xaa}); + const auto zeroBytesWords = sceAgcDriverNopDataWritePacket(storage.data() + 4, 0x01020304, data.data(), 0); + std::memcpy(&header, storage.data() + 4, sizeof(header)); + std::memcpy(&address, storage.data() + 8, sizeof(address)); + if (zeroBytesWords != 2 || header != 0xc0001000U || address != 0x01020304U) return 7; + if (storage[12] != std::byte{0xaa}) return 8; + return 0; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 97f02e7f2f..8a0b4d0ddc 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -4804,11 +4804,14 @@ void idlePublishWaitTests(const Device& device, Recorder& recorder) { recorder.Sync(); Require(words[2 * unit] == 0x44 && words[4 * unit - 1] == 0x45, "a CPU store made after the idle flush returned was overwritten by its publish"); Require(words[2 * unit + 1] == 0x22 && words[3 * unit - 1] == 0x22 && words[3 * unit] == 0x33 && words[1 * unit] == 0x11 && words[4 * unit] == 0x11, "the idle publish's bytes are wrong"); + words[5 * unit] = 0x11; + CollectWritesUncached(address, bytes); retile(address + 5 * unit, 0x55); StorageTexture::FlushAllPending("label"); - Require(recorder.PendingWriteOverlaps(address + 5 * unit, unit), "a publish of a flush other than the idle one was waited"); + const bool pending = recorder.PendingWriteOverlaps(address + 5 * unit, unit); recorder.Sync(); - Require(words[5 * unit] == 0x55 && words[6 * unit - 1] == 0x55, "the label flush's publish did not land"); + Require(words[5 * unit] == 0x55 && words[6 * unit - 1] == 0x55, "the label flush's publish did not land: the unit read as written after its retile, so the publish dropped it as stale"); + Require(pending, "a publish of a flush other than the idle one was waited"); std::cout << "idle publish wait verified\n"; } diff --git a/core/libs/prx/libSceAgcDriver/tests/SavedExecMaskLoop.hpp b/core/libs/prx/libSceAgcDriver/tests/SavedExecMaskLoop.hpp new file mode 100644 index 0000000000..d6aa73bc18 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/SavedExecMaskLoop.hpp @@ -0,0 +1,19 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_SAVEDEXECMASKLOOP_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_SAVEDEXECMASKLOOP_HPP + +#include +#include + +namespace AgcTest { + +inline std::vector SavedExecMaskLoop(std::uint32_t rounds) { + std::vector code{0xbea2047eu, 0xbea40380u}; + for (std::uint32_t step = 0; step < rounds; ++step) code.insert(code.end(), {0xbefe0422u, 0x7c080080u, 0x8aa26a7eu}); + const auto branch = 0xbf850000u | static_cast(2 - static_cast(code.size() + 3)); + code.insert(code.end(), {0x80248124u, 0xbf0a8424u, branch, 0xbefe0422u}); + return code; +} + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/tests/Screenshot.cpp b/core/libs/prx/libSceAgcDriver/tests/Screenshot.cpp index e6202ceb1a..4adf0c824d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Screenshot.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Screenshot.cpp @@ -21,9 +21,9 @@ void require(bool condition, const char* message) { void setScreenshotDirectory(const std::filesystem::path& directory) { #ifdef _WIN32 - require(_putenv_s("ANYPS5_SCREENSHOTS", directory.string().c_str()) == 0, "set ANYPS5_SCREENSHOTS"); + require(_wputenv_s(L"ANYPS5_SCREENSHOTS", directory.wstring().c_str()) == 0, "set ANYPS5_SCREENSHOTS"); #else - require(::setenv("ANYPS5_SCREENSHOTS", directory.string().c_str(), 1) == 0, "set ANYPS5_SCREENSHOTS"); + require(::setenv("ANYPS5_SCREENSHOTS", directory.c_str(), 1) == 0, "set ANYPS5_SCREENSHOTS"); #endif } @@ -73,7 +73,7 @@ void rejectsInvalidFrames() { int main() try { const auto root = std::filesystem::temp_directory_path() / ("anyps5_screenshot-" + std::to_string(std::random_device{}())); - const auto directory = root / "nested"; + const auto directory = root / std::filesystem::path(u8"nested-a\u00e7\u00e3o-\u65e5\u672c"); std::filesystem::remove_all(root); setScreenshotDirectory(directory); requestsMerge(); diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 16e54ee281..a35b9ab285 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -20,6 +20,7 @@ #include "IntermediateRepresentation/IrBuilder.hpp" #include "IntermediateRepresentation/IrProgram.hpp" #include "SpirvBackend/SpirvAnalysis.hpp" +#include "SavedExecMaskLoop.hpp" #if ANYPS5_ENABLE_SPIRV_TOOLS #include "SpirvBackend/SpirvOptimizer.hpp" #endif @@ -4088,6 +4089,18 @@ void verifyHalfWaveReduction() { {0u, 0x38000000u, 0x88000000u, spv::OpGroupNonUniformBitwiseOr, "half-wave reduction: the wave64 OR scan did not read lane 31 as a subgroup OR on 32 lanes"}, }}; for (const auto& reduction : scans) require(reductions(pixel(scan(reduction.identity, reduction.vector, reduction.scalar), 32u, true), reduction.reduce) == 1u, reduction.message); + auto loopScan = scan(0xffffffffu, 0x26000000u, 0x83800000u); + const auto savedMaskLoop = AgcTest::SavedExecMaskLoop(8u); + loopScan.insert(loopScan.begin(), savedMaskLoop.begin(), savedMaskLoop.end()); + auto helperLoop = loopScan; + helperLoop[0] = 0xbea204c1u; + expectFailure([&] { static_cast(pixel(helperLoop, 32u, true)); }, "v_readlane_b32 of lane 63 is outside the 32-lane host subgroup", "half-wave reduction: a loop initialized with helper lanes was proven to exclude them"); + auto upperHelperLoop = loopScan; + upperHelperLoop[0] = 0xbea2037eu; + upperHelperLoop.insert(upperHelperLoop.begin() + 1, 0xbea303c1u); + expectFailure([&] { static_cast(pixel(upperHelperLoop, 32u, true)); }, "v_readlane_b32 of lane 63 is outside the 32-lane host subgroup", "half-wave reduction: the low-word loop proof was reused for helper-inclusive upper lanes"); + require(reductions(pixel(loopScan, 32u, true), spv::OpGroupNonUniformUMin) == 1u, "half-wave reduction: a loop-carried saved EXEC mask exhausted the entry-lane proof on 32 lanes"); + require(halves(pixel(loopScan, 64u, true), spv::OpGroupNonUniformUMin, 0xffffffffu), "half-wave reduction: a loop-carried saved EXEC mask did not preserve the two half-wave UMin reductions"); auto comparedScan = scan(0u, 0x38000000u, 0x88000000u); *std::find(comparedScan.begin(), comparedScan.end(), 0xbe98047eu) = 0x8a986a7eu; comparedScan.insert(comparedScan.begin(), 0x7c080080u); @@ -7587,6 +7600,24 @@ void verifyDynamicStorageMipChain() { require(ResourceMaterializer::TryRuntimeImageMode(image, allLevels, modes).mode.has_value(), "a dynamic-mip storage image with sixteen levels was rejected"); } +void verifyArrayVolumeStoreMode() { + using namespace ShaderRecompiler; + const DescriptorValue descriptor{{0x11800100u, 0xc3800000u, 0x0003c003u, 0xd0900facu, 0u, 0x00700000u, 0u, 0u}, 8u}; + ImageResource store{}; + store.resourceClass = ImageResourceClass::Storage; + store.dimension = RdnaImageDimension::Dim3D; + store.written = true; + store.fmaskCompatible = false; + const auto modes = ResourceMaterializer::RuntimeImageModes(store); + require(modes[ResourceMaterializer::RuntimeImageMode(store, descriptor, modes)].dimension == RdnaImageDimension::Dim2DArray, "3D image store on a 2D-array T# did not select an array runtime mode"); + store.read = true; + expectFailure([&] { static_cast(ResourceMaterializer::RuntimeImageMode(store, descriptor, ResourceMaterializer::RuntimeImageModes(store))); }, "incompatible with the static runtime image interface", "3D image load and store on a 2D-array T# was accepted"); + store.read = false; + store.atomic = true; + const DescriptorValue uintDescriptor{{0x11800100u, 0xc1400000u, 0x0003c003u, 0xd0900facu, 0u, 0x00700000u, 0u, 0u}, 8u}; + expectFailure([&] { static_cast(ResourceMaterializer::RuntimeImageMode(store, uintDescriptor, ResourceMaterializer::RuntimeImageModes(store))); }, "incompatible with the static runtime image interface", "3D image atomic on a 2D-array T# was accepted"); +} + void verifySrgbDepthCompareModes() { using namespace ShaderRecompiler; for (const auto decodeFormats : {0u, 1u, 2u, 3u}) { @@ -7761,6 +7792,7 @@ int main(int argc, char** argv) { verifyFmaskImageMode(); verifyCubeDepthCompareMode(); verifyVolumeArrayLoadMode(); + verifyArrayVolumeStoreMode(); verifyVolumeBiasTableRejection(); verifyDynamicStorageMipChain(); verifySrgbDepthCompareModes(); diff --git a/core/libs/prx/libSceAgcDriver/tests/TessellationLowering.cpp b/core/libs/prx/libSceAgcDriver/tests/TessellationLowering.cpp index 1b1ec2d0da..2ccb9129ac 100644 --- a/core/libs/prx/libSceAgcDriver/tests/TessellationLowering.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/TessellationLowering.cpp @@ -1,11 +1,16 @@ #include "IntermediateRepresentation/IrBuilder.hpp" #include "IntermediateRepresentation/IrProgram.hpp" +#include "Optimization/ShaderStageInputInfo.hpp" #include "Optimization/TessellationLowering.hpp" +#include "SpirvBackend/SpirvEmitter.hpp" +#include #include #include #include #include +#include #include +#include using namespace ShaderRecompiler; @@ -135,6 +140,52 @@ bool RefusedBecause(IrProgram& program, const char* reason) { return !outcome.lowered && outcome.reason.find(reason) != std::string::npos; } +std::uint32_t EmittedEvaluationControlPoint(std::uint32_t address) { + constexpr std::uint32_t ControlStride = 16u; + IrProgram program; + IrBuilder builder{program}; + program.SetWaveSize(64); + program.Resources().stage = IrShaderStage::TessellationEvaluation; + program.Resources().srtPlanComplete = true; + program.Resources().resourceTrackingComplete = true; + program.Metadata().shaderInfoComplete = true; + program.Metadata().bindingLayoutComplete = true; + program.Metadata().tessellationControlStride = ControlStride; + auto& block = program.CreateBlock(); + program.SetEntryBlock(block); + program.BlockOrder().push_back(&block); + BlockInfo info; + info.id = block.Id(); + program.Metadata().blockInfo.push_back(info); + builder.SetInsertionPoint(block); + auto& read = builder.Emit(IrOpcode::GetTessellationAttribute, IrType::U32, {&builder.Constant(static_cast(TessellationAttribute::EvaluationInput)), &builder.Constant(address), &builder.ConstantBool(true)}); + static_cast(builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&read})); + ShaderVertexInputInfo vertex; + vertex.tess.inputControlPoints = InputControlPoints; + vertex.tess.outputControlPoints = InputControlPoints; + vertex.tess.domain = 1u; + vertex.tess.partitioning = 2u; + vertex.tess.outputTopology = 2u; + ShaderStageInputInfo inputs; + inputs.vertex = &vertex; + CompiledBindingLayout bindings; + bindings.layout = program.Metadata().bindings; + SpirvTargetOptions target{}; + target.vulkanVersion = 0x00401000; + target.spirvVersion = 0x00010300; + target.subgroupSize = 64; + const auto words = SpirvEmitter().Emit(program, inputs, bindings, target); + std::unordered_map constants; + for (std::size_t at = 5; at < words.size(); at += words[at] >> 16u) { + const auto count = words[at] >> 16u; + if (count == 0u || at + count > words.size()) throw std::runtime_error("the domain program compiled to a malformed SPIR-V instruction"); + const auto opcode = words[at] & 0xffffu; + if (opcode == spv::OpConstant && count == 4u) constants[words[at + 2u]] = words[at + 3u]; + if (opcode == spv::OpAccessChain && count == 7u) return constants.at(words[at + 4u]); + } + throw std::runtime_error("the domain program reads no control point attribute"); +} + bool run(const char* name, const std::function& body) { bool passed = false; try { @@ -206,5 +257,9 @@ int main() { return patch->Opcode() == IrOpcode::IMul32 && patch->Argument(1)->Resolve()->ImmediateU32() == PatchStride; }); + passed &= run("a domain read at an immediate address past the last control point reads the last control point", [] { + return EmittedEvaluationControlPoint(InputControlPoints * 16u) == InputControlPoints - 1u; + }); + return passed ? 0 : 1; } diff --git a/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp b/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp index 31ff43033e..9feb514924 100644 --- a/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp @@ -143,6 +143,25 @@ void repackedFormatTests() { Require(TextureComponentChannel(guest, VK_COMPONENT_SWIZZLE_A) == VK_COMPONENT_SWIZZLE_A && TextureComponentChannel(guest, VK_COMPONENT_SWIZZLE_ZERO) == VK_COMPONENT_SWIZZLE_ZERO, "format 43 keeps alpha and constant selectors"); } +void gcnBufferFormatTests() { + using ShaderRecompiler::IrBufferFormat; + const auto& formats = ShaderRecompiler::GcnBufferFormats(); + const auto gcn = [&](std::uint32_t data, std::uint32_t number) { return static_cast(formats[(data << 3u) | number]); }; + std::array seen{}; + for (const auto format : formats) ++seen[format]; + for (std::uint32_t format = 1; format < seen.size(); ++format) Require(seen[format] == 1u, "unified buffer format " + std::to_string(format) + " must come from exactly one GCN format pair"); + Require(gcn(0u, 4u) == IrBufferFormat::Invalid && gcn(15u, 7u) == IrBufferFormat::Invalid, "GCN DATA_FORMAT 0 and 15 have no buffer form"); + Require(gcn(1u, 7u) == IrBufferFormat::Invalid && gcn(4u, 0u) == IrBufferFormat::Invalid, "8-bit float and 32-bit UNORM have no buffer form"); + Require(gcn(1u, 0u) == IrBufferFormat::Format8UNorm && gcn(2u, 7u) == IrBufferFormat::Format16Float, "8 and 16-bit pairs must map"); + Require(gcn(4u, 4u) == IrBufferFormat::Format32UInt && gcn(4u, 7u) == IrBufferFormat::Format32Float, "32-bit pairs must map"); + Require(gcn(6u, 7u) == IrBufferFormat::Format11_11_10Float && gcn(7u, 7u) == IrBufferFormat::Format10_11_11Float, "GCN 10_11_11 and 11_11_10 must map by bit layout"); + Require(gcn(8u, 0u) == IrBufferFormat::Format2_10_10_10UNorm && gcn(9u, 0u) == IrBufferFormat::Format10_10_10_2UNorm, "GCN 10_10_10_2 and 2_10_10_10 must map by bit layout"); + Require(gcn(10u, 0u) == IrBufferFormat::Format8_8_8_8UNorm && gcn(14u, 7u) == IrBufferFormat::Format32_32_32_32Float, "four-channel pairs must map"); + const std::uint32_t word3 = 0x00000facu | (7u << 12u) | (14u << 15u) | (7u << 27u); + Require(ShaderRecompiler::GcnBufferWord3(16u << 16u, word3) == (0x00000facu | (77u << 12u) | (1u << 28u)), "a strided GCN V# must check its index range"); + Require(ShaderRecompiler::GcnBufferWord3(0u, word3) == (0x00000facu | (77u << 12u) | (3u << 28u)), "a GCN V# without a stride must check its byte range"); +} + void convertedDccClearTests() { alignas(64) std::array keys{}; GuestTextureResource resource{}; @@ -268,4 +287,5 @@ void RunTextureFormatTests() { convertedDccClearTests(); singleDccKeysTests(); packedFloatDccClearTests(); + gcnBufferFormatTests(); } diff --git a/core/libs/prx/libSceAgcDriver/tests/UnsupportedDiagnostics.cpp b/core/libs/prx/libSceAgcDriver/tests/UnsupportedDiagnostics.cpp new file mode 100644 index 0000000000..db2f77f1ec --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/UnsupportedDiagnostics.cpp @@ -0,0 +1,10 @@ +#include "SceTypes.hpp" + +extern "C" void* APS5_VABI sceAgcDriverGetCoredumpAddress(); +extern "C" int APS5_VABI sceAgcDriverSetTargetRingForDiag(); + +int main() { + if (sceAgcDriverGetCoredumpAddress() != nullptr) return 1; + if (sceAgcDriverSetTargetRingForDiag() != static_cast(0x8A6D0001)) return 2; + return 0; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp index e7ad973bba..267c945d64 100644 --- a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp @@ -174,6 +174,21 @@ void CheckSharedBlock() { if (!Watched(outside, unwatched.size())) Require(StoredOver(outside, unwatched.size(), TrackerGeneration()), "an unwatched range reads as not stored over"); } +void CheckStoredOverUsesPageEvidence() { + constexpr std::size_t Page = 4096; + auto* memory = static_cast(AllocateWatched(2 * Block)); + const auto base = reinterpret_cast(memory); + std::memset(memory, 0x11, 2 * Block); + const auto before = CollectWritesUncached(base, 2 * Block); + Require(before != 0, "the stored-over page fixture was not collected"); + + static_cast(memory)[0] = 0x22; + const auto after = CollectWritesUncached(base, Block); + Require(after > before, "the page-local CPU write was not collected"); + Require(StoredOver(base, Page, before), "a collected CPU write on a page was not reported as stored over"); + Require(!StoredOver(base + Page, Page, before), "a page-local CPU write made an untouched page in the same block count as stored over"); +} + void CheckStoredAllSince() { constexpr std::size_t bytes = 4 * Block; auto* memory = static_cast(AllocateWatched(bytes)); @@ -217,6 +232,39 @@ void CheckStoredAllSince() { MarkWritten(base, 64); Require(StoredAllSince_nid_no_patch(base, 64, before), "a complete non-block-aligned range store is absent"); + before = TrackerGeneration(); + MarkPossiblyWritten(base + 2048, 16); + Require(StoredOver(base + 2048, 16, before), "a possible driver write did not invalidate its exact range"); + Require(!StoredOver(base + 2064, 16, before), "a possible driver write invalidated an adjacent range"); + Require(!UnchangedSince(base + 2048, 16, before), "a possible driver write left its exact range unchanged"); + Require(UnchangedSince(base + 2064, 16, before), "a possible driver write changed an adjacent range"); + Require(!StoredAllSince_nid_no_patch(base + 2048, 16, before), "a possible driver write proved complete byte coverage"); + for (unsigned repeat = 0; repeat < 6; ++repeat) MarkPossiblyWritten(base + 2048, 16); + Require(StoredOver(base + 2048, 16, before), "repeated possible writes lost their overlap evidence"); + Require(!StoredOver(base + 2064, 16, before), "repeated possible writes invalidated an adjacent range"); + Require(!StoredAllSince_nid_no_patch(base + 2048, 16, before), "repeated possible writes proved complete byte coverage"); + + before = TrackerGeneration(); + constexpr unsigned PossibleIntervals = 8; + auto beforeNewest = before; + for (unsigned interval = 0; interval < PossibleIntervals; ++interval) { + beforeNewest = TrackerGeneration(); + MarkPossiblyWritten(base + 4096 + static_cast(interval) * 32, 16); + } + Require(StoredOver(base + 4096, 16, before), "an evicted possible-write interval was forgotten instead of marking its block"); + Require(StoredOver(base + 4096 + 16, 16, before), "an evicted possible write did not mark the rest of its block"); + Require(StoredOver(base + 4096 + (PossibleIntervals - 1) * 32, 16, beforeNewest), "the newest possible-write interval was evicted"); + const auto beforeRecord = TrackerGeneration(); + MarkPossiblyWritten(base + 4096, 16); + Require(StoredOver(base + 4096, 16, beforeRecord), "a possible-write interval was not retained after being re-recorded"); + Require(!StoredAllSince_nid_no_patch(base + 4096, 16, before), "possible-write eviction or re-recording proved complete byte coverage"); + + before = TrackerGeneration(); + MarkWritten(base + 2560, 16); + MarkPossiblyWritten(base, Block); + Require(StoredAllSince_nid_no_patch(base + 2560, 16, before), "a possible whole-block range erased an exact partial-store proof"); + Require(!StoredAllSince_nid_no_patch(base, Block, before), "a possible whole-block range proved complete block coverage"); + before = TrackerGeneration(); MarkWritten(base + 512, 16); MarkWritten(base + 544, 16); @@ -348,6 +396,42 @@ void CheckSharedBlockProofs() { Require(!UnchangedSince(base + Block + 14 * page + 0x800, 16, beforeNeighbour), "a range in the page an own store dirtied reads as unchanged"); } +void CheckRepeatedPossibleStores() { + void* memory = AllocateWatched(3 * Block); + const auto base = reinterpret_cast(memory); + const auto boundary = base + 2 * Block - 0x9000; + std::memset(memory, 0x11, 3 * Block); + + const auto synced = CollectWrites(base, 3 * Block); + Require(synced != 0, "the test block is not collected"); + + const auto exact = TrackerGeneration(); + MarkWritten(base + Block, 0x100); + for (std::uint64_t store = 0; store < 5; ++store) MarkPossiblyWritten(boundary + store * 0x1000, 0x100); + Require(StoredAllSince_nid_no_patch(base + Block, 0x100, exact), "possible writes beyond the ring forgot an exact store's coverage"); + + void* fresh = AllocateWatched(2 * Block); + const auto freshBase = reinterpret_cast(fresh); + std::memset(fresh, 0x11, 2 * Block); + const auto freshSynced = CollectWrites(freshBase, 2 * Block); + const auto neighbour = freshBase + Block - 0x9000; + MarkPossiblyWritten(neighbour, 0x100); + const auto secondStored = MarkPossiblyWritten(neighbour + 0x1000, 0x100); + const auto thirdStored = MarkPossiblyWritten(neighbour + 0x2000, 0x100); + MarkPossiblyWritten(neighbour + 0x3000, 0x100); + const auto settled = TrackerGeneration(); + MarkPossiblyWritten(neighbour, 0x100); + MarkPossiblyWritten(neighbour, 0x100); + MarkPossiblyWritten(neighbour + 0x5000, 0x100); + Require(!StoredOver(freshBase, static_cast(neighbour - freshBase), secondStored), "a full possible ring evicted a newer piece instead of its oldest"); + Require(StoredOver(freshBase, static_cast(neighbour - freshBase), freshSynced), "an evicted possible write is not taken as a store over the rest of the block"); + + const auto partial = TrackerGeneration(); + MarkPossiblyWritten(neighbour + 0x5080, 0x100); + Require(StoredOver(neighbour + 0x5000, 0x10, settled) && StoredOver(neighbour + 0x5100, 0x10, partial), "a possible write that overlaps an earlier piece without containing it replaced it"); + Require(!StoredOver(freshBase, static_cast(neighbour - freshBase), thirdStored), "a possible write that overlaps without containing evicted a newer piece instead of the oldest"); +} + void CheckOwnStore() { void* memory = AllocateWatched(2 * Block); const auto base = reinterpret_cast(memory); @@ -1450,8 +1534,10 @@ int main() { } #endif CheckSharedBlock(); + CheckStoredOverUsesPageEvidence(); CheckStoredAllSince(); CheckSharedBlockProofs(); + CheckRepeatedPossibleStores(); CheckOwnStore(); CheckUnwatch(); CheckNewestStamp(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageArrayWindowPastLayerLimit.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageArrayWindowPastLayerLimit.cpp index 0d29ae08e4..d6a5e73941 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageArrayWindowPastLayerLimit.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageArrayWindowPastLayerLimit.cpp @@ -30,7 +30,10 @@ constexpr std::uint32_t ClampEdge = 2; alignas(256) std::array Input{}; alignas(256) std::array Output{}; -alignas(65536) std::array Texels{}; +constexpr std::size_t TexelBytes = 4u << 20u; +constexpr std::size_t TexelAlignment = 65536; +std::array TexelStorage{}; +const std::span Texels{reinterpret_cast((reinterpret_cast(TexelStorage.data()) + TexelAlignment - 1) & ~(TexelAlignment - 1)), TexelBytes}; alignas(256) constexpr std::array Code{ 0x34020084, 0xe0381000, 0x80000201, 0xbf8c3f70, 0xf09c0f20, 0x00820602, 0xf0001f20, 0x00020a04, @@ -62,7 +65,7 @@ std::array Texel(std::uint32_t x, std::uint32_t guestLayer) { } void FillTexels() { - Texels.fill(0); + std::ranges::fill(Texels, std::uint8_t{0}); const auto surface = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(TextureDescriptor())); const auto& mip = surface.mips.at(0); Require(surface.layers == LastArray + 1u && surface.guestBytes <= Texels.size(), "the test array does not fit its storage"); @@ -117,7 +120,7 @@ void Run(AgcDriver::VulkanDevice& device, std::uint32_t baseArray = BaseArray) { device.WaitIdle(); for (std::uint32_t tid = 0; tid < Threads; ++tid) { const float u = std::bit_cast(Input[tid * 4u + 0u]); - const float layer = std::clamp(std::nearbyint(std::bit_cast(Input[tid * 4u + 1u])), 0.0f, static_cast(Layers - 1u)); + const float layer = std::clamp(std::nearbyint(std::bit_cast(Input[tid * 4u + 1u])), 0.0f, static_cast(LastArray - baseArray)); const auto x = static_cast(std::clamp(static_cast(std::floor(u * static_cast(Width))), 0, static_cast(Width) - 1)); Expect(tid, "image_sample_lz on a window of a 1D array past maxImageArrayLayers", &Output[tid * 8u], Texel(x, baseArray + static_cast(layer))); Expect(tid, "image_load on a window of a 1D array past maxImageArrayLayers", &Output[tid * 8u + 4u], Texel(Input[tid * 4u + 2u], baseArray + Input[tid * 4u + 3u])); @@ -128,6 +131,7 @@ void Run(AgcDriver::VulkanDevice& device, std::uint32_t baseArray = BaseArray) { int main() { try { + Require(reinterpret_cast(Texels.data()) % TexelAlignment == 0, "the test array is not 64 KiB aligned"); const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; FillTexels(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreVolumeThroughArray.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreVolumeThroughArray.cpp new file mode 100644 index 0000000000..b82fa4ba07 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreVolumeThroughArray.cpp @@ -0,0 +1,173 @@ +#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 + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Size = 4; +constexpr std::uint32_t Depth = 2; +constexpr std::uint32_t Format32UInt = 20; +constexpr std::uint32_t Type2DArray = 13; +constexpr std::uint32_t IdentitySwizzle = 0xfacu; +constexpr std::size_t TexelBytes = 16384; +constexpr std::size_t BlockBytes = 65536; + +alignas(256) std::array Input{}; + +alignas(256) constexpr std::array StoreCode{ + 0x34020084, 0xe0381000, 0x80001401, 0xbf8c3f70, 0xf0201110, 0x00011714, 0x7e2c0282, 0xf0201110, + 0x00011714, 0xbf810000, +}; + +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 volume through array: cannot allocate the guest block"); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + std::free(block); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint8_t* Data() { return block; } + +private: + std::uint8_t* block = nullptr; +}; + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +std::uint32_t Value(std::uint32_t tid) { + return (tid + 1u) * 0x9e3779b1u; +} + +std::vector Initial() { + std::vector bytes(TexelBytes); + for (std::size_t index = 0; index < bytes.size(); ++index) bytes[index] = static_cast(0x80u + 37u * index + 11u * (index >> 4u)); + return bytes; +} + +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 ArrayDescriptor(const void* data) { + const auto address = static_cast(reinterpret_cast(data)); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format32UInt << 20u) | (((Size - 1u) & 3u) << 30u), + ((Size - 1u) >> 2u) | ((Size - 1u) << 14u), + IdentitySwizzle | (Type2DArray << 28u), + Depth - 1u, + 0u, + 0u, + 0u, + }; +} + +void Dispatch(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::size_t bytes) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const std::array words{tid % Size, (tid / Size) % Size, tid / (Size * Size), Value(tid)}; + std::copy(words.begin(), words.end(), Input.begin() + tid * 4u); + } + const auto initial = Initial(); + std::copy(initial.begin(), initial.end(), texels); + std::vector userData(16, 0u); + const auto buffer = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto texture = ArrayDescriptor(texels); + 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(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); + AgcDriver::Graphics::StorageTexture::FlushPending(reinterpret_cast(texels), bytes, nullptr, "test"); + device.WaitIdle(); +} + +void Check(const std::uint8_t* texels) { + const auto geometry = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(ArrayDescriptor(texels))); + const auto& mip = geometry.mips.at(0); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto x = tid % Size; + const auto y = (tid / Size) % Size; + const auto z = tid / (Size * Size); + std::uint32_t actual = 0; + std::memcpy(&actual, texels + geometry.GuestLayerOffset(z) + mip.tiledOffset + static_cast(y) * mip.pitchBytes + x * 4u, 4u); + Require(actual == Value(tid), "image_store 3d through a 2D-array T#: texel (" + std::to_string(x) + ", " + std::to_string(y) + ", " + std::to_string(z) + ") is " + Hex(actual) + ", expected " + Hex(Value(tid))); + } + const auto initial = Initial(); + Require(std::equal(texels + geometry.guestBytes, texels + TexelBytes, initial.begin() + static_cast(geometry.guestBytes)), "image_store 3d through a 2D-array T#: a slice past the array changed bytes after it"); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + GuestBlock block; + auto* texels = block.Data(); + const auto geometry = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(ArrayDescriptor(texels))); + Require(geometry.guestBytes <= TexelBytes, "image store volume through array: the array does not fit the texel storage"); + Dispatch(*device, texels, geometry.guestBytes); + Check(texels); + std::puts("image store volume through array tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleCenteredLines.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleCenteredLines.cpp new file mode 100644 index 0000000000..a5e2dfc7f6 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleCenteredLines.cpp @@ -0,0 +1,330 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.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 +#include +#include +#include + +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Side = 16; +constexpr std::uint32_t Samples = 4; +constexpr std::uint32_t SampleShift = 2; +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t WaveSize = 64; +constexpr std::uint32_t Float16x4Info = 0x0730u; +constexpr std::uint32_t MultisampledAttrib = (SampleShift << 12u) | (SampleShift << 15u); +constexpr std::uint32_t TiledAttrib3 = 0x4dc6c000u; +constexpr std::uint32_t AaConfig4x = 0x0020c002u; +constexpr std::uint32_t MsaaEnableClear = 2; +constexpr std::uint32_t StandardLocations4x = 0x622ae6aeu; +constexpr std::uint32_t Format16161616Float = 71; +constexpr std::uint32_t Type2DMsaa = 14; +constexpr std::uint32_t TileRenderTarget = 0x1b; +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Words = 32; +constexpr std::uint32_t LoadResult = 12; +constexpr std::array Primitives{2u, 3u}; +constexpr std::array Drawn{0.25f, 0.5f, 0.125f, 1.0f}; +constexpr std::array DrawnBytes{std::byte{64}, std::byte{128}, std::byte{32}, std::byte{255}}; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +alignas(256) constexpr std::array PixelCode{ + 0x7e0802ff, 0x3e800000, 0x7e0a02ff, 0x3f000000, 0x7e0c02ff, 0x3e000000, 0x7e0e02ff, 0x3f800000, 0xf800180f, 0x07060504, 0xbf810000, +}; + +alignas(256) constexpr std::array Unorm8PixelCode{ + 0x7e0802ff, std::bit_cast(64.0f / 255.0f), 0x7e0a02ff, std::bit_cast(128.0f / 255.0f), 0x7e0c02ff, std::bit_cast(32.0f / 255.0f), 0x7e0e02ff, std::bit_cast(1.0f), 0xf800180f, 0x07060504, 0xbf810000, +}; + +alignas(256) constexpr std::array LoadCode{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301008, 0x80000401, 0xe030100c, + 0x80000501, 0xe0301010, 0x80000601, 0xe0301014, 0x80000701, 0xbf8c3f70, 0xf0001f30, 0x00011005, + 0xbf8c3f70, 0xe0701030, 0x80001001, 0xe0701034, 0x80001101, 0xe0701038, 0x80001201, 0xe070103c, + 0x80001301, 0xbf810000, +}; + +alignas(256) constexpr std::array, 4> Lines{{ + {-0.828125f, -0.4375f, 0.5f, 1.0f}, {0.90625f, 0.140625f, 0.5f, 1.0f}, + {-0.265625f, 0.90625f, 0.5f, 1.0f}, {0.34375f, -0.921875f, 0.5f, 1.0f}, +}}; + +alignas(256) std::array Buffer{}; +alignas(256) std::array, Primitives.size()> LinePixels{}; + +struct Block { + explicit Block(std::size_t bytes) : bytes(bytes) { +#ifdef _WIN32 + data = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + data = static_cast(std::aligned_alloc(BlockBytes, bytes)); +#endif + Require(data != nullptr, "cannot allocate the multisampled color target"); + std::memset(data, 0, bytes); + GuestAllocations::Mutation mutation; + mutation.Add(data, bytes, true, true, true); + } + ~Block() { + { + GuestAllocations::Mutation mutation; + mutation.Remove(data); + } +#ifdef _WIN32 + VirtualFree(data, 0, MEM_RELEASE); +#else + std::free(data); +#endif + } + Block(const Block&) = delete; + Block& operator=(const Block&) = delete; + std::uint64_t Address() const { return reinterpret_cast(data); } + std::size_t bytes; + std::uint8_t* data = nullptr; +}; + +AgcDriver::QueueState LineQueue(const Block& block, std::uint32_t primitive) { + const auto half = std::bit_cast(static_cast(Side) / 2.0f); + const auto extent = (Side << 16u) | Side; + AgcDriver::QueueState queue; + queue.userConfig[0x242] = primitive; + queue.context = { + {0x2d5, 0x2000}, {0x1b6, 0}, {0x207, 0}, {0x200, 0}, {0x203, 0x800}, + {0x2dc, 0xaa00}, {0x2f8, AaConfig4x}, {0x292, MsaaEnableClear}, {0x293, 0}, + {0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa}, + {0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff}, + {0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240}, + {0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0010}, + {0x1c4, 0}, {0x1c5, 9}, {0x1c3, 4}, + {0x318, static_cast(block.Address() >> 8u)}, {0x390, static_cast(block.Address() >> 40u)}, + {0x31b, 0}, {0x31c, Float16x4Info}, {0x31d, MultisampledAttrib}, + {0x3b0, ((Side - 1u) << 14u) | (Side - 1u)}, {0x3b8, TiledAttrib3}, {0x1e0, 0}, + {0xc, 0}, {0xd, extent}, {0x81, 0x80000000}, {0x82, extent}, + {0x90, 0x80000000}, {0x91, extent}, {0x94, 0x80000000}, {0x95, extent}, + {0x10f, half}, {0x110, half}, {0x111, std::bit_cast(-static_cast(Side) / 2.0f)}, {0x112, half}, + {0x113, std::bit_cast(1.0f)}, {0x114, 0}, {0xb4, 0}, {0xb5, std::bit_cast(1.0f)} + }; + for (std::uint32_t offset = 0x2fe; offset <= 0x30d; ++offset) queue.context[offset] = (offset - 0x2feu) % 4u == 0 ? StandardLocations4x : 0u; + queue.shader[0x008] = 0x100; + queue.shader[0x009] = 0; + return queue; +} + +AgcDriver::Graphics::State DecodeLines(const Block& block, std::uint32_t primitive) { + const auto state = AgcDriver::Graphics::DecodeState(LineQueue(block, primitive)); + const auto& color = state.color; + Require(state.topology == (primitive == 2u ? VK_PRIMITIVE_TOPOLOGY_LINE_LIST : VK_PRIMITIVE_TOPOLOGY_LINE_STRIP), "VGT_PRIMITIVE_TYPE " + std::to_string(primitive) + " did not decode as a line list or strip"); + Require(state.colors.size() == 1 && color.address == block.Address() && color.format == VK_FORMAT_R16G16B16A16_SFLOAT && color.samples == Samples && color.extent.width == Side && color.extent.height == Side, "the target did not decode as a 16x16 RGBA16F surface of 4 samples"); + Require(state.samples == Samples && !state.depth && !state.perSampleShading && !state.alphaToCoverage, "the lines did not decode as a 4x draw without depth, sample shading or alpha-to-coverage"); + const auto locations = AgcDriver::Graphics::SampleLocations(state); + for (std::uint32_t sample = 0; sample < Samples; ++sample) Require(locations[sample].x == 0.5f && locations[sample].y == 0.5f, "the decoded 4x line state does not put every sample at the pixel center"); + Require(state.viewport.x == 0.0f && state.viewport.y == static_cast(Side) && state.viewport.width == static_cast(Side) && state.viewport.height == -static_cast(Side) && state.scissor.offset.x == 0 && state.scissor.offset.y == 0 && state.scissor.extent.width == Side && state.scissor.extent.height == Side, "the decoded viewport or scissor does not cover the 16x16 target"); + return state; +} + +AgcDriver::Graphics::State OneSample(AgcDriver::Graphics::State state, std::span pixels, const AgcDriver::Graphics::ColorTargetLayout& linear) { + state.color = {reinterpret_cast(pixels.data()), {Side, Side}, VK_FORMAT_R8G8B8A8_UNORM, linear.Bytes(), 0xe4u}; + state.colors = {state.color}; + state.samples = 1; + state.customSampleLocations = false; + state.sampleLocations.fill(0); + state.sampleLocationGrid = false; + return state; +} + +void DrawLines(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::State& state, std::span pixelCode) { + const auto target = device.Target(); + std::vector vertexUserData(4, 0u); + const auto address = reinterpret_cast(Lines.data()); + const std::array vertexBuffer{static_cast(address), static_cast((address >> 32u) & 0xffffu) | (16u << 16u), static_cast(Lines.size()), 0x01016facu}; + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {WaveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionX) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionY); + pixel.posX = true; + pixel.posY = true; + pixel.targetOutputMode[0] = 9; + pixel.targetExportMapping.fill(0xe4u); + const std::vector pixelUserData(8, 0u); + const auto codeAddress = reinterpret_cast(pixelCode.data()); + const std::array pixelMemory{{{codeAddress, std::as_bytes(pixelCode)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, codeAddress, pixelCode, 0, {}}, + {WaveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} + }}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Lines.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); +} + +std::vector> LoadSamples(AgcDriver::VulkanDevice& device, const Block& block) { + const auto address = block.Address(); + const std::array texture{ + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format16161616Float << 20u) | (((Side - 1u) & 3u) << 30u), + ((Side - 1u) >> 2u) | ((Side - 1u) << 14u), + 0xfacu | (SampleShift << 16u) | (TileRenderTarget << 20u) | (Type2DMsaa << 28u), + 0u, + SampleShift << 4u, + 0u, + 0u, + }; + const auto bufferAddress = reinterpret_cast(Buffer.data()); + const std::array buffer{static_cast(bufferAddress), static_cast((bufferAddress >> 32u) & 0xffffu), static_cast(Buffer.size() * 4u), 0x31016facu}; + std::vector userData(16, 0u); + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + const std::array memory{{{reinterpret_cast(LoadCode.data()), std::as_bytes(std::span(LoadCode))}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(LoadCode.data()), LoadCode, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto program = ShaderRecompiler::Recompile(request); + std::vector> loaded(Side * Side * Samples); + for (std::uint32_t first = 0; first < loaded.size(); first += Threads) { + Buffer.fill(0xdeadbeefu); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto index = first + tid; + const auto pixel = index / Samples; + const std::array words{0u, 0u, 0u, pixel % Side, pixel / Side, index % Samples}; + std::copy(words.begin(), words.end(), &Buffer[tid * Words]); + } + device.Dispatch(program, 1, 1, 1, {}, reinterpret_cast(LoadCode.data())); + device.WaitIdle(); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + for (std::uint32_t component = 0; component < 4u; ++component) loaded[first + tid][component] = std::bit_cast(Buffer[tid * Words + LoadResult + component]); + } + } + return loaded; +} + +bool NearEndpoint(std::uint32_t x, std::uint32_t y) { + return std::any_of(Lines.begin(), Lines.end(), [&](const std::array& vertex) { + const double px = (static_cast(vertex[0]) + 1.0) * 0.5 * Side; + const double py = Side - (static_cast(vertex[1]) + 1.0) * 0.5 * Side; + return std::fabs(x + 0.5 - px) < 1.5 && std::fabs(y + 0.5 - py) < 1.5; + }); +} + +void CompareLines(AgcDriver::VulkanDevice& device, const Block& block, std::uint32_t primitive, std::span pixels) { + const auto decoded = DecodeLines(block, primitive); + DrawLines(device, decoded, PixelCode); + const auto loaded = LoadSamples(device, block); + const AgcDriver::Graphics::ColorTargetLayout linear(Side, Side, AgcDriver::Graphics::ColorTileMode::Linear, 4u); + Require(linear.Bytes() <= pixels.size(), "the one-sample line target does not fit its memory"); + std::fill(pixels.begin(), pixels.end(), std::byte{0}); + DrawLines(device, OneSample(decoded, pixels, linear), Unorm8PixelCode); + std::uint32_t covered = 0; + std::uint32_t single = 0; + std::uint32_t compared = 0; + std::uint32_t differing = 0; + for (std::uint32_t y = 0; y < Side; ++y) { + for (std::uint32_t x = 0; x < Side; ++x) { + char where[80]; + std::snprintf(where, sizeof(where), "VGT_PRIMITIVE_TYPE %u: pixel (%u, %u)", primitive, x, y); + std::uint32_t drawn = 0; + for (std::uint32_t sample = 0; sample < Samples; ++sample) { + const auto& texel = loaded[(y * Side + x) * Samples + sample]; + Require(texel == Drawn || texel == std::array{}, std::string(where) + " of the 4x target holds neither the line color nor the cleared value"); + drawn += texel == Drawn ? 1u : 0u; + } + Require(drawn == 0u || drawn == Samples, std::string(where) + " has " + std::to_string(drawn) + " of its 4 samples covered by a line with every sample at the pixel center"); + const auto bytes = pixels.begin() + static_cast(linear.Offset(x, y)); + const bool oneSample = std::equal(DrawnBytes.begin(), DrawnBytes.end(), bytes); + Require(oneSample || std::all_of(bytes, bytes + 4, [](std::byte value) { return value == std::byte{0}; }), std::string(where) + " of the one-sample target holds neither the line color nor zero"); + covered += drawn == Samples ? 1u : 0u; + single += oneSample ? 1u : 0u; + if (NearEndpoint(x, y)) continue; + ++compared; + differing += (drawn == Samples) != oneSample ? 1u : 0u; + Require(!oneSample || drawn == Samples, std::string(where) + " is drawn by the one-sample line but not covered in the 4x target"); + } + } + Require(covered >= 20u && covered <= 2u * single && compared >= 200u, "VGT_PRIMITIVE_TYPE " + std::to_string(primitive) + ": the 4x lines covered " + std::to_string(covered) + " pixels and the one-sample lines " + std::to_string(single) + ", with " + std::to_string(compared) + " pixels compared"); + std::printf("VGT_PRIMITIVE_TYPE %u: 4x lines with every sample at the pixel center covered %u pixels in full and none in part; the one-sample lines drew %u; %u of the %u pixels away from the endpoints differ\n", primitive, covered, single, differing, compared); +} + +} + +int main() { + try { + auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (!AgcDriver::VulkanDeviceTestAccess::ContextOf(*device).storageImageMultisample) { + std::puts("skipped, the device has no shaderStorageImageMultisample to load the samples of a 4x target"); + return VulkanTestSkipped; + } + if ((AgcDriver::VulkanDeviceTestAccess::ContextOf(*device).sampleLocationSampleCounts & Samples) == 0u) { + std::puts("skipped, the device has no VK_EXT_sample_locations at 4 samples to put every sample at the pixel center"); + return VulkanTestSkipped; + } + Block listTarget(4u * BlockBytes); + Block stripTarget(4u * BlockBytes); + struct ReleaseDevice { + std::unique_ptr& value; + ~ReleaseDevice() { value.reset(); } + } releaseDevice{device}; + const std::array targets{&listTarget, &stripTarget}; + for (std::size_t index = 0; index < Primitives.size(); ++index) CompareLines(*device, *targets[index], Primitives[index], LinePixels[index]); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleRetirement.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleRetirement.cpp index b603cc7326..27f7d4ae49 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleRetirement.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleRetirement.cpp @@ -14,6 +14,7 @@ #else #include #endif +#include #include #include #include @@ -66,6 +67,15 @@ PFN_vkDestroyImageView realDestroyImageView = nullptr; PFN_vkCmdClearColorImage realClearColorImage = nullptr; PFN_vkCmdCopyImage realCopyImage = nullptr; +[[noreturn]] void FailViewOutlivesImage() { + std::fputs("an MSAA image would be destroyed before one of its views; stopping\n", stderr); + std::fflush(stderr); +#ifdef _WIN32 + TerminateProcess(GetCurrentProcess(), 1); +#endif + std::_Exit(1); +} + [[noreturn]] void FailPendingImage() { std::fputs("an unsubmitted MSAA image would be destroyed; stopping without submitting it\n", stderr); std::fflush(stderr); @@ -104,6 +114,7 @@ VKAPI_ATTR void VKAPI_CALL CountDestroyImage(VkDevice device, VkImage image, con for (const auto& [pending, generation] : pendingImages) { if (pending == image && images.at(image).generation == generation) FailPendingImage(); } + if (std::any_of(views.begin(), views.end(), [&](const auto& entry) { return entry.second == image; })) FailViewOutlivesImage(); realDestroyImage(device, image, callbacks); images.erase(image); ++destroyed; @@ -514,6 +525,37 @@ public: AwaitImages(0, "promoted MSAA targets were not fully released"); } + void StorageTwin() { + GuestBlock block; + const auto target = Target(block); + auto twin = target; + twin.format = VK_FORMAT_R8G8B8A8_SRGB; + const auto resource = Resource(block); + VkImage renderedImage = VK_NULL_HANDLE, twinImage = VK_NULL_HANDLE, promotedImage = VK_NULL_HANDLE, promotedTwinImage = VK_NULL_HANDLE; + Image rendered{}, promoted{}; + VkImageView twinView = VK_NULL_HANDLE; + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + rendered = ImageFor(MultisampleTargetView(context, target), &renderedImage); + twinView = MultisampleTargetView(context, twin); + ImageFor(twinView, &twinImage); + Require(twinImage == renderedImage, "an sRGB use of an MSAA surface rendered as UNORM does not reach its samples"); + } + Sync(); + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + promoted = ImageFor(MultisampleTargetStorageView(context, resource), &promotedImage); + Require(promoted.generation != rendered.generation, "MSAA storage promotion kept the rendered image"); + const auto promotedTwin = MultisampleTargetView(context, twin); + ImageFor(promotedTwin, &promotedTwinImage); + Require(promotedTwinImage == promotedImage, "after storage promotion an sRGB use of the MSAA surface still views the retired image"); + } + Sync(); + block.Unmap(); + Clear(); + AwaitImages(0, "MSAA targets with twin views were not fully released"); + } + private: AgcDriver::VulkanDevice& device; Context context; @@ -533,6 +575,7 @@ int main() { run.SampledRemap(); run.StorageRemap(); run.OrderRemap(); + if (context.storageImageMultisample) run.StorageTwin(); std::puts("retired MSAA allocations stay bounded, recorded clears and storage promotions retain their images, sampled aliases release their parents after the last batch, and sample-order targets keep their retired images until their batch completes"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleSrgbTwin.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleSrgbTwin.cpp new file mode 100644 index 0000000000..a28966b839 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleSrgbTwin.cpp @@ -0,0 +1,247 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/Shutdown.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 +#include + +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} + +namespace { + +using AgcDriver::Graphics::ColorTarget; +using AgcDriver::Graphics::Require; + +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t Side = 64; + +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, "multisampled sRGB twin: cannot allocate a guest block"); + std::memset(block, 0, BlockBytes); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint64_t Address() const { return static_cast(reinterpret_cast(block)); } + +private: + std::byte* block = nullptr; +}; + +GuestBlock& NewBlock() { + static auto* blocks = new std::deque(); + return blocks->emplace_back(); +} + +ColorTarget MultisampledTarget(const GuestBlock& color, VkFormat format) { + ColorTarget target{}; + target.address = color.Address(); + target.surfaceAddress = target.address; + target.extent = {Side, Side}; + target.surfaceExtent = target.extent; + target.format = format; + target.tileMode = AgcDriver::Graphics::ColorTileMode::RenderTarget; + target.elementBytes = 4; + target.bytes = BlockBytes / 4; + target.samples = 4; + return target; +} + +struct Acquired { + VkImageView view = VK_NULL_HANDLE; + VkImage image = VK_NULL_HANDLE; + std::shared_ptr owner; +}; + +Acquired Acquire(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::Context& context, const ColorTarget& target) { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + Acquired acquired; + acquired.view = AgcDriver::Graphics::MultisampleTargetView(context, target, acquired.owner, &acquired.image); + Require(acquired.view != VK_NULL_HANDLE && acquired.image != VK_NULL_HANDLE && acquired.owner != nullptr, "the multisampled target has no view"); + device.SubmitRecorded(true); + device.WaitIdle(); + return acquired; +} + +std::array UniformTexel(const AgcDriver::Graphics::Context& context, VkImage multisampled, VkFormat format) { + VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + info.imageType = VK_IMAGE_TYPE_2D; + info.format = format; + info.extent = {Side, Side, 1}; + info.mipLevels = 1; + info.arrayLayers = 1; + info.samples = VK_SAMPLE_COUNT_1_BIT; + info.tiling = VK_IMAGE_TILING_OPTIMAL; + info.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT; + info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + VkImage resolved = VK_NULL_HANDLE; + AgcDriver::Graphics::Check(context.Function("vkCreateImage")(context.device, &info, nullptr, &resolved), "vkCreateImage resolved texels"); + VkMemoryRequirements requirements{}; + context.Function("vkGetImageMemoryRequirements")(context.device, resolved, &requirements); + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = requirements.size; + allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + VkDeviceMemory memory = VK_NULL_HANDLE; + AgcDriver::Graphics::Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &memory), "vkAllocateMemory resolved texels"); + AgcDriver::Graphics::Check(context.Function("vkBindImageMemory")(context.device, resolved, memory, 0), "vkBindImageMemory resolved texels"); + AgcDriver::Graphics::Buffer readback(context, static_cast(Side) * Side * 4u, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + { + AgcDriver::Graphics::CommandBatch batch(context); + const auto commands = batch.Handle(); + std::array barriers{}; + for (auto& barrier : barriers) { + barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + barrier.srcQueueFamilyIndex = barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + } + barriers[0].image = multisampled; + barriers[0].srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT; + barriers[0].dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + barriers[0].oldLayout = barriers[0].newLayout = VK_IMAGE_LAYOUT_GENERAL; + barriers[1].image = resolved; + barriers[1].dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + barriers[1].oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + barriers[1].newLayout = VK_IMAGE_LAYOUT_GENERAL; + context.Function("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, static_cast(barriers.size()), barriers.data()); + VkImageResolve region{}; + region.srcSubresource = region.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + region.extent = {Side, Side, 1}; + context.Function("vkCmdResolveImage")(commands, multisampled, VK_IMAGE_LAYOUT_GENERAL, resolved, VK_IMAGE_LAYOUT_GENERAL, 1, ®ion); + VkMemoryBarrier toCopy{VK_STRUCTURE_TYPE_MEMORY_BARRIER}; + toCopy.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + toCopy.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + context.Function("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1, &toCopy, 0, nullptr, 0, nullptr); + VkBufferImageCopy copy{}; + copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + copy.imageExtent = {Side, Side, 1}; + context.Function("vkCmdCopyImageToBuffer")(commands, resolved, VK_IMAGE_LAYOUT_GENERAL, readback.Handle(), 1, ©); + VkMemoryBarrier toHost{VK_STRUCTURE_TYPE_MEMORY_BARRIER}; + toHost.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + toHost.dstAccessMask = VK_ACCESS_HOST_READ_BIT; + context.Function("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, 0, 1, &toHost, 0, nullptr, 0, nullptr); + batch.SubmitAndWait(); + } + readback.Invalidate(); + const auto bytes = readback.Bytes(); + std::array texel{}; + std::copy_n(bytes.begin(), texel.size(), texel.begin()); + for (std::size_t offset = 0; offset < bytes.size(); offset += texel.size()) Require(std::equal(texel.begin(), texel.end(), bytes.begin() + static_cast(offset)), "a cleared multisampled target holds different texels"); + context.Function("vkDestroyImage")(context.device, resolved, nullptr); + context.Function("vkFreeMemory")(context.device, memory, nullptr); + return texel; +} + +std::array Bytes(std::uint32_t pattern) { + std::array bytes{}; + std::memcpy(bytes.data(), &pattern, bytes.size()); + return bytes; +} + +void FillTakenThroughTwin(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::Context& context, VkFormat created, VkFormat used, std::uint32_t pattern) { + auto& color = NewBlock(); + const auto target = MultisampledTarget(color, created); + const auto rendered = Acquire(device, context, target); + const auto bytes = static_cast(AgcDriver::Graphics::MultisampledColorBytes(target)); + Require(AgcDriver::Graphics::MultisampledColorFillAbsorbs(color.Address(), bytes, pattern), "the color fill is not taken as the target's clear in its creation format"); + AgcDriver::Graphics::NoteColorMetadataFill(color.Address(), bytes, pattern); + const auto twin = Acquire(device, context, MultisampledTarget(color, used)); + Require(twin.image == rendered.image, "the twin use of the filled target does not reach its samples"); + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + Require(UniformTexel(context, twin.image, created) == Bytes(pattern), "a fill taken as a clear through the twin format does not store the guest's bytes"); +} + +bool TwinFormatsShareTheirSamples(AgcDriver::VulkanDevice& device) { + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device); + if ((context.limits.framebufferColorSampleCounts & VK_SAMPLE_COUNT_4_BIT) == 0) { + std::cout << "skipped, the device has no 4x color targets\n"; + return false; + } + struct ClearTargets { + AgcDriver::VulkanDevice& device; + VkDevice handle; + ~ClearTargets() { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + device.SubmitRecorded(true); + device.WaitIdle(); + AgcDriver::Graphics::ClearMultisampleTargets(handle); + } + } clearTargets{device, context.device}; + for (const auto [first, second] : {std::pair{VK_FORMAT_B8G8R8A8_SRGB, VK_FORMAT_B8G8R8A8_UNORM}, std::pair{VK_FORMAT_R8G8B8A8_UNORM, VK_FORMAT_R8G8B8A8_SRGB}}) { + if (AgcDriver::Graphics::SrgbTwinFormat(context, first) != second && AgcDriver::Graphics::SrgbTwinFormat(context, second) != first) continue; + auto& color = NewBlock(); + const auto rendered = Acquire(device, context, MultisampledTarget(color, first)); + const auto twin = Acquire(device, context, MultisampledTarget(color, second)); + Require(twin.image == rendered.image && twin.owner == rendered.owner, "a use of a multisampled surface in its sRGB twin format does not reach the samples rendered in the other format"); + Require(twin.view != rendered.view, "a use of a multisampled surface in its sRGB twin format is given the view of the other format"); + const auto again = Acquire(device, context, MultisampledTarget(color, first)); + Require(again.image == rendered.image && again.view == rendered.view, "returning to the first format does not give back the first format's view"); + } + if (AgcDriver::Graphics::SrgbTwinFormat(context, VK_FORMAT_R8G8B8A8_UNORM) == VK_FORMAT_R8G8B8A8_SRGB) { + FillTakenThroughTwin(device, context, VK_FORMAT_R8G8B8A8_UNORM, VK_FORMAT_R8G8B8A8_SRGB, 0x80808080u); + FillTakenThroughTwin(device, context, VK_FORMAT_R8G8B8A8_SRGB, VK_FORMAT_R8G8B8A8_UNORM, 0xff0000ffu); + } + auto& other = NewBlock(); + const auto unorm = Acquire(device, context, MultisampledTarget(other, VK_FORMAT_R8G8B8A8_UNORM)); + const auto floating = Acquire(device, context, MultisampledTarget(other, VK_FORMAT_R16G16_SFLOAT)); + Require(floating.image != unorm.image, "a multisampled surface used in an unrelated format of the same size shares the other format's samples"); + return true; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const bool tested = TwinFormatsShareTheirSamples(*device); + LibcRunShutdown_nid_postfix(); + if (!tested) return VulkanTestSkipped; + std::cout << "multisampled sRGB twin tests passed\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + try { + LibcRunShutdown_nid_postfix(); + } catch (...) { + } + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/NativeDepthBdaClear.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/NativeDepthBdaClear.cpp index d1564e3562..6df1dfa361 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/NativeDepthBdaClear.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/NativeDepthBdaClear.cpp @@ -25,7 +25,9 @@ #include #include #include +#include #include +#include namespace AgcDriver { @@ -93,7 +95,9 @@ private: #endif }; -int RunNativeClear(AgcDriver::VulkanDevice& device) { +enum class ClearConsumer { Owner, Texture, Attachment }; + +int RunNativeClear(AgcDriver::VulkanDevice& device, ClearConsumer consumer) { using namespace ShaderRecompiler; using namespace AgcDriver::Graphics; const std::lock_guard lock(Memory::GpuMutex()); @@ -153,10 +157,28 @@ int RunNativeClear(AgcDriver::VulkanDevice& device) { device.WaitIdle(); target.clearDepth = 1.0f; Require(DepthSurfaceView(context, target) == view, "changing the native clear value replaced its attachment"); - NoteHtileDepthClear(target.htileAddress, metadataBytes, metadataBytes); + std::optional> htileClear; + if (NoteHtileDepthClear(target.htileAddress, metadataBytes, metadataBytes)) htileClear.emplace(target.htileAddress, metadataBytes); + Require(htileClear.has_value(), "the full BDA clear was not registered for completion"); const auto generation = Memory::TrackerGeneration(); userData[4] = 0xfffffff0u; - device.Dispatch(compile(), groups, 1u, 1u, {}, reinterpret_cast(code.data())); + device.Dispatch(compile(), groups, 1u, 1u, {}, reinterpret_cast(code.data()), nullptr, nullptr, htileClear); + std::puts("Native full BDA clear: checking sampled owner before explicit WaitIdle"); + Require(context.recorder->PendingWriteOverlaps(target.htileAddress, metadataBytes), "the recognized HTILE clear is missing its pending write range"); + switch (consumer) { + case ClearConsumer::Owner: + Require(DepthSurfaceHolds(context, DecodeTextureResource(words), sampled.get()), "a pending full BDA clear lost its native sampled owner"); + break; + case ClearConsumer::Texture: + Require(CachedSampledTexture(context, words) == sampled, "a pending full BDA clear lost its native sampled view"); + break; + case ClearConsumer::Attachment: + Require(DepthSurfaceView(context, target) == view, "a pending full BDA clear lost its native attachment"); + break; + } + std::printf("Native full BDA clear: consumer=%u settled the recognized clear before WaitIdle\n", static_cast(consumer)); + Require(!context.recorder->PendingWriteOverlaps(target.htileAddress, metadataBytes), "the native HTILE consumer did not settle its clear's pending write"); + Require(Memory::StoredAllSince_nid_no_patch(target.htileAddress, metadataBytes, generation), "the native HTILE consumer did not complete its recognized clear"); device.WaitIdle(); for (std::size_t offset = 0; offset < BlockBytes; offset += 4u) { std::uint32_t value = 0; @@ -164,6 +186,7 @@ int RunNativeClear(AgcDriver::VulkanDevice& device) { Require(value == (offset < metadataBytes ? 0xfffffff0u : 0u), "the native GPU clear did not write its exact metadata range"); } std::printf("Native full BDA clear: exact %zu-byte metadata readback passed; complete=%d\n", metadataBytes, Memory::StoredAllSince_nid_no_patch(target.htileAddress, metadataBytes, generation)); + Require(Memory::StoredAllSince_nid_no_patch(target.htileAddress, metadataBytes, generation), "the completed recognized native clear did not record exact coverage"); Require(CachedSampledTexture(context, words) == sampled, "a completed full BDA clear lost its native sampled view"); Require(DepthSurfaceView(context, target) == view, "a completed full BDA clear replaced its native attachment"); device.WaitIdle(); @@ -178,7 +201,11 @@ int main() { try { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; - return RunNativeClear(*device); + for (const auto consumer : {ClearConsumer::Owner, ClearConsumer::Texture, ClearConsumer::Attachment}) { + const auto result = RunNativeClear(*device, consumer); + if (result != 0) return result; + } + return 0; } catch (const std::exception& error) { std::fprintf(stderr, "%s\n", error.what()); return 1; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp index cda5b9280a..641eaf93b0 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp @@ -7,6 +7,9 @@ #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" #include "prx/libc/include/GuestArena.hpp" #include "prx/libc/include/GuestAllocations.hpp" #include "prx/libc/include/GuestWriteWatch.hpp" @@ -21,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -35,6 +39,12 @@ #include #endif +namespace AgcDriver::DriverDetail { +struct DriverTestAccess { + static std::shared_ptr GetDevice() { return Driver::Get().device.Load(); } +}; +} + namespace { using namespace AgcDriver::Graphics; @@ -137,7 +147,7 @@ private: class TargetMemory { public: - explicit TargetMemory(std::size_t bytes) : bytes(bytes) { + explicit TargetMemory(std::size_t bytes, bool registerAllocation = false) : bytes(bytes), registered(registerAllocation) { #ifdef _WIN32 pointer = GuestArena::GuestArenaAllocate_nid_postfix(bytes, 65536u); GuestArena::GuestArenaCommit_nid_postfix(pointer, bytes, PAGE_READWRITE, bytes); @@ -153,8 +163,10 @@ public: #endif Require(pointer != nullptr, "multisample target fixture allocation failed"); std::memset(pointer, 0xa5, bytes); + if (registered) GuestAllocations::Mutation().Add(pointer, bytes, true, true, true); } ~TargetMemory() { + if (registered) GuestAllocations::Mutation().Remove(pointer); #ifdef _WIN32 GuestArena::GuestArenaReset_nid_postfix(pointer, bytes); GuestArena::GuestArenaRelease_nid_postfix(pointer, bytes); @@ -168,8 +180,49 @@ public: private: void* pointer = nullptr; std::size_t bytes; + bool registered = false; }; +alignas(256) constexpr std::array HtileMaskKernel{ + 0xd7460000u, 0x04010c06u, 0x34000084u, 0xdc388000u, 0x04020000u, 0xbf8c3f70u, 0xd7710008u, 0x00120805u, + 0xd7710009u, 0x00120a05u, 0xd771000au, 0x00120c05u, 0xd771000bu, 0x00120e05u, 0xdc788000u, 0x00020800u, + 0xbf810000u, +}; + +std::vector DriverPm4Packet(std::uint32_t opcode, std::initializer_list body) { + std::vector words{0xc0000000u | (static_cast(body.size() - 1u) << 16u) | (opcode << 8u)}; + words.insert(words.end(), body); + return words; +} + +Context SubmitDriverHtileClear(std::uint64_t address, std::unique_lock& gpuLock) { + const auto shader = reinterpret_cast(HtileMaskKernel.data()); + std::vector commands; + const auto append = [&commands](std::vector packet) { commands.insert(commands.end(), packet.begin(), packet.end()); }; + append(DriverPm4Packet(0x76u, {0x20cu, static_cast(shader >> 8u)})); + append(DriverPm4Packet(0x76u, {0x20du, static_cast(shader >> 40u) & 0xffu})); + append(DriverPm4Packet(0x76u, {0x207u, 64u})); + append(DriverPm4Packet(0x76u, {0x208u, 1u})); + append(DriverPm4Packet(0x76u, {0x209u, 1u})); + append(DriverPm4Packet(0x76u, {0x213u, (6u << 1u) | 0x80u})); + append(DriverPm4Packet(0x76u, {0x240u, 0u})); + append(DriverPm4Packet(0x76u, {0x241u, 0u})); + append(DriverPm4Packet(0x76u, {0x242u, static_cast(address)})); + append(DriverPm4Packet(0x76u, {0x243u, static_cast(address >> 32u)})); + append(DriverPm4Packet(0x76u, {0x244u, 0xfffc00f0u})); + append(DriverPm4Packet(0x76u, {0x245u, 0u})); + append(DriverPm4Packet(0x15u, {32u, 1u, 1u, 0x41u})); + Packet packet{commands.data(), static_cast(commands.size()), 0u, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, "native depth HTILE clear: PM4 submission failed"); + gpuLock.unlock(); + try { AgcDriverWaitIdle_nid_postfix(); } + catch (...) { gpuLock.lock(); throw; } + gpuLock.lock(); + const auto driverDevice = AgcDriver::DriverDetail::DriverTestAccess::GetDevice(); + Require(driverDevice != nullptr, "HTILE clear driver device is missing"); + return driverDevice->graphicsContext(); +} + template void TargetRefuses(Action action, std::string_view expected) { std::string message; @@ -548,7 +601,8 @@ void CheckDrawFeedback() { Require(device != nullptr, "native depth draw feedback requires Vulkan"); constexpr VkExtent2D extent{5u, 3u}; constexpr std::uint32_t samples = 4u; - TargetMemory memory(static_cast(MultisampledDepthBytes_nid_no_patch(extent, 4u, samples))), metadata(65536u); + const auto depthBytes = static_cast(MultisampledDepthBytes_nid_no_patch(extent, 4u, samples)); + TargetMemory memory(depthBytes), metadata(65536u); std::memset(metadata.Data(), 0, 65536u); DepthTarget depth{memory.Address(), 0u, extent, VK_FORMAT_D32_SFLOAT, 0.0f, 0u, metadata.Address(), false, samples}; depth.depthSwizzle = 24u; @@ -581,6 +635,9 @@ void CheckDrawFeedback() { const std::array initial{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0u}, {ShaderRecompiler::ShaderStage::Fragment, &producer, pixelOffset}}}; device->Draw(state, draw, initial); device->WaitIdle(); + std::array probe{}; + TargetRefuses([&] { AgcDriver::GuestMemory::Read(depth.address, probe); }, "sample-preserving guest publication"); + Require(DepthSurfaceAt(depth.address), "a refused guest read retired a native depth surface a draw wrote"); reader.spirv = DepthFeedbackFragmentShader(binding, true); reader.variantId = 3u; const auto words = DepthSampleWords(depth); @@ -597,10 +654,25 @@ void CheckDrawFeedback() { const std::array shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0u}, {ShaderRecompiler::ShaderStage::Fragment, &reader, pixelOffset}}}; TargetRefuses([&] { device->Draw(state, draw, shaders); }, "sampling a native multisampled depth target the draw attaches"); device->WaitIdle(); - std::puts("Native depth feedback refused through the draw resource path"); + TargetMemory testedMemory(depthBytes), testedMetadata(65536u); + std::memset(testedMetadata.Data(), 0, 65536u); + const auto guestByte = [](std::size_t offset) { return static_cast(offset * 7u + 3u); }; + for (std::size_t offset = 0u; offset < depthBytes; ++offset) testedMemory.Data()[offset] = guestByte(offset); + auto tested = depth; + tested.address = testedMemory.Address(); + tested.htileAddress = testedMetadata.Address(); + state.depth = tested; + state.depthWrite = false; + device->Draw(state, draw, initial); + device->WaitIdle(); + Require(DepthSurfaceAt(tested.address), "a depth-test-only draw left no resident native depth surface"); + AgcDriver::GuestMemory::Read(tested.address + depthBytes - 1u, probe); + Require(probe[0] == guestByte(depthBytes - 1u), "a guest read after a depth-test-only draw did not return its guest bytes"); + Require(!DepthSurfaceAt(tested.address), "a guest read left a native depth surface only depth-tested resident"); + std::puts("Native depth feedback refused through the draw resource path; a written surface refused and a depth-tested one retired on guest read"); } -enum class ClearFault { None, PartialStore, CpuWrite, DepthWrite, BadMetadata, UnprovenDispatch, PendingWrite, Overlap }; +enum class ClearFault { None, PartialStore, CpuWrite, DepthWrite, BadMetadata, UnprovenDispatch, PendingWrite, Overlap, PossibleStore, CompletedPossibleStore, DriverDispatch, ImportWindowWrite }; void CheckClearDispatch() { const std::array code{ @@ -609,7 +681,7 @@ void CheckClearDispatch() { 0xd771000a, 0x00120c05, 0xd771000b, 0x00120e05, 0xdc788000, 0x00020800, 0xbf810000, }; - const std::array userData{0u, 0u, 65536u, 0u, 0xf0u, 0xf0u}; + const std::array userData{0u, 0u, 65536u, 0u, 0xfffc00f0u, 0u}; const ShaderRecompiler::ShaderComputeStageInfo compute{{64u, 1u, 1u}, 0u, {true, false, false}, false, 1u}; const std::array packet{0u, 32u, 1u, 1u, 1u}; Require(HtileDepthClearBytes_nid_no_patch(code, userData, compute, packet, 0u) == 32768u, "complete HTILE mask dispatch was not recognized"); @@ -649,10 +721,10 @@ void CheckClearDispatch() { } } -void CheckDepthTarget(Context context, VkFormat format, std::uint32_t fragments, bool one, bool clearing = false, std::optional depthOnlyOverride = std::nullopt, ClearFault fault = ClearFault::None, bool iterate256 = true) { +void CheckDepthTarget(Context context, VkFormat format, std::uint32_t fragments, bool one, bool clearing, std::optional depthOnlyOverride, ClearFault fault, std::unique_lock& gpuLock, bool iterate256 = true) { const VkExtent2D extent{5u, 3u}; const auto bytes = static_cast(MultisampledDepthBytes_nid_no_patch(extent, format == VK_FORMAT_D16_UNORM ? 2u : 4u, fragments)); - TargetMemory memory(bytes), metadata(65536u); + TargetMemory memory(bytes), metadata(65536u, fault == ClearFault::DriverDispatch); const auto metadataBytes = HtileMetadataBytes(extent); const bool depthOnly = depthOnlyOverride.value_or(one); const std::uint32_t pattern = one ? (depthOnly ? 0xfffffff0u : 0xfffc00f0u) : 0u; @@ -679,6 +751,7 @@ void CheckDepthTarget(Context context, VkFormat format, std::uint32_t fragments, } const auto view = DepthSurfaceView(context, target); Require(DepthSurfaceView(context, target) == view, "native depth attachment did not reuse its image"); + NoteDepthSurfaceWrite(context, target); std::array beforeSampling{}; TargetRefuses([&] { AgcDriver::GuestMemory::Read(target.address + bytes - 1u, beforeSampling); }, "sample-preserving guest publication"); TargetRefuses([&] { RequireNoMultisampleTarget(target.htileAddress + metadataBytes - 1u, 1u, "a DMA copy"); }, "sample-preserving guest publication"); @@ -770,17 +843,65 @@ void CheckDepthTarget(Context context, VkFormat format, std::uint32_t fragments, target.clearDepth = clearOne ? 1.0f : 0.0f; Require(DepthSurfaceView(context, target) == view, "updating the native clear value replaced its attachment"); const std::uint32_t clearPattern = clearOne ? (depthOnly ? 0xfffffff0u : 0xfffc00f0u) : 0u; - const auto nativeBytes = fault == ClearFault::UnprovenDispatch ? 0u : static_cast(metadataBytes); - NoteHtileDepthClear(target.htileAddress, static_cast(metadataBytes), nativeBytes); - if (fault == ClearFault::Overlap) NoteHtileDepthClear(target.htileAddress, static_cast(metadataBytes), nativeBytes); - const auto storedBytes = fault == ClearFault::PartialStore ? 4u : static_cast(metadataBytes); - AgcDriver::GuestMemory::StoreOwnBytes(target.htileAddress, storedBytes, [&] { - for (std::size_t i = 0u; i < storedBytes / 4u; ++i) std::memcpy(metadata.Data() + i * 4u, &clearPattern, 4u); - if (fault == ClearFault::BadMetadata) metadata.Data()[0] = std::byte{0x0f}; - }); - if (fault == ClearFault::CpuWrite) metadata.Data()[metadataBytes - 1u] ^= std::byte{0x01}; - if (fault == ClearFault::DepthWrite) memory.Data()[bytes - 1u] ^= std::byte{0x01}; - if (fault == ClearFault::PendingWrite) QueueTargetByteWrite(context, recorder, target.htileAddress, std::byte{0}); + if (fault == ClearFault::DriverDispatch && clearOne) { + struct ShutdownDriver { + ~ShutdownDriver() { + try { AgcDriverShutdown_nid_postfix(); } catch (...) {} + } + } shutdownDriver; + const auto generation = AgcDriver::GuestMemory::TrackerGeneration(); + const auto driverContext = SubmitDriverHtileClear(target.htileAddress, gpuLock); + recorder.Activate(); + Require(Recorder::Active() == &recorder, "driver idle did not restore the native depth fixture recorder"); + if (!AgcDriver::GuestMemory::Watched(target.htileAddress, metadataBytes)) { + Require(PrepareImportWatch(driverContext) == ImportWatch::Unwatch, + "the HTILE metadata range lost write-watch coverage on a device whose host imports stay watched"); + std::puts("skipped native HTILE write-coverage assertion: the write tracker does not cover the imported metadata range; the production depth-sampling path may still decline this clear"); + TargetRefuses([&] { Require(DepthSurfaceHolds(context, DecodeTextureResource(words), texture.get()), "native depth owner was not retained"); }, + "native multisample depth clear requires tracked HTILE generations"); + return; + } else { + const bool storedAll = AgcDriver::GuestMemory::StoredAllSince_nid_no_patch(target.htileAddress, metadataBytes, generation); + Require(storedAll, "driver idle returned before recording the completed HTILE dispatch"); + } + } else { + const auto nativeBytes = fault == ClearFault::UnprovenDispatch ? 0u : static_cast(metadataBytes); + if (fault == ClearFault::PartialStore || fault == ClearFault::PossibleStore || fault == ClearFault::CompletedPossibleStore) { + for (std::size_t i = 0u; i < metadataBytes / 4u; ++i) std::memcpy(metadata.Data() + i * 4u, &clearPattern, 4u); + } + const bool completionCandidate = NoteHtileDepthClear(target.htileAddress, static_cast(metadataBytes), nativeBytes); + if (fault == ClearFault::ImportWindowWrite) { + const auto beforeImport = AgcDriver::GuestMemory::TrackerGeneration(); + Require(AgcDriver::GuestMemory::ImportWatched(target.htileAddress, static_cast(metadataBytes), [&] { + metadata.Data()[0] ^= std::byte{0x01}; + return true; + }), "HTILE host import simulation failed"); + Require(AgcDriver::GuestMemory::CollectWritesUncached(target.htileAddress, static_cast(metadataBytes)) > beforeImport, "HTILE import window did not stamp its changed page"); + } + if (completionCandidate) ArmHtileDepthClear(target.htileAddress, static_cast(metadataBytes)); + if (fault == ClearFault::Overlap) NoteHtileDepthClear(target.htileAddress, static_cast(metadataBytes), nativeBytes); + const auto storedBytes = fault == ClearFault::PartialStore ? 4u : static_cast(metadataBytes); + if (fault == ClearFault::PossibleStore || fault == ClearFault::CompletedPossibleStore) { + const auto generation = AgcDriver::GuestMemory::TrackerGeneration(); + AgcDriver::GuestMemory::MarkPossiblyWritten(target.htileAddress, metadataBytes); + Require(!AgcDriver::GuestMemory::StoredAllSince_nid_no_patch(target.htileAddress, metadataBytes, generation), "a possible HTILE range proved complete coverage"); + if (fault == ClearFault::CompletedPossibleStore) CompleteHtileDepthClear(target.htileAddress, static_cast(metadataBytes)); + } else AgcDriver::GuestMemory::StoreOwnBytes(target.htileAddress, storedBytes, [&] { + for (std::size_t i = 0u; i < storedBytes / 4u; ++i) std::memcpy(metadata.Data() + i * 4u, &clearPattern, 4u); + if (fault == ClearFault::BadMetadata) metadata.Data()[0] = std::byte{0x0f}; + }); + if (fault == ClearFault::CpuWrite) metadata.Data()[metadataBytes - 1u] ^= std::byte{0x01}; + if (fault == ClearFault::DepthWrite) memory.Data()[bytes - 1u] ^= std::byte{0x01}; + if (fault == ClearFault::PendingWrite) { + QueueTargetByteWrite(context, recorder, target.htileAddress, std::byte{0}); + std::string pendingRefusal; + try { CachedSampledTexture(context, words); } + catch (const std::exception& error) { pendingRefusal = error.what(); } + const bool refused = pendingRefusal.find("external GPU writes") != std::string::npos; + if (!refused) std::fprintf(stderr, "Pending HTILE texture refusal: %s\n", pendingRefusal.c_str()); + Require(refused, "an unrecognized pending HTILE write was accepted or refused for another reason"); + } + } Require(DepthSurfaceHolds(context, DecodeTextureResource(words), texture.get()), "completed native depth clear lost its sampled view"); recorder.Sync(); Require(DepthSurfaceView(context, target) == view && CachedSampledTexture(context, words) == texture, "completed native depth clear replaced an attachment or cached view"); @@ -843,6 +964,59 @@ void CheckDepthTarget(Context context, VkFormat format, std::uint32_t fragments, std::printf("Native depth format=%u samples=%u clear=%u iterate256=%u: imported, rasterized twice, sampled and refused conflicts\n", static_cast(format), fragments, one, iterate256); } +void CheckUnwrittenGuestAccess(Context context, VkFormat format, std::uint32_t fragments) { + const VkExtent2D extent{5u, 3u}; + const auto bytes = static_cast(MultisampledDepthBytes_nid_no_patch(extent, format == VK_FORMAT_D16_UNORM ? 2u : 4u, fragments)); + const auto metadataBytes = static_cast(HtileMetadataBytes(extent)); + TargetMemory memory(bytes), metadata(65536u), unrelated(65536u); + const auto guestByte = [](std::size_t offset) { return static_cast(offset * 7u + 3u); }; + for (std::size_t offset = 0u; offset < bytes; ++offset) memory.Data()[offset] = guestByte(offset); + std::memset(metadata.Data(), 0, metadataBytes); + DepthTarget target{memory.Address(), 0u, extent, format, 0.0f, 0u, metadata.Address(), false, fragments}; + target.depthSwizzle = 24u; + target.iterate256 = target.htilePipeAligned = target.htileDepthOnly = true; + Recorder recorder(context); + recorder.Activate(); + struct Cleanup { + Recorder& recorder; + Context context; + ~Cleanup() { recorder.Sync(); ClearCachedTextures(context.device); ClearDepthSurfaces(context.device); } + } cleanup{recorder, context}; + const auto resident = [&] { return HasResidentDepthGuards_nid_no_patch() && DepthSurfaceAt(target.address); }; + auto view = DepthSurfaceView(context, target); + Require(view != VK_NULL_HANDLE && resident(), "unwritten native depth fixture has no resident owner"); + auto otherDevice = context; + otherDevice.device = VK_NULL_HANDLE; + NoteDepthSurfaceWrite(otherDevice, target); + auto unattached = target; + unattached.address += 65536u; + NoteDepthSurfaceWrite(context, unattached); + std::array probe{}; + AgcDriver::GuestMemory::Read(unrelated.Address(), probe); + Require(resident(), "an unrelated guest read retired a native depth surface"); + for (const bool readMetadata : {false, true}) { + if (readMetadata) RequireNoMultisampleTarget(target.htileAddress + metadataBytes - 1u, 1u, "a DMA copy"); + else { + AgcDriver::GuestMemory::Read(target.address + bytes - 1u, probe); + Require(probe[0] == guestByte(bytes - 1u), "a guest read over an unwritten native depth surface did not return its guest bytes"); + } + Require(!resident(), "a guest access left an unwritten native depth surface resident"); + RequireNoResidentDepthRead_nid_no_patch(target.address, bytes); + RequireNoResidentDepthRead_nid_no_patch(target.htileAddress, metadataBytes); + recorder.Sync(); + const auto rebound = DepthSurfaceView(context, target); + Require(rebound != VK_NULL_HANDLE && rebound != view && resident(), "rebinding after a guest access did not import a new native depth surface"); + view = rebound; + } + NoteDepthSurfaceWrite(context, target); + TargetRefuses([&] { AgcDriver::GuestMemory::Read(target.address, probe); }, "sample-preserving guest publication"); + TargetRefuses([&] { RequireNoMultisampleTarget(target.htileAddress, 1u, "a DMA copy"); }, "sample-preserving guest publication"); + Require(resident(), "a refused guest access retired a written native depth surface"); + for (std::size_t offset = 0u; offset < bytes; ++offset) Require(memory.Data()[offset] == guestByte(offset), "retiring an unwritten native depth surface changed its guest bytes"); + Require(std::all_of(metadata.Data(), metadata.Data() + metadataBytes, [](std::byte value) { return value == std::byte{0}; }), "retiring an unwritten native depth surface changed its HTILE"); + std::printf("Native depth format=%u samples=%u: guest access retired the unwritten surface and refused the written one\n", static_cast(format), fragments); +} + void CheckHtileRouting(Context context, VkFormat format, std::uint32_t fragments) { const VkExtent2D extent{5u, 3u}; const auto bytes = static_cast(MultisampledDepthBytes_nid_no_patch(extent, format == VK_FORMAT_D16_UNORM ? 2u : 4u, fragments)); @@ -1031,6 +1205,7 @@ void CheckRetirement(Context context, VkFormat format, std::uint32_t samples, st const auto words = DepthSampleWords(target); const auto resource = DecodeTextureResource(words); const auto firstView = DepthSurfaceView(context, target); + NoteDepthSurfaceWrite(context, target); auto firstTexture = DepthSurfaceTexture(context, words, resource, VkComponentMapping{}); Require(firstTexture != nullptr, "retirement fixture has no native sampled view"); ColorTarget color{colorMemory.Address(), extent, VK_FORMAT_R8G8B8A8_UNORM, extent.width * extent.height * 4u, 0u}; @@ -1129,7 +1304,7 @@ int main(int argc, char** argv) { } CheckDrawFeedback(); const auto context = device->GetContext(); - const std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + std::unique_lock lock(AgcDriver::GuestMemory::GpuMutex()); Require(DepthSupported(context, VK_FORMAT_D32_SFLOAT, 4u), "the native depth regression requires sampled D32 with four samples"); if (argc == 2 && std::string_view(argv[1]) == "--clear-refusals") { for (const auto& [fault, expected] : std::array{ @@ -1137,10 +1312,11 @@ int main(int argc, char** argv) { std::pair{ClearFault::CpuWrite, "CPU writes raced"}, std::pair{ClearFault::DepthWrite, "guest writes"}, std::pair{ClearFault::BadMetadata, "does not prove its clear value"}, + std::pair{ClearFault::PossibleStore, "no complete tracked HTILE store"}, std::pair{ClearFault::UnprovenDispatch, "incomplete or unproven"}, std::pair{ClearFault::PendingWrite, "external GPU writes"}, std::pair{ClearFault::Overlap, "overlapping native multisample depth"}}) { - TargetRefuses([&] { CheckDepthTarget(context, VK_FORMAT_D32_SFLOAT, 4u, false, true, false, fault); }, expected); + TargetRefuses([&] { CheckDepthTarget(context, VK_FORMAT_D32_SFLOAT, 4u, false, true, false, fault, lock); }, expected); std::printf("Native depth clear fault=%u refused: %s\n", static_cast(fault), expected); } return 0; @@ -1152,10 +1328,16 @@ int main(int argc, char** argv) { for (const auto samples : {2u, 4u, 8u}) { if (!DepthSupported(context, format, samples)) { unavailable += 4u; continue; } for (const bool depthOnly : {false, true}) { - for (const bool one : {false, true}) { CheckDepthTarget(context, format, samples, one, true, depthOnly); ++checked; } + for (const bool one : {false, true}) { CheckDepthTarget(context, format, samples, one, true, depthOnly, ClearFault::None, lock); ++checked; } } } } + CheckDepthTarget(context, VK_FORMAT_D32_SFLOAT, 4u, false, true, false, ClearFault::CompletedPossibleStore, lock); + ++checked; + CheckDepthTarget(context, VK_FORMAT_D32_SFLOAT, 4u, false, true, false, ClearFault::ImportWindowWrite, lock); + ++checked; + CheckDepthTarget(context, VK_FORMAT_D32_SFLOAT, 4u, false, true, false, ClearFault::DriverDispatch, lock); + ++checked; std::printf("Native depth completed clear cases checked=%u unavailable=%u\n", checked, unavailable); return 0; } @@ -1176,9 +1358,10 @@ int main(int argc, char** argv) { for (const auto samples : {2u, 4u, 8u}) { if (!DepthSupported(context, format, samples)) { unavailable += 4u; continue; } for (const bool one : {false, true}) { - for (const bool iterate256 : {true, false}) { CheckDepthTarget(context, format, samples, one, false, std::nullopt, ClearFault::None, iterate256); ++checked; } + for (const bool iterate256 : {true, false}) { CheckDepthTarget(context, format, samples, one, false, std::nullopt, ClearFault::None, lock, iterate256); ++checked; } } CheckHtileRouting(context, format, samples); + CheckUnwrittenGuestAccess(context, format, samples); } } CheckEligibility(context); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PackedFloatColorTarget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PackedFloatColorTarget.cpp index 9b322cb6d0..238cb5ce89 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PackedFloatColorTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PackedFloatColorTarget.cpp @@ -45,6 +45,7 @@ constexpr std::uint32_t Height = 128; constexpr std::uint32_t ScissorWidth = 128; constexpr std::size_t BlockBytes = 65536; constexpr std::uint32_t Float11_11_10Info = 0x4171cu; +constexpr std::uint32_t StandardOrderInfo = Float11_11_10Info & ~0x1800u; constexpr std::uint32_t TiledAttrib3 = 0x4dc6c000u; constexpr std::uint32_t LinearAttrib3 = 0x09000000u; constexpr std::uint32_t Float32Export = 9; @@ -58,7 +59,7 @@ constexpr std::array, 3> Triangle{{ {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f} }}; -alignas(256) std::array, 32> Programs{}; +alignas(256) std::array, 64> Programs{}; std::size_t programCount = 0; std::uint32_t SmallFloat(float value, std::uint32_t mantissaBits) { @@ -78,7 +79,8 @@ std::uint32_t SmallFloat(float value, std::uint32_t mantissaBits) { return (static_cast(exponent - 1 + 15) << mantissaBits) | static_cast(std::floor((fraction * 2.0 - 1.0) * scale)); } -std::uint32_t Packed(std::array rgb) { +std::uint32_t Packed(std::array rgb, bool standardOrder) { + if (standardOrder) return SmallFloat(rgb[0], 5) | (SmallFloat(rgb[1], 6) << 10u) | (SmallFloat(rgb[2], 6) << 21u); return SmallFloat(rgb[2], 5) | (SmallFloat(rgb[1], 6) << 10u) | (SmallFloat(rgb[0], 6) << 21u); } @@ -170,17 +172,17 @@ struct Block { std::uint8_t* data = nullptr; }; -AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block, std::uint32_t attrib3) { +AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block, std::uint32_t attrib3, bool standardOrder) { AgcDriver::Registers cx; cx[0x318] = static_cast(block.Address() >> 8u); cx[0x31b] = 0; - cx[0x31c] = Float11_11_10Info; + cx[0x31c] = standardOrder ? StandardOrderInfo : Float11_11_10Info; cx[0x31d] = 0; cx[0x3b0] = ((Width - 1u) << 14u) | (Height - 1u); cx[0x3b8] = attrib3; cx[0x390] = static_cast(block.Address() >> 40u); auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); - Require(color.format == VK_FORMAT_R32_UINT && color.elementBytes == 4 && color.componentMapping == 0xc6u && color.packing == ShaderRecompiler::ColorExportPacking::Float11_11_10 && color.bytes == block.bytes, "the 11_11_10 float target did not decode as a packed R32_UINT surface"); + Require(color.format == VK_FORMAT_R32_UINT && color.elementBytes == 4 && color.componentMapping == (standardOrder ? 0xe4u : 0xc6u) && color.packing == ShaderRecompiler::ColorExportPacking::Float11_11_10 && color.bytes == block.bytes, "the 11_11_10 float target did not decode as a packed R32_UINT surface"); return color; } @@ -290,10 +292,10 @@ std::string Describe(std::array rgb) { return text; } -void TargetTests(AgcDriver::VulkanDevice& device, Block& block, std::uint32_t attrib3, ColorTileMode mode, const char* kind) { +void TargetTests(AgcDriver::VulkanDevice& device, Block& block, std::uint32_t attrib3, ColorTileMode mode, const char* kind, bool standardOrder) { const ColorTargetLayout layout(Width, Height, mode, 4); Require(layout.Bytes() == block.bytes, "the test surface size disagrees with its layout"); - const auto color = DecodeTarget(block, attrib3); + const auto color = DecodeTarget(block, attrib3, standardOrder); auto before = Fill(block); const float nan = std::numeric_limits::quiet_NaN(); const float infinity = std::numeric_limits::infinity(); @@ -311,12 +313,12 @@ void TargetTests(AgcDriver::VulkanDevice& device, Block& block, std::uint32_t at }}; for (const auto& rgb : cases) { Draw(device, color, Float32Export, Float32Program(rgb)); - ExpectSurface(device, block, layout, before, Packed(rgb), std::string("a 32_ABGR export of ") + Describe(rgb) + " into a " + kind + " 11_11_10 float target"); + ExpectSurface(device, block, layout, before, Packed(rgb, standardOrder), std::string("a 32_ABGR export of ") + Describe(rgb) + " into a " + kind + " 11_11_10 float target"); before = ReadBack(device, block); } const std::array halves{0.5f, 0.75f, 0.125f}; Draw(device, color, Float16Export, Float16Program(halves)); - ExpectSurface(device, block, layout, before, Packed(halves), std::string("an FP16_ABGR export of ") + Describe(halves) + " into a " + kind + " 11_11_10 float target"); + ExpectSurface(device, block, layout, before, Packed(halves, standardOrder), std::string("an FP16_ABGR export of ") + Describe(halves) + " into a " + kind + " 11_11_10 float target"); } } @@ -327,8 +329,10 @@ int main() { if (!device) return VulkanTestSkipped; Block tiled(ColorTargetLayout(Width, Height, ColorTileMode::RenderTarget, 4).Bytes()); Block linear(ColorTargetLayout(Width, Height, ColorTileMode::Linear, 4).Bytes()); - TargetTests(*device, tiled, TiledAttrib3, ColorTileMode::RenderTarget, "tiled"); - TargetTests(*device, linear, LinearAttrib3, ColorTileMode::Linear, "linear"); + for (const bool standardOrder : {false, true}) { + TargetTests(*device, tiled, TiledAttrib3, ColorTileMode::RenderTarget, standardOrder ? "tiled standard-order" : "tiled", standardOrder); + TargetTests(*device, linear, LinearAttrib3, ColorTileMode::Linear, standardOrder ? "linear standard-order" : "linear", standardOrder); + } std::puts("11_11_10 float color target tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedFill.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedFill.cpp index 1c13bc169d..c501698a57 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedFill.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedFill.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -75,16 +76,29 @@ void AppendLabel(std::vector& words, std::uint32_t* target, std:: } struct Blocker { - std::atomic release{false}; + struct State { + std::atomic release{false}; + std::atomic done{false}; + }; + std::shared_ptr state = std::make_shared(); std::uint32_t* target = nullptr; - ~Blocker() { release = true; } + ~Blocker() { Release(); } + void Release() { state->release = true; } void Enqueue(std::vector writes = {}) { - DrawPipeline::Queue0().Enqueue([this] { - while (!release.load()) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + DrawPipeline::Queue0().Enqueue([state = state, target = target] { + while (!state->release.load()) std::this_thread::sleep_for(std::chrono::milliseconds(1)); if (target != nullptr) std::atomic_ref(*target).store(1); - std::lock_guard gpuLock(AgcDriver::GuestMemory::GpuMutex()); + { std::lock_guard gpuLock(AgcDriver::GuestMemory::GpuMutex()); } + state->done = true; }, std::move(writes)); } + void Join() const { + const auto start = std::chrono::steady_clock::now(); + while (!state->done.load()) { + Require(std::chrono::steady_clock::now() - start < std::chrono::seconds(20), "the blocker never ran"); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + } }; void SubmitBehind(Blocker& blocker, std::vector& words) { @@ -107,7 +121,7 @@ void ReleaseWhen(Blocker& blocker, std::size_t reached, std::size_t passed) { Require(now - start < std::chrono::seconds(20), "the queue worker never reached the fill"); std::this_thread::sleep_for(std::chrono::milliseconds(1)); } - blocker.release = true; + blocker.Release(); } void Finish() { @@ -118,6 +132,11 @@ void Finish() { std::_Exit(1); } idle.get(); + const auto start = std::chrono::steady_clock::now(); + while (DrawPipeline::Queue0().Outstanding() != 0) { + Require(std::chrono::steady_clock::now() - start < std::chrono::seconds(20), "the draw pipeline kept an item after the queue went idle"); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(Output.data()), sizeof(Output)); AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(Scratch.data()), sizeof(Scratch)); } @@ -135,6 +154,7 @@ void InputsAfterQueuedLabels() { SubmitBehind(blocker, words); ReleaseWhen(blocker, 4, 5); Finish(); + blocker.Join(); for (std::size_t i = 0; i < Output.size(); ++i) { const auto expected = i < 128 ? 0xababababu : 0xcafebabeu; Require(Output[i] == expected, "a fill read its control or pattern before the queued labels that write them, at " + std::to_string(i)); @@ -156,6 +176,7 @@ void FillIntoFullPipeline() { SubmitBehind(blocker, words); ReleaseWhen(blocker, depth, depth + 1); Finish(); + blocker.Join(); for (std::size_t i = 0; i + 2 < depth; ++i) Require(Labels[i] == i + 1, "a queued label was not stored"); for (std::size_t i = 0; i < Output.size(); ++i) Require(Output[i] == (i < 256 ? 0x5a5a5a5au : 0xcafebabeu), "a fill behind a full draw pipeline did not land at " + std::to_string(i)); } @@ -180,8 +201,9 @@ void SatisfiedWaitKeepsUnrelatedQueuedWork() { while (DrawPipeline::Queue0().Outstanding() < 2 && std::chrono::steady_clock::now() < deadline) std::this_thread::sleep_for(std::chrono::milliseconds(1)); const bool keptQueued = DrawPipeline::Queue0().Outstanding() >= 2; - blocker.release = true; + blocker.Release(); Finish(); + blocker.Join(); Require(keptQueued, "a satisfied WAIT_REG_MEM drained unrelated queued work"); Require(marker == 1, "a label after a satisfied WAIT_REG_MEM did not execute"); } @@ -211,11 +233,12 @@ void SatisfiedWaitDrainsOverlappingQueuedWork() { std::this_thread::sleep_for(std::chrono::milliseconds(1)); const bool drained = DrawPipeline::DrainRequestsByOpcode(0x3c) > drainsBefore; const bool labelQueuedEarly = DrawPipeline::Queue0().Outstanding() >= 2; - blocker.release = true; + blocker.Release(); Require(submitted.wait_for(std::chrono::seconds(20)) == std::future_status::ready, "WAIT_REG_MEM submission did not finish after the overlapping write"); submitted.get(); Finish(); + blocker.Join(); Require(drained && !labelQueuedEarly, "a satisfied WAIT_REG_MEM skipped an overlapping queued write"); Require(marker == 1, "a label after an overlapping WAIT_REG_MEM did not execute"); } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedIndirectDraw.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedIndirectDraw.cpp index 437a8ecaa2..93abe602a5 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedIndirectDraw.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedIndirectDraw.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -138,20 +139,34 @@ void Flush(const std::array& memory) { } struct Blocker { - std::atomic release{false}; + struct State { + std::atomic release{false}; + std::atomic done{false}; + }; + std::shared_ptr state = std::make_shared(); + void Release() { state->release = true; } void Enqueue() { - DrawPipeline::Queue0().Enqueue([this] { - while (!release.load()) std::this_thread::sleep_for(std::chrono::milliseconds(1)); - std::lock_guard gpuLock(AgcDriver::GuestMemory::GpuMutex()); + DrawPipeline::Queue0().Enqueue([state = state] { + while (!state->release.load()) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + { std::lock_guard gpuLock(AgcDriver::GuestMemory::GpuMutex()); } + state->done = true; }, {}); } + void Join() const { + const auto start = std::chrono::steady_clock::now(); + while (!state->done.load()) { + Require(std::chrono::steady_clock::now() - start < std::chrono::seconds(20), "the blocker never ran"); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + } }; -void SubmitBehind(Blocker& blocker, std::vector& commands) { +void SubmitBehind(Blocker& blocker, std::vector& commands, std::size_t queuedBefore) { std::unique_lock gpuLock(AgcDriver::GuestMemory::GpuMutex()); Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; AgcDriver::Submit(&packet, 0); - std::this_thread::sleep_for(std::chrono::milliseconds(500)); + const auto start = std::chrono::steady_clock::now(); + while (DrawPipeline::Queue0().Outstanding() < queuedBefore && std::chrono::steady_clock::now() - start < std::chrono::seconds(60)) std::this_thread::sleep_for(std::chrono::milliseconds(1)); blocker.Enqueue(); } @@ -190,19 +205,20 @@ void IndirectDrawsQueueBehindTheCommitter() { AppendTarget(commands, SecondMemory, Clipped); AppendIndirect(commands); Blocker blocker; - SubmitBehind(blocker, commands); + SubmitBehind(blocker, commands, 3); const auto start = std::chrono::steady_clock::now(); while (DrawPipeline::Queue0().Outstanding() < 3) { if (std::chrono::steady_clock::now() - start > std::chrono::seconds(10)) { const auto outstanding = DrawPipeline::Queue0().Outstanding(); - blocker.release = true; + blocker.Release(); Finish(); Require(false, "the indirect draws drained the pipeline instead of queuing behind it (" + std::to_string(outstanding) + " items outstanding, expected the blocker and two draws)"); } std::this_thread::sleep_for(std::chrono::milliseconds(1)); } - blocker.release = true; + blocker.Release(); Finish(); + blocker.Join(); Flush(FirstMemory); Flush(SecondMemory); SameAsDirect(FirstMemory, "the first queued indirect draw"); @@ -219,12 +235,13 @@ void IndirectDrawWaitsForItsQueuedRecordWrite() { AppendTarget(commands, WrittenMemory, Clipped); AppendIndirect(commands); Blocker blocker; - SubmitBehind(blocker, commands); + const auto drainsBefore = DrawPipeline::DrainRequestsByOpcode(0x100); + SubmitBehind(blocker, commands, 1); const auto start = std::chrono::steady_clock::now(); - while (DrawPipeline::Queue0().Outstanding() < 2 && std::chrono::steady_clock::now() - start < std::chrono::seconds(10)) std::this_thread::sleep_for(std::chrono::milliseconds(1)); - std::this_thread::sleep_for(std::chrono::milliseconds(500)); - blocker.release = true; + while (DrawPipeline::DrainRequestsByOpcode(0x100) == drainsBefore && std::chrono::steady_clock::now() - start < std::chrono::seconds(10)) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + blocker.Release(); Finish(); + blocker.Join(); Flush(WrittenMemory); Require(LateRecords[0] == 3, "the queued label did not store the record's vertex count"); SameAsDirect(WrittenMemory, "the indirect draw whose record a queued label wrote"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PixelBarycentricEmulation.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PixelBarycentricEmulation.cpp index f9813bbe14..f364c83039 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PixelBarycentricEmulation.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PixelBarycentricEmulation.cpp @@ -57,6 +57,14 @@ std::array, 3> ClipPositions() { return positions; } +constexpr std::array StripCorners{{{-0.8f, -0.8f, 1.0f}, {0.8f, -0.8f, 1.0f}, {-0.8f, 0.8f, 1.0f}, {0.8f, 0.8f, 1.0f}}}; + +std::array, 4> StripPositions() { + std::array, 4> positions{}; + for (std::size_t i = 0; i < StripCorners.size(); ++i) positions[i] = {StripCorners[i].x, StripCorners[i].y, 0.5f, 1.0f}; + return positions; +} + std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { const auto address = reinterpret_cast(data); return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; @@ -67,11 +75,13 @@ enum class Emulation { Tessellation, }; -ShaderRecompiler::RecompileResult Draw(AgcDriver::VulkanDevice& device, Emulation emulation, std::span pixelCode, std::uint32_t inputs, VkCullModeFlags cullMode = VK_CULL_MODE_NONE, bool flat = false, VkProvokingVertexModeEXT provoking = VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT) { +ShaderRecompiler::RecompileResult Draw(AgcDriver::VulkanDevice& device, Emulation emulation, std::span pixelCode, std::uint32_t inputs, VkCullModeFlags cullMode = VK_CULL_MODE_NONE, bool flat = false, VkProvokingVertexModeEXT provoking = VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT, bool strip = false) { Pixels.fill(Unwritten); auto target = device.Target(); target.fragmentShaderBarycentricEnabled = false; - static const auto positions = ClipPositions(); + static const auto trianglePositions = ClipPositions(); + static const auto stripPositions = StripPositions(); + const auto positions = strip ? std::span>(stripPositions) : std::span>(trianglePositions); std::vector vertexUserData(4, 0u); const auto vertexBuffer = BufferDescriptor(positions.data(), 16u, static_cast(positions.size())); @@ -125,7 +135,7 @@ ShaderRecompiler::RecompileResult Draw(AgcDriver::VulkanDevice& device, Emulatio state.colors = {state.color}; state.hasColorTarget = true; state.renderExtent = {Width, Height}; - state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.topology = strip ? VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP : VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; state.negativeOneToOne = false; state.scissor = {{0, 0}, {Width, Height}}; @@ -282,6 +292,87 @@ void CheckWinding(AgcDriver::VulkanDevice& device, Emulation emulation, const st } } +struct StripPixel { + bool inside; + std::array order; + std::array weights; +}; + +StripPixel StripBarycentrics(std::uint32_t x, std::uint32_t y, bool snapped) { + std::array, 4> window{}; + for (std::size_t i = 0; i < StripCorners.size(); ++i) { + window[i] = {(StripCorners[i].x + 1.0) * Width / 2.0, (1.0 - StripCorners[i].y) * Height / 2.0}; + if (snapped) { + for (auto& coordinate : window[i]) coordinate = std::round(coordinate * 256.0) / 256.0; + } + } + const double px = x + 0.5; + const double py = y + 0.5; + const auto edge = [](const std::array& a, const std::array& b, double cx, double cy) { return (b[0] - a[0]) * (cy - a[1]) - (b[1] - a[1]) * (cx - a[0]); }; + for (const std::array order : {std::array{0, 1, 2}, std::array{1, 3, 2}}) { + const auto& a = window[order[0]]; + const auto& b = window[order[1]]; + const auto& c = window[order[2]]; + const double area = edge(a, b, c[0], c[1]); + const std::array weights{edge(b, c, px, py) / area, edge(c, a, px, py) / area, edge(a, b, px, py) / area}; + if (std::min({weights[0], weights[1], weights[2]}) > 0.02) return {true, order, weights}; + } + return {false, {}, {}}; +} + +void CheckStrip(AgcDriver::VulkanDevice& device, Emulation emulation, const std::string& name) { + Draw(device, emulation, BarycentricPixelCode, PixelInputBit(PixelInput::LinearCenter), VK_CULL_MODE_NONE, false, VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT, true); + std::array checked{}; + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto expected = StripBarycentrics(x, y, false); + if (!expected.inside) continue; + const auto snapped = StripBarycentrics(x, y, true); + for (std::uint32_t channel = 0; channel < 4; ++channel) { + const auto reference = static_cast(expected.weights[1u + (channel & 1u)]); + const auto snappedReference = snapped.inside && snapped.order == expected.order ? static_cast(snapped.weights[1u + (channel & 1u)]) : reference; + Require(MatchesEither(Pixel(x, y, channel), reference, snappedReference), name + " triangle strip: pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") channel " + std::to_string(channel) + " is " + std::to_string(Pixel(x, y, channel)) + ", expected " + std::to_string(reference)); + } + ++checked[expected.order[0]]; + } + } + Require(checked[0] > 200u && checked[1] > 200u, name + " triangle strip: too few pixels of each triangle were checked"); + static const auto positions = StripPositions(); + Draw(device, emulation, VertexValuePixelCode, PixelInputBit(PixelInput::PerspectiveCenter), VK_CULL_MODE_NONE, false, VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT, true); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto expected = StripBarycentrics(x, y, false); + if (!expected.inside) continue; + const auto first = positions[expected.order[0]][0]; + const std::array values{positions[expected.order[1]][0] - first, positions[expected.order[2]][0] - first, first}; + for (std::uint32_t channel = 0; channel < 3; ++channel) { + Require(Pixel(x, y, channel) == values[channel], name + " triangle strip v_interp_mov: pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") channel " + std::to_string(channel) + " is " + std::to_string(Pixel(x, y, channel)) + ", expected " + std::to_string(values[channel])); + } + } + } + std::printf("barycentric emulation through the %s passed for a triangle strip: %u and %u pixels of its even and odd triangle\n", name.c_str(), checked[0], checked[1]); +} + +void CheckStripFlatLast(AgcDriver::VulkanDevice& device, Emulation emulation, const std::string& name) { + Draw(device, emulation, FlatBarycentricPixelCode, PixelInputBit(PixelInput::PerspectiveCenter), VK_CULL_MODE_NONE, true, VK_PROVOKING_VERTEX_MODE_LAST_VERTEX_EXT, true); + const auto positions = StripPositions(); + std::array checked{}; + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto expected = StripBarycentrics(x, y, false); + if (!expected.inside) continue; + const auto triangle = expected.order[0]; + const auto provoking = triangle == 0u ? 2u : 3u; + for (std::uint32_t channel = 2; channel < 4; ++channel) { + Require(Pixel(x, y, channel) == positions[provoking][0], name + " flat last-provoking strip: pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") flat channel " + std::to_string(channel) + " is " + std::to_string(Pixel(x, y, channel)) + ", expected vertex " + std::to_string(provoking) + " value " + std::to_string(positions[provoking][0])); + } + ++checked[triangle]; + } + } + Require(checked[0] > 200u && checked[1] > 200u, name + " flat last-provoking strip: too few pixels of each triangle were checked"); + std::printf("flat last-provoking strip through %s passed: %u and %u pixels of its even and odd triangle\n", name.c_str(), checked[0], checked[1]); +} + void RunEmulation(AgcDriver::VulkanDevice& device, Emulation emulation) { const std::string name = emulation == Emulation::Geometry ? "geometry stage" : "tessellation stages"; const auto perspective = PixelInputBit(PixelInput::PerspectiveCenter); @@ -305,8 +396,10 @@ void RunEmulation(AgcDriver::VulkanDevice& device, Emulation emulation) { if (emulation == Emulation::Tessellation) { Draw(device, emulation, FlatBarycentricPixelCode, perspective, VK_CULL_MODE_NONE, true, VK_PROVOKING_VERTEX_MODE_LAST_VERTEX_EXT); CheckFlat(name + " flat input, last provoking vertex", 2u); + CheckStripFlatLast(device, emulation, name); } - std::printf("barycentric emulation through the %s passed: %u perspective and %u linear I/J pixels, raw vertex values, host interpolation, face culling and flat inputs\n", name.c_str(), smooth, noPerspective); + CheckStrip(device, emulation, name); + std::printf("barycentric emulation through the %s passed: %u perspective and %u linear I/J pixels, raw vertex values, host interpolation, face culling, flat inputs and triangle strips\n", name.c_str(), smooth, noPerspective); } } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PixelSampleCoverage.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PixelSampleCoverage.cpp index 7c6d5398c2..ce79a9b375 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PixelSampleCoverage.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PixelSampleCoverage.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -30,6 +31,14 @@ #include #include +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context Context(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} + namespace { using AgcDriver::Graphics::Require; @@ -54,7 +63,12 @@ constexpr std::array, 3> HalfTriangle{{ {-1.0f, -1.0f, 0.5f, 1.0f}, {1.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 1.0f, 0.5f, 1.0f} }}; -alignas(256) std::array, 4> Programs{}; +constexpr std::array, 3> OtherHalfTriangle{{ + {1.0f, -1.0f, 0.5f, 1.0f}, {1.0f, 1.0f, 0.5f, 1.0f}, {-1.0f, 1.0f, 0.5f, 1.0f} +}}; + +alignas(256) std::array, 8> Programs{}; +alignas(256) std::array DepthMemory{}; std::size_t programCount = 0; std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { @@ -76,6 +90,22 @@ std::span Program(std::uint32_t coverageVgpr) { return std::span(code.data(), at); } +std::span SampleIndexProgram() { + Require(programCount < Programs.size(), "too many test programs"); + auto& code = Programs[programCount++]; + std::size_t at = 0; + code[at++] = 0xd5480018u; + code[at++] = 0x02111100u; + code[at++] = 0x7e000c00u | (20u << 17u) | (256u + 24u); + code[at++] = 0x7e000280u | (21u << 17u); + code[at++] = 0x7e000280u | (22u << 17u); + code[at++] = 0x7e0002f2u | (23u << 17u); + code[at++] = 0xf800180fu; + code[at++] = 0x17161514u; + code[at++] = 0xbf810000u; + return std::span(code.data(), at); +} + struct Block { explicit Block(std::size_t bytes) : bytes(bytes), watched(AgcDriver::GuestMemory::WriteWatched()) { #ifdef _WIN32 @@ -150,7 +180,7 @@ AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block, std::uint32_t return color; } -void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, std::uint32_t inputAddr, std::span pixelCode, std::span, 3> triangle = Triangle, std::uint32_t rasterSamples = 1) { +void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, std::uint32_t inputAddr, std::span pixelCode, std::span, 3> triangle = Triangle, std::uint32_t rasterSamples = 1, const AgcDriver::Graphics::DepthTarget* depth = nullptr, bool perSample = false) { const auto target = device.Target(); constexpr std::uint32_t waveSize = 64; std::vector vertexUserData(4, 0u); @@ -171,6 +201,8 @@ void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarge pixel.inputAddr = inputAddr; pixel.ancillary = (inputAddr & ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::Ancillary)) != 0u; pixel.hasPerspectiveCenterVgpr = (inputAddr & ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PerspectiveCenter)) != 0u; + pixel.sampleShading = perSample; + pixel.iteratesSamples = perSample; pixel.targetOutputMode.at(0) = Float32AbgrExport; pixel.targetExportMapping.fill(0xe4u); const std::vector pixelUserData(8, 0u); @@ -207,7 +239,15 @@ void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarge state.blend = blend; state.blendConstants = {}; state.samples = rasterSamples; - state.mergedCoverage = rasterSamples > 1; + state.mergedCoverage = rasterSamples > 1 && depth == nullptr; + state.truncatedCoverage = rasterSamples > 1 && depth != nullptr; + state.perSampleShading = perSample; + if (depth != nullptr) { + state.depth = *depth; + state.depthTest = true; + state.depthWrite = false; + state.depthCompare = VK_COMPARE_OP_ALWAYS; + } const AgcDriver::Pm4::DrawParameters draw{0, static_cast(triangle.size()), 0, 1, 0, false}; device.Draw(state, draw, shaders); } @@ -273,6 +313,67 @@ bool ExpectMerged(AgcDriver::VulkanDevice& device) { return true; } +std::string ExpectTruncated(AgcDriver::VulkanDevice& device) { + if ((AgcDriver::VulkanDeviceTestAccess::Context(device).limits.framebufferDepthSampleCounts & VK_SAMPLE_COUNT_2_BIT) == 0) return "the device has no two-sample depth targets (framebufferDepthSampleCounts)"; + constexpr std::size_t bytes = std::size_t{Width} * Height * 16u; + AgcDriver::Graphics::DepthTarget depth{reinterpret_cast(DepthMemory.data()), 0u, {Width, Height}, VK_FORMAT_D32_SFLOAT, 0.0f, 0}; + depth.samples = 2; + std::array written{}; + std::uint32_t untouched = 0; + for (const auto* triangle : {&HalfTriangle, &OtherHalfTriangle}) { + Block block(bytes); + std::fill(block.data, block.data + bytes, std::uint8_t{0xff}); + const auto color = DecodeTarget(block, Rgba32FloatInfo); + try { + Draw(device, color, ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::SampleCoverage), Program(0u), *triangle, 2u, &depth); + } catch (const std::exception& error) { + if (std::string_view(error.what()).find("truncated coverage reduction") == std::string_view::npos) throw; + return "the device lacks VK_AMD_mixed_attachment_samples and VK_NV_framebuffer_mixed_samples with truncated coverage reduction"; + } + const auto stored = ReadBack(device, block.Address(), bytes); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto* texel = stored.data() + (static_cast(y) * Width + x) * 16u; + if (std::all_of(texel, texel + 16, [](std::uint8_t byte) { return byte == 0xffu; })) { + ++untouched; + continue; + } + std::array value{}; + std::memcpy(value.data(), texel, sizeof(value)); + const auto mask = static_cast(value[0]); + Require(value[0] == static_cast(mask) && mask >= 1u && mask <= 3u && value[1] == 0.0f && value[2] == 0.0f && value[3] == 1.0f, "truncated coverage: pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") holds (" + std::to_string(value[0]) + ", " + std::to_string(value[1]) + ", " + std::to_string(value[2]) + ", " + std::to_string(value[3]) + ")"); + ++written[mask]; + } + } + } + Require(written[2] == 0, "truncated coverage: " + std::to_string(written[2]) + " pixels that cover only sample 1 were written into the one-sample target"); + Require(written[1] != 0 && written[3] != 0 && untouched != 0, "truncated coverage: two half-covering triangles wrote " + std::to_string(written[3]) + " fully covered and " + std::to_string(written[1]) + " sample-0-only pixels and left " + std::to_string(untouched)); + std::uint32_t perSampleWritten = 0; + std::uint32_t perSampleUntouched = 0; + for (const auto* triangle : {&HalfTriangle, &OtherHalfTriangle}) { + Block block(bytes); + std::fill(block.data, block.data + bytes, std::uint8_t{0xff}); + const auto color = DecodeTarget(block, Rgba32FloatInfo); + Draw(device, color, ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::Ancillary), SampleIndexProgram(), *triangle, 2u, &depth, true); + const auto stored = ReadBack(device, block.Address(), bytes); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto* texel = stored.data() + (static_cast(y) * Width + x) * 16u; + if (std::all_of(texel, texel + 16, [](std::uint8_t byte) { return byte == 0xffu; })) { + ++perSampleUntouched; + continue; + } + std::array value{}; + std::memcpy(value.data(), texel, sizeof(value)); + Require(value == std::array{0.0f, 0.0f, 0.0f, 1.0f}, "truncated coverage per sample: pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") holds (" + std::to_string(value[0]) + ", " + std::to_string(value[1]) + ", " + std::to_string(value[2]) + ", " + std::to_string(value[3]) + "), not sample 0's output"); + ++perSampleWritten; + } + } + } + Require(perSampleWritten == written[1] + written[3] && perSampleUntouched == untouched, "truncated coverage per sample: the draws wrote " + std::to_string(perSampleWritten) + " pixels where one invocation per pixel wrote " + std::to_string(written[1] + written[3])); + return {}; +} + } int main() { @@ -285,6 +386,7 @@ int main() { Expect(*device, PixelInputBit(PixelInput::PerspectiveCenter) | PixelInputBit(PixelInput::SampleCoverage), 2u, "SAMPLE_COVERAGE after the perspective center"); Expect(*device, PixelInputBit(PixelInput::Ancillary) | PixelInputBit(PixelInput::SampleCoverage), 1u, "SAMPLE_COVERAGE after ANCILLARY"); if (!ExpectMerged(*device)) std::puts("merged coverage not tested: the device lacks VK_NV_framebuffer_mixed_samples with merged coverage reduction"); + if (const auto skipped = ExpectTruncated(*device); !skipped.empty()) std::printf("truncated coverage not tested: %s\n", skipped.c_str()); std::puts("pixel sample coverage tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp index 4d4f77cca1..abe81b5503 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp @@ -9,6 +9,9 @@ #include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" #include "prx/libSceAgcDriver/Graphics/include/State.hpp" #include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" #include "Recompiler.hpp" #include "Optimization/ResourceProgram.hpp" #include "VulkanTestDevice.hpp" @@ -17,6 +20,8 @@ #define NOMINMAX #endif #include +#else +#include #endif #include #include @@ -120,14 +125,31 @@ constexpr std::array, 3> Triangle{{ alignas(256) std::array Buffer{}; struct Block { - explicit Block(std::size_t bytes) : bytes(bytes) { + explicit Block(std::size_t bytes, bool watched = false) : bytes(bytes), watched(watched) { #ifdef _WIN32 - data = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + if (watched) { + data = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, BlockBytes)); + GuestArena::GuestArenaCommit_nid_postfix(data, bytes, PAGE_READWRITE, bytes); + } else { + data = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + } #else - data = static_cast(std::aligned_alloc(BlockBytes, bytes)); + if (watched) { + void* raw = mmap(nullptr, bytes + BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(raw != MAP_FAILED, "cannot map the color target"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + BlockBytes - 1) & ~(static_cast(BlockBytes) - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned + bytes != begin + bytes + BlockBytes) munmap(reinterpret_cast(aligned + bytes), begin + BlockBytes - aligned); + data = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(data, bytes); + } else { + data = static_cast(std::aligned_alloc(BlockBytes, bytes)); + } #endif - Require(data != nullptr, "cannot allocate the multisampled color target"); + Require(data != nullptr, "cannot allocate the color target"); std::memset(data, 0, bytes); + Require(!watched || AgcDriver::GuestMemory::Watched(Address(), bytes), "the color target is not write-watched"); GuestAllocations::Mutation mutation; mutation.Add(data, bytes, true, true, true); } @@ -137,15 +159,26 @@ struct Block { mutation.Remove(data); } #ifdef _WIN32 - VirtualFree(data, 0, MEM_RELEASE); + if (watched) { + GuestArena::GuestArenaReset_nid_postfix(data, bytes); + GuestArena::GuestArenaRelease_nid_postfix(data, bytes); + } else { + VirtualFree(data, 0, MEM_RELEASE); + } #else - std::free(data); + if (watched) { + munmap(data, bytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(data, bytes); + } else { + std::free(data); + } #endif } Block(const Block&) = delete; Block& operator=(const Block&) = delete; std::uint64_t Address() const { return reinterpret_cast(data); } std::size_t bytes; + bool watched; std::uint8_t* data = nullptr; }; @@ -214,7 +247,7 @@ AgcDriver::Graphics::State TargetState(const AgcDriver::Graphics::ColorTarget& c state.blends = {blend}; state.blend = blend; state.blendConstants = {}; - state.samples = Samples; + state.samples = color.samples; return state; } @@ -349,19 +382,19 @@ void RequireStoreRefused(AgcDriver::VulkanDevice& device, const std::array> RunLoad(AgcDriver::VulkanDevice& device, const std::array& texture, bool arrayed) { +std::vector> RunLoad(AgcDriver::VulkanDevice& device, const std::array& texture, bool arrayed, std::uint32_t fragments) { std::vector userData; const auto program = CompileLoad(device, texture, userData, arrayed); - std::vector> loaded(Side * Side * Samples); + std::vector> loaded(Side * Side * fragments); const auto codeAddress = reinterpret_cast((arrayed ? ArrayLoadCode : LoadCode).data()); for (std::uint32_t first = 0; first < loaded.size(); first += Threads) { Buffer.fill(0xdeadbeefu); for (std::uint32_t tid = 0; tid < Threads; ++tid) { const auto index = first + tid; - const auto pixel = index / Samples; + const auto pixel = index / fragments; const std::array words = arrayed - ? std::array{0u, 0u, pixel % Side, pixel / Side, 0u, index % Samples} - : std::array{0u, 0u, 0u, pixel % Side, pixel / Side, index % Samples}; + ? std::array{0u, 0u, pixel % Side, pixel / Side, 0u, index % fragments} + : std::array{0u, 0u, 0u, pixel % Side, pixel / Side, index % fragments}; std::copy(words.begin(), words.end(), &Buffer[tid * Words]); } device.Dispatch(program, 1, 1, 1, {}, codeAddress); @@ -373,9 +406,9 @@ std::vector> RunLoad(AgcDriver::VulkanDevice& device, const return loaded; } -std::vector> LoadSamples(AgcDriver::VulkanDevice& device, const std::array& texture) { - auto loaded = RunLoad(device, texture, false); - const auto loadedArray = RunLoad(device, texture, true); +std::vector> LoadSamples(AgcDriver::VulkanDevice& device, const std::array& texture, std::uint32_t fragments = Samples) { + auto loaded = RunLoad(device, texture, false, fragments); + const auto loadedArray = RunLoad(device, texture, true, fragments); Require(loadedArray == loaded, "Dim2DMsaaArray image_load disagreed with Dim2DMsaa image_load on the multisampled color target"); return loaded; } @@ -643,12 +676,210 @@ int NativeVertexInput(std::unique_ptr& device) { return 0; } +alignas(256) constexpr std::array NativeFmaskCode{0x34020084u, 0xe0381000u, 0x80000201u, 0xbf8c3f70u, 0xf0001f08u, 0x00020602u, 0xbf8c3f70u, 0xe0781000u, 0x80010601u, 0xbf810000u}; +alignas(256) std::array NativeFmaskInput{}; +alignas(256) std::array NativeFmaskOutput{}; + +std::array NativeFmaskDescriptor(const Block& fmask, const Block& cmask, std::uint32_t pipeXor) { + const auto address = fmask.Address() | pipeXor; + const auto metadata = cmask.Address(); + return {static_cast(address >> 8u), static_cast(address >> 40u) | (166u << 20u) | (((Side - 1u) & 3u) << 30u), ((Side - 1u) >> 2u) | ((Side - 1u) << 14u), 4u | (1u << 3u) | (4u << 6u) | (24u << 20u) | (9u << 28u), 0u, 0u, (1u << 19u) | (1u << 21u) | (static_cast(metadata >> 8u) << 24u), static_cast(metadata >> 16u)}; +} + +ShaderRecompiler::RecompileResult CompileNativeFmask(AgcDriver::VulkanDevice& device, const std::array& image, bool registration) { + std::vector userData(16u); + const auto buffer = [](const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return std::array{static_cast(address), static_cast(address >> 32u) & 0xffffu, bytes, 0x31016facu}; + }; + const auto input = buffer(NativeFmaskInput.data(), sizeof(NativeFmaskInput)); + const auto output = buffer(NativeFmaskOutput.data(), Threads * 16u); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4u); + std::copy(image.begin(), image.end(), userData.begin() + 8u); + const std::array memory{{{reinterpret_cast(NativeFmaskCode.data()), std::as_bytes(std::span(NativeFmaskCode))}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + auto target = device.Target(); + target.registrationModules = registration; + ShaderRecompiler::RecompileRequest request{{ShaderStage::Compute, reinterpret_cast(NativeFmaskCode.data()), NativeFmaskCode, 0u, {}}, {32u, 0u, userData, compute, std::nullopt, std::nullopt, memory}, target, {0u, 0u, 0u, 128u}}; + request.useCache = false; + return ShaderRecompiler::Recompile(request); +} + +void DispatchNativeFmask(AgcDriver::VulkanDevice& device, const std::array& image, bool registration) { + device.Dispatch(CompileNativeFmask(device, image, registration), 1u, 1u, 1u); + device.WaitIdle(); +} + +void NativeFmask(std::unique_ptr& device) { + using namespace AgcDriver::Graphics; + Block colorMemory(4u * BlockBytes, true); + Block dcc(BlockBytes, true); + Block cmask(BlockBytes, true); + Block fmask(BlockBytes, true); + Block unrelated(BlockBytes, true); + struct ReleaseDevice { + std::unique_ptr& value; + ~ReleaseDevice() { value.reset(); } + } releaseDevice{device}; + constexpr std::uint32_t sentinel = 0xdeadbeefu; + for (const auto pipeXor : {0u, 0xf00u}) { + std::memset(colorMemory.data, 0, colorMemory.bytes); + std::memset(dcc.data, 0, dcc.bytes); + std::memset(cmask.data, 0xcc, cmask.bytes); + std::memset(fmask.data, 0x11, fmask.bytes); + AgcDriver::Registers cx{{0x318, static_cast(colorMemory.Address() >> 8u)}, {0x31b, 0u}, {0x31c, Float16x4Info | 0x10006000u}, {0x31d, (3u << 12u) | (3u << 15u)}, {0x31e, 0u}, {0x31f, static_cast(cmask.Address() >> 8u)}, {0x321, static_cast((fmask.Address() | pipeXor) >> 8u)}, {0x325, static_cast(dcc.Address() >> 8u)}, {0x390, static_cast(colorMemory.Address() >> 40u)}, {0x398, static_cast(cmask.Address() >> 40u)}, {0x3a0, static_cast(fmask.Address() >> 40u)}, {0x3a8, static_cast(dcc.Address() >> 40u)}, {0x3b0, ((Side - 1u) << 14u) | (Side - 1u)}, {0x3b8, TiledAttrib3}}; + const auto color = DecodeColorBuffer(cx, 0u); + Require(color.samples == 8u && color.cmaskAddress == cmask.Address(), "native FMASK fixture lost its decoded target association"); + std::array, 8> expected{}; + for (std::uint32_t sample = 0u; sample < 8u; ++sample) { + expected[sample] = {static_cast(sample + 1u) / 8.0f, 0.25f, 0.5f, 1.0f}; + auto pixel = PixelCode; + pixel[1] = std::bit_cast(expected[sample][0]); + pixel[3] = std::bit_cast(expected[sample][1]); + pixel[5] = std::bit_cast(expected[sample][2]); + pixel[7] = std::bit_cast(expected[sample][3]); + auto state = TargetState(color); + state.quadSampleMask.fill(static_cast(1u << sample)); + Draw(*device, state, 0u, 2u, pixel); + } + const auto native = LoadSamples(*device, TargetDescriptor(colorMemory, 3u), 8u); + std::vector covered; + for (std::uint32_t pixel = 0; pixel < Side * Side; ++pixel) { + bool all = true; + for (std::size_t sample = 0; sample < 8u; ++sample) all = all && native[pixel * 8u + sample] == expected[sample]; + if (all) covered.push_back(pixel); + } + Require(!covered.empty(), "native FMASK fixture did not render eight distinct sample colors"); + for (std::uint32_t lane = 0; lane < Threads; ++lane) { + const auto pixel = covered[lane % covered.size()]; + NativeFmaskInput[lane * 4u] = pixel % Side; + NativeFmaskInput[lane * 4u + 1u] = pixel / Side; + NativeFmaskInput[lane * 4u + 2u] = 0u; + NativeFmaskInput[lane * 4u + 3u] = 0u; + } + const auto image = NativeFmaskDescriptor(fmask, cmask, pipeXor); + const auto check = [&] { + for (std::uint32_t lane = 0; lane < Threads; ++lane) { + const std::array mapping{0x76543210u, 1u, 0x76543210u, 0u}; + for (std::size_t component = 0; component < 4u; ++component) Require(NativeFmaskOutput[lane * 4u + component] == mapping[component], "native FMASK load did not preserve its expanded sample indices and selectors"); + for (std::uint32_t sample = 0; sample < 8u; ++sample) { + const auto fragment = NativeFmaskOutput[lane * 4u] >> (4u * sample) & 15u; + const auto pixel = NativeFmaskInput[lane * 4u + 1u] * Side + NativeFmaskInput[lane * 4u]; + Require(native[pixel * 8u + fragment] == expected[sample], "native FMASK remapped a sampled color to another fragment"); + } + } + Require(std::all_of(NativeFmaskOutput.begin() + Threads * 4u, NativeFmaskOutput.end(), [](auto word) { return word == sentinel; }), "native FMASK wrote beyond its output"); + }; + for (const auto registration : {false, true}) { + for (std::uint32_t repeat = 0; repeat < 2u; ++repeat) { + NativeFmaskOutput.fill(sentinel); + DispatchNativeFmask(*device, image, registration); + check(); + } + const auto program = CompileNativeFmask(*device, image, registration); + const auto codeAddress = reinterpret_cast(NativeFmaskCode.data()); + std::shared_ptr recipe; + NativeFmaskOutput.fill(sentinel); + device->Dispatch(program, 1u, 1u, 1u, {}, codeAddress, nullptr, &recipe); + device->WaitIdle(); + check(); + Require(recipe == nullptr, "native FMASK emitted a reusable recipe without an ownership proof"); + const auto prepared = device->PrepareDispatch(program, {}, Threads, codeAddress); + Require(prepared != nullptr, "native FMASK did not prepare a deferred dispatch"); + NativeFmaskOutput.fill(sentinel); + device->Dispatch(program, 1u, 1u, 1u, {}, codeAddress, prepared); + device->WaitIdle(); + check(); + } + Require(LoadSamples(*device, TargetDescriptor(colorMemory, 3u), 8u) == native, "FMASK sampling changed the native samples"); + Require(std::all_of(fmask.data, fmask.data + fmask.bytes, [](auto byte) { return byte == 0x11u; }) && std::all_of(cmask.data, cmask.data + cmask.bytes, [](auto byte) { return byte == 0xccu; }), "native FMASK sampling published or changed guest metadata"); + const auto refused = [&](const auto& descriptor, std::string_view reason) { + NativeFmaskOutput.fill(sentinel); + std::string error; + try { DispatchNativeFmask(*device, descriptor, false); } catch (const std::runtime_error& e) { error = e.what(); } + Require(error.find(reason) != std::string::npos, "native FMASK missing refusal: " + error); + Require(std::all_of(NativeFmaskOutput.begin(), NativeFmaskOutput.end(), [sentinel](auto word) { return word == sentinel; }), "rejected native FMASK wrote output"); + }; + refused(NativeFmaskDescriptor(unrelated, cmask, pipeXor), "descriptor does not match"); + refused(NativeFmaskDescriptor(fmask, unrelated, pipeXor), "descriptor does not match"); + refused(NativeFmaskDescriptor(fmask, cmask, pipeXor ^ 0x100u), "descriptor does not match"); + auto resized = image; + resized[2] += 1u; + refused(resized, "extent or subresource does not match"); + const auto deferredProgram = CompileNativeFmask(*device, image, false); + const auto codeAddress = reinterpret_cast(NativeFmaskCode.data()); + const auto preparedBeforeStore = device->PrepareDispatch(deferredProgram, {}, Threads, codeAddress); + Require(preparedBeforeStore != nullptr, "native FMASK did not prepare its metadata invalidation probe"); + std::array same{}; + same.fill(std::byte{0x11}); + AgcDriver::GuestMemory::Write(fmask.Address(), same); + refused(image, "writes to a resident native FMASK"); + NativeFmaskOutput.fill(sentinel); + std::string deferredError; + try { + device->Dispatch(deferredProgram, 1u, 1u, 1u, {}, codeAddress, preparedBeforeStore); + device->WaitIdle(); + } catch (const std::runtime_error& error) { + deferredError = error.what(); + } + Require(deferredError.find("writes to a resident native FMASK") != std::string::npos, "deferred native FMASK missed its metadata store: " + deferredError); + Require(std::all_of(NativeFmaskOutput.begin(), NativeFmaskOutput.end(), [](auto word) { return word == sentinel; }), "rejected deferred native FMASK wrote output"); + device->SubmitRecorded(true); + device->WaitIdle(); + ClearMultisampleTargets(device->Device()); + ClearCachedTextures(device->Device()); + cx[0x31c] &= ~0x2000u; + cx[0x31f] = 0u; + cx[0x398] = 0u; + const auto withoutCmask = DecodeColorBuffer(cx, 0u); + Require(withoutCmask.cmaskAddress == 0u, "the no-CMASK control retained metadata"); + Draw(*device, TargetState(withoutCmask)); + const auto uncompressed = LoadSamples(*device, TargetDescriptor(colorMemory, 3u), 8u); + Require(std::find(uncompressed.begin(), uncompressed.end(), Drawn) != uncompressed.end(), "tracking native FMASK broke ordinary DCC rendering without CMASK"); + device->SubmitRecorded(true); + device->WaitIdle(); + ClearMultisampleTargets(device->Device()); + ClearCachedTextures(device->Device()); + Draw(*device, TargetState(color)); + auto recreated = color; + recreated.pipeBankXor ^= 0x4000u; + recreated.fmaskAddress = 0u; + recreated.fmaskPipeBankXor = 0u; + Draw(*device, TargetState(recreated)); + std::array formerFmask{}; + formerFmask.fill(std::byte{0x22}); + AgcDriver::GuestMemory::Write(fmask.Address(), formerFmask); + Draw(*device, TargetState(recreated)); + auto recreatedDescriptor = TargetDescriptor(colorMemory, 3u); + recreatedDescriptor[0] |= recreated.pipeBankXor >> 8u; + const auto recreatedSamples = LoadSamples(*device, recreatedDescriptor, 8u); + Require(std::find(recreatedSamples.begin(), recreatedSamples.end(), Drawn) != recreatedSamples.end(), "former FMASK metadata affected the recreated native target"); + device->SubmitRecorded(true); + device->WaitIdle(); + ClearMultisampleTargets(device->Device()); + ClearCachedTextures(device->Device()); + std::printf("native FMASK: XOR=0x%x, eight distinct samples, both registration modes, cached reuse, association/extent/write refusals and unchanged guest metadata passed\n", pipeXor); + } +} + } int main(int argc, char** argv) { try { auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; + if (argc == 2 && std::string_view(argv[1]) == "--native-fmask") { + { + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(*device); + if (!context.storageImageMultisample || (context.limits.framebufferColorSampleCounts & VK_SAMPLE_COUNT_8_BIT) == 0) { + std::puts("native FMASK skipped, the device does not render or store 8-sample color"); + return VulkanTestSkipped; + } + } + NativeFmask(device); + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--native-vertex-input") return NativeVertexInput(device); Block block(4u * BlockBytes); Block other(BlockBytes); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ShutdownThreads.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ShutdownThreads.cpp index 8f835d52de..82832ba244 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ShutdownThreads.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ShutdownThreads.cpp @@ -76,6 +76,41 @@ void ClosingJoinsPreparation() { Require(ranHere && RegistrationPreparationThreads() == 0, "a closed preparation pool restarted after being joined"); } +void ClosingDropsQueuedPrebuilds() { + using namespace AgcDriver::DriverDetail; + std::promise started; + auto running = started.get_future(); + std::promise gate; + auto opened = gate.get_future().share(); + bool queuedRan = false; + SubmitLibraryPrebuild([&] { started.set_value(); opened.wait(); }); + for (int index = 0; index < 3; ++index) SubmitLibraryPrebuild([&] { queuedRan = true; }); + running.wait(); + auto entering = std::make_shared>(); + auto entered = entering->get_future(); + auto awaited = std::make_shared>(); + auto woken = awaited->get_future(); + std::thread([entering, awaited] { + entering->set_value(); + AwaitLibraryPrebuilds(); + awaited->set_value(); + }).detach(); + entered.wait(); + const bool waiting = woken.wait_for(std::chrono::milliseconds(20)) == std::future_status::timeout; + auto closing = std::async(std::launch::async, [] { CloseRegistrationPreparation(); }); + const bool waited = closing.wait_for(std::chrono::milliseconds(20)) == std::future_status::timeout; + gate.set_value(); + const bool returned = closing.wait_for(std::chrono::seconds(10)) == std::future_status::ready; + const bool wokeUp = woken.wait_for(std::chrono::seconds(10)) == std::future_status::ready; + closing.get(); + Require(waiting, "a waiter for the library prebuilds returned while one was still running"); + Require(waited, "closing returned while a library prebuild was still running"); + Require(returned, "closing did not return once the running library prebuild ended"); + Require(wokeUp, "a waiter for the library prebuilds did not return after closing dropped the queued ones"); + Require(!queuedRan, "closing ran the queued library prebuilds"); + Require(RegistrationPreparationThreads() == 0, "closing the library prebuilds left their thread running"); +} + void StoppingJoinsCompareHelpers() { using namespace AgcDriver::Graphics; const std::vector first(std::size_t{2} << 20u, std::byte{1}); @@ -168,6 +203,7 @@ int main(int argc, char** argv) { try { const std::string_view mode = argc > 1 ? argv[1] : ""; if (mode == "--joined") ClosingJoinsPreparation(); + else if (mode == "--queued-prebuilds") ClosingDropsQueuedPrebuilds(); else if (mode == "--compare-helpers") StoppingJoinsCompareHelpers(); else if (mode == "--draw-committer") ShutdownJoinsDrawCommitter(); else if (mode == "--shader-cache-writer") ShutdownJoinsShaderCacheWriter(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SparseBufferHoles.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SparseBufferHoles.cpp index a7a25f9f88..64835ea370 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SparseBufferHoles.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SparseBufferHoles.cpp @@ -325,7 +325,7 @@ void Run(const Context& context, const CopyPipeline& pipeline, bool writable, co std::memset(readback.Bytes().data(), 0xcc, BufferBytes); VkBuffer primed = VK_NULL_HANDLE; { - Buffer earlier(context, BufferBytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + Buffer earlier(context, BufferBytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT); primed = earlier.Handle(); std::memset(earlier.Bytes().data(), 0xa5, BufferBytes); } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp index 87c935f0d2..8eee7b840b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include @@ -946,6 +947,28 @@ void RequireMergedHullWithoutSubgroups(const ShaderRecompiler::SpirvTarget& targ } } +void RequireMergedHullConstantIndices(const ShaderRecompiler::SpirvTarget& target) { + std::uint32_t cursor = 0; + const auto hull = Compile(target, ShaderStage::TessellationControl, MergedHullCode, cursor, MergedTriangles); + const auto words = hull.spirv.Words(); + std::unordered_set constants; + std::unordered_set folded; + for (std::size_t at = 5; at + 1u < words.size(); at += words[at] >> 16u) { + const auto count = words[at] >> 16u; + Require(count != 0u && at + count <= words.size(), "the merged hull program compiled to a malformed SPIR-V instruction"); + const auto opcode = words[at] & 0xffffu; + if (opcode == spv::OpConstant && count >= 3u) constants.insert(words[at + 2u]); + const bool integer = opcode == spv::OpIAdd || opcode == spv::OpIMul || opcode == spv::OpUDiv || opcode == spv::OpUMod || opcode == spv::OpShiftRightLogical || opcode == spv::OpBitwiseAnd; + const auto operand = [&](std::uint32_t id) { return constants.contains(id) || folded.contains(id); }; + if (integer && count == 5u && operand(words[at + 3u]) && operand(words[at + 4u])) folded.insert(words[at + 2u]); + if (opcode == spv::OpAccessChain) { + for (std::uint32_t index = 4u; index < count; ++index) { + Require(!folded.contains(words[at + index]), "the merged hull program indexes a tessellation attribute with arithmetic on constants instead of a constant, which AMD's Windows driver reads wrongly"); + } + } + } +} + void RequireRegisteredMergedStages() { const auto low = [](const auto& code) { return static_cast(reinterpret_cast(code.data()) >> 8u); }; const auto high = [](const auto& code) { return static_cast(reinterpret_cast(code.data()) >> 40u); }; @@ -1003,6 +1026,7 @@ int main() { ExpectRefusal(target, ShaderStage::TessellationControl, MergedHullOutsidePatch, "reads LDS outside its patch's control point records", MergedTriangles); ExpectRefusal(target, ShaderStage::TessellationControl, MergedHullReadLane, "another lane's register", MergedTriangles); RequireMergedHullWithoutSubgroups(target); + RequireMergedHullConstantIndices(target); RequirePatches(*device, target, MergedHullCode, "merged hull program", MergedTriangles, MergedHullCode); RequirePatches(*device, target, MergedHullRegisters, "merged hull program storing its local part's registers", MergedTriangles, MergedHullRegisters); RequireCounterClockwise(*device, target); diff --git a/core/libs/prx/libSceAjm.native/AjmLibrary.cmake b/core/libs/prx/libSceAjm.native/AjmLibrary.cmake new file mode 100644 index 0000000000..ec9f5b9a81 --- /dev/null +++ b/core/libs/prx/libSceAjm.native/AjmLibrary.cmake @@ -0,0 +1,15 @@ +function(add_sce_ajm_library target) + set(ajmDir ${CMAKE_CURRENT_FUNCTION_LIST_DIR}) + add_library(${target} SHARED EXCLUDE_FROM_ALL + ${ajmDir}/Export.cpp + ${ajmDir}/src/Ajm.cpp + ${ARGN} + ) + target_include_directories(${target} PRIVATE + ${LIBS_INCLUDE_DIR} + ${CMAKE_SOURCE_DIR}/3rdparty/LibAtrac9/C/src + ${CMAKE_SOURCE_DIR}/3rdparty/vgmstream/src/coding/libs + ) + target_link_libraries(${target} PRIVATE atrac9 atrac9_vibration ffmpeg libc libkernel) + configure_windows_unwind(${target}) +endfunction() diff --git a/core/libs/prx/libSceAjm.native/CMakeLists.txt b/core/libs/prx/libSceAjm.native/CMakeLists.txt index 51b7c2507f..20f6cf1f20 100644 --- a/core/libs/prx/libSceAjm.native/CMakeLists.txt +++ b/core/libs/prx/libSceAjm.native/CMakeLists.txt @@ -29,22 +29,13 @@ add_library(atrac9_vibration STATIC EXCLUDE_FROM_ALL ) set_target_properties(atrac9_vibration PROPERTIES POSITION_INDEPENDENT_CODE ON C_VISIBILITY_PRESET hidden) -add_library(libSceAjm.native SHARED EXCLUDE_FROM_ALL - ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/Ajm.cpp -) - -target_include_directories(libSceAjm.native PRIVATE - ${LIBS_INCLUDE_DIR} - ${LIBATRAC9_DIR} - ${VGMSTREAM_ATRAC9_DIR} -) -target_link_libraries(libSceAjm.native PRIVATE atrac9 atrac9_vibration ffmpeg libc libkernel) -configure_windows_unwind(libSceAjm.native) +include(${CMAKE_CURRENT_SOURCE_DIR}/AjmLibrary.cmake) +add_sce_ajm_library(libSceAjm.native) if(BUILD_TESTING) add_test_executable(atrac9_gradient_tests tests/Atrac9Gradient.c ${LIBATRAC9_DIR}/bit_reader.c) target_include_directories(atrac9_gradient_tests PRIVATE ${LIBATRAC9_PATCHED_DIR} ${LIBATRAC9_DIR}) + target_link_libraries(atrac9_gradient_tests PRIVATE atrac9) target_compile_options(atrac9_gradient_tests PRIVATE -ffunction-sections -fdata-sections) if(APPLE) target_link_options(atrac9_gradient_tests PRIVATE LINKER:-dead_strip) @@ -71,6 +62,11 @@ if(BUILD_TESTING) target_link_libraries(guest_ajm_vibration_tests PRIVATE libSceAjm.native libc) configure_windows_unwind(guest_ajm_vibration_tests) add_test(NAME guest_ajm_vibration COMMAND guest_ajm_vibration_tests) + add_test_executable(guest_ajm_wvorbis_tests ${LIBS_INCLUDE_DIR}/tests/GuestAjmWVorbis.cpp) + target_include_directories(guest_ajm_wvorbis_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_ajm_wvorbis_tests PRIVATE libSceAjm.native libc) + configure_windows_unwind(guest_ajm_wvorbis_tests) + add_test(NAME guest_ajm_wvorbis COMMAND guest_ajm_wvorbis_tests) if(UNIX) add_test_executable(guest_ajm_teardown_tests ${LIBS_INCLUDE_DIR}/tests/GuestAjmTeardown.cpp) target_include_directories(guest_ajm_teardown_tests PRIVATE ${LIBS_INCLUDE_DIR}) @@ -79,7 +75,7 @@ if(BUILD_TESTING) add_test(NAME guest_ajm_teardown COMMAND guest_ajm_teardown_tests $) endif() if(WIN32) - set_tests_properties(guest_ajm guest_ajm_vibration PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_ajm guest_ajm_vibration guest_ajm_wvorbis PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/prx/libSceAjm.native/Export.cpp b/core/libs/prx/libSceAjm.native/Export.cpp index 2a7d6a4f1f..3b0b5e4a8a 100644 --- a/core/libs/prx/libSceAjm.native/Export.cpp +++ b/core/libs/prx/libSceAjm.native/Export.cpp @@ -4,6 +4,7 @@ #include "prx/libc/include/General.hpp" #include #include +#include // Entry points this library does not implement: reported before the unimplemented-call exception so an // APS5_TRACE_AJM run shows which one a title reached. @@ -43,6 +44,38 @@ static_assert(offsetof(WVorbisHeaderPacketParameter, setupPacket) == 32); static_assert(sizeof(WVorbisSetupPacketParameter) == 56); static_assert(offsetof(WVorbisSetupPacketParameter, input) == 16); +namespace { + +constexpr int AJM_WVORBIS_ERROR_INVALID_PARAMETER = static_cast(0x8A6E0001); +constexpr int AJM_WVORBIS_ERROR_INVALID_SIZE = static_cast(0x8A6E0002); +constexpr int AJM_WVORBIS_ERROR_OUTPUT_TOO_SMALL = static_cast(0x8A6E0003); +constexpr int AJM_WVORBIS_ERROR_INVALID_SETUP = static_cast(0x8A6E0004); +constexpr std::size_t WVORBIS_LENGTH_TABLE_SIZE = 10; +constexpr std::size_t VORBIS_IDENTIFICATION_SIZE = 30; +constexpr std::size_t VORBIS_COMMENT_SIZE = 16; +constexpr std::size_t WVORBIS_SETUP_MIN_SIZE = 9; +constexpr std::size_t WVORBIS_SETUP_PART1_LENGTH_OFFSET = 7; +constexpr std::int32_t VORBIS_BLOCK_SIZE_MIN = 64; +constexpr std::int32_t VORBIS_BLOCK_SIZE_MAX = 8192; +constexpr std::uint8_t VORBIS_MAGIC[] {'v', 'o', 'r', 'b', 'i', 's'}; + +std::int32_t WVorbisBlockSize(std::int32_t log2) { + return static_cast(1u << (static_cast(log2) & 31u)); +} + +std::uint8_t* PutLittle(std::uint8_t* out, std::uint32_t value, std::size_t bytes) { + for (std::size_t i = 0; i < bytes; ++i) out[i] = static_cast(value >> (8 * i)); + return out + bytes; +} + +std::uint8_t* PutVorbisHeaderStart(std::uint8_t* out, std::uint8_t type) { + *out++ = type; + std::memcpy(out, VORBIS_MAGIC, sizeof(VORBIS_MAGIC)); + return out + sizeof(VORBIS_MAGIC); +} + +} + extern "C" { int APS5_VABI sceAjmBatchJobEncode(AjmBatchInfo* info, uint32_t instance, const void* pcm_input, size_t pcm_input_size, void* bitstream_output, size_t bitstream_output_size, void* result) { @@ -63,8 +96,45 @@ const char* APS5_VABI sceAjmStrError(int error) { return nullptr; } -int APS5_VABI sceAjmDecWVorbisCreateHeaderPacket(WVorbisHeaderPacketParameter* parameter) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceAjmDecWVorbisCreateHeaderPacket(WVorbisHeaderPacketParameter* param) { + if (param == nullptr) return AJM_WVORBIS_ERROR_INVALID_PARAMETER; + if (param->size != sizeof(WVorbisHeaderPacketParameter)) return AJM_WVORBIS_ERROR_INVALID_SIZE; + if (param->bytesPerSecond < 0) return AJM_WVORBIS_ERROR_INVALID_PARAMETER; + if (WVorbisBlockSize(param->shortBlockExponent) < VORBIS_BLOCK_SIZE_MIN) return AJM_WVORBIS_ERROR_INVALID_PARAMETER; + if (param->longBlockExponent < param->shortBlockExponent || WVorbisBlockSize(param->longBlockExponent) > VORBIS_BLOCK_SIZE_MAX) + return AJM_WVORBIS_ERROR_INVALID_PARAMETER; + if (param->setupPacket == nullptr) return AJM_WVORBIS_ERROR_INVALID_PARAMETER; + if (param->setupPacketSize < WVORBIS_SETUP_MIN_SIZE) return AJM_WVORBIS_ERROR_INVALID_SETUP; + if (param->output == nullptr) return AJM_WVORBIS_ERROR_INVALID_PARAMETER; + constexpr std::size_t headers = WVORBIS_LENGTH_TABLE_SIZE + VORBIS_IDENTIFICATION_SIZE + VORBIS_COMMENT_SIZE; + if (param->outputCapacity < headers || param->setupPacketSize > param->outputCapacity - headers) return AJM_WVORBIS_ERROR_OUTPUT_TOO_SMALL; + + const auto* setup = static_cast(param->setupPacket); + const auto part1 = static_cast(setup[WVORBIS_SETUP_PART1_LENGTH_OFFSET] | setup[WVORBIS_SETUP_PART1_LENGTH_OFFSET + 1] << 8); + auto* out = static_cast(param->output); + out = PutLittle(out, 0, 2); + out = PutLittle(out, VORBIS_IDENTIFICATION_SIZE, 2); + out = PutLittle(out, VORBIS_COMMENT_SIZE, 2); + out = PutLittle(out, part1, 2); + out = PutLittle(out, static_cast(param->setupPacketSize) - part1, 2); + + out = PutVorbisHeaderStart(out, 1); + out = PutLittle(out, 0, 4); + out = PutLittle(out, static_cast(param->channels), 1); + out = PutLittle(out, param->sampleRate, 4); + out = PutLittle(out, 0, 4); + out = PutLittle(out, static_cast(param->bytesPerSecond) * 8u, 4); + out = PutLittle(out, 0, 4); + *out++ = static_cast((param->shortBlockExponent & 0xF) | (param->longBlockExponent & 0xF) << 4); + *out++ = 1; + + out = PutVorbisHeaderStart(out, 3); + out = PutLittle(out, 0, 4); + out = PutLittle(out, 0, 4); + *out++ = 1; + + std::memcpy(out, setup, param->setupPacketSize); + param->outputSize = headers + param->setupPacketSize; return 0; } diff --git a/core/libs/prx/libSceAjm.native/src/Ajm.cpp b/core/libs/prx/libSceAjm.native/src/Ajm.cpp index 69fc72c729..1a27be97b1 100644 --- a/core/libs/prx/libSceAjm.native/src/Ajm.cpp +++ b/core/libs/prx/libSceAjm.native/src/Ajm.cpp @@ -851,14 +851,15 @@ std::uint32_t ReadBigEndian(const std::uint8_t* data, std::size_t bytes) { int ParseMp3Header(const std::uint8_t* stream, std::uint32_t streamSize, int parseOfl, AjmDecMp3ParseFrame* frame); -void ParseMp3Ofl(const std::uint8_t* stream, std::uint32_t streamSize, std::uint32_t header, AjmDecMp3ParseFrame* frame) { +void ParseMp3Ofl(const std::uint8_t* stream, std::uint32_t streamSize, std::uint32_t header, AjmDecMp3ParseFrame* frame, int parseOfl) { const std::size_t end = std::min(streamSize, frame->frame_size); - if (end <= 4) return; - const std::size_t available = end - 4; + const std::size_t sideInfoStart = 4 + (((header >> 16u) & 1u) == 0 ? 2u : 0u); + if (end <= sideInfoStart) return; + const std::size_t available = end - sideInfoStart; std::size_t bit = 0; const auto readBits = [&](std::uint32_t count) { std::uint32_t value = 0; - for (std::uint32_t i = 0; i < count; ++i, ++bit) value = (value << 1u) | (bit / 8 < available ? (stream[4 + bit / 8] >> (7 - bit % 8)) & 1u : 0u); + for (std::uint32_t i = 0; i < count; ++i, ++bit) value = (value << 1u) | (bit / 8 < available ? (stream[sideInfoStart + bit / 8] >> (7 - bit % 8)) & 1u : 0u); return value; }; const bool mpeg1 = ((header >> 19u) & 3u) == 3u; @@ -872,11 +873,20 @@ void ParseMp3Ofl(const std::uint8_t* stream, std::uint32_t streamSize, std::uint } } bit += mainDataBits; - std::size_t at = 4 + std::min((bit + 7) / 8, available); + std::size_t at = sideInfoStart + std::min((bit + 7) / 8, available); - if (at + 8 <= end && (std::memcmp(stream + at, "Xing", 4) == 0 || std::memcmp(stream + at, "Info", 4) == 0)) { - const std::uint8_t flags = stream[at + 7]; - std::size_t field = at + 8; + std::size_t tagAt = at; + if (tagAt + 8 > end || (std::memcmp(stream + tagAt, "Xing", 4) != 0 && std::memcmp(stream + tagAt, "Info", 4) != 0)) { + const std::size_t fixedOffset = mpeg1 ? (mono ? 21u : 36u) : (mono ? 13u : 21u); + tagAt = 0; + if (fixedOffset + 8 <= end && (std::memcmp(stream + fixedOffset, "Xing", 4) == 0 || std::memcmp(stream + fixedOffset, "Info", 4) == 0)) { + tagAt = fixedOffset; + } + } + + if (tagAt != 0) { + const std::uint8_t flags = stream[tagAt + 7]; + std::size_t field = tagAt + 8; std::uint32_t frames = 0; if (flags & 0x01u) { if (field + 4 > end) return; @@ -903,30 +913,34 @@ void ParseMp3Ofl(const std::uint8_t* stream, std::uint32_t streamSize, std::uint frame->encoder_delay = frame->samples_per_channel + delay + 529; frame->ofl_type = MP3_OFL_LAME; } - } else if (at != 36 && end >= 40 && std::memcmp(stream + 36, "VBRI", 4) == 0) { - NotImplemented_nid_no_patch("sceAjmDecMp3ParseFrame (VBRI header outside MPEG-1 stereo)"); - } else if (at + 26 <= end && std::memcmp(stream + at, "VBRI", 4) == 0) { - frame->encoder_delay = ReadBigEndian(stream + at + 6, 2); + return; + } + + const std::size_t vbriAt = end >= 40 && std::memcmp(stream + 36, "VBRI", 4) == 0 ? 36 : at; + if (vbriAt + 26 <= end && std::memcmp(stream + vbriAt, "VBRI", 4) == 0) { + frame->encoder_delay = ReadBigEndian(stream + vbriAt + 6, 2); frame->ofl_type = MP3_OFL_VBRI; - if (frame->frame_size <= streamSize) { + if (parseOfl == 1 && frame->frame_size <= streamSize) { AjmDecMp3ParseFrame next{}; - if (ParseMp3Header(stream + frame->frame_size, streamSize - static_cast(frame->frame_size), 1, &next) == 0 && next.ofl_type == MP3_OFL_FGH) { + if (ParseMp3Header(stream + frame->frame_size, streamSize - static_cast(frame->frame_size), 2, &next) == 0 && next.ofl_type == MP3_OFL_FGH) { frame->encoder_delay += next.encoder_delay; frame->total_samples = next.total_samples; frame->ofl_type = MP3_OFL_VBRI_AND_FGH; } } - } else if (at + 10 <= end) { - while (at + 9 < end && stream[at] != 0xB4u) ++at; - if (at + 9 >= end) return; - std::uint8_t crc = 0xFF; - for (std::size_t i = 0; i < 9; ++i) { - for (int shift = 7; shift >= 0; --shift) { - const bool same = ((crc >> 7u) & 1u) == ((stream[at + i] >> shift) & 1u); - crc = static_cast(same ? (crc << 1u) ^ 0x45u : crc << 1u); - } + return; + } + + while (at + 9 < end && stream[at] != 0xB4u) ++at; + if (at + 9 >= end) return; + std::uint8_t crc = 0xFF; + for (std::size_t i = 0; i < 9; ++i) { + for (int shift = 7; shift >= 0; --shift) { + const bool same = ((crc >> 7u) & 1u) == ((stream[at + i] >> shift) & 1u); + crc = static_cast(same ? (crc << 1u) ^ 0x45u : crc << 1u); } - if (stream[at + 9] != crc) return; + } + if (stream[at + 9] == crc) { frame->encoder_delay = ReadBigEndian(stream + at + 1, 2); frame->total_samples = ReadBigEndian(stream + at + 3, 4); frame->ofl_type = MP3_OFL_FGH; @@ -944,13 +958,13 @@ int ParseMp3Header(const std::uint8_t* stream, std::uint32_t streamSize, int par }; const std::uint32_t header = (std::uint32_t{stream[0]} << 24u) | (std::uint32_t{stream[1]} << 16u) | (std::uint32_t{stream[2]} << 8u) | stream[3]; const std::uint32_t version = (header >> 19u) & 3u; + const std::uint32_t layer = (header >> 17u) & 3u; const std::uint32_t sampleRate = sampleRates[version][(header >> 10u) & 3u]; const std::uint32_t bitrate = kbps[version][(header >> 12u) & 15u] * 1000u; - const bool valid = (header >> 21u) == 0x7FFu && sampleRate != 0 && bitrate != 0; + const bool valid = (header >> 21u) == 0x7FFu && layer != 0 && sampleRate != 0 && bitrate != 0; AJM_TRACE("[ajm] parse mp3 frame %02x %02x %02x %02x (%u bytes, parse ofl %d) -> %s, %u Hz, %u bps\n", stream[0], stream[1], stream[2], stream[3], streamSize, parseOfl, valid ? "ok" : "invalid", sampleRate, bitrate); if (!valid) return SCE_AJM_ERROR_INVALID_PARAMETER; - if (parseOfl && ((header >> 17u) & 3u) != 1u) NotImplemented_nid_no_patch("sceAjmDecMp3ParseFrame (original file length lookup outside layer III)"); - if (parseOfl && ((header >> 16u) & 1u) == 0) NotImplemented_nid_no_patch("sceAjmDecMp3ParseFrame (original file length lookup in a CRC-protected frame)"); + if (parseOfl && layer != 1u) return SCE_AJM_ERROR_INVALID_PARAMETER; const bool mpeg1 = version == 3; frame->frame_size = (mpeg1 ? 144u : 72u) * bitrate / sampleRate + ((header >> 9u) & 1u); frame->num_channels = ((header >> 6u) & 3u) == 3u ? 1u : 2u; @@ -961,7 +975,7 @@ int ParseMp3Header(const std::uint8_t* stream, std::uint32_t streamSize, int par frame->num_frames = 0; frame->total_samples = 0; frame->ofl_type = MP3_OFL_NONE; - if (parseOfl) ParseMp3Ofl(stream, streamSize, header, frame); + if (parseOfl) ParseMp3Ofl(stream, streamSize, header, frame, parseOfl); return 0; } diff --git a/core/libs/prx/libSceAjm/CMakeLists.txt b/core/libs/prx/libSceAjm/CMakeLists.txt new file mode 100644 index 0000000000..7a9c708eae --- /dev/null +++ b/core/libs/prx/libSceAjm/CMakeLists.txt @@ -0,0 +1,2 @@ +include(${CMAKE_CURRENT_SOURCE_DIR}/../libSceAjm.native/AjmLibrary.cmake) +add_sce_ajm_library(libSceAjm ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp) diff --git a/core/libs/prx/libSceAjm/Export.cpp b/core/libs/prx/libSceAjm/Export.cpp index fb34206c21..508f721ede 100644 --- a/core/libs/prx/libSceAjm/Export.cpp +++ b/core/libs/prx/libSceAjm/Export.cpp @@ -5,6 +5,19 @@ extern "C" { -APS5_DUMMY_FUN +int APS5_VABI sceAjmBatchJobControlBufferRa() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAjmBatchJobRunSplitBufferRa() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAjmBatchStartBuffer() { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/core/libs/prx/libSceAmpr/Export.cpp b/core/libs/prx/libSceAmpr/Export.cpp index fe55109c4c..ca51ae70f2 100644 --- a/core/libs/prx/libSceAmpr/Export.cpp +++ b/core/libs/prx/libSceAmpr/Export.cpp @@ -845,11 +845,6 @@ int APS5_VABI sceAmprAmmWaitCommandBufferCompletion(std::uint32_t id) { return AmmSubmitted_nid_no_patch(id) ? 0 : SCE_KERNEL_ERROR_ESRCH; } -int APS5_VABI _ZNSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - void APS5_VABI _Unlock_shared_ptr_spin_lock_nid_postfix(void) { sharedPtrSpinLock.clear(std::memory_order_release); } @@ -858,21 +853,11 @@ int APS5_VABI _Cnd_init_with_name_nid_postfix(void** handle, const char*) { return _Cnd_init_nid_postfix(handle); } -int APS5_VABI _ZTVSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI _ZNKSt8time_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE3getES3_S3_RSt8ios_baseRNSt5_IosbIiE8_IostateEP2tmPKcSE__nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI _ZNSt8time_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI _ZNSt8time_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE7_GetcatEPPKNSt6locale5facetEPKS5__nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceAppContent/Export.cpp b/core/libs/prx/libSceAppContent/Export.cpp index 8bfb24b850..6854c950da 100644 --- a/core/libs/prx/libSceAppContent/Export.cpp +++ b/core/libs/prx/libSceAppContent/Export.cpp @@ -313,9 +313,8 @@ int APS5_VABI sceAppContentRequestPatchInstall(void) { return 0; } -int APS5_VABI sceAppContentTemporaryDataMount(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAppContentTemporaryDataMount(AppContentMountPoint* mountPoint) { + return sceAppContentTemporaryDataMount2(TEMPORARY_DATA_OPTION_FORMAT, mountPoint); } } diff --git a/core/libs/prx/libSceAudio3d/CMakeLists.txt b/core/libs/prx/libSceAudio3d/CMakeLists.txt index 791d2fac7e..2d4e024734 100644 --- a/core/libs/prx/libSceAudio3d/CMakeLists.txt +++ b/core/libs/prx/libSceAudio3d/CMakeLists.txt @@ -22,6 +22,15 @@ if(BUILD_TESTING) set_tests_properties(guest_audio3d_port PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_audio3d_speaker_array_tests ${LIBS_INCLUDE_DIR}/tests/GuestAudio3dSpeakerArray.cpp) + target_include_directories(guest_audio3d_speaker_array_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_audio3d_speaker_array_tests PRIVATE libSceAudio3d libSceAudioOut libc) + configure_windows_unwind(guest_audio3d_speaker_array_tests) + add_test(NAME guest_audio3d_speaker_array COMMAND guest_audio3d_speaker_array_tests) + if(WIN32) + set_tests_properties(guest_audio3d_speaker_array PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_audio3d_mixer_tests ${LIBS_INCLUDE_DIR}/tests/GuestAudio3dMixer.cpp) target_include_directories(guest_audio3d_mixer_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/core/libs/prx/libSceAudio3d/Export.cpp b/core/libs/prx/libSceAudio3d/Export.cpp index 4420537497..eed6da479e 100644 --- a/core/libs/prx/libSceAudio3d/Export.cpp +++ b/core/libs/prx/libSceAudio3d/Export.cpp @@ -472,29 +472,42 @@ int APS5_VABI sceAudio3dReportUnregisterHandler(void) { return 0; } -int APS5_VABI sceAudio3dGetSpeakerArrayMemorySize(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +size_t APS5_VABI sceAudioOut2GetSpeakerArrayMemorySize(uint32_t num_speakers, uint8_t is_3d, uint8_t is_ambisonics); +int APS5_VABI sceAudioOut2SpeakerArrayCreate(AudioOut2SpeakerArrayHandle* handle, const void* vbap_params, const void* ambi_params); +int APS5_VABI sceAudioOut2SpeakerArrayDestroy(AudioOut2SpeakerArrayHandle handle); +int APS5_VABI sceAudioOut2GetSpeakerArrayCoefficients(AudioOut2SpeakerArrayHandle handle, AudioOut2Position pos, float spread, float* coefficients, uint32_t num_coefficients, uint8_t height_aware, float downmix_spread_radius); + +struct Audio3dSpeakerArrayParameters { + const AudioOut2Position* positions; + std::uint32_t numSpeakers; + std::uint8_t is3d; + void* memory; + std::size_t memorySize; +}; + +static_assert(sizeof(Audio3dSpeakerArrayParameters) == 0x20); + +size_t APS5_VABI sceAudio3dGetSpeakerArrayMemorySize(uint32_t num_speakers, uint8_t is_3d) { + return sceAudioOut2GetSpeakerArrayMemorySize(num_speakers, is_3d, 0); } -int APS5_VABI sceAudio3dCreateSpeakerArray(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAudio3dCreateSpeakerArray(AudioOut2SpeakerArrayHandle* handle, const Audio3dSpeakerArrayParameters* parameters) { + if (!handle || !parameters) return AUDIO3D_ERROR_INVALID_PARAMETER; + unsigned char extended[0x28] = {}; + std::memcpy(extended, parameters, sizeof(*parameters)); + return sceAudioOut2SpeakerArrayCreate(handle, extended, nullptr); } -int APS5_VABI sceAudio3dDeleteSpeakerArray(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAudio3dDeleteSpeakerArray(AudioOut2SpeakerArrayHandle handle) { + return sceAudioOut2SpeakerArrayDestroy(handle); } -int APS5_VABI sceAudio3dGetSpeakerArrayMixCoefficients(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAudio3dGetSpeakerArrayMixCoefficients(AudioOut2SpeakerArrayHandle handle, AudioOut2Position pos, float spread, float* coefficients, uint32_t num_coefficients) { + return sceAudioOut2GetSpeakerArrayCoefficients(handle, pos, spread, coefficients, num_coefficients, 0, 0.1f); } -int APS5_VABI sceAudio3dGetSpeakerArrayMixCoefficients2(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAudio3dGetSpeakerArrayMixCoefficients2(AudioOut2SpeakerArrayHandle handle, AudioOut2Position pos, float spread, float* coefficients, uint32_t num_coefficients, uint8_t height_aware, float downmix_spread_radius) { + return sceAudioOut2GetSpeakerArrayCoefficients(handle, pos, spread, coefficients, num_coefficients, height_aware, downmix_spread_radius); } } diff --git a/core/libs/prx/libSceAudioIn/Export.cpp b/core/libs/prx/libSceAudioIn/Export.cpp index 3f4d0837fb..0a1712b5da 100644 --- a/core/libs/prx/libSceAudioIn/Export.cpp +++ b/core/libs/prx/libSceAudioIn/Export.cpp @@ -191,9 +191,9 @@ int32_t APS5_VABI sceAudioInAsyncOpen(int32_t userId, uint32_t type, uint32_t in } int32_t APS5_VABI sceAudioInIsPadEmbeddedMic(int32_t handle) { - (void)handle; - NotImplemented_nid_no_patch(__func__); - return 0; + std::lock_guard lock(g_mutex); + if (!find(handle)) return AUDIO_IN_ERROR_INVALID_HANDLE; + return 0; } int32_t APS5_VABI sceAudioInGetGain(int32_t handle, float* gain) { @@ -258,14 +258,14 @@ int APS5_VABI sceAudioInGetHandleStatusInfo(void) { return 0; } -int APS5_VABI sceAudioInOpenEx(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAudioInOpenEx(int user_id, uint32_t type, uint32_t index, uint32_t extra, uint32_t len, uint32_t freq, uint32_t param) { + (void)extra; + return sceAudioInOpen(user_id, type, index, len, freq, param); } -int APS5_VABI sceAudioInHqOpenEx(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAudioInHqOpenEx(int user_id, uint32_t type, uint32_t index, uint32_t extra, uint32_t len, uint32_t freq, uint32_t param) { + (void)extra; + return sceAudioInOpen(user_id, type, index, len, freq, param); } int APS5_VABI sceAudioInDeviceOpenEx(void) { @@ -289,12 +289,10 @@ int APS5_VABI sceAudioInSetSparkSideTone(void) { } int APS5_VABI sceAudioInSetCompressorPreGain(void) { - NotImplemented_nid_no_patch(__func__); return 0; } int APS5_VABI sceAudioInSetNoiseGateThreshold(void) { - NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceAudioOut/CMakeLists.txt b/core/libs/prx/libSceAudioOut/CMakeLists.txt index af79a20cf1..d13571aad4 100644 --- a/core/libs/prx/libSceAudioOut/CMakeLists.txt +++ b/core/libs/prx/libSceAudioOut/CMakeLists.txt @@ -47,6 +47,21 @@ if(BUILD_TESTING) endif() endforeach() + add_test_executable(audio_out_buffer_size_tests tests/AudioOutBufferSize.cpp src/AudioOut.cpp src/AudioOut2PadMix.cpp src/AudioOutShutdown.cpp) + target_compile_definitions(audio_out_buffer_size_tests PRIVATE SDL_MAIN_HANDLED) + target_include_directories(audio_out_buffer_size_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_SDL2_INCLUDE_DIR}) + target_link_libraries(audio_out_buffer_size_tests PRIVATE libc libkernel libScePad) + configure_windows_unwind(audio_out_buffer_size_tests) + foreach(bufferSizeMode IN ITEMS pass_through wrap mix threshold) + set(bufferSizeTest "audio_out_buffer_size_${bufferSizeMode}") + add_test(NAME ${bufferSizeTest} COMMAND audio_out_buffer_size_tests ${bufferSizeMode}) + set_tests_properties(${bufferSizeTest} PROPERTIES TIMEOUT 5) + if(WIN32) + set_tests_properties(${bufferSizeTest} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() + add_test_executable(audio_out_vibration_tests tests/AudioOutVibration.cpp) target_include_directories(audio_out_vibration_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(audio_out_vibration_tests PRIVATE libSceAudioOut libScePad libc) diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2Low.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2Low.cpp index eaba6e102d..eac5204b6d 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2Low.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2Low.cpp @@ -22,6 +22,10 @@ int APS5_VABI sceAudioOut2LoInit(void) { return sceAudioOut2Initialize(); } +int APS5_VABI sceAudioOut2LoTerminate(void) { + return 0; +} + int APS5_VABI sceAudioOut2LoContextQueryMemory(const AudioOut2ContextParam* params, size_t* memory_size) { return sceAudioOut2ContextQueryMemory(params, memory_size); } diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp index e48537db95..fa03043577 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp @@ -22,6 +22,11 @@ int APS5_VABI sceAudioOut2MasteringInit(uint32_t flags) { return 0; } +int APS5_VABI sceAudioOut2MasteringInitEx(uint32_t flags) { + if ((flags & 0x7FFFFFFFu) != 0) NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI sceAudioOut2MasteringTerm(void) { return 0; } diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2SpeakerArray.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2SpeakerArray.cpp index 26170f4622..7b6a07017e 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2SpeakerArray.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2SpeakerArray.cpp @@ -68,7 +68,7 @@ extern "C" { int APS5_VABI sceAudioOut2SpeakerArrayCreate(AudioOut2SpeakerArrayHandle* handle, const void* vbap_params, const void* ambi_params) { const auto* params = static_cast(vbap_params); - if (handle == nullptr || params == nullptr || ambi_params == nullptr || params->positions == nullptr || + if (handle == nullptr || params == nullptr || params->positions == nullptr || params->numSpeakers == 0 || params->numSpeakers > MaxSpeakers || params->memory == nullptr || params->memorySize < sizeof(SpeakerArray) || reinterpret_cast(params->memory) % alignof(SpeakerArray) != 0) { APS5_INVALID_ARG_EX; diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp index b9d9d152cf..62492d43da 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp @@ -15,6 +15,11 @@ int APS5_VABI sceAudioOut2Initialize(void) { return 0; } +int APS5_VABI sceAudioOut2InitializeEx(std::uint32_t param) { + (void)param; + return sceAudioOut2Initialize(); +} + int APS5_VABI sceAudioOut2Set3DLatency(int userId, std::uint32_t output) { if (output > MAX_SUPPORTED_3D_OUTPUT) return SCE_AUDIO_OUT2_ERROR_INVALID_PARAM; if (userId != 0 && (userId != 0xFF || output == 0)) return SCE_AUDIO_OUT2_ERROR_INVALID_PARAM; diff --git a/core/libs/prx/libSceAudioOut/src/Unimplemented.cpp b/core/libs/prx/libSceAudioOut/src/Unimplemented.cpp index 02abcf4fd9..24829f4035 100644 --- a/core/libs/prx/libSceAudioOut/src/Unimplemented.cpp +++ b/core/libs/prx/libSceAudioOut/src/Unimplemented.cpp @@ -365,11 +365,6 @@ int APS5_VABI sceAudioOut2GetSystemStateEx(void) { return 0; } -int APS5_VABI sceAudioOut2MasteringInitEx(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceAudioOut2SetDebugHeadphoneMode(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -380,11 +375,6 @@ int APS5_VABI sceAudioOut2DebugStateCtrl(void) { return 0; } -int APS5_VABI sceAudioOut2LoTerminate(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceAudioOut2ContextBedWrite(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -395,11 +385,6 @@ int APS5_VABI sceAudioOut2PortCreateEx(void) { return 0; } -int APS5_VABI sceAudioOut2InitializeEx(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceAudioOut2Terminate(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOut2Latency.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOut2Latency.cpp index 3ab1cd4a0f..03143cc923 100644 --- a/core/libs/prx/libSceAudioOut/tests/AudioOut2Latency.cpp +++ b/core/libs/prx/libSceAudioOut/tests/AudioOut2Latency.cpp @@ -9,10 +9,12 @@ extern "C" { int APS5_VABI sceAudioOut2Initialize(); +int APS5_VABI sceAudioOut2InitializeEx(std::uint32_t); int APS5_VABI sceAudioOut2LoInit(); int APS5_VABI sceAudioOut2LoTerminate(); int APS5_VABI sceAudioOut2Set3DLatency(int, std::uint32_t); int APS5_VABI sceAudioOut2MasteringInit(std::uint32_t); +int APS5_VABI sceAudioOut2MasteringInitEx(std::uint32_t); int APS5_VABI sceAudioOut2MasteringTerm(); int APS5_VABI sceAudioOut2MasteringSetParam(const void*, std::uint32_t, std::uint32_t); int APS5_VABI sceAudioOut2MasteringGetState(void*, std::uint32_t, std::uint64_t); @@ -68,6 +70,10 @@ void TestSet3DLatency() { void TestMasteringInit() { Require(sceAudioOut2MasteringInit(0) == 0, "flags 0 must be accepted"); Require(ThrowsRuntimeError([] { sceAudioOut2MasteringInit(1); }), "flags 1 must throw"); + Require(sceAudioOut2MasteringInitEx(0) == 0, "InitEx flags 0 must be accepted"); + Require(sceAudioOut2MasteringInitEx(0x80000000u) == 0, "InitEx flag bit 31 must be accepted"); + Require(ThrowsRuntimeError([] { sceAudioOut2MasteringInitEx(1); }), "InitEx flag bit 0 must throw"); + Require(ThrowsRuntimeError([] { sceAudioOut2MasteringInitEx(0x40000000u); }), "InitEx flag bit 30 must throw"); } void TestMasteringTerm() { @@ -125,7 +131,9 @@ void TestEnableChat() { void TestLowLatencyService() { Require(sceAudioOut2LoInit() == 0, "starting the low-latency service must be accepted"); Require(sceAudioOut2LoInit() == 0, "starting the low-latency service again must be accepted"); - Require(ThrowsRuntimeError([] { sceAudioOut2LoTerminate(); }), "stopping the low-latency service must throw"); + Require(sceAudioOut2LoTerminate() == 0, "stopping the low-latency service returns 0"); + Require(sceAudioOut2LoTerminate() == 0, "stopping a stopped low-latency service returns 0"); + Require(sceAudioOut2InitializeEx(0) == 0 && sceAudioOut2InitializeEx(1) == 0, "the extended initialization succeeds"); } } diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOutBufferSize.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOutBufferSize.cpp new file mode 100644 index 0000000000..d51eda399a --- /dev/null +++ b/core/libs/prx/libSceAudioOut/tests/AudioOutBufferSize.cpp @@ -0,0 +1,100 @@ +#include "SDL.h" +#include "prx/libSceAudioOut/include/AudioOutSdl.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceAudioOutOpen(int, int, int, std::uint32_t, std::uint32_t, std::uint32_t); +int APS5_VABI sceAudioOutClose(int); +int APS5_VABI sceAudioOutOutput(int, const void*); +} + +namespace { +constexpr SDL_AudioDeviceID device = 7; +int opened = 0; +int closed = 0; +int queued = 0; +std::uint32_t queuedBytes = 0; +const void* supplied = nullptr; +std::uint32_t suppliedBytes = 0; + +void Require(bool value, const char* message) { + if (!value) throw std::runtime_error(message); +} + +void RunPassThrough() { + std::vector samples(512u * 8u); + for (std::size_t i = 0; i < samples.size(); ++i) samples[i] = static_cast(i % 97u) / 97.0f; + const int handle = sceAudioOutOpen(0, 0, 0, 512, 48000, 5); + Require(handle > 0 && opened == 1, "a 512-frame F32 8-channel port did not open its device"); + supplied = samples.data(); + suppliedBytes = static_cast(samples.size() * sizeof(float)); + Require(sceAudioOutOutput(handle, samples.data()) == 512, "a 512-frame output did not return 512"); + Require(queued == 1 && queuedBytes == 16384, "a 512-frame F32 8-channel output did not queue 16384 bytes"); + Require(sceAudioOutClose(handle) == 0 && closed == 1, "the port did not close its device"); +} + +void RunOverflow(std::uint32_t frames, std::uint32_t format, std::uint32_t frameBytes) { + std::vector frame(frameBytes); + const int handle = sceAudioOutOpen(0, 0, 0, frames, 48000, format); + Require(handle > 0 && opened == 1, "a port with an oversized length did not open its device"); + supplied = frame.data(); + suppliedBytes = frameBytes; + bool threw = false; + try { + static_cast(sceAudioOutOutput(handle, frame.data())); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw, "an output whose buffer size overflows did not throw"); + Require(queued == 0, "an output whose buffer size overflows queued audio"); + Require(sceAudioOutClose(handle) == 0 && closed == 1, "the port did not close after the refused output"); +} +} + +int InitializeSdlAudio_nid_no_patch() { return 0; } +extern "C" Uint32 SDLCALL SDL_WasInit(Uint32) { return SDL_INIT_AUDIO; } +extern "C" int SDLCALL SDL_GetNumAudioDevices(int) { return 1; } +extern "C" const char* SDLCALL SDL_GetAudioDeviceName(int, int) { return "Main speakers"; } +extern "C" int SDLCALL SDL_GetAudioDeviceSpec(int, int, SDL_AudioSpec* spec) { *spec = {}; spec->channels = 2; return 0; } +extern "C" const char* SDLCALL SDL_GetCurrentAudioDriver() { return "dummy"; } +extern "C" const char* SDLCALL SDL_GetError() { return "test error"; } +extern "C" SDL_AudioDeviceID SDLCALL SDL_OpenAudioDevice(const char* name, int capture, const SDL_AudioSpec* desired, SDL_AudioSpec* obtained, int) { + Require(name == nullptr && capture == 0 && desired->callback == nullptr, "the main port did not open the default device"); + ++opened; + *obtained = *desired; + return device; +} +extern "C" void SDLCALL SDL_PauseAudioDevice(SDL_AudioDeviceID id, int) { Require(id == device, "wrong device paused"); } +extern "C" Uint32 SDLCALL SDL_GetQueuedAudioSize(SDL_AudioDeviceID id) { Require(id == device, "wrong queue queried"); return 0; } +extern "C" int SDLCALL SDL_QueueAudio(SDL_AudioDeviceID id, const void* data, Uint32 bytes) { + Require(id == device, "wrong queue fed"); + ++queued; + queuedBytes = bytes; + if (bytes <= suppliedBytes) Require(std::memcmp(data, supplied, bytes) == 0, "the queued audio is not the supplied samples"); + return 0; +} +extern "C" void SDLCALL SDL_ClearQueuedAudio(SDL_AudioDeviceID id) { Require(id == device, "wrong queue cleared"); } +extern "C" void SDLCALL SDL_CloseAudioDevice(SDL_AudioDeviceID id) { Require(id == device, "wrong device closed"); ++closed; } + +int main(int argc, char** argv) { + try { + Require(argc == 2, "invalid test arguments"); + const std::string_view mode = argv[1]; + if (mode == "pass_through") RunPassThrough(); + else if (mode == "wrap") RunOverflow(0x80000000u, 5, 8u * sizeof(float)); + else if (mode == "mix") RunOverflow(0x80000000u, 7, 8u * sizeof(float)); + else if (mode == "threshold") RunOverflow(0x40000000u, 0, sizeof(std::int16_t)); + else Require(false, "invalid buffer size mode"); + std::puts("audio out buffer size tests passed"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/libs/prx/libSceAudioPropagation/CMakeLists.txt b/core/libs/prx/libSceAudioPropagation/CMakeLists.txt index 29417b8962..0575687fe3 100644 --- a/core/libs/prx/libSceAudioPropagation/CMakeLists.txt +++ b/core/libs/prx/libSceAudioPropagation/CMakeLists.txt @@ -5,7 +5,7 @@ configure_windows_unwind(libSceAudioPropagation) if(BUILD_TESTING) add_test_executable(guest_audio_propagation_tests ${LIBS_INCLUDE_DIR}/tests/GuestAudioPropagation.cpp) target_include_directories(guest_audio_propagation_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_audio_propagation_tests PRIVATE libSceAudioPropagation libc) + target_link_libraries(guest_audio_propagation_tests PRIVATE libSceAudioPropagation libc ${CMAKE_DL_LIBS}) configure_windows_unwind(guest_audio_propagation_tests) add_test(NAME guest_audio_propagation COMMAND guest_audio_propagation_tests) if(WIN32) diff --git a/core/libs/prx/libSceAudioPropagation/Export.cpp b/core/libs/prx/libSceAudioPropagation/Export.cpp index fb835aca4d..2b461aa9cc 100644 --- a/core/libs/prx/libSceAudioPropagation/Export.cpp +++ b/core/libs/prx/libSceAudioPropagation/Export.cpp @@ -380,56 +380,6 @@ int APS5_VABI sceAudioPropagationSystemQueryInfo(AudioPropagationHandle system, return 0; } -int APS5_VABI sceAudioPropagationAudioPathInit() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAudioPropagationAudioPathPointInit() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAudioPropagationMaterialInit() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAudioPropagationPortalSettingsInit() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAudioPropagationRayInit() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAudioPropagationSourceRenderInfoInit() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAudioPropagationSourceSetAudioPathsParamInit() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAudioPropagationSystem() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAudioPropagationSystemMemoryInit() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAudioPropagationSystemOptionInit() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceAudioPropagationReportRegisterHandler(std::uint32_t category, ReportHandler handler, void* userData, ReportRegistration** registration) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceCesCs/Export.cpp b/core/libs/prx/libSceCesCs/Export.cpp index 25dcb94514..fe24b79623 100644 --- a/core/libs/prx/libSceCesCs/Export.cpp +++ b/core/libs/prx/libSceCesCs/Export.cpp @@ -21,10 +21,10 @@ constexpr int CES_ERROR_UNASSIGNED_CODE = static_cast(0x805C0020); constexpr int CES_ERROR_INVALID_DST_BUFFER = static_cast(0x805C0030); constexpr int CES_ERROR_DST_BUFFER_END = static_cast(0x805C0031); -constexpr uint8_t PROFILES[3] = {0, 1, 2}; -constexpr const uint8_t* CP1252_PROFILE = &PROFILES[0]; -constexpr const uint8_t* CP1251_PROFILE = &PROFILES[1]; -constexpr const uint8_t* CP1250_PROFILE = &PROFILES[2]; +constexpr uint8_t PROFILES[3][8] = {{0}, {1}, {2}}; +constexpr const uint8_t* CP1252_PROFILE = PROFILES[0]; +constexpr const uint8_t* CP1251_PROFILE = PROFILES[1]; +constexpr const uint8_t* CP1250_PROFILE = PROFILES[2]; constexpr uint16_t CP1252_HIGH[32] = { 0x20AC, 0x0081, 0x201A, 0x0192, 0x201E, 0x2026, 0x2020, 0x2021, 0x02C6, 0x2030, 0x0160, 0x2039, 0x0152, 0x008D, 0x017D, 0x008F, diff --git a/core/libs/prx/libSceCommonDialog/CommonDialog.hpp b/core/libs/prx/libSceCommonDialog/CommonDialog.hpp index f1949f6eeb..260eb371bc 100644 --- a/core/libs/prx/libSceCommonDialog/CommonDialog.hpp +++ b/core/libs/prx/libSceCommonDialog/CommonDialog.hpp @@ -248,6 +248,7 @@ inline bool ValidateCommonDialogBaseParam(const CommonDialogBaseParam& param) { } constexpr int COMMON_DIALOG_OK = 0; +constexpr int COMMON_DIALOG_ERROR_ALREADY_SYSTEM_INITIALIZED = static_cast(0x80B80002); constexpr int COMMON_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast(0x80B80004); #endif diff --git a/core/libs/prx/libSceCommonDialog/Export.cpp b/core/libs/prx/libSceCommonDialog/Export.cpp index bb18755ae2..47f9a84bb1 100644 --- a/core/libs/prx/libSceCommonDialog/Export.cpp +++ b/core/libs/prx/libSceCommonDialog/Export.cpp @@ -12,7 +12,7 @@ extern "C" { int APS5_VABI sceCommonDialogInitialize(void) { bool expected = false; if (!g_initialized.compare_exchange_strong(expected, true)) { - return COMMON_DIALOG_ERROR_ALREADY_INITIALIZED; + return COMMON_DIALOG_ERROR_ALREADY_SYSTEM_INITIALIZED; } return COMMON_DIALOG_OK; } diff --git a/core/libs/prx/libSceFont/src/SystemFont.cpp b/core/libs/prx/libSceFont/src/SystemFont.cpp index 5bcfb0789e..c6e3d87339 100644 --- a/core/libs/prx/libSceFont/src/SystemFont.cpp +++ b/core/libs/prx/libSceFont/src/SystemFont.cpp @@ -154,8 +154,12 @@ std::vector Font::SystemFontCandidates(std::uint32_t fontS } std::filesystem::path Font::SystemFontDirectory() { +#ifdef _WIN32 + const wchar_t* configured = _wgetenv(L"ANYPS5_SYSTEM_FONTS"); +#else const char* configured = std::getenv("ANYPS5_SYSTEM_FONTS"); - if (!configured || configured[0] == '\0') return ExecutableDirectory() / "anyps5-fonts"; +#endif + if (!configured || configured[0] == 0) return ExecutableDirectory() / "anyps5-fonts"; const std::filesystem::path directory(configured); std::error_code error; if (!std::filesystem::is_directory(directory, error)) throw std::runtime_error("libSceFont: ANYPS5_SYSTEM_FONTS is not a directory: " + directory.string()); diff --git a/core/libs/prx/libSceFontFt/Export.cpp b/core/libs/prx/libSceFontFt/Export.cpp index b0a213d62f..628a572b94 100644 --- a/core/libs/prx/libSceFontFt/Export.cpp +++ b/core/libs/prx/libSceFontFt/Export.cpp @@ -78,8 +78,7 @@ int APS5_VABI sceFontFtSupportTrueType(FontLibrary library) { } int APS5_VABI sceFontFtSupportTrueTypeGx(FontLibrary library) { - NotImplemented_nid_no_patch(__func__); - return 0; + return FontFt::SupportFormats(library, {"truetype"}); } int APS5_VABI sceFontFtSupportType1(FontLibrary library) { diff --git a/core/libs/prx/libSceFontFt/src/Driver.cpp b/core/libs/prx/libSceFontFt/src/Driver.cpp index 6ed7e5203e..a4561830ea 100644 --- a/core/libs/prx/libSceFontFt/src/Driver.cpp +++ b/core/libs/prx/libSceFontFt/src/Driver.cpp @@ -12,7 +12,11 @@ #include #include #include +#include #include +#ifdef _WIN32 +#include +#endif #include #include FT_FREETYPE_H @@ -198,6 +202,41 @@ int APS5_VABI LibrarySupport(FontLibNative* library, std::uint32_t formats) { return SCE_FONT_OK; } +#ifdef _WIN32 +void CloseMappedFile(FT_Stream stream) { + UnmapViewOfFile(stream->base); + delete stream; +} + +FT_Error OpenFileFace(FT_Library library, const char* path, FT_Long subFontIndex, FT_Face* face) { + const HANDLE file = CreateFileW(std::filesystem::path(reinterpret_cast(path)).c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + if (file == INVALID_HANDLE_VALUE) return FT_Err_Cannot_Open_Resource; + LARGE_INTEGER size{}; + if (!GetFileSizeEx(file, &size) || size.QuadPart <= 0 || size.QuadPart > std::numeric_limits::max()) { + CloseHandle(file); + return FT_Err_Cannot_Open_Stream; + } + const HANDLE mapping = CreateFileMappingW(file, nullptr, PAGE_READONLY, 0, 0, nullptr); + CloseHandle(file); + if (!mapping) return FT_Err_Cannot_Open_Stream; + void* view = MapViewOfFile(mapping, FILE_MAP_READ, 0, 0, 0); + CloseHandle(mapping); + if (!view) return FT_Err_Cannot_Open_Stream; + auto* stream = new FT_StreamRec{}; + stream->base = static_cast(view); + stream->size = static_cast(size.QuadPart); + stream->close = &CloseMappedFile; + FT_Open_Args args{}; + args.flags = FT_OPEN_STREAM; + args.stream = stream; + return FT_Open_Face(library, &args, subFontIndex, face); +} +#else +FT_Error OpenFileFace(FT_Library library, const char* path, FT_Long subFontIndex, FT_Face* face) { + return FT_New_Face(library, path, subFontIndex, face); +} +#endif + int APS5_VABI LibraryOpenFont(FontLibNative* library, std::uint32_t mode, const void* fontAddress, std::uint32_t fontSize, std::uint32_t subFontIndex, std::uint32_t uniqueWord, FontObj** inoutFontObj) { (void)uniqueWord; if (!library || !fontAddress || !inoutFontObj) return SCE_FONT_ERROR_INVALID_PARAMETER; @@ -222,7 +261,7 @@ int APS5_VABI LibraryOpenFont(FontLibNative* library, std::uint32_t mode, const if (fromMemory) { error = FT_New_Memory_Face(*ftLibrary, static_cast(fontAddress), static_cast(fontSize), static_cast(subFontIndex), &face); } else { - error = FT_New_Face(*ftLibrary, static_cast(fontAddress), static_cast(subFontIndex), &face); + error = OpenFileFace(*ftLibrary, static_cast(fontAddress), static_cast(subFontIndex), &face); } if (error != 0 || !face) { if (fromMemory || error == FT_Err_Unknown_File_Format) return SCE_FONT_ERROR_NO_SUPPORT_FORMAT; diff --git a/core/libs/prx/libSceFreeType/CMakeLists.txt b/core/libs/prx/libSceFreeType/CMakeLists.txt index e0a3b3e116..3dd0cd443c 100644 --- a/core/libs/prx/libSceFreeType/CMakeLists.txt +++ b/core/libs/prx/libSceFreeType/CMakeLists.txt @@ -1,6 +1,7 @@ add_library(libSceFreeType SHARED EXCLUDE_FROM_ALL Export.cpp src/GuestObjects.cpp + src/Names.cpp ) target_include_directories(libSceFreeType PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(libSceFreeType PRIVATE freetype) @@ -20,7 +21,21 @@ if(BUILD_TESTING) target_link_libraries(guest_freetype_charmap_tests PRIVATE libSceFreeType freetype libc) configure_windows_unwind(guest_freetype_charmap_tests) add_test(NAME guest_freetype_charmap COMMAND guest_freetype_charmap_tests) + add_test_executable(guest_freetype_names_tests ${LIBS_INCLUDE_DIR}/tests/GuestFreeTypeNames.cpp) + target_include_directories(guest_freetype_names_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_freetype_names_tests PRIVATE libSceFreeType freetype libc) + configure_windows_unwind(guest_freetype_names_tests) + add_test(NAME guest_freetype_names COMMAND guest_freetype_names_tests) + add_test_executable(guest_freetype_sfnt_names_tests ${LIBS_INCLUDE_DIR}/tests/GuestFreeTypeSfntNames.cpp) + target_include_directories(guest_freetype_sfnt_names_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_freetype_sfnt_names_tests PRIVATE libSceFreeType freetype libc) + configure_windows_unwind(guest_freetype_sfnt_names_tests) + add_test(NAME guest_freetype_sfnt_names COMMAND guest_freetype_sfnt_names_tests) if(WIN32) + set_tests_properties(guest_freetype_sfnt_names PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + set_tests_properties(guest_freetype_names PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") set_tests_properties(guest_freetype PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") set_tests_properties(guest_freetype_charmap PROPERTIES ENVIRONMENT_MODIFICATION diff --git a/core/libs/prx/libSceFreeType/Export.cpp b/core/libs/prx/libSceFreeType/Export.cpp index 0c4f8d79b1..1a935d33fa 100644 --- a/core/libs/prx/libSceFreeType/Export.cpp +++ b/core/libs/prx/libSceFreeType/Export.cpp @@ -149,6 +149,28 @@ int APS5_VABI FT_Set_Pixel_Sizes_nid_postfix(GuestFaceRec* face, std::uint32_t p return error; } +int APS5_VABI FT_Set_Char_Size_nid_postfix(GuestFaceRec* face, std::int64_t charWidth, std::int64_t charHeight, + std::uint32_t horizontalResolution, std::uint32_t verticalResolution) { + GuestFace* owner = FaceOf(face); + if (!owner) return FT_Err_Invalid_Face_Handle; + if (!owner->host->size) return FT_Err_Invalid_Size_Handle; + if (!charWidth) charWidth = charHeight; + else if (!charHeight) charHeight = charWidth; + if (charWidth < 64) charWidth = 64; + if (charHeight < 64) charHeight = 64; + if (!horizontalResolution) horizontalResolution = verticalResolution; + else if (!verticalResolution) verticalResolution = horizontalResolution; + if (!horizontalResolution) horizontalResolution = verticalResolution = 72; + const auto maximum = std::numeric_limits::max(); + if (charWidth > (maximum - 36) / horizontalResolution || charHeight > (maximum - 36) / verticalResolution) { + throw std::runtime_error("FT_Set_Char_Size: a character size or resolution outside the host arithmetic range is not implemented"); + } + const FT_Error error = FT_Set_Char_Size(owner->host, static_cast(charWidth), static_cast(charHeight), + horizontalResolution, verticalResolution); + SyncSize(*owner); + return error; +} + std::uint32_t APS5_VABI FT_Get_Char_Index_nid_postfix(GuestFaceRec* face, std::uint64_t charcode) { const GuestFace* owner = FaceOf(face); return FT_Get_Char_Index(owner ? owner->host : nullptr, static_cast(static_cast(charcode))); diff --git a/core/libs/prx/libSceFreeType/include/GuestFreeType.hpp b/core/libs/prx/libSceFreeType/include/GuestFreeType.hpp index 1a2a074378..28fead1ee8 100644 --- a/core/libs/prx/libSceFreeType/include/GuestFreeType.hpp +++ b/core/libs/prx/libSceFreeType/include/GuestFreeType.hpp @@ -40,6 +40,15 @@ struct GuestCharMapRec { std::uint16_t encoding_id; }; +struct GuestSfntName { + std::uint16_t platform_id; + std::uint16_t encoding_id; + std::uint16_t language_id; + std::uint16_t name_id; + unsigned char* string; + std::uint32_t string_len; +}; + struct GuestSizeMetrics { std::uint16_t x_ppem; std::uint16_t y_ppem; @@ -152,6 +161,7 @@ struct GuestFaceRec { static_assert(sizeof(GuestGeneric) == 16 && offsetof(GuestGeneric, finalizer) == 8); static_assert(sizeof(GuestBitmapSize) == 32 && offsetof(GuestBitmapSize, size) == 8); static_assert(sizeof(GuestCharMapRec) == 16 && offsetof(GuestCharMapRec, platform_id) == 12); +static_assert(sizeof(GuestSfntName) == 24 && offsetof(GuestSfntName, string) == 8 && offsetof(GuestSfntName, string_len) == 16); static_assert(sizeof(GuestSizeMetrics) == 56 && offsetof(GuestSizeMetrics, x_scale) == 8); static_assert(sizeof(GuestSizeRec) == 88 && offsetof(GuestSizeRec, metrics) == 24); static_assert(sizeof(GuestBitmap) == 40 && offsetof(GuestBitmap, buffer) == 16 && offsetof(GuestBitmap, palette) == 32); diff --git a/core/libs/prx/libSceFreeType/src/Names.cpp b/core/libs/prx/libSceFreeType/src/Names.cpp new file mode 100644 index 0000000000..23d3b8ad8f --- /dev/null +++ b/core/libs/prx/libSceFreeType/src/Names.cpp @@ -0,0 +1,55 @@ +#include "prx/libSceFreeType/include/GuestObjects.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" + +#include FT_SFNT_NAMES_H + +#include + +using namespace FreeType; + +extern "C" { + +int APS5_VABI FT_Get_Glyph_Name_nid_postfix(GuestFaceRec* face, std::uint32_t glyphIndex, void* buffer, std::uint32_t bufferSize) { + const GuestFace* owner = FaceOf(face); + const FT_Face host = owner ? owner->host : nullptr; + if (!host || !buffer || bufferSize == 0) return FT_Get_Glyph_Name(host, glyphIndex, buffer, bufferSize); + GuestArena::HostWrite write(buffer, bufferSize); + if (static_cast(glyphIndex) >= host->num_glyphs) { + static_cast(buffer)[0] = '\0'; + return FT_Err_Invalid_Glyph_Index; + } + return FT_Get_Glyph_Name(host, glyphIndex, buffer, bufferSize); +} + +std::uint32_t APS5_VABI FT_Get_Name_Index_nid_postfix(GuestFaceRec* face, const char* glyphName) { + const GuestFace* owner = FaceOf(face); + return FT_Get_Name_Index(owner ? owner->host : nullptr, glyphName); +} + +const char* APS5_VABI FT_Get_Postscript_Name_nid_postfix(GuestFaceRec* face) { + const GuestFace* owner = FaceOf(face); + return FT_Get_Postscript_Name(owner ? owner->host : nullptr); +} + +std::uint32_t APS5_VABI FT_Get_Sfnt_Name_Count_nid_postfix(GuestFaceRec* face) { + const GuestFace* owner = FaceOf(face); + return FT_Get_Sfnt_Name_Count(owner ? owner->host : nullptr); +} + +int APS5_VABI FT_Get_Sfnt_Name_nid_postfix(GuestFaceRec* face, std::uint32_t index, GuestSfntName* name) { + const GuestFace* owner = FaceOf(face); + FT_SfntName native{}; + const FT_Error error = FT_Get_Sfnt_Name(owner ? owner->host : nullptr, index, name ? &native : nullptr); + if (!error) { + name->platform_id = native.platform_id; + name->encoding_id = native.encoding_id; + name->language_id = native.language_id; + name->name_id = native.name_id; + name->string = native.string; + name->string_len = native.string_len; + } + return error; +} + +} diff --git a/core/libs/prx/libSceGameCustomDataDialog/Export.cpp b/core/libs/prx/libSceGameCustomDataDialog/Export.cpp new file mode 100644 index 0000000000..5b647ad000 --- /dev/null +++ b/core/libs/prx/libSceGameCustomDataDialog/Export.cpp @@ -0,0 +1,31 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceGameCustomDataDialogGetResult() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGameCustomDataDialogInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGameCustomDataDialogOpen() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGameCustomDataDialogTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGameCustomDataDialogUpdateStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceGameLiveStreaming/Export.cpp b/core/libs/prx/libSceGameLiveStreaming/Export.cpp index d4a4a9bdbd..891a864574 100644 --- a/core/libs/prx/libSceGameLiveStreaming/Export.cpp +++ b/core/libs/prx/libSceGameLiveStreaming/Export.cpp @@ -5,12 +5,15 @@ #include "prx/libSceUserService/UserService.hpp" #include #include +#include +#include static std::mutex g_mutex; static bool g_initialized = false; static constexpr int ErrorInvalidArgument = static_cast(0x80A00002); static constexpr int ErrorAlreadyInitialized = static_cast(0x80A00003); static constexpr int ErrorNotInitialized = static_cast(0x80A00004); +static constexpr std::size_t ProgramInfoSize = 0x134C; struct GameLiveStreamingProgramInfo; @@ -53,7 +56,10 @@ int APS5_VABI sceGameLiveStreamingGetCurrentStatus2(GameLiveStreamingStatus2* st } int APS5_VABI sceGameLiveStreamingGetProgramInfo(GameLiveStreamingProgramInfo* info) { - NotImplemented_nid_no_patch(__func__); + std::lock_guard lock(g_mutex); + if (!g_initialized) return ErrorNotInitialized; + if (info == nullptr) return ErrorInvalidArgument; + std::memset(info, 0, ProgramInfoSize); return 0; } @@ -267,8 +273,10 @@ int APS5_VABI sceGameLiveStreamingStopDebugBroadcast(void) { return 0; } -int APS5_VABI sceGameLiveStreamingEnableLiveStreaming(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceGameLiveStreamingEnableLiveStreaming(bool isEnableLiveStreaming) { + std::lock_guard lock(g_mutex); + if (!g_initialized) throw std::logic_error(std::string(__func__) + ": not initialized"); + (void)isEnableLiveStreaming; return 0; } diff --git a/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp b/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp index f8a8c67c6a..9b7f2684c4 100644 --- a/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp +++ b/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp @@ -3,6 +3,14 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +namespace { + +constexpr int Failure = static_cast(0x8EEE00FFu); +constexpr int NoInterface = static_cast(0x80000000u); +constexpr int ValidationNotEnabled = static_cast(0x80D13FFFu); + +} // namespace + extern "C" { int APS5_VABI sceGnmBeginWorkload(void) { @@ -26,13 +34,11 @@ int APS5_VABI sceGnmCreateWorkloadStream(void) { } int APS5_VABI sceGnmDebugModuleReset(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Failure; } int APS5_VABI sceGnmDebugReset(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Failure; } int APS5_VABI sceGnmDestroyWorkloadStream(void) { @@ -151,28 +157,23 @@ int APS5_VABI sceGnmDrawOpaqueAuto(void) { } int APS5_VABI sceGnmDriverInternalRetrieveGnmInterface(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return NoInterface; } int APS5_VABI sceGnmDriverInternalRetrieveGnmInterfaceForGpuDebugger(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return NoInterface; } int APS5_VABI sceGnmDriverInternalRetrieveGnmInterfaceForReplay(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return NoInterface; } int APS5_VABI sceGnmDriverInternalRetrieveGnmInterfaceForResourceRegistration(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return NoInterface; } int APS5_VABI sceGnmDriverInternalRetrieveGnmInterfaceForValidation(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return NoInterface; } int APS5_VABI sceGnmDriverInternalVirtualQuery(void) { @@ -181,7 +182,6 @@ int APS5_VABI sceGnmDriverInternalVirtualQuery(void) { } int APS5_VABI sceGnmDriverTraceInProgress(void) { - NotImplemented_nid_no_patch(__func__); return 0; } @@ -191,13 +191,11 @@ int APS5_VABI sceGnmEndWorkload(void) { } int APS5_VABI sceGnmFindResources(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Failure; } int APS5_VABI sceGnmFindResourcesPublic(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Failure; } int APS5_VABI sceGnmGetCoredumpAddress(void) { @@ -216,8 +214,7 @@ int APS5_VABI sceGnmGetCoredumpProtectionFaultTimestamp(void) { } int APS5_VABI sceGnmGetDbgGcHandle(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return -1; } int APS5_VABI sceGnmGetGpuBlockStatus(void) { @@ -251,13 +248,11 @@ int APS5_VABI sceGnmGetOffChipTessellationBufferSize(void) { } int APS5_VABI sceGnmGetOwnerName(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Failure; } int APS5_VABI sceGnmGetPhysicalCounterFromVirtualized(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Failure; } int APS5_VABI sceGnmGetProtectionFaultTimeStamp(void) { @@ -266,33 +261,27 @@ int APS5_VABI sceGnmGetProtectionFaultTimeStamp(void) { } int APS5_VABI sceGnmGetResourceBaseAddressAndSizeInBytes(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Failure; } int APS5_VABI sceGnmGetResourceName(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Failure; } int APS5_VABI sceGnmGetResourceRegistrationBuffers(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Failure; } int APS5_VABI sceGnmGetResourceShaderGuid(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Failure; } int APS5_VABI sceGnmGetResourceType(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Failure; } int APS5_VABI sceGnmGetResourceUserData(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Failure; } int APS5_VABI sceGnmGetShaderProgramBaseAddress(void) { @@ -356,8 +345,7 @@ int APS5_VABI sceGnmPaHeartbeat(void) { } int APS5_VABI sceGnmQueryResourceRegistrationUserMemoryRequirements(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Failure; } int APS5_VABI sceGnmRaiseUserExceptionEvent(void) { @@ -366,8 +354,7 @@ int APS5_VABI sceGnmRaiseUserExceptionEvent(void) { } int APS5_VABI sceGnmRegisterGdsResource(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Failure; } int APS5_VABI sceGnmRegisterGnmLiveCallbackConfig(void) { @@ -376,18 +363,15 @@ int APS5_VABI sceGnmRegisterGnmLiveCallbackConfig(void) { } int APS5_VABI sceGnmRegisterOwner(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Failure; } int APS5_VABI sceGnmRegisterOwnerForSystem(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Failure; } int APS5_VABI sceGnmRegisterResource(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Failure; } int APS5_VABI sceGnmRequestFlipAndSubmitDoneForWorkload(void) { @@ -401,43 +385,35 @@ int APS5_VABI sceGnmResetVgtControl(void) { } int APS5_VABI sceGnmSdmaClose(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Failure; } int APS5_VABI sceGnmSdmaConstFill(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Failure; } int APS5_VABI sceGnmSdmaCopyLinear(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Failure; } int APS5_VABI sceGnmSdmaCopyTiled(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Failure; } int APS5_VABI sceGnmSdmaCopyWindow(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Failure; } int APS5_VABI sceGnmSdmaFlush(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Failure; } int APS5_VABI sceGnmSdmaGetMinCmdSize(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Failure; } int APS5_VABI sceGnmSdmaOpen(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Failure; } int APS5_VABI sceGnmSetCsShaderWithModifier(void) { @@ -461,23 +437,19 @@ int APS5_VABI sceGnmSetPsShader350(void) { } int APS5_VABI sceGnmSetResourceRegistrationUserMemory(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Failure; } int APS5_VABI sceGnmSetResourceUserData(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Failure; } int APS5_VABI sceGnmSetSpiEnableSqCounters(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Failure; } int APS5_VABI sceGnmSetSpiEnableSqCountersForUnitInstance(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Failure; } int APS5_VABI sceGnmSetVgtControl(void) { @@ -506,18 +478,15 @@ int APS5_VABI sceGnmSubmitCommandBuffersForWorkload(void) { } int APS5_VABI sceGnmUnregisterAllResourcesForOwner(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Failure; } int APS5_VABI sceGnmUnregisterOwnerAndResources(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Failure; } int APS5_VABI sceGnmUnregisterResource(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Failure; } int APS5_VABI sceGnmUpdateGsShader(void) { @@ -546,33 +515,27 @@ int APS5_VABI sceGnmUpdateVsShader(void) { } int APS5_VABI sceGnmValidateDisableDiagnostics(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return ValidationNotEnabled; } int APS5_VABI sceGnmValidateDisableDiagnostics2(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return ValidationNotEnabled; } int APS5_VABI sceGnmValidateDispatchCommandBuffers(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return ValidationNotEnabled; } int APS5_VABI sceGnmValidateDrawCommandBuffers(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return ValidationNotEnabled; } int APS5_VABI sceGnmValidateGetDiagnosticInfo(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return ValidationNotEnabled; } int APS5_VABI sceGnmValidateGetDiagnostics(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return ValidationNotEnabled; } int APS5_VABI sceGnmValidateGetVersion(void) { @@ -581,18 +544,15 @@ int APS5_VABI sceGnmValidateGetVersion(void) { } int APS5_VABI sceGnmValidateOnSubmitEnabled(void) { - NotImplemented_nid_no_patch(__func__); return 0; } int APS5_VABI sceGnmValidateResetState(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return ValidationNotEnabled; } int APS5_VABI sceGnmValidationRegisterMemoryCheckCallback(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return ValidationNotEnabled; } } diff --git a/core/libs/prx/libSceHmd2/CMakeLists.txt b/core/libs/prx/libSceHmd2/CMakeLists.txt index fc0c32cbf4..6d7565ac4a 100644 --- a/core/libs/prx/libSceHmd2/CMakeLists.txt +++ b/core/libs/prx/libSceHmd2/CMakeLists.txt @@ -14,3 +14,15 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endif() + +if(BUILD_TESTING) + add_test_executable(guest_hmd2_gaze_tests ${LIBS_INCLUDE_DIR}/tests/GuestHmd2Gaze.cpp) + target_include_directories(guest_hmd2_gaze_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_hmd2_gaze_tests PRIVATE libSceHmd2 libc) + configure_windows_unwind(guest_hmd2_gaze_tests) + add_test(NAME guest_hmd2_gaze COMMAND guest_hmd2_gaze_tests) + if(WIN32) + set_tests_properties(guest_hmd2_gaze PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceHmd2/Export.cpp b/core/libs/prx/libSceHmd2/Export.cpp index a0dd351bdf..d744bf2741 100644 --- a/core/libs/prx/libSceHmd2/Export.cpp +++ b/core/libs/prx/libSceHmd2/Export.cpp @@ -291,14 +291,40 @@ std::int32_t APS5_VABI sceHmd2Close(std::int32_t handle) { return 0; } -int APS5_VABI sceHmd2GazeGetResult(const Hmd2::GazeResultParameter* parameter, Hmd2::GazeResult* result) { - NotImplemented_nid_no_patch(__func__); +static std::int32_t Hmd2GazeHandle(const Hmd2::GazeResultParameter* param, const void* result, std::uint32_t resultSize, const char* name) { + if (!param || !result) return Hmd2::ParameterNull; + std::uint32_t size; + std::memcpy(&size, result, sizeof(size)); + if (param->size != sizeof(Hmd2::GazeResultParameter) || size != resultSize) return Hmd2::ParameterInvalid; + std::lock_guard lock(stateMutex); + if (openHandle == 0 || param->handle != openHandle) return Hmd2::InvalidHandle; + NotImplemented_nid_no_patch(name); return 0; } -int APS5_VABI sceHmd2GazeGetResultForFoveatedRendering(const Hmd2::GazeResultParameter* parameter, Hmd2::FoveatedRenderingResult* result) { - NotImplemented_nid_no_patch(__func__); - return 0; +static void Hmd2StoreInvalidGaze(std::byte* result, std::size_t offset) { + const std::int32_t invalidIndex = -1; + const float invalidPair[2] = {-0.0f, 0.0f}; + const float invalidScalar = -1.0f; + std::memcpy(result + 4, &invalidIndex, sizeof(invalidIndex)); + std::memcpy(result + offset, invalidPair, sizeof(invalidPair)); + std::memcpy(result + offset + sizeof(invalidPair), &invalidScalar, sizeof(invalidScalar)); +} + +int APS5_VABI sceHmd2GazeGetResult(const Hmd2::GazeResultParameter* param, Hmd2::GazeResult* result) { + const std::int32_t status = Hmd2GazeHandle(param, result, 0x38, __func__); + if (status != Hmd2::InvalidHandle) return status; + auto* bytes = reinterpret_cast(result); + std::memset(bytes + 4, 0, 0x38 - 4); + Hmd2StoreInvalidGaze(bytes, 0x14); + return status; +} + +int APS5_VABI sceHmd2GazeGetResultForFoveatedRendering(const Hmd2::GazeResultParameter* param, Hmd2::FoveatedRenderingResult* result) { + const std::int32_t status = Hmd2GazeHandle(param, result, 0x18, __func__); + if (status != Hmd2::InvalidHandle) return status; + Hmd2StoreInvalidGaze(reinterpret_cast(result), 8); + return status; } std::int32_t APS5_VABI sceHmd2GetDeviceInformation(Hmd2::DeviceInformation* info) { diff --git a/core/libs/prx/libSceHttp/Export.cpp b/core/libs/prx/libSceHttp/Export.cpp index 1cde24b012..cce00df0ae 100644 --- a/core/libs/prx/libSceHttp/Export.cpp +++ b/core/libs/prx/libSceHttp/Export.cpp @@ -317,9 +317,8 @@ int APS5_VABI sceHttpSendRequest(int request_id, const void* post_data, size_t s } int APS5_VABI sceHttpSetAuthEnabled(int id, int enable) { - (void)id; - (void)enable; - return 0; + if (!HttpInitialised()) return ERROR_BEFORE_INIT; + return SetHttpOption(id, &HttpOptions::authEnabled, enable != 0) ? 0 : ERROR_INVALID_ID; } int APS5_VABI sceHttpSetAuthInfoCallback(int id, HttpAuthInfoCallback callback, void* userArg) { @@ -727,12 +726,17 @@ int APS5_VABI sceHttpAddRequestHeaderRaw(int id, const char* header, int mode) { } int APS5_VABI sceHttpRemoveRequestHeader(int id, const char* name) { - NotImplemented_nid_no_patch(__func__); + (void)id; + (void)name; return 0; } -int APS5_VABI sceHttpGetAuthEnabled(int id, int* enabled) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceHttpGetAuthEnabled(int id, int* enable) { + if (!HttpInitialised()) return ERROR_BEFORE_INIT; + if (!enable) return ERROR_INVALID_VALUE; + const auto options = FindHttpOptions(id); + if (!options) return ERROR_INVALID_ID; + *enable = options->authEnabled ? 1 : 0; return 0; } @@ -822,9 +826,10 @@ int APS5_VABI sceHttpSetDefaultAcceptEncodingGZIPEnabled(int http_ctx_id, int en return SetHttpContextAcceptEncodingGzip(http_ctx_id, enable != 0) ? 0 : ERROR_INVALID_ID; } -int APS5_VABI sceHttpSetProxy(int id, int mode, int authentication, const char* hostname, std::uint16_t port) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceHttpSetProxy(int id, int, int, const char* hostname, std::uint16_t) { + if (!HttpInitialised()) return ERROR_BEFORE_INIT; + if (!hostname) return ERROR_INVALID_VALUE; + return FindHttpOptions(id) ? 0 : ERROR_INVALID_ID; } int APS5_VABI sceHttpSetSocketCreationCallback(std::int32_t id, HttpSocketCreationCallback callback, void* userArg) { diff --git a/core/libs/prx/libSceHttp/src/Options.hpp b/core/libs/prx/libSceHttp/src/Options.hpp index beca67b395..c92168167a 100644 --- a/core/libs/prx/libSceHttp/src/Options.hpp +++ b/core/libs/prx/libSceHttp/src/Options.hpp @@ -6,6 +6,7 @@ struct HttpOptions { bool autoRedirect = true; + bool authEnabled = false; bool nonblock = false; bool acceptEncodingGzip = false; HttpEpollHandle epoll = nullptr; diff --git a/core/libs/prx/libSceHttp/src/Uri.cpp b/core/libs/prx/libSceHttp/src/Uri.cpp index b1cc8db443..2f1620bfac 100644 --- a/core/libs/prx/libSceHttp/src/Uri.cpp +++ b/core/libs/prx/libSceHttp/src/Uri.cpp @@ -10,6 +10,8 @@ #include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libSceHttp/src/HttpErrors.hpp" +extern "C" int APS5_VABI sceHttpUriSweepPath(char* dst, const char* src, size_t srcSize); + namespace { constexpr uint32_t URI_BUILD_WITH_SCHEME = 0x01; @@ -29,6 +31,7 @@ struct UriParts { std::string_view password; std::string_view hostname; std::string path; + size_t pathSpace = 0; std::string_view query; std::string_view fragment; uint16_t port = 0; @@ -55,8 +58,8 @@ bool equalsIgnoreCase(std::string_view left, std::string_view right) { } uint16_t defaultPort(std::string_view scheme) { - if (equalsIgnoreCase(scheme, "http")) return 80; - if (equalsIgnoreCase(scheme, "https")) return 443; + if (scheme.size() >= 5 && equalsIgnoreCase(scheme.substr(0, 5), "https")) return 443; + if (scheme.size() >= 4 && equalsIgnoreCase(scheme.substr(0, 4), "http")) return 80; return 0; } @@ -64,31 +67,21 @@ bool isHostCharacter(char c) { return isAsciiAlpha(c) || isAsciiDigit(c) || c == '-' || c == '.' || c == '_'; } -std::string removeDotSegments(std::string_view input) { - std::string output; - while (!input.empty()) { - if (input.starts_with("../")) input.remove_prefix(3); - else if (input.starts_with("./")) input.remove_prefix(2); - else if (input.starts_with("/./")) input.remove_prefix(2); - else if (input == "/.") input = "/"; - else if (input.starts_with("/../") || input == "/..") { - input = input.size() == 3 ? std::string_view("/") : input.substr(3); - const size_t slash = output.rfind('/'); - output.erase(slash == std::string::npos ? 0 : slash); - } else if (input == "." || input == "..") input = {}; - else { - const size_t end = input.find('/', 1); - const size_t length = end == std::string_view::npos ? input.size() : end; - output.append(input.substr(0, length)); - input.remove_prefix(length); - } - } - return output; +bool isUserinfoCharacter(char c) { + return isAsciiAlpha(c) || isAsciiDigit(c) || std::string_view("-._~!$&'()*+,;=%:").find(c) != std::string_view::npos; +} + +std::string sweptPath(std::string_view path) { + const std::string source(path); + std::string swept(source.size() + 1, '\0'); + sceHttpUriSweepPath(swept.data(), source.c_str(), source.size() + 1); + swept.resize(std::strlen(swept.c_str())); + return swept; } size_t schemeLength(std::string_view uri) { size_t schemeEnd = 0; - if (!uri.empty() && isAsciiAlpha(uri[0])) { + if (!uri.empty() && (isAsciiAlpha(uri[0]) || uri[0] == '+' || uri[0] == '-' || uri[0] == '.')) { schemeEnd = 1; while (schemeEnd < uri.size() && (isAsciiAlpha(uri[schemeEnd]) || isAsciiDigit(uri[schemeEnd]) || uri[schemeEnd] == '+' || uri[schemeEnd] == '-' || uri[schemeEnd] == '.')) ++schemeEnd; @@ -110,49 +103,51 @@ int parseUri(std::string_view uri, UriParts& parts) { parts.opaque = !uri.starts_with("//"); if (!parts.opaque) uri.remove_prefix(2); - std::string_view authority = uri.substr(0, uri.find_first_of("/?#")); - uri.remove_prefix(authority.size()); - - const size_t at = authority.rfind('@'); - if (at != std::string_view::npos) { - const std::string_view userinfo = authority.substr(0, at); + size_t userinfoEnd = 0; + while (userinfoEnd < uri.size() && isUserinfoCharacter(uri[userinfoEnd])) ++userinfoEnd; + if (userinfoEnd < uri.size() && uri[userinfoEnd] == '@') { + const std::string_view userinfo = uri.substr(0, userinfoEnd); const size_t colon = userinfo.find(':'); parts.username = userinfo.substr(0, colon); if (colon != std::string_view::npos) parts.password = userinfo.substr(colon + 1); - authority.remove_prefix(at + 1); + uri.remove_prefix(userinfoEnd + 1); } - std::string_view port; - if (authority.starts_with('[')) { - const size_t close = authority.find(']'); + if (uri.starts_with('[')) { + const size_t close = uri.find(']'); if (close == std::string_view::npos) return ERROR_INVALID_URL; - parts.hostname = authority.substr(1, close - 1); - authority.remove_prefix(close + 1); - if (!authority.empty() && authority[0] != ':') return ERROR_INVALID_URL; - if (!authority.empty()) port = authority.substr(1); + parts.hostname = uri.substr(1, close - 1); for (const char c : parts.hostname) { if (!isHostCharacter(c) && c != ':') return ERROR_INVALID_URL; } + uri.remove_prefix(close + 1); } else { - const size_t colon = authority.find(':'); - parts.hostname = authority.substr(0, colon); - if (colon != std::string_view::npos) port = authority.substr(colon + 1); - for (const char c : parts.hostname) { - if (!isHostCharacter(c)) return ERROR_INVALID_URL; + size_t hostEnd = 0; + if (!uri.starts_with('.')) { + while (hostEnd < uri.size() && isHostCharacter(uri[hostEnd])) ++hostEnd; } + parts.hostname = uri.substr(0, hostEnd); + uri.remove_prefix(hostEnd); } - if (port.size() > 5) return ERROR_INVALID_URL; - uint32_t portValue = 0; - for (const char c : port) { - if (!isAsciiDigit(c)) return ERROR_INVALID_URL; - portValue = portValue * 10 + static_cast(c - '0'); + parts.port = defaultPort(parts.scheme); + if (uri.starts_with(':')) { + uri.remove_prefix(1); + const std::string_view port = uri.substr(0, uri.find('/')); + uri.remove_prefix(port.size()); + if (port.size() > 5) return ERROR_INVALID_URL; + uint32_t portValue = 0; + for (const char c : port) { + if (!isAsciiDigit(c)) return ERROR_INVALID_URL; + portValue = portValue * 10 + static_cast(c - '0'); + } + if (portValue > 0x10000) return ERROR_INVALID_URL; + if (!port.empty()) parts.port = static_cast(portValue); } - if (portValue > 0xFFFF) return ERROR_INVALID_URL; - parts.port = port.empty() ? defaultPort(parts.scheme) : static_cast(portValue); const std::string_view path = uri.substr(0, uri.find_first_of("?#")); - parts.path = removeDotSegments(path); + parts.path = sweptPath(path); + parts.pathSpace = path.size(); uri.remove_prefix(path.size()); if (uri.starts_with('?')) { @@ -164,8 +159,8 @@ int parseUri(std::string_view uri, UriParts& parts) { } size_t poolSize(const UriParts& parts) { - const std::string_view fields[] = {parts.scheme, parts.username, parts.password, parts.hostname, parts.path, parts.query, parts.fragment}; - size_t size = 0; + const std::string_view fields[] = {parts.scheme, parts.username, parts.password, parts.hostname, parts.query, parts.fragment}; + size_t size = parts.pathSpace + 1; for (const std::string_view field : fields) size += field.size() + 1; return size; } @@ -345,10 +340,12 @@ int APS5_VABI sceHttpUriParse(SceHttpUriElement* out, const char* src_url, void* if (const int result = parseUri(src_url, parts); result != 0) return result; const std::string_view fields[] = {parts.scheme, parts.username, parts.password, parts.hostname, parts.path, parts.query, parts.fragment}; + const size_t spaces[] = {parts.scheme.size(), parts.username.size(), parts.password.size(), parts.hostname.size(), parts.pathSpace, parts.query.size(), + parts.fragment.size()}; const size_t size = poolSize(parts); + if (write && prepare < size) return ERROR_OUT_OF_MEMORY; if (require) *require = size; if (!write) return 0; - if (prepare < size) return ERROR_OUT_OF_MEMORY; char* next = static_cast(pool); char* strings[7]; @@ -356,7 +353,7 @@ int APS5_VABI sceHttpUriParse(SceHttpUriElement* out, const char* src_url, void* strings[i] = next; if (!fields[i].empty()) std::memcpy(next, fields[i].data(), fields[i].size()); next[fields[i].size()] = '\0'; - next += fields[i].size() + 1; + next += spaces[i] + 1; } *out = {}; out->opaque = parts.opaque; diff --git a/core/libs/prx/libSceImeDialog/Export.cpp b/core/libs/prx/libSceImeDialog/Export.cpp index c9b181b025..c19acdd36a 100644 --- a/core/libs/prx/libSceImeDialog/Export.cpp +++ b/core/libs/prx/libSceImeDialog/Export.cpp @@ -17,6 +17,7 @@ constexpr std::uint32_t EndStatusUserCanceled = 1; constexpr std::uint32_t EndStatusAborted = 2; constexpr std::uint32_t MaxTextLength = 2048; constexpr int ErrorBusy = static_cast(0x80bc0001u); +constexpr int ErrorSuspending = static_cast(0x80bc0009u); constexpr int ErrorNotOpened = static_cast(0x80bc0002u); constexpr int ErrorNotInUse = static_cast(0x80bc0107u); constexpr int ErrorInvalidMaxTextLength = static_cast(0x80bc0016u); @@ -190,18 +191,20 @@ int APS5_VABI sceImeDialogForceClose(void) { } int APS5_VABI sceImeDialogGetCurrentStarState(int64_t state) { - NotImplemented_nid_no_patch(__func__); + std::lock_guard lock(g_mutex); + if (g_dialog.status == StatusNone) return ErrorNotInUse; + if (state == 0) return ErrorInvalidAddress; + *reinterpret_cast(state) = 0; return 0; } int APS5_VABI sceImeDialogInitInternal(Param* param, ExtendedParam* extended) { - NotImplemented_nid_no_patch(__func__); - return 0; + return sceImeDialogInit(param, extended); } -int APS5_VABI sceImeDialogSetPanelPosition(int32_t posx, int32_t posy) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceImeDialogSetPanelPosition(int32_t, int32_t) { + std::lock_guard lock(g_mutex); + return g_dialog.status == StatusNone ? ErrorNotInUse : ErrorSuspending; } diff --git a/core/libs/prx/libSceInvitationDialog/Export.cpp b/core/libs/prx/libSceInvitationDialog/Export.cpp new file mode 100644 index 0000000000..8ae801bc26 --- /dev/null +++ b/core/libs/prx/libSceInvitationDialog/Export.cpp @@ -0,0 +1,31 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceInvitationDialogGetResult() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceInvitationDialogInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceInvitationDialogOpen() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceInvitationDialogTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceInvitationDialogUpdateStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceJson2/CMakeLists.txt b/core/libs/prx/libSceJson2/CMakeLists.txt index f3e7cb682f..dbc704366f 100644 --- a/core/libs/prx/libSceJson2/CMakeLists.txt +++ b/core/libs/prx/libSceJson2/CMakeLists.txt @@ -8,8 +8,19 @@ if(BUILD_TESTING) target_link_libraries(guest_json_tests PRIVATE libSceJson2 libc) configure_windows_unwind(guest_json_tests) add_test(NAME guest_json COMMAND guest_json_tests) + add_test(NAME guest_json_numeric_locale COMMAND guest_json_tests --numeric-locale) + set_tests_properties(guest_json_numeric_locale PROPERTIES SKIP_RETURN_CODE 77) + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + set(numeric_locale_dir ${CMAKE_CURRENT_BINARY_DIR}/numeric-locale) + set(numeric_locale_data ${LIBS_INCLUDE_DIR}/tests/numeric-locale) + add_test(NAME guest_json_numeric_locale_setup COMMAND sh -c + "rm -rf '${numeric_locale_dir}' && mkdir -p '${numeric_locale_dir}' && I18NPATH='${numeric_locale_dir}' localedef -c -f '${numeric_locale_data}/comma.charmap' -i '${numeric_locale_data}/comma.locale' '${numeric_locale_dir}/xx_COMMA'; test -f '${numeric_locale_dir}/xx_COMMA/LC_NUMERIC'") + set_tests_properties(guest_json_numeric_locale_setup PROPERTIES FIXTURES_SETUP json_numeric_locale) + set_tests_properties(guest_json_numeric_locale PROPERTIES FIXTURES_REQUIRED json_numeric_locale + ENVIRONMENT "LOCPATH=${numeric_locale_dir}") + endif() if(WIN32) - set_tests_properties(guest_json PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_json guest_json_numeric_locale PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/prx/libSceJson2/Export.cpp b/core/libs/prx/libSceJson2/Export.cpp index 5b92c48d68..51c4bb6730 100644 --- a/core/libs/prx/libSceJson2/Export.cpp +++ b/core/libs/prx/libSceJson2/Export.cpp @@ -1,14 +1,17 @@ #include #include #include +#include #include #include #include #include #include #include +#include #include #include +#include #include #include #include "SceTypes.hpp" @@ -299,8 +302,9 @@ void AppendEscaped(std::string& out, const std::string& text) { std::string RealText(double value) { if (!std::isfinite(value)) throw std::runtime_error("sce::Json: cannot serialize a non-finite number"); char buffer[32]; - std::snprintf(buffer, sizeof(buffer), "%.17g", value); - return buffer; + const auto result = std::to_chars(buffer, buffer + sizeof(buffer), value, std::chars_format::general, 17); + if (result.ec != std::errc{}) throw std::runtime_error("sce::Json: real number formatting failed"); + return std::string(buffer, result.ptr); } void Serialize(std::string& out, const Node& n) { @@ -510,8 +514,16 @@ private: } } } - const double real = std::strtod(text.c_str(), nullptr); - if (!std::isfinite(real)) throw std::runtime_error("sce::Json::Parser::parse: number " + text + " overflows a double; the console's behaviour is unverified"); + double real = 0; + const auto parsed = std::from_chars(text.data(), text.data() + text.size(), real); + bool converted = parsed.ec == std::errc{} && parsed.ptr == text.data() + text.size(); + if (parsed.ec == std::errc::result_out_of_range) { + std::istringstream stream(text); + stream.imbue(std::locale::classic()); + stream >> real; + converted = !stream.fail() && stream.eof(); + } + if (!converted || !std::isfinite(real)) throw std::runtime_error("sce::Json::Parser::parse: number " + text + " overflows a double; the console's behaviour is unverified"); out.type = TypeReal; out.real = real; return true; diff --git a/core/libs/prx/libSceJxr/CMakeLists.txt b/core/libs/prx/libSceJxr/CMakeLists.txt new file mode 100644 index 0000000000..251402ba35 --- /dev/null +++ b/core/libs/prx/libSceJxr/CMakeLists.txt @@ -0,0 +1,15 @@ +add_library(libSceJxr SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceJxr PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceJxr) + +if(BUILD_TESTING) + add_test_executable(guest_jxr_tests ${LIBS_INCLUDE_DIR}/tests/GuestJxr.cpp) + target_include_directories(guest_jxr_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_jxr_tests PRIVATE libSceJxr libc) + configure_windows_unwind(guest_jxr_tests) + add_test(NAME guest_jxr COMMAND guest_jxr_tests) + if(WIN32) + set_tests_properties(guest_jxr PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceJxr/Export.cpp b/core/libs/prx/libSceJxr/Export.cpp index 608aedc418..de2c55aa4e 100644 --- a/core/libs/prx/libSceJxr/Export.cpp +++ b/core/libs/prx/libSceJxr/Export.cpp @@ -3,12 +3,21 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +namespace { +constexpr std::int64_t WmpErrSuccess = 0; +} + extern "C" { +void APS5_VABI free_nid_postfix(void* memory); + APS5_DUMMY_FUN -int APS5_VABI PKFree_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int64_t APS5_VABI PKFree_nid_postfix(void** memory) { + if (memory != nullptr) { + free_nid_postfix(*memory); + *memory = nullptr; + } + return WmpErrSuccess; } int APS5_VABI PixelFormatLookup_nid_postfix(void) { diff --git a/core/libs/prx/libSceKeyboard/Export.cpp b/core/libs/prx/libSceKeyboard/Export.cpp index c7be6fe290..9cbc7cd436 100644 --- a/core/libs/prx/libSceKeyboard/Export.cpp +++ b/core/libs/prx/libSceKeyboard/Export.cpp @@ -27,6 +27,10 @@ int APS5_VABI sceKeyboardReadState(int32_t handle, KeyboardData* data) { return Keyboard::ReadState(handle, data); } +int APS5_VABI sceKeyboardGetConnection(std::int32_t handle, bool* connected) { + return Keyboard::GetConnection(handle, connected); +} + int APS5_VABI sceKeyboardGetKey2Char(std::int32_t handle, std::int32_t arrange, std::uint8_t led, std::uint8_t modifierKey, std::uint16_t keyCode, KeyboardCharData* charData) { return Keyboard::GetKey2Char(handle, arrange, led, modifierKey, keyCode, charData); } @@ -47,8 +51,7 @@ int APS5_VABI sceKeyboardDeviceOpen(std::uint64_t deviceId, std::int32_t index, } int APS5_VABI sceKeyboardDisconnectAll(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Keyboard::DisconnectAll(); } int APS5_VABI sceKeyboardDisconnectDevice(std::int32_t deviceId) { @@ -61,11 +64,6 @@ int APS5_VABI sceKeyboardDisconnectPort(std::int32_t first, std::int32_t second) return 0; } -int APS5_VABI sceKeyboardGetConnection(std::int32_t handle, bool* connected) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceKeyboardGetDeviceInfo(std::int32_t deviceId, KeyboardDeviceInfo* info) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceKeyboard/include/KeyboardState.hpp b/core/libs/prx/libSceKeyboard/include/KeyboardState.hpp index 11c32a5447..3552af3153 100644 --- a/core/libs/prx/libSceKeyboard/include/KeyboardState.hpp +++ b/core/libs/prx/libSceKeyboard/include/KeyboardState.hpp @@ -18,6 +18,8 @@ int Open(int userId, std::int32_t type, std::int32_t index); int Close(std::int32_t handle); int Read(std::int32_t handle, KeyboardData* data, std::int32_t num); int ReadState(std::int32_t handle, KeyboardData* data); +int GetConnection(std::int32_t handle, bool* connected); +int DisconnectAll(); int GetKey2Char(std::int32_t handle, std::int32_t arrange, std::uint32_t led, std::uint32_t modifierKey, std::uint16_t keyCode, KeyboardCharData* charData); void Publish(const KeyboardInputEvent& event); } diff --git a/core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp b/core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp index bd9cf7d8f8..954040b147 100644 --- a/core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp +++ b/core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp @@ -154,6 +154,23 @@ int ReadState(std::int32_t handle, KeyboardData* data) { return KEYBOARD_OK; } +int GetConnection(std::int32_t handle, bool* isConnected) { + std::lock_guard lock(keyboardMutex); + if (!initialized) return KEYBOARD_ERROR_NOT_INITIALIZED; + if (handle != KEYBOARD_HANDLE || !opened) return KEYBOARD_ERROR_INVALID_HANDLE; + if (isConnected == nullptr) return KEYBOARD_ERROR_INVALID_ARG; + *isConnected = connected; + return KEYBOARD_OK; +} + +int DisconnectAll() { + KeyboardInputEvent event{}; + event.connectionChange = true; + event.connected = false; + Publish(event); + return KEYBOARD_OK; +} + int GetKey2Char(std::int32_t handle, std::int32_t arrange, std::uint32_t ledState, std::uint32_t modifierKey, std::uint16_t keyCode, KeyboardCharData* charData) { { std::lock_guard lock(keyboardMutex); diff --git a/core/libs/prx/libSceKeyboard/tests/Keyboard.cpp b/core/libs/prx/libSceKeyboard/tests/Keyboard.cpp index 7c97932bff..0307b994df 100644 --- a/core/libs/prx/libSceKeyboard/tests/Keyboard.cpp +++ b/core/libs/prx/libSceKeyboard/tests/Keyboard.cpp @@ -13,6 +13,7 @@ int APS5_VABI sceKeyboardOpen(int, std::int32_t, std::int32_t, const void*); int APS5_VABI sceKeyboardClose(std::int32_t); int APS5_VABI sceKeyboardRead(std::int32_t, KeyboardData*, std::int32_t); int APS5_VABI sceKeyboardReadState(std::int32_t, KeyboardData*); +int APS5_VABI sceKeyboardGetConnection(std::int32_t, bool*); int APS5_VABI sceKeyboardGetKey2Char(std::int32_t, std::int32_t, std::uint8_t, std::uint8_t, std::uint16_t, KeyboardCharData*); int APS5_VABI sceKeyboardConnectPort(void); int APS5_VABI sceKeyboardDisconnectAll(void); @@ -44,14 +45,20 @@ static std::uint16_t Jis(std::uint32_t modifierKey, std::uint16_t keyCode, std:: int main() { KeyboardData data[16]{}; KeyboardCharData charData{}; + bool connected = false; Require(sceKeyboardOpen(1, 0, 0, nullptr) == KEYBOARD_ERROR_NOT_INITIALIZED); + Require(sceKeyboardGetConnection(KEYBOARD_HANDLE, &connected) == KEYBOARD_ERROR_NOT_INITIALIZED); Require(sceKeyboardInit() == KEYBOARD_OK); Require(sceKeyboardInit() == KEYBOARD_OK); Require(sceKeyboardOpen(1, 0, 1, nullptr) == KEYBOARD_ERROR_INVALID_ARG); Require(sceKeyboardOpen(1, 1, 0, nullptr) == KEYBOARD_ERROR_INVALID_ARG); Require(sceKeyboardGetKey2Char(KEYBOARD_HANDLE, KEYBOARD_ARRANGEMENT_101, 0, 0, 0x04, &charData) == KEYBOARD_ERROR_INVALID_HANDLE); + Require(sceKeyboardGetConnection(KEYBOARD_HANDLE, &connected) == KEYBOARD_ERROR_INVALID_HANDLE); Require(sceKeyboardOpen(1, 0, 0, nullptr) == KEYBOARD_HANDLE); Require(sceKeyboardOpen(1, 0, 0, nullptr) == KEYBOARD_ERROR_ALREADY_OPENED); + Require(sceKeyboardGetConnection(42, &connected) == KEYBOARD_ERROR_INVALID_HANDLE); + Require(sceKeyboardGetConnection(KEYBOARD_HANDLE, nullptr) == KEYBOARD_ERROR_INVALID_ARG); + Require(sceKeyboardGetConnection(KEYBOARD_HANDLE, &connected) == KEYBOARD_OK && connected); Require(sceKeyboardRead(42, data, 1) == KEYBOARD_ERROR_INVALID_HANDLE); Require(sceKeyboardRead(KEYBOARD_HANDLE, nullptr, 1) == KEYBOARD_ERROR_INVALID_ARG); Require(sceKeyboardRead(KEYBOARD_HANDLE, data, 17) == KEYBOARD_ERROR_INVALID_ARG); @@ -96,9 +103,11 @@ int main() { window.window.event = SDL_WINDOWEVENT_CLOSE; input.HandleEvent(window, 7); Require(sceKeyboardRead(KEYBOARD_HANDLE, data, 1) == 1 && !data[0].connected && data[0].length == 1 && data[0].key_code[0] == 0); + Require(sceKeyboardGetConnection(KEYBOARD_HANDLE, &connected) == KEYBOARD_OK && !connected); window.window.event = SDL_WINDOWEVENT_FOCUS_GAINED; input.HandleEvent(window, 7); Require(sceKeyboardRead(KEYBOARD_HANDLE, data, 1) == 1 && data[0].connected); + Require(sceKeyboardGetConnection(KEYBOARD_HANDLE, &connected) == KEYBOARD_OK && connected); for (std::uint16_t key = 0x04; key < 0x04 + 20; ++key) { KeyboardInputEvent press{}; @@ -143,11 +152,31 @@ int main() { Require(sceKeyboardGetKey2Char(KEYBOARD_HANDLE, 2, 0, 0, 0x04, &charData) == KEYBOARD_ERROR_INVALID_ARG); Require(sceKeyboardClose(KEYBOARD_HANDLE) == KEYBOARD_OK); + Require(sceKeyboardGetConnection(KEYBOARD_HANDLE, &connected) == KEYBOARD_ERROR_INVALID_HANDLE); Require(sceKeyboardRead(KEYBOARD_HANDLE, data, 1) == KEYBOARD_ERROR_INVALID_HANDLE); Require(sceKeyboardReadState(KEYBOARD_HANDLE, data) == KEYBOARD_ERROR_INVALID_HANDLE); Require(sceKeyboardClose(KEYBOARD_HANDLE) == KEYBOARD_ERROR_INVALID_HANDLE); Require(sceKeyboardOpen(1, 0, 0, nullptr) == KEYBOARD_HANDLE); Require(sceKeyboardRead(KEYBOARD_HANDLE, data, 1) == 1 && data[0].length == 1 && data[0].key_code[0] == 0); + Require(data[0].connected); + KeyboardInputEvent pressed{}; + pressed.keyCode = 0x04; + pressed.pressed = true; + KeyboardPublishInput_nid_postfix(pressed); + Require(sceKeyboardRead(KEYBOARD_HANDLE, data, 1) == 1 && data[0].key_code[0] == 0x04); + Require(sceKeyboardDisconnectAll() == KEYBOARD_OK); + Require(sceKeyboardRead(KEYBOARD_HANDLE, data, 1) == 1 && !data[0].connected && data[0].length == 1 && data[0].key_code[0] == 0); + Require(sceKeyboardReadState(KEYBOARD_HANDLE, data) == KEYBOARD_OK && !data[0].connected && data[0].key_code[0] == 0); + Require(sceKeyboardDisconnectAll() == KEYBOARD_OK); + Require(sceKeyboardRead(KEYBOARD_HANDLE, data, 1) == 0); + pressed.keyCode = 0x05; + KeyboardPublishInput_nid_postfix(pressed); + Require(sceKeyboardRead(KEYBOARD_HANDLE, data, 1) == 0); + window.window.event = SDL_WINDOWEVENT_FOCUS_LOST; + input.HandleEvent(window, 7); + window.window.event = SDL_WINDOWEVENT_FOCUS_GAINED; + input.HandleEvent(window, 7); + Require(sceKeyboardRead(KEYBOARD_HANDLE, data, 1) == 1 && data[0].connected); Require(sceKeyboardClose(KEYBOARD_HANDLE) == KEYBOARD_OK); bool threwConnect = false; @@ -157,12 +186,4 @@ int main() { threwConnect = true; } Require(threwConnect); - - bool threwDisconnect = false; - try { - sceKeyboardDisconnectAll(); - } catch (const std::runtime_error&) { - threwDisconnect = true; - } - Require(threwDisconnect); } diff --git a/core/libs/prx/libSceLibcInternal/UserContext.cpp b/core/libs/prx/libSceLibcInternal/UserContext.cpp index f5f6c7eae8..3f743af8f4 100644 --- a/core/libs/prx/libSceLibcInternal/UserContext.cpp +++ b/core/libs/prx/libSceLibcInternal/UserContext.cpp @@ -20,12 +20,51 @@ constexpr std::uint64_t MinimumStackSize = 2048; extern "C" { +#if defined(_WIN32) && defined(__SEH__) +[[noreturn]] void APS5_VABI Aps5MakecontextStart_nid_no_patch(UserUcontext* context, ContextFunction function, const std::uint64_t* arguments); + +[[noreturn]] void APS5_VABI Aps5MakecontextFinish_nid_no_patch(UserUcontext* context) { + if (context->link == nullptr) LibcExit_nid_no_patch(0); + setcontext_nid_postfix(context->link); + std::abort(); +} + +asm(R"( +.text +.globl Aps5MakecontextStart_nid_no_patch +.def Aps5MakecontextStart_nid_no_patch; .scl 2; .type 32; .endef +.seh_proc Aps5MakecontextStart_nid_no_patch +Aps5MakecontextStart_nid_no_patch: + pushq %rbp + .seh_pushreg %rbp + subq $48, %rsp + .seh_stackalloc 48 + leaq 48(%rsp), %rbp + .seh_setframe %rbp, 48 + .seh_endprologue + movq %rdi, -8(%rbp) + movq %rsi, %rax + movq %rdx, %r10 + movq (%r10), %rdi + movq 8(%r10), %rsi + movq 16(%r10), %rdx + movq 24(%r10), %rcx + movq 32(%r10), %r8 + movq 40(%r10), %r9 + call *%rax + movq -8(%rbp), %rdi + call Aps5MakecontextFinish_nid_no_patch + ud2 +.seh_endproc +)"); +#else [[noreturn]] void APS5_VABI Aps5MakecontextStart_nid_no_patch(UserUcontext* context, ContextFunction function, const std::uint64_t* arguments) { function(arguments[0], arguments[1], arguments[2], arguments[3], arguments[4], arguments[5]); if (context->link == nullptr) LibcExit_nid_no_patch(0); setcontext_nid_postfix(context->link); std::abort(); } +#endif void APS5_VABI makecontext_nid_postfix(UserUcontext* context, void (APS5_VABI* start)(), int count, ...) { if (context == nullptr || context->mcontext.len != sizeof(UserMcontext)) return; diff --git a/core/libs/prx/libSceLibreSsl/CMakeLists.txt b/core/libs/prx/libSceLibreSsl/CMakeLists.txt new file mode 100644 index 0000000000..d9d266270c --- /dev/null +++ b/core/libs/prx/libSceLibreSsl/CMakeLists.txt @@ -0,0 +1,16 @@ +add_library(libSceLibreSsl SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Digest.cpp) +target_include_directories(libSceLibreSsl PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceLibreSsl) + +if(BUILD_TESTING) + add_test_executable(guest_libressl_digest_tests ${LIBS_INCLUDE_DIR}/tests/GuestLibreSslDigest.cpp) + target_include_directories(guest_libressl_digest_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_libressl_digest_tests PRIVATE libSceLibreSsl libc) + configure_windows_unwind(guest_libressl_digest_tests) + add_test(NAME guest_libressl_digest COMMAND guest_libressl_digest_tests) + set_tests_properties(guest_libressl_digest PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_libressl_digest PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceLibreSsl/Export.cpp b/core/libs/prx/libSceLibreSsl/Export.cpp index 93b05e0fec..670bb8911e 100644 --- a/core/libs/prx/libSceLibreSsl/Export.cpp +++ b/core/libs/prx/libSceLibreSsl/Export.cpp @@ -846,36 +846,6 @@ int APS5_VABI HMAC_Update_nid_postfix(void) { return 0; } -int APS5_VABI MD4_Final_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI MD4_Init_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI MD4_Update_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI MD5_Final_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI MD5_Init_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI MD5_Update_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI OBJ_cleanup_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -1116,86 +1086,6 @@ int APS5_VABI RSA_verify_PKCS1_PSS_nid_postfix(void) { return 0; } -int APS5_VABI SHA1_Final_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI SHA1_Init_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI SHA1_Update_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI SHA224_Final_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI SHA224_Init_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI SHA224_Update_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI SHA256_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI SHA256_Final_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI SHA256_Init_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI SHA256_Update_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI SHA384_Final_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI SHA384_Init_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI SHA384_Update_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI SHA512_Final_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI SHA512_Init_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI SHA512_Update_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI SSL_CIPHER_get_bits_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceLibreSsl/src/Digest.cpp b/core/libs/prx/libSceLibreSsl/src/Digest.cpp new file mode 100644 index 0000000000..e58974597e --- /dev/null +++ b/core/libs/prx/libSceLibreSsl/src/Digest.cpp @@ -0,0 +1,549 @@ +#include +#include +#include +#include +#include +#include +#include "prx/libc/include/General.hpp" + +namespace { +struct Md4Context { + std::uint32_t A, B, C, D; + std::uint32_t Nl, Nh; + std::uint32_t data[16]; + std::uint32_t num; +}; + +struct Md5Context { + std::uint32_t A, B, C, D; + std::uint32_t Nl, Nh; + std::uint32_t data[16]; + std::uint32_t num; +}; + +struct Sha1Context { + std::uint32_t h[5]; + std::uint32_t Nl, Nh; + std::uint32_t data[16]; + std::uint32_t num; +}; + +struct Sha256Context { + std::uint32_t h[8]; + std::uint32_t Nl, Nh; + std::uint32_t data[16]; + std::uint32_t num, mdLength; +}; + +struct Sha512Context { + std::uint64_t h[8]; + std::uint64_t Nl, Nh; + std::uint64_t data[16]; + std::uint32_t num, mdLength; +}; + +static_assert(sizeof(Md4Context) == 92); +static_assert(sizeof(Md5Context) == 92); +static_assert(sizeof(Sha1Context) == 96); +static_assert(sizeof(Sha256Context) == 112); +static_assert(sizeof(Sha512Context) == 216); + +constexpr std::size_t Sha224Length = 28; +constexpr std::size_t Sha256Length = 32; +constexpr std::size_t Sha384Length = 48; +constexpr std::size_t Sha512Length = 64; + +std::uint32_t LoadLittle32(const unsigned char* p) { + return std::uint32_t(p[0]) | std::uint32_t(p[1]) << 8 | std::uint32_t(p[2]) << 16 | std::uint32_t(p[3]) << 24; +} + +std::uint32_t LoadBig32(const unsigned char* p) { + return std::uint32_t(p[0]) << 24 | std::uint32_t(p[1]) << 16 | std::uint32_t(p[2]) << 8 | std::uint32_t(p[3]); +} + +std::uint64_t LoadBig64(const unsigned char* p) { + return std::uint64_t(LoadBig32(p)) << 32 | LoadBig32(p + 4); +} + +void StoreLittle32(unsigned char* p, std::uint32_t value) { + for (int i = 0; i < 4; ++i) p[i] = static_cast(value >> (8 * i)); +} + +void StoreBig32(unsigned char* p, std::uint32_t value) { + for (int i = 0; i < 4; ++i) p[i] = static_cast(value >> (24 - 8 * i)); +} + +void StoreBig64(unsigned char* p, std::uint64_t value) { + StoreBig32(p, static_cast(value >> 32)); + StoreBig32(p + 4, static_cast(value)); +} + +void Md4Block(Md4Context& c, const unsigned char* block) { + std::uint32_t x[16]; + for (int i = 0; i < 16; ++i) x[i] = LoadLittle32(block + 4 * i); + std::uint32_t a = c.A, b = c.B, cc = c.C, d = c.D; + const auto f = [](std::uint32_t x1, std::uint32_t y, std::uint32_t z) { return (x1 & y) | (~x1 & z); }; + const auto g = [](std::uint32_t x1, std::uint32_t y, std::uint32_t z) { return (x1 & y) | (x1 & z) | (y & z); }; + const auto h = [](std::uint32_t x1, std::uint32_t y, std::uint32_t z) { return x1 ^ y ^ z; }; + static constexpr int shifts1[4] {3, 7, 11, 19}; + static constexpr int shifts2[4] {3, 5, 9, 13}; + static constexpr int shifts3[4] {3, 9, 11, 15}; + static constexpr int order2[16] {0, 4, 8, 12, 1, 5, 9, 13, 2, 6, 10, 14, 3, 7, 11, 15}; + static constexpr int order3[16] {0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15}; + for (int i = 0; i < 16; ++i) { + const auto t = std::rotl(a + f(b, cc, d) + x[i], shifts1[i % 4]); + a = d; d = cc; cc = b; b = t; + } + for (int i = 0; i < 16; ++i) { + const auto t = std::rotl(a + g(b, cc, d) + x[order2[i]] + 0x5a827999u, shifts2[i % 4]); + a = d; d = cc; cc = b; b = t; + } + for (int i = 0; i < 16; ++i) { + const auto t = std::rotl(a + h(b, cc, d) + x[order3[i]] + 0x6ed9eba1u, shifts3[i % 4]); + a = d; d = cc; cc = b; b = t; + } + c.A += a; c.B += b; c.C += cc; c.D += d; +} + +void Md5Block(Md5Context& c, const unsigned char* block) { + static constexpr std::uint32_t constants[64] { + 0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, 0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501, + 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821, + 0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa, 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8, + 0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, 0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a, + 0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c, 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70, + 0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05, 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665, + 0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039, 0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1, + 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391, + }; + static constexpr int shifts[16] {7, 12, 17, 22, 5, 9, 14, 20, 4, 11, 16, 23, 6, 10, 15, 21}; + std::uint32_t x[16]; + for (int i = 0; i < 16; ++i) x[i] = LoadLittle32(block + 4 * i); + std::uint32_t a = c.A, b = c.B, cc = c.C, d = c.D; + for (int i = 0; i < 64; ++i) { + std::uint32_t mix; + int index; + switch (i / 16) { + case 0: mix = (b & cc) | (~b & d); index = i; break; + case 1: mix = (d & b) | (~d & cc); index = (5 * i + 1) % 16; break; + case 2: mix = b ^ cc ^ d; index = (3 * i + 5) % 16; break; + default: mix = cc ^ (b | ~d); index = (7 * i) % 16; break; + } + const auto t = d; + d = cc; + cc = b; + b += std::rotl(a + mix + constants[i] + x[index], shifts[(i / 16) * 4 + i % 4]); + a = t; + } + c.A += a; c.B += b; c.C += cc; c.D += d; +} + +void Sha1Block(Sha1Context& c, const unsigned char* block) { + std::uint32_t w[80]; + for (int i = 0; i < 16; ++i) w[i] = LoadBig32(block + 4 * i); + for (int i = 16; i < 80; ++i) w[i] = std::rotl(w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16], 1); + std::uint32_t a = c.h[0], b = c.h[1], cc = c.h[2], d = c.h[3], e = c.h[4]; + for (int i = 0; i < 80; ++i) { + std::uint32_t f, k; + if (i < 20) { f = (b & cc) | (~b & d); k = 0x5a827999; } + else if (i < 40) { f = b ^ cc ^ d; k = 0x6ed9eba1; } + else if (i < 60) { f = (b & cc) | (b & d) | (cc & d); k = 0x8f1bbcdc; } + else { f = b ^ cc ^ d; k = 0xca62c1d6; } + const auto t = std::rotl(a, 5) + f + e + k + w[i]; + e = d; d = cc; cc = std::rotl(b, 30); b = a; a = t; + } + c.h[0] += a; c.h[1] += b; c.h[2] += cc; c.h[3] += d; c.h[4] += e; +} + +void Sha256Block(Sha256Context& c, const unsigned char* block) { + static constexpr std::uint32_t constants[64] { + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, + 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, + 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, + 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, + 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2, + }; + std::uint32_t w[64]; + for (int i = 0; i < 16; ++i) w[i] = LoadBig32(block + 4 * i); + for (int i = 16; i < 64; ++i) { + const auto s0 = std::rotr(w[i - 15], 7) ^ std::rotr(w[i - 15], 18) ^ (w[i - 15] >> 3); + const auto s1 = std::rotr(w[i - 2], 17) ^ std::rotr(w[i - 2], 19) ^ (w[i - 2] >> 10); + w[i] = w[i - 16] + s0 + w[i - 7] + s1; + } + std::uint32_t v[8]; + std::memcpy(v, c.h, sizeof(v)); + for (int i = 0; i < 64; ++i) { + const auto s1 = std::rotr(v[4], 6) ^ std::rotr(v[4], 11) ^ std::rotr(v[4], 25); + const auto choice = (v[4] & v[5]) ^ (~v[4] & v[6]); + const auto t1 = v[7] + s1 + choice + constants[i] + w[i]; + const auto s0 = std::rotr(v[0], 2) ^ std::rotr(v[0], 13) ^ std::rotr(v[0], 22); + const auto majority = (v[0] & v[1]) ^ (v[0] & v[2]) ^ (v[1] & v[2]); + std::memmove(v + 1, v, 7 * sizeof(v[0])); + v[4] += t1; + v[0] = t1 + s0 + majority; + } + for (int i = 0; i < 8; ++i) c.h[i] += v[i]; +} + +void Sha512Block(Sha512Context& c, const unsigned char* block) { + static constexpr std::uint64_t constants[80] { + 0x428a2f98d728ae22, 0x7137449123ef65cd, 0xb5c0fbcfec4d3b2f, 0xe9b5dba58189dbbc, 0x3956c25bf348b538, + 0x59f111f1b605d019, 0x923f82a4af194f9b, 0xab1c5ed5da6d8118, 0xd807aa98a3030242, 0x12835b0145706fbe, + 0x243185be4ee4b28c, 0x550c7dc3d5ffb4e2, 0x72be5d74f27b896f, 0x80deb1fe3b1696b1, 0x9bdc06a725c71235, + 0xc19bf174cf692694, 0xe49b69c19ef14ad2, 0xefbe4786384f25e3, 0x0fc19dc68b8cd5b5, 0x240ca1cc77ac9c65, + 0x2de92c6f592b0275, 0x4a7484aa6ea6e483, 0x5cb0a9dcbd41fbd4, 0x76f988da831153b5, 0x983e5152ee66dfab, + 0xa831c66d2db43210, 0xb00327c898fb213f, 0xbf597fc7beef0ee4, 0xc6e00bf33da88fc2, 0xd5a79147930aa725, + 0x06ca6351e003826f, 0x142929670a0e6e70, 0x27b70a8546d22ffc, 0x2e1b21385c26c926, 0x4d2c6dfc5ac42aed, + 0x53380d139d95b3df, 0x650a73548baf63de, 0x766a0abb3c77b2a8, 0x81c2c92e47edaee6, 0x92722c851482353b, + 0xa2bfe8a14cf10364, 0xa81a664bbc423001, 0xc24b8b70d0f89791, 0xc76c51a30654be30, 0xd192e819d6ef5218, + 0xd69906245565a910, 0xf40e35855771202a, 0x106aa07032bbd1b8, 0x19a4c116b8d2d0c8, 0x1e376c085141ab53, + 0x2748774cdf8eeb99, 0x34b0bcb5e19b48a8, 0x391c0cb3c5c95a63, 0x4ed8aa4ae3418acb, 0x5b9cca4f7763e373, + 0x682e6ff3d6b2b8a3, 0x748f82ee5defb2fc, 0x78a5636f43172f60, 0x84c87814a1f0ab72, 0x8cc702081a6439ec, + 0x90befffa23631e28, 0xa4506cebde82bde9, 0xbef9a3f7b2c67915, 0xc67178f2e372532b, 0xca273eceea26619c, + 0xd186b8c721c0c207, 0xeada7dd6cde0eb1e, 0xf57d4f7fee6ed178, 0x06f067aa72176fba, 0x0a637dc5a2c898a6, + 0x113f9804bef90dae, 0x1b710b35131c471b, 0x28db77f523047d84, 0x32caab7b40c72493, 0x3c9ebe0a15c9bebc, + 0x431d67c49c100d4c, 0x4cc5d4becb3e42b6, 0x597f299cfc657e2a, 0x5fcb6fab3ad6faec, 0x6c44198c4a475817, + }; + std::uint64_t w[80]; + for (int i = 0; i < 16; ++i) w[i] = LoadBig64(block + 8 * i); + for (int i = 16; i < 80; ++i) { + const auto s0 = std::rotr(w[i - 15], 1) ^ std::rotr(w[i - 15], 8) ^ (w[i - 15] >> 7); + const auto s1 = std::rotr(w[i - 2], 19) ^ std::rotr(w[i - 2], 61) ^ (w[i - 2] >> 6); + w[i] = w[i - 16] + s0 + w[i - 7] + s1; + } + std::uint64_t v[8]; + std::memcpy(v, c.h, sizeof(v)); + for (int i = 0; i < 80; ++i) { + const auto s1 = std::rotr(v[4], 14) ^ std::rotr(v[4], 18) ^ std::rotr(v[4], 41); + const auto choice = (v[4] & v[5]) ^ (~v[4] & v[6]); + const auto t1 = v[7] + s1 + choice + constants[i] + w[i]; + const auto s0 = std::rotr(v[0], 28) ^ std::rotr(v[0], 34) ^ std::rotr(v[0], 39); + const auto majority = (v[0] & v[1]) ^ (v[0] & v[2]) ^ (v[1] & v[2]); + std::memmove(v + 1, v, 7 * sizeof(v[0])); + v[4] += t1; + v[0] = t1 + s0 + majority; + } + for (int i = 0; i < 8; ++i) c.h[i] += v[i]; +} + +template TContext& RequireContext(TContext* context, const char* function) { + if (!context) throw std::invalid_argument(std::string(function) + ": null context"); + return *context; +} + +void RequireInput(const void* data, std::size_t length, const char* function) { + if (!data && length) throw std::invalid_argument(std::string(function) + ": null input"); +} + +void RequireDigest(const unsigned char* digest, const char* function) { + if (!digest) throw std::invalid_argument(std::string(function) + ": null digest buffer"); +} + +void RequireBuffered(std::uint32_t buffered, std::size_t block, const char* function) { + if (buffered >= block) throw std::invalid_argument(std::string(function) + ": buffered byte count is not below the block size"); +} + +template void Update32(TContext& c, const void* input, std::size_t length, TBlock block) { + constexpr std::size_t blockSize = 64; + auto* buffer = reinterpret_cast(c.data); + auto* data = static_cast(input); + const std::uint64_t bits = (std::uint64_t(c.Nh) << 32 | c.Nl) + (std::uint64_t(length) << 3); + c.Nl = static_cast(bits); + c.Nh = static_cast(bits >> 32); + if (c.num) { + const std::size_t room = blockSize - c.num; + if (length < room) { + std::memcpy(buffer + c.num, data, length); + c.num += static_cast(length); + return; + } + std::memcpy(buffer + c.num, data, room); + block(c, buffer); + data += room; + length -= room; + c.num = 0; + std::memset(buffer, 0, blockSize); + } + for (; length >= blockSize; data += blockSize, length -= blockSize) block(c, data); + if (length) { + std::memcpy(buffer, data, length); + c.num = static_cast(length); + } +} + +template void Final32(TContext& c, bool bigEndian, TBlock block) { + constexpr std::size_t blockSize = 64; + auto* buffer = reinterpret_cast(c.data); + std::size_t n = c.num; + buffer[n++] = 0x80; + if (n > blockSize - 8) { + std::memset(buffer + n, 0, blockSize - n); + n = 0; + block(c, buffer); + } + std::memset(buffer + n, 0, blockSize - 8 - n); + if (bigEndian) { + StoreBig32(buffer + 56, c.Nh); + StoreBig32(buffer + 60, c.Nl); + } else { + StoreLittle32(buffer + 56, c.Nl); + StoreLittle32(buffer + 60, c.Nh); + } + block(c, buffer); + c.num = 0; + std::memset(buffer, 0, blockSize); +} + +void Sha512Update(Sha512Context& c, const void* input, std::size_t length) { + constexpr std::size_t blockSize = 128; + auto* buffer = reinterpret_cast(c.data); + auto* data = static_cast(input); + const std::uint64_t low = c.Nl + (std::uint64_t(length) << 3); + if (low < c.Nl) ++c.Nh; + c.Nh += std::uint64_t(length) >> 61; + c.Nl = low; + if (c.num) { + const std::size_t room = blockSize - c.num; + if (length < room) { + std::memcpy(buffer + c.num, data, length); + c.num += static_cast(length); + return; + } + std::memcpy(buffer + c.num, data, room); + Sha512Block(c, buffer); + data += room; + length -= room; + c.num = 0; + } + for (; length >= blockSize; data += blockSize, length -= blockSize) Sha512Block(c, data); + if (length) { + std::memcpy(buffer, data, length); + c.num = static_cast(length); + } +} + +int Sha512Final(unsigned char* digest, Sha512Context& c) { + constexpr std::size_t blockSize = 128; + auto* buffer = reinterpret_cast(c.data); + std::size_t n = c.num; + buffer[n++] = 0x80; + if (n > blockSize - 16) { + std::memset(buffer + n, 0, blockSize - n); + n = 0; + Sha512Block(c, buffer); + } + std::memset(buffer + n, 0, blockSize - 16 - n); + StoreBig64(buffer + 112, c.Nh); + StoreBig64(buffer + 120, c.Nl); + Sha512Block(c, buffer); + if (!digest) return 0; + if (c.mdLength == Sha224Length || c.mdLength == Sha256Length) + NotImplemented_nid_no_patch("SHA512_Final: SHA-512/224 and SHA-512/256 digest lengths"); + if (c.mdLength != Sha384Length && c.mdLength != Sha512Length) return 0; + for (std::size_t i = 0; i < c.mdLength / 8; ++i) StoreBig64(digest + 8 * i, c.h[i]); + return 1; +} + +int Sha256Final(unsigned char* digest, Sha256Context& c, const char* function) { + RequireDigest(digest, function); + RequireBuffered(c.num, 64, function); + Final32(c, true, Sha256Block); + if (c.mdLength > Sha256Length) return 0; + for (std::size_t i = 0; i < c.mdLength / 4; ++i) StoreBig32(digest + 4 * i, c.h[i]); + return 1; +} +} + +extern "C" { + +int APS5_VABI MD4_Init_nid_postfix(Md4Context* context) { + auto& c = RequireContext(context, __func__); + std::memset(&c, 0, sizeof(c)); + c.A = 0x67452301; + c.B = 0xefcdab89; + c.C = 0x98badcfe; + c.D = 0x10325476; + return 1; +} + +int APS5_VABI MD4_Update_nid_postfix(Md4Context* context, const void* data, std::size_t length) { + auto& c = RequireContext(context, __func__); + if (!length) return 1; + RequireInput(data, length, __func__); + RequireBuffered(c.num, 64, __func__); + Update32(c, data, length, Md4Block); + return 1; +} + +int APS5_VABI MD4_Final_nid_postfix(unsigned char* digest, Md4Context* context) { + auto& c = RequireContext(context, __func__); + RequireDigest(digest, __func__); + RequireBuffered(c.num, 64, __func__); + Final32(c, false, Md4Block); + StoreLittle32(digest, c.A); + StoreLittle32(digest + 4, c.B); + StoreLittle32(digest + 8, c.C); + StoreLittle32(digest + 12, c.D); + return 1; +} + +int APS5_VABI MD5_Init_nid_postfix(Md5Context* context) { + auto& c = RequireContext(context, __func__); + std::memset(&c, 0, sizeof(c)); + c.A = 0x67452301; + c.B = 0xefcdab89; + c.C = 0x98badcfe; + c.D = 0x10325476; + return 1; +} + +int APS5_VABI MD5_Update_nid_postfix(Md5Context* context, const void* data, std::size_t length) { + auto& c = RequireContext(context, __func__); + if (!length) return 1; + RequireInput(data, length, __func__); + RequireBuffered(c.num, 64, __func__); + Update32(c, data, length, Md5Block); + return 1; +} + +int APS5_VABI MD5_Final_nid_postfix(unsigned char* digest, Md5Context* context) { + auto& c = RequireContext(context, __func__); + RequireDigest(digest, __func__); + RequireBuffered(c.num, 64, __func__); + Final32(c, false, Md5Block); + StoreLittle32(digest, c.A); + StoreLittle32(digest + 4, c.B); + StoreLittle32(digest + 8, c.C); + StoreLittle32(digest + 12, c.D); + return 1; +} + +int APS5_VABI SHA1_Init_nid_postfix(Sha1Context* context) { + auto& c = RequireContext(context, __func__); + std::memset(&c, 0, sizeof(c)); + c.h[0] = 0x67452301; + c.h[1] = 0xefcdab89; + c.h[2] = 0x98badcfe; + c.h[3] = 0x10325476; + c.h[4] = 0xc3d2e1f0; + return 1; +} + +int APS5_VABI SHA1_Update_nid_postfix(Sha1Context* context, const void* data, std::size_t length) { + auto& c = RequireContext(context, __func__); + if (!length) return 1; + RequireInput(data, length, __func__); + RequireBuffered(c.num, 64, __func__); + Update32(c, data, length, Sha1Block); + return 1; +} + +int APS5_VABI SHA1_Final_nid_postfix(unsigned char* digest, Sha1Context* context) { + auto& c = RequireContext(context, __func__); + RequireDigest(digest, __func__); + RequireBuffered(c.num, 64, __func__); + Final32(c, true, Sha1Block); + for (int i = 0; i < 5; ++i) StoreBig32(digest + 4 * i, c.h[i]); + return 1; +} + +int APS5_VABI SHA224_Init_nid_postfix(Sha256Context* context) { + static constexpr std::uint32_t initial[8] {0xc1059ed8, 0x367cd507, 0x3070dd17, 0xf70e5939, 0xffc00b31, 0x68581511, 0x64f98fa7, 0xbefa4fa4}; + auto& c = RequireContext(context, __func__); + std::memset(&c, 0, sizeof(c)); + std::memcpy(c.h, initial, sizeof(initial)); + c.mdLength = Sha224Length; + return 1; +} + +int APS5_VABI SHA256_Init_nid_postfix(Sha256Context* context) { + static constexpr std::uint32_t initial[8] {0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19}; + auto& c = RequireContext(context, __func__); + std::memset(&c, 0, sizeof(c)); + std::memcpy(c.h, initial, sizeof(initial)); + c.mdLength = Sha256Length; + return 1; +} + +int APS5_VABI SHA256_Update_nid_postfix(Sha256Context* context, const void* data, std::size_t length) { + auto& c = RequireContext(context, __func__); + if (!length) return 1; + RequireInput(data, length, __func__); + RequireBuffered(c.num, 64, __func__); + Update32(c, data, length, Sha256Block); + return 1; +} + +int APS5_VABI SHA224_Update_nid_postfix(Sha256Context* context, const void* data, std::size_t length) { + return SHA256_Update_nid_postfix(context, data, length); +} + +int APS5_VABI SHA256_Final_nid_postfix(unsigned char* digest, Sha256Context* context) { + return Sha256Final(digest, RequireContext(context, __func__), __func__); +} + +int APS5_VABI SHA224_Final_nid_postfix(unsigned char* digest, Sha256Context* context) { + return Sha256Final(digest, RequireContext(context, __func__), __func__); +} + +unsigned char* APS5_VABI SHA256_nid_postfix(const unsigned char* data, std::size_t length, unsigned char* digest) { + RequireInput(data, length, __func__); + if (!digest) NotImplemented_nid_no_patch("SHA256: null digest buffer"); + Sha256Context c; + SHA256_Init_nid_postfix(&c); + if (length) Update32(c, data, length, Sha256Block); + Sha256Final(digest, c, __func__); + return digest; +} + +int APS5_VABI SHA384_Init_nid_postfix(Sha512Context* context) { + static constexpr std::uint64_t initial[8] { + 0xcbbb9d5dc1059ed8, 0x629a292a367cd507, 0x9159015a3070dd17, 0x152fecd8f70e5939, + 0x67332667ffc00b31, 0x8eb44a8768581511, 0xdb0c2e0d64f98fa7, 0x47b5481dbefa4fa4, + }; + auto& c = RequireContext(context, __func__); + std::memset(&c, 0, sizeof(c)); + std::memcpy(c.h, initial, sizeof(initial)); + c.mdLength = Sha384Length; + return 1; +} + +int APS5_VABI SHA512_Init_nid_postfix(Sha512Context* context) { + static constexpr std::uint64_t initial[8] { + 0x6a09e667f3bcc908, 0xbb67ae8584caa73b, 0x3c6ef372fe94f82b, 0xa54ff53a5f1d36f1, + 0x510e527fade682d1, 0x9b05688c2b3e6c1f, 0x1f83d9abfb41bd6b, 0x5be0cd19137e2179, + }; + auto& c = RequireContext(context, __func__); + std::memset(&c, 0, sizeof(c)); + std::memcpy(c.h, initial, sizeof(initial)); + c.mdLength = Sha512Length; + return 1; +} + +int APS5_VABI SHA512_Update_nid_postfix(Sha512Context* context, const void* data, std::size_t length) { + auto& c = RequireContext(context, __func__); + if (!length) return 1; + RequireInput(data, length, __func__); + RequireBuffered(c.num, 128, __func__); + Sha512Update(c, data, length); + return 1; +} + +int APS5_VABI SHA384_Update_nid_postfix(Sha512Context* context, const void* data, std::size_t length) { + return SHA512_Update_nid_postfix(context, data, length); +} + +int APS5_VABI SHA512_Final_nid_postfix(unsigned char* digest, Sha512Context* context) { + auto& c = RequireContext(context, __func__); + RequireBuffered(c.num, 128, __func__); + return Sha512Final(digest, c); +} + +int APS5_VABI SHA384_Final_nid_postfix(unsigned char* digest, Sha512Context* context) { + return SHA512_Final_nid_postfix(digest, context); +} + +} diff --git a/core/libs/prx/libSceLibreSsl3/CMakeLists.txt b/core/libs/prx/libSceLibreSsl3/CMakeLists.txt new file mode 100644 index 0000000000..1a3d213378 --- /dev/null +++ b/core/libs/prx/libSceLibreSsl3/CMakeLists.txt @@ -0,0 +1,22 @@ +add_library(libSceLibreSsl3 SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Digest.cpp) +target_include_directories(libSceLibreSsl3 PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(libSceLibreSsl3 PRIVATE libSceLibreSsl) +configure_windows_unwind(libSceLibreSsl3) + +if(BUILD_TESTING) + add_test_executable(guest_crypto_memcmp_tests ${LIBS_INCLUDE_DIR}/tests/GuestCryptoMemcmp.cpp) + target_include_directories(guest_crypto_memcmp_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_crypto_memcmp_tests PRIVATE libSceLibreSsl3) + add_test(NAME guest_crypto_memcmp COMMAND guest_crypto_memcmp_tests) + set_tests_properties(guest_crypto_memcmp PROPERTIES TIMEOUT 20) + add_test_executable(guest_libressl3_digest_tests ${LIBS_INCLUDE_DIR}/tests/GuestLibreSsl3Digest.cpp) + target_include_directories(guest_libressl3_digest_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_libressl3_digest_tests PRIVATE libSceLibreSsl3 libc) + configure_windows_unwind(guest_libressl3_digest_tests) + add_test(NAME guest_libressl3_digest COMMAND guest_libressl3_digest_tests) + set_tests_properties(guest_libressl3_digest PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_crypto_memcmp guest_libressl3_digest 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 d3289b659b..623b05d144 100644 --- a/core/libs/prx/libSceLibreSsl3/Export.cpp +++ b/core/libs/prx/libSceLibreSsl3/Export.cpp @@ -31,9 +31,13 @@ int APS5_VABI BIO_vfree_nid_postfix(void) { return 0; } -int APS5_VABI CRYPTO_memcmp_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI CRYPTO_memcmp_nid_postfix(const void* first, const void* second, std::size_t length) { + const auto* firstBytes = static_cast(first); + const auto* secondBytes = static_cast(second); + unsigned int difference = 0; + for (std::size_t index = 0; index < length; ++index) + difference |= firstBytes[index] ^ secondBytes[index]; + return static_cast(difference); } int APS5_VABI DH_get0_key_nid_postfix(void) { @@ -216,11 +220,6 @@ int APS5_VABI EVP_sha224_nid_postfix(void) { return 0; } -int APS5_VABI MD5_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI OBJ_cmp_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -281,11 +280,6 @@ int APS5_VABI RSA_up_ref_nid_postfix(void) { return 0; } -int APS5_VABI SHA1_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI SSL_CTX_add0_chain_cert_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceLibreSsl3/src/Digest.cpp b/core/libs/prx/libSceLibreSsl3/src/Digest.cpp new file mode 100644 index 0000000000..373654f3d5 --- /dev/null +++ b/core/libs/prx/libSceLibreSsl3/src/Digest.cpp @@ -0,0 +1,30 @@ +#include +#include "prx/libc/include/General.hpp" + +extern "C" { +int APS5_VABI MD5_Init_nid_postfix(void* context); +int APS5_VABI MD5_Update_nid_postfix(void* context, const void* data, std::size_t length); +int APS5_VABI MD5_Final_nid_postfix(unsigned char* digest, void* context); +int APS5_VABI SHA1_Init_nid_postfix(void* context); +int APS5_VABI SHA1_Update_nid_postfix(void* context, const void* data, std::size_t length); +int APS5_VABI SHA1_Final_nid_postfix(unsigned char* digest, void* context); + +unsigned char* APS5_VABI MD5_nid_postfix(const unsigned char* data, std::size_t length, unsigned char* digest) { + if (!digest) NotImplemented_nid_no_patch("MD5: null digest buffer"); + alignas(8) unsigned char context[92]; + MD5_Init_nid_postfix(context); + MD5_Update_nid_postfix(context, data, length); + MD5_Final_nid_postfix(digest, context); + return digest; +} + +unsigned char* APS5_VABI SHA1_nid_postfix(const unsigned char* data, std::size_t length, unsigned char* digest) { + if (!digest) NotImplemented_nid_no_patch("SHA1: null digest buffer"); + alignas(8) unsigned char context[96]; + SHA1_Init_nid_postfix(context); + SHA1_Update_nid_postfix(context, data, length); + SHA1_Final_nid_postfix(digest, context); + return digest; +} + +} diff --git a/core/libs/prx/libSceNKWebKit/Export.cpp b/core/libs/prx/libSceNKWebKit/Export.cpp index d81f4a5e32..a6caa403e8 100644 --- a/core/libs/prx/libSceNKWebKit/Export.cpp +++ b/core/libs/prx/libSceNKWebKit/Export.cpp @@ -5,6 +5,754 @@ extern "C" { -APS5_DUMMY_FUN +int APS5_VABI sceNKWebKitUnknown____URPJNZ18_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("++-URPJNZ18", sceNKWebKitUnknown____URPJNZ18_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown___67uYkhqBU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("++67uYkhqBU", sceNKWebKitUnknown___67uYkhqBU_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown___8RYr2gjrw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("++8RYr2gjrw", sceNKWebKitUnknown___8RYr2gjrw_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown___BntMcao1o_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("++BntMcao1o", sceNKWebKitUnknown___BntMcao1o_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown___R8IXi10PI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("++R8IXi10PI", sceNKWebKitUnknown___R8IXi10PI_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown___j2h_QO22w_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("++j2h+QO22w", sceNKWebKitUnknown___j2h_QO22w_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown____7XwmYLnI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+-+7XwmYLnI", sceNKWebKitUnknown____7XwmYLnI_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown___wmVyLhgm0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+-wmVyLhgm0", sceNKWebKitUnknown___wmVyLhgm0_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__0QEt8ZQWgg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+0QEt8ZQWgg", sceNKWebKitUnknown__0QEt8ZQWgg_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__0YNxcA_duk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+0YNxcA-duk", sceNKWebKitUnknown__0YNxcA_duk_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__0ZZzGYxx_I_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+0ZZzGYxx+I", sceNKWebKitUnknown__0ZZzGYxx_I_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__0rdnkSicqk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+0rdnkSicqk", sceNKWebKitUnknown__0rdnkSicqk_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__1Ex5GCP8Xs_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+1Ex5GCP8Xs", sceNKWebKitUnknown__1Ex5GCP8Xs_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__1FfZ_0sxvQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+1FfZ+0sxvQ", sceNKWebKitUnknown__1FfZ_0sxvQ_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__1ObAtzCrYE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+1ObAtzCrYE", sceNKWebKitUnknown__1ObAtzCrYE_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__1n_h4FUu9E_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+1n+h4FUu9E", sceNKWebKitUnknown__1n_h4FUu9E_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__26aZpK535w_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+26aZpK535w", sceNKWebKitUnknown__26aZpK535w_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__2XuBaAXb_4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+2XuBaAXb+4", sceNKWebKitUnknown__2XuBaAXb_4_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__2rTX2KM7Ms_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+2rTX2KM7Ms", sceNKWebKitUnknown__2rTX2KM7Ms_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__31dbB3S_6Y_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+31dbB3S-6Y", sceNKWebKitUnknown__31dbB3S_6Y_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__3p6wRzzzlQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+3p6wRzzzlQ", sceNKWebKitUnknown__3p6wRzzzlQ_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__4_GNOUIBt0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+4-GNOUIBt0", sceNKWebKitUnknown__4_GNOUIBt0_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__45BiDnCTxw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+45BiDnCTxw", sceNKWebKitUnknown__45BiDnCTxw_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__4lVmXsIwqM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+4lVmXsIwqM", sceNKWebKitUnknown__4lVmXsIwqM_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__5E2YZZIstM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+5E2YZZIstM", sceNKWebKitUnknown__5E2YZZIstM_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__5V_UOk8zS4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+5V-UOk8zS4", sceNKWebKitUnknown__5V_UOk8zS4_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__63IB3UdMOY_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+63IB3UdMOY", sceNKWebKitUnknown__63IB3UdMOY_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__6boXsT6XFA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+6boXsT6XFA", sceNKWebKitUnknown__6boXsT6XFA_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__79w68212vg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+79w68212vg", sceNKWebKitUnknown__79w68212vg_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__7HpqmfJz48_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+7HpqmfJz48", sceNKWebKitUnknown__7HpqmfJz48_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__8T1y8Y_lgQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+8T1y8Y-lgQ", sceNKWebKitUnknown__8T1y8Y_lgQ_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__8VuTEFp13s_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+8VuTEFp13s", sceNKWebKitUnknown__8VuTEFp13s_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__9TKACifR2g_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+9TKACifR2g", sceNKWebKitUnknown__9TKACifR2g_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__9ZBRWNf4og_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+9ZBRWNf4og", sceNKWebKitUnknown__9ZBRWNf4og_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__9us0nrDaCU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+9us0nrDaCU", sceNKWebKitUnknown__9us0nrDaCU_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__AIEKk6K05M_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+AIEKk6K05M", sceNKWebKitUnknown__AIEKk6K05M_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__Abk7CfTivU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+Abk7CfTivU", sceNKWebKitUnknown__Abk7CfTivU_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__AnsPWo12m8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+AnsPWo12m8", sceNKWebKitUnknown__AnsPWo12m8_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__BDqCDede1o_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+BDqCDede1o", sceNKWebKitUnknown__BDqCDede1o_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__BSUP6XSeAw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+BSUP6XSeAw", sceNKWebKitUnknown__BSUP6XSeAw_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__BZCJi3OIiQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+BZCJi3OIiQ", sceNKWebKitUnknown__BZCJi3OIiQ_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__ByVxWl1pW8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+ByVxWl1pW8", sceNKWebKitUnknown__ByVxWl1pW8_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__CgSI_4D534_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+CgSI-4D534", sceNKWebKitUnknown__CgSI_4D534_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__CpPA9_CKUA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+CpPA9+CKUA", sceNKWebKitUnknown__CpPA9_CKUA_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__DcUw5YwkSY_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+DcUw5YwkSY", sceNKWebKitUnknown__DcUw5YwkSY_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__DpDBrSZWUc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+DpDBrSZWUc", sceNKWebKitUnknown__DpDBrSZWUc_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__E3D_W4kpl8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+E3D-W4kpl8", sceNKWebKitUnknown__E3D_W4kpl8_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__EN6MgNmVv4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+EN6MgNmVv4", sceNKWebKitUnknown__EN6MgNmVv4_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__ErgockOJwc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+ErgockOJwc", sceNKWebKitUnknown__ErgockOJwc_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__FKudG3uS8A_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+FKudG3uS8A", sceNKWebKitUnknown__FKudG3uS8A_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__FWUl_T8yG8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+FWUl-T8yG8", sceNKWebKitUnknown__FWUl_T8yG8_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__Fca5Eyr4BU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+Fca5Eyr4BU", sceNKWebKitUnknown__Fca5Eyr4BU_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__FcvpbbdrZM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+FcvpbbdrZM", sceNKWebKitUnknown__FcvpbbdrZM_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__FvUhrQaiJQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+FvUhrQaiJQ", sceNKWebKitUnknown__FvUhrQaiJQ_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__GOwQvxqztE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+GOwQvxqztE", sceNKWebKitUnknown__GOwQvxqztE_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__Gp0M0sPj9I_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+Gp0M0sPj9I", sceNKWebKitUnknown__Gp0M0sPj9I_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__H_bnpXdWtk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+H-bnpXdWtk", sceNKWebKitUnknown__H_bnpXdWtk_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__H5PTMieXNE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+H5PTMieXNE", sceNKWebKitUnknown__H5PTMieXNE_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__HuPyGuX_jY_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+HuPyGuX+jY", sceNKWebKitUnknown__HuPyGuX_jY_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__IMGO_dXHis_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+IMGO+dXHis", sceNKWebKitUnknown__IMGO_dXHis_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__ImJ026_DAw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+ImJ026-DAw", sceNKWebKitUnknown__ImJ026_DAw_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__IoqHgIw0PM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+IoqHgIw0PM", sceNKWebKitUnknown__IoqHgIw0PM_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__JnngM7WyHI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+JnngM7WyHI", sceNKWebKitUnknown__JnngM7WyHI_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__Kb7j57jDzs_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+Kb7j57jDzs", sceNKWebKitUnknown__Kb7j57jDzs_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__Kx_IV_21hk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+Kx-IV+21hk", sceNKWebKitUnknown__Kx_IV_21hk_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__LUabdo811U_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+LUabdo811U", sceNKWebKitUnknown__LUabdo811U_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__LiOmqon2ZI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+LiOmqon2ZI", sceNKWebKitUnknown__LiOmqon2ZI_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__LoAenYdWlM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+LoAenYdWlM", sceNKWebKitUnknown__LoAenYdWlM_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__LrmM3KN3N8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+LrmM3KN3N8", sceNKWebKitUnknown__LrmM3KN3N8_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__MzGFuNN348_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+MzGFuNN348", sceNKWebKitUnknown__MzGFuNN348_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__N_GwpCV4yk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+N-GwpCV4yk", sceNKWebKitUnknown__N_GwpCV4yk_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__N6UhRaLrJE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+N6UhRaLrJE", sceNKWebKitUnknown__N6UhRaLrJE_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__NGrXdCa0q8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+NGrXdCa0q8", sceNKWebKitUnknown__NGrXdCa0q8_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__NiLTGGKF_E_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+NiLTGGKF+E", sceNKWebKitUnknown__NiLTGGKF_E_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__Nqfy8HVG84_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+Nqfy8HVG84", sceNKWebKitUnknown__Nqfy8HVG84_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__NuhuZH75ak_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+NuhuZH75ak", sceNKWebKitUnknown__NuhuZH75ak_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__OCxbaKiBUo_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+OCxbaKiBUo", sceNKWebKitUnknown__OCxbaKiBUo_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__OOjOkuWL6g_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+OOjOkuWL6g", sceNKWebKitUnknown__OOjOkuWL6g_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__OkTn3_PBxM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+OkTn3+PBxM", sceNKWebKitUnknown__OkTn3_PBxM_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__PDx3sfB_l4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+PDx3sfB-l4", sceNKWebKitUnknown__PDx3sfB_l4_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__POsWAWjXo8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+POsWAWjXo8", sceNKWebKitUnknown__POsWAWjXo8_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__PeIrQMgYpw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+PeIrQMgYpw", sceNKWebKitUnknown__PeIrQMgYpw_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__Q7xLvpc0PA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+Q7xLvpc0PA", sceNKWebKitUnknown__Q7xLvpc0PA_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__QL3zQ6ZNy4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+QL3zQ6ZNy4", sceNKWebKitUnknown__QL3zQ6ZNy4_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__QqzEnc5c80_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+QqzEnc5c80", sceNKWebKitUnknown__QqzEnc5c80_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__R0GXLrsPM0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+R0GXLrsPM0", sceNKWebKitUnknown__R0GXLrsPM0_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__R2eXISeSvw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+R2eXISeSvw", sceNKWebKitUnknown__R2eXISeSvw_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__RkwSDm5kGU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+RkwSDm5kGU", sceNKWebKitUnknown__RkwSDm5kGU_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__RtBREW92M0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+RtBREW92M0", sceNKWebKitUnknown__RtBREW92M0_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__S72K_fa2_8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+S72K+fa2-8", sceNKWebKitUnknown__S72K_fa2_8_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__S7ftAAWhoQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+S7ftAAWhoQ", sceNKWebKitUnknown__S7ftAAWhoQ_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__SHzgUiuQDE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+SHzgUiuQDE", sceNKWebKitUnknown__SHzgUiuQDE_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__Sgy63JgEgQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+Sgy63JgEgQ", sceNKWebKitUnknown__Sgy63JgEgQ_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__Sozphy1Wa8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+Sozphy1Wa8", sceNKWebKitUnknown__Sozphy1Wa8_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__TPntL1OK80_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+TPntL1OK80", sceNKWebKitUnknown__TPntL1OK80_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__TT_AplhQWA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+TT+AplhQWA", sceNKWebKitUnknown__TT_AplhQWA_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__TjNmuhxdBQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+TjNmuhxdBQ", sceNKWebKitUnknown__TjNmuhxdBQ_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__U_nxwdrnDY_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+U-nxwdrnDY", sceNKWebKitUnknown__U_nxwdrnDY_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__UKI_MUuiCU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+UKI-MUuiCU", sceNKWebKitUnknown__UKI_MUuiCU_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__VvktcfIouQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+VvktcfIouQ", sceNKWebKitUnknown__VvktcfIouQ_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__WS2yRv5aBI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+WS2yRv5aBI", sceNKWebKitUnknown__WS2yRv5aBI_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__XZfnHByVO0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+XZfnHByVO0", sceNKWebKitUnknown__XZfnHByVO0_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__YBfWz5udq4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+YBfWz5udq4", sceNKWebKitUnknown__YBfWz5udq4_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__YPwmEWTZwE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+YPwmEWTZwE", sceNKWebKitUnknown__YPwmEWTZwE_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__YYEOsi3IMk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+YYEOsi3IMk", sceNKWebKitUnknown__YYEOsi3IMk_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__YyqQTUIQJQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+YyqQTUIQJQ", sceNKWebKitUnknown__YyqQTUIQJQ_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__ZgGbFykhls_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+ZgGbFykhls", sceNKWebKitUnknown__ZgGbFykhls_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__ZwjLJ74Y38_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+ZwjLJ74Y38", sceNKWebKitUnknown__ZwjLJ74Y38_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__b1xwU7aUwU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+b1xwU7aUwU", sceNKWebKitUnknown__b1xwU7aUwU_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__b5TfVQ3_4s_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+b5TfVQ3-4s", sceNKWebKitUnknown__b5TfVQ3_4s_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__bDb7iby5NI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+bDb7iby5NI", sceNKWebKitUnknown__bDb7iby5NI_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__baqz7ahOng_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+baqz7ahOng", sceNKWebKitUnknown__baqz7ahOng_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__bvtn3pCtek_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+bvtn3pCtek", sceNKWebKitUnknown__bvtn3pCtek_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__byORTSkWQo_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+byORTSkWQo", sceNKWebKitUnknown__byORTSkWQo_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__dAwquupevQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+dAwquupevQ", sceNKWebKitUnknown__dAwquupevQ_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__dTlroLZao8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+dTlroLZao8", sceNKWebKitUnknown__dTlroLZao8_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__eVN0UdRT8c_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+eVN0UdRT8c", sceNKWebKitUnknown__eVN0UdRT8c_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__fTKZiGqo8E_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+fTKZiGqo8E", sceNKWebKitUnknown__fTKZiGqo8E_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__feeFP8Y2_c_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+feeFP8Y2+c", sceNKWebKitUnknown__feeFP8Y2_c_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__iLBOBrdx5A_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+iLBOBrdx5A", sceNKWebKitUnknown__iLBOBrdx5A_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__iLdIVuhDhY_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+iLdIVuhDhY", sceNKWebKitUnknown__iLdIVuhDhY_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__jG4wqqA8yg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+jG4wqqA8yg", sceNKWebKitUnknown__jG4wqqA8yg_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__jLvgfDA97c_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+jLvgfDA97c", sceNKWebKitUnknown__jLvgfDA97c_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__ji7IBGwnCM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+ji7IBGwnCM", sceNKWebKitUnknown__ji7IBGwnCM_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__k3JGU_UUXo_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+k3JGU-UUXo", sceNKWebKitUnknown__k3JGU_UUXo_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__kX_CdbguB0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+kX-CdbguB0", sceNKWebKitUnknown__kX_CdbguB0_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__keSg8ay8cE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+keSg8ay8cE", sceNKWebKitUnknown__keSg8ay8cE_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__lI9zCCS_HQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+lI9zCCS-HQ", sceNKWebKitUnknown__lI9zCCS_HQ_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__lwLqZJLo3g_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+lwLqZJLo3g", sceNKWebKitUnknown__lwLqZJLo3g_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__mS029ncrbo_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+mS029ncrbo", sceNKWebKitUnknown__mS029ncrbo_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__mYy22Ucg7Y_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+mYy22Ucg7Y", sceNKWebKitUnknown__mYy22Ucg7Y_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__nHWcLWOmb4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+nHWcLWOmb4", sceNKWebKitUnknown__nHWcLWOmb4_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__nSg5kHtXkg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+nSg5kHtXkg", sceNKWebKitUnknown__nSg5kHtXkg_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__nc3IouJMEA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+nc3IouJMEA", sceNKWebKitUnknown__nc3IouJMEA_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__ndmq8LhAjA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+ndmq8LhAjA", sceNKWebKitUnknown__ndmq8LhAjA_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__nrEMsyYHiw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+nrEMsyYHiw", sceNKWebKitUnknown__nrEMsyYHiw_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__oJHrfcyJJM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+oJHrfcyJJM", sceNKWebKitUnknown__oJHrfcyJJM_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__ovAafdG0O8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+ovAafdG0O8", sceNKWebKitUnknown__ovAafdG0O8_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__pWGccNXto8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+pWGccNXto8", sceNKWebKitUnknown__pWGccNXto8_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__q0GELad_mY_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+q0GELad-mY", sceNKWebKitUnknown__q0GELad_mY_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__qXuNlEFSRQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+qXuNlEFSRQ", sceNKWebKitUnknown__qXuNlEFSRQ_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__qsTbJFS86A_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+qsTbJFS86A", sceNKWebKitUnknown__qsTbJFS86A_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__r_TJDA2yIM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+r+TJDA2yIM", sceNKWebKitUnknown__r_TJDA2yIM_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__r0UH41I1dI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+r0UH41I1dI", sceNKWebKitUnknown__r0UH41I1dI_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__r8OGkuL4dc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+r8OGkuL4dc", sceNKWebKitUnknown__r8OGkuL4dc_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__rFA8ZR_qaI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+rFA8ZR+qaI", sceNKWebKitUnknown__rFA8ZR_qaI_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__rPvFDsPbXM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+rPvFDsPbXM", sceNKWebKitUnknown__rPvFDsPbXM_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__sUvE9LF80k_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+sUvE9LF80k", sceNKWebKitUnknown__sUvE9LF80k_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__suuwfNT5Oc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+suuwfNT5Oc", sceNKWebKitUnknown__suuwfNT5Oc_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__t_MaTLADe4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+t-MaTLADe4", sceNKWebKitUnknown__t_MaTLADe4_nid_no_patch); } diff --git a/core/libs/prx/libSceNet/CMakeLists.txt b/core/libs/prx/libSceNet/CMakeLists.txt index 95faf4584b..9ecb525065 100644 --- a/core/libs/prx/libSceNet/CMakeLists.txt +++ b/core/libs/prx/libSceNet/CMakeLists.txt @@ -8,6 +8,17 @@ if(BUILD_TESTING) target_include_directories(net_dontwait_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(net_dontwait_tests PRIVATE libSceNet libkernel libc) configure_windows_unwind(net_dontwait_tests) + add_test_executable(guest_net_buffer_validation_tests ${LIBS_INCLUDE_DIR}/tests/GuestNetBufferValidation.cpp) + target_include_directories(guest_net_buffer_validation_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_net_buffer_validation_tests PRIVATE libSceNet libkernel libc) + configure_windows_unwind(guest_net_buffer_validation_tests) + add_test(NAME guest_net_buffer_validation COMMAND guest_net_buffer_validation_tests) + set_tests_properties(guest_net_buffer_validation PROPERTIES TIMEOUT 20) + if(WIN32) + target_link_libraries(guest_net_buffer_validation_tests PRIVATE ws2_32) + set_tests_properties(guest_net_buffer_validation PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() foreach(api IN ITEMS send sendto sendmsg recv recvfrom recvmsg duplex blocking) add_test(NAME net_dontwait_${api} COMMAND net_dontwait_tests ${api}) set_tests_properties(net_dontwait_${api} PROPERTIES TIMEOUT 20) @@ -77,4 +88,19 @@ if(BUILD_TESTING) set_tests_properties(guest_sce_net PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(net_epoll_lifetime_tests ${LIBS_INCLUDE_DIR}/tests/NetEpollLifetime.cpp) + target_include_directories(net_epoll_lifetime_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(net_epoll_lifetime_tests PRIVATE libkernel libc libSceNetCtl) + configure_windows_unwind(net_epoll_lifetime_tests) + if(WIN32) + target_link_libraries(net_epoll_lifetime_tests PRIVATE ws2_32) + endif() + foreach(operation IN ITEMS reuse control close delete modify replace abort destroy) + add_test(NAME net_epoll_lifetime_${operation} COMMAND net_epoll_lifetime_tests ${operation}) + set_tests_properties(net_epoll_lifetime_${operation} PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(net_epoll_lifetime_${operation} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() endif() diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 808cdc7364..450868e186 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -43,6 +43,7 @@ #include "prx/libkernel/Socket/include/SocketPoll.hpp" #include "prx/libkernel/Socket/include/SocketRuntime.hpp" #include "prx/libkernel/Time/include/Time.hpp" +#include "src/ResolverTestHooks.hpp" // Guest socket descriptors retain PS5 semantics while their transport is backed by host sockets. @@ -455,7 +456,8 @@ HostMutex g_mutex; HostCondition g_cv; std::map g_socks; std::set g_epolls; -std::map> g_epoll_socks; +using EpollRegistrations = std::map>; +std::map g_epoll_socks; std::set g_epoll_aborted; std::map g_pools; @@ -477,6 +479,7 @@ int g_next_epoll = 0x4000; int g_next_pool = 1; int g_next_resolver = 0x100; bool g_net_inited = false; +std::atomic g_resolverLookupForTests{nullptr}; int* errno_slot() { static thread_local int e = 0; @@ -616,15 +619,6 @@ std::int64_t message_length(const NetMsghdr* message) { return static_cast(total); } -int receive_error(const Sock& socket) { -#ifdef _WIN32 - if (WSAGetLastError() == WSAETIMEDOUT && socket.type != NET_SOCK_STREAM && socket.native->receiveTimeout > 0) return NET_EAGAIN; -#else - (void)socket; -#endif - return native_error(); -} - bool message_flags_supported(int flags, int accepted) { return (flags & ~accepted) == 0; } @@ -652,7 +646,7 @@ auto native_transfer(const NativeSocketHandle& socket, int flags, bool send, TOp if (timeout > 0) { const auto remaining = deadline - std::chrono::steady_clock::now(); if (remaining <= std::chrono::steady_clock::duration::zero()) { - WSASetLastError(completeLength > 0 ? WSAEWOULDBLOCK : WSAETIMEDOUT); + WSASetLastError(send && completeLength == 0 ? WSAETIMEDOUT : WSAEWOULDBLOCK); return transferred > 0 ? static_cast(transferred) : static_cast(-1); } milliseconds = static_cast(std::min(std::chrono::ceil(remaining).count(), milliseconds)); @@ -787,6 +781,10 @@ bool parse_ipv4_parts(const char* text, std::uint32_t& address) { } // namespace +void GuestSceNetTest::SetResolverLookupForTests_nid_no_patch(ResolverLookup lookup) noexcept { + g_resolverLookupForTests.store(lookup); +} + extern "C" { extern const std::uint32_t sce_net_in6addr_any[4] = {}; @@ -1022,6 +1020,7 @@ int APS5_VABI sceNetSocketClose(int s) { if (it != g_socks.end()) { native = it->second.native; g_socks.erase(it); + for (auto& epoll : g_epoll_socks) epoll.second.erase(s); found = true; } } @@ -1260,7 +1259,7 @@ int64_t APS5_VABI sceNetRecv(int s, void* buf, size_t len, int flags) { #ifdef _WIN32 if (result < 0 && socket.type == NET_SOCK_DGRAM && WSAGetLastError() == WSAEMSGSIZE) return static_cast(len); #endif - return result >= 0 ? result : fail(receive_error(socket)); + return result >= 0 ? result : fail(native_error()); } int64_t APS5_VABI sceNetRecvfrom(int s, void* buf, size_t len, int flags, void* from, uint32_t* fromlen) { @@ -1301,7 +1300,7 @@ int64_t APS5_VABI sceNetRecvfrom(int s, void* buf, size_t len, int flags, void* #ifdef _WIN32 if (result < 0 && socket.type == NET_SOCK_DGRAM && WSAGetLastError() == WSAEMSGSIZE) result = static_cast(len); #endif - if (result < 0) return fail(receive_error(socket)); + if (result < 0) return fail(native_error()); if (from && !native_to_guest_address(peer, from, fromlen)) return fail(NET_EAFNOSUPPORT); return result; } @@ -1419,7 +1418,7 @@ int64_t APS5_VABI sceNetRecvmsg(int s, NetMsghdr* msg, int flags) { }); if (received < 0) { #ifdef _WIN32 - if (!datagram || WSAGetLastError() != WSAEMSGSIZE) return fail(receive_error(socket)); + if (!datagram || WSAGetLastError() != WSAEMSGSIZE) return fail(native_error()); received = static_cast(buffer.size()); #else return fail(native_error()); @@ -1507,6 +1506,7 @@ int APS5_VABI sceNetSetsockopt(int s, int level, int optname, const void* optval if (value < 0) return fail(NET_EINVAL); if (set_native_timeout(socket.native->value, optname, value) != 0) return fail(native_error()); } else if (optname == NET_SO_REUSEADDR || optname == NET_SO_BROADCAST || optname == NET_SO_SNDBUF || optname == NET_SO_RCVBUF) { + if ((optname == NET_SO_SNDBUF || optname == NET_SO_RCVBUF) && value < 1) return fail(NET_EINVAL); const int native_option = optname == NET_SO_REUSEADDR ? SO_REUSEADDR : optname == NET_SO_BROADCAST ? SO_BROADCAST : optname == NET_SO_SNDBUF ? SO_SNDBUF : SO_RCVBUF; if (::setsockopt(socket.native->value, SOL_SOCKET, native_option, reinterpret_cast(&value), sizeof(value)) != 0) return fail(native_error()); @@ -1752,7 +1752,7 @@ int APS5_VABI sceNetEpollCreate(const char* name, int flags) { std::lock_guard lk(g_mutex); const int id = g_next_epoll++; g_epolls.insert(id); - g_epoll_socks.emplace(id, std::map{}); + g_epoll_socks.emplace(id, EpollRegistrations{}); return id; } @@ -1817,16 +1817,17 @@ int APS5_VABI sceNetEpollControl(int eid, int op, int id, const NetEpollEvent* e } if (op != NET_EPOLL_CTL_ADD && op != NET_EPOLL_CTL_MOD && op != NET_EPOLL_CTL_DEL) return fail(NET_EINVAL); if (op != NET_EPOLL_CTL_DEL && !event) return fail(NET_EINVAL); - if (op != NET_EPOLL_CTL_DEL && g_socks.count(id) == 0) return fail(NET_EBADF); + if (g_socks.count(id) == 0) return fail(NET_EBADF); auto& registrations = g_epoll_socks[eid]; if (op == NET_EPOLL_CTL_ADD) { - if (!registrations.emplace(id, *event).second) return fail(NET_EADDRINUSE); + if (registrations.count(id) != 0) return fail(NET_EADDRINUSE); + registrations.emplace(id, std::make_shared(*event)); } else if (op == NET_EPOLL_CTL_DEL) { if (registrations.erase(id) == 0) return fail(NET_ENOENT); } else { auto registration = registrations.find(id); if (registration == registrations.end()) return fail(NET_ENOENT); - registration->second = *event; + registration->second = std::make_shared(*event); } g_cv.NotifyAll(); return 0; @@ -1834,12 +1835,16 @@ int APS5_VABI sceNetEpollControl(int eid, int op, int id, const NetEpollEvent* e int APS5_VABI sceNetEpollWait(int eid, NetEpollEvent* events, int maxevents, int timeout) { if (!events || maxevents <= 0) return fail(NET_EINVAL); + struct Watch { + int descriptor; + std::shared_ptr registration; + std::shared_ptr native; + }; const auto deadline = timeout > 0 ? std::chrono::steady_clock::now() + std::chrono::microseconds(timeout) : std::chrono::steady_clock::time_point::max(); for (;;) { - std::vector registered_events; - std::vector> native_sockets; + std::vector watches; { std::lock_guard lk(g_mutex); if (g_epolls.count(eid) == 0) return fail(NET_EBADF); @@ -1849,12 +1854,11 @@ int APS5_VABI sceNetEpollWait(int eid, NetEpollEvent* events, int maxevents, int for (const auto& entry : registration->second) { const auto socket = g_socks.find(entry.first); if (socket == g_socks.end()) continue; - registered_events.push_back({entry.second.events, 0, static_cast(entry.first), entry.second.data}); - native_sockets.push_back(socket->second.native); + watches.push_back({entry.first, entry.second, socket->second.native}); } } } - if (registered_events.empty()) { + if (watches.empty()) { if (timeout == 0) return 0; std::unique_lock lk(g_mutex); const auto now = std::chrono::steady_clock::now(); @@ -1866,18 +1870,18 @@ int APS5_VABI sceNetEpollWait(int eid, NetEpollEvent* events, int maxevents, int } #ifdef _WIN32 - std::vector descriptors(registered_events.size()); + std::vector descriptors(watches.size()); for (std::size_t index = 0; index < descriptors.size(); ++index) { - descriptors[index].fd = native_sockets[index]->value; - if (registered_events[index].events & 1) descriptors[index].events |= POLLRDNORM; - if (registered_events[index].events & 2) descriptors[index].events |= POLLWRNORM; + descriptors[index].fd = watches[index].native->value; + if (watches[index].registration->events & 1) descriptors[index].events |= POLLRDNORM; + if (watches[index].registration->events & 2) descriptors[index].events |= POLLWRNORM; } #else - std::vector descriptors(registered_events.size()); + std::vector descriptors(watches.size()); for (std::size_t index = 0; index < descriptors.size(); ++index) { - descriptors[index].fd = native_sockets[index]->value; - if (registered_events[index].events & 1) descriptors[index].events |= POLLIN; - if (registered_events[index].events & 2) descriptors[index].events |= POLLOUT; + descriptors[index].fd = watches[index].native->value; + if (watches[index].registration->events & 1) descriptors[index].events |= POLLIN; + if (watches[index].registration->events & 2) descriptors[index].events |= POLLOUT; } #endif const auto now = std::chrono::steady_clock::now(); @@ -1898,27 +1902,32 @@ int APS5_VABI sceNetEpollWait(int eid, NetEpollEvent* events, int maxevents, int std::lock_guard lk(g_mutex); if (g_epolls.count(eid) == 0) return fail(NET_EBADF); if (g_epoll_aborted.erase(eid) != 0) return fail(NET_ECONNABORTED); - } - if (ready_count > 0) { - int count = 0; - for (std::size_t index = 0; index < descriptors.size() && count < maxevents; ++index) { - const auto native_events = descriptors[index].revents; - if (!native_events) continue; - std::uint32_t guest_events = 0; + if (ready_count > 0) { + int count = 0; + const auto& registrations = g_epoll_socks.at(eid); + for (std::size_t index = 0; index < descriptors.size() && count < maxevents; ++index) { + const auto registration = registrations.find(watches[index].descriptor); + if (registration == registrations.end() || registration->second != watches[index].registration) continue; + const auto native_events = descriptors[index].revents; + if (!native_events) continue; + std::uint32_t guest_events = 0; #ifdef _WIN32 - if (native_events & (POLLRDNORM | POLLPRI)) guest_events |= 1; - if (native_events & POLLWRNORM) guest_events |= 2; + if (native_events & (POLLRDNORM | POLLPRI)) guest_events |= 1; + if (native_events & POLLWRNORM) guest_events |= 2; #else - if (native_events & (POLLIN | POLLPRI)) guest_events |= 1; - if (native_events & POLLOUT) guest_events |= 2; + if (native_events & (POLLIN | POLLPRI)) guest_events |= 1; + if (native_events & POLLOUT) guest_events |= 2; #endif - if (native_events & POLLERR) guest_events |= 8; - if (native_events & POLLHUP) guest_events |= 16; - events[count] = registered_events[index]; - events[count].events = guest_events; - ++count; + if (native_events & POLLERR) guest_events |= 8; + if (native_events & POLLHUP) guest_events |= 16; + events[count] = *watches[index].registration; + events[count].ident = static_cast(watches[index].descriptor); + events[count].reserved = 0; + events[count].events = guest_events; + ++count; + } + if (count > 0) return count; } - if (count > 0) return count; } if (timeout == 0) return 0; } @@ -1988,6 +1997,28 @@ int APS5_VABI sceNetEtherNtostr(const NetEtherAddr* n, char* str, size_t len) { return 0; } +int APS5_VABI sceNetEtherStrton(const char* text, NetEtherAddr* address) { + if (text == nullptr || address == nullptr) return fail(NET_EINVAL); + for (std::size_t index = 0; index < 17; ++index) { + if (text[index] == '\0') return fail(NET_EINVAL); + } + const auto hexDigit = [](char character) { + if (character >= '0' && character <= '9') return character - '0'; + if (character >= 'a' && character <= 'z') return character - 'a' + 10; + if (character >= 'A' && character <= 'Z') return character - 'A' + 10; + return 0x7f; + }; + NetEtherAddr parsed{}; + for (std::size_t index = 0; index < sizeof(parsed.data); ++index) { + if (static_cast(text[index * 3]) >= 128 || static_cast(text[index * 3 + 1]) >= 128) { + NotImplemented_nid_no_patch(__func__); + } + parsed.data[index] = static_cast(hexDigit(text[index * 3]) * 16 + hexDigit(text[index * 3 + 1])); + } + *address = parsed; + return 0; +} + int APS5_VABI sceNetGetMacAddress(NetEtherAddr* addr, int flags) { (void)flags; if (addr == nullptr) { @@ -2019,6 +2050,14 @@ int lookup_host(int rid, const char* hostname, const char* func, int family, std if (!initialize_sockets()) return fail(5); std::uint64_t generation = 0; if (const int error = begin_lookup(rid, &Resolver::abort_ntoa, generation)) return error; + if (const auto lookup = g_resolverLookupForTests.load()) { + if (!lookup(hostname, family, addresses) || addresses.empty()) { + set_errno(NET_ENOENT); + log_soft(func, "host DNS lookup failed"); + return end_lookup(rid, generation, NET_ERROR_RESOLVER_ENODNS); + } + return end_lookup(rid, generation, 0); + } addrinfo hints{}; hints.ai_family = family; addrinfo* results = nullptr; @@ -2435,12 +2474,11 @@ int APS5_VABI sceNetIoctl(int socketId, std::uint32_t command, void* argument) { void* APS5_VABI sceNetMemoryAllocate(std::int32_t size, int flags) { constexpr int zeroFill = 2; + constexpr std::size_t blockAlignment = 64; if (size <= 0) return nullptr; - if ((flags & 4) != 0) - throw std::runtime_error("sceNetMemoryAllocate: pool reserve allocation is not implemented"); void* memory = nullptr; try { - memory = GuestHeap::GuestHeapAllocate_nid_postfix(static_cast(size)); + memory = GuestHeap::GuestHeapAlign_nid_postfix(blockAlignment, static_cast(size)); } catch (const std::bad_alloc&) { return nullptr; } catch (const std::length_error&) { @@ -3077,9 +3115,4 @@ int APS5_VABI sceNetEmulationGet(NetEmulationParameter* parameter, std::uint32_t return 0; } -int APS5_VABI sceNetEtherStrton(const char* str, void* address) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - } diff --git a/core/libs/prx/libSceNet/src/ResolverTestHooks.hpp b/core/libs/prx/libSceNet/src/ResolverTestHooks.hpp new file mode 100644 index 0000000000..cde7e83a8a --- /dev/null +++ b/core/libs/prx/libSceNet/src/ResolverTestHooks.hpp @@ -0,0 +1,10 @@ +#pragma once + +#include +#include +#include + +namespace GuestSceNetTest { +using ResolverLookup = bool (*)(const char*, int, std::vector>&); +void SetResolverLookupForTests_nid_no_patch(ResolverLookup lookup) noexcept; +} diff --git a/core/libs/prx/libSceNetCtl/Export.cpp b/core/libs/prx/libSceNetCtl/Export.cpp index 30be5f6e85..54c9d941fe 100644 --- a/core/libs/prx/libSceNetCtl/Export.cpp +++ b/core/libs/prx/libSceNetCtl/Export.cpp @@ -95,7 +95,7 @@ struct CallbackSlot { int lastEvent = 0; }; -static std::atomic_bool g_initialized{false}; +static std::atomic_bool g_initialized{true}; static std::mutex g_callbackLock; static CallbackSlot g_callbacks[MAX_CALLBACKS] = {}; static CallbackSlot g_callbacksV6[MAX_CALLBACKS] = {}; @@ -131,7 +131,7 @@ static bool HasIpv4Callback() { return false; } -static bool HostHasAddress(int family) { +static bool QueryHostHasAddress(int family) { #ifdef _WIN32 static const int socket_status = [] { WSADATA data{}; @@ -188,6 +188,74 @@ struct HostIpv4Interface { std::vector dns; }; +#ifdef _WIN32 +namespace { + +struct HostAddressCache { + std::mutex lock; + bool registered = false; + bool tried = false; + std::atomic generation{1}; + std::uint64_t seen[3]{}; + bool value[3]{}; + std::uint64_t interfaceSeen = 0; + bool interfaceFound = false; + HostIpv4Interface interfaceValue; +}; + +HostAddressCache& AddressCache() { + static HostAddressCache cache; + return cache; +} + +void CALLBACK HostAddressChanged(PVOID, PMIB_UNICASTIPADDRESS_ROW, MIB_NOTIFICATION_TYPE) { + AddressCache().generation.fetch_add(1, std::memory_order_acq_rel); +} + +void CALLBACK HostInterfaceChanged(PVOID, PMIB_IPINTERFACE_ROW, MIB_NOTIFICATION_TYPE) { + AddressCache().generation.fetch_add(1, std::memory_order_acq_rel); +} + +void CALLBACK HostRouteChanged(PVOID, PMIB_IPFORWARD_ROW2, MIB_NOTIFICATION_TYPE) { + AddressCache().generation.fetch_add(1, std::memory_order_acq_rel); +} + +void WatchHostAddresses(HostAddressCache& cache) { + if (cache.tried) return; + cache.tried = true; + HANDLE addresses = nullptr; + HANDLE interfaces = nullptr; + HANDLE routes = nullptr; + cache.registered = NotifyUnicastIpAddressChange(AF_UNSPEC, HostAddressChanged, nullptr, FALSE, &addresses) == NO_ERROR && + NotifyIpInterfaceChange(AF_UNSPEC, HostInterfaceChanged, nullptr, FALSE, &interfaces) == NO_ERROR && + NotifyRouteChange2(AF_UNSPEC, HostRouteChanged, nullptr, FALSE, &routes) == NO_ERROR; + if (!cache.registered) { + if (addresses != nullptr) CancelMibChangeNotify2(addresses); + if (interfaces != nullptr) CancelMibChangeNotify2(interfaces); + if (routes != nullptr) CancelMibChangeNotify2(routes); + } +} + +} +#endif + +static bool HostHasAddress(int family) { +#ifdef _WIN32 + auto& cache = AddressCache(); + const int slot = family == AF_INET ? 0 : family == AF_INET6 ? 1 : 2; + std::lock_guard guard(cache.lock); + WatchHostAddresses(cache); + const auto generation = cache.generation.load(std::memory_order_acquire); + if (cache.registered && cache.seen[slot] == generation) return cache.value[slot]; + const bool value = QueryHostHasAddress(family); + cache.value[slot] = value; + cache.seen[slot] = generation; + return value; +#else + return QueryHostHasAddress(family); +#endif +} + static bool UsableIpv4(in_addr address) { const auto host = ntohl(address.s_addr); return host != 0 && (host >> 24) != 127; @@ -310,7 +378,7 @@ static std::vector HostDnsServers() { } #endif -static bool FindHostIpv4Interface(HostIpv4Interface& found) { +static bool QueryHostIpv4Interface(HostIpv4Interface& found) { #ifdef _WIN32 ULONG size = 0; const ULONG flags = GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_INCLUDE_GATEWAYS; @@ -371,6 +439,25 @@ static bool FindHostIpv4Interface(HostIpv4Interface& found) { #endif } +static bool FindHostIpv4Interface(HostIpv4Interface& found) { +#ifdef _WIN32 + auto& cache = AddressCache(); + std::lock_guard guard(cache.lock); + WatchHostAddresses(cache); + const auto generation = cache.generation.load(std::memory_order_acquire); + if (!cache.registered || cache.interfaceSeen != generation) { + HostIpv4Interface value; + cache.interfaceFound = QueryHostIpv4Interface(value); + cache.interfaceValue = std::move(value); + cache.interfaceSeen = generation; + } + if (cache.interfaceFound) found = cache.interfaceValue; + return cache.interfaceFound; +#else + return QueryHostIpv4Interface(found); +#endif +} + extern "C" std::size_t NetCtlHostDnsServers_nid_no_patch(std::uint32_t* servers, std::size_t capacity) { HostIpv4Interface host; if (!FindHostIpv4Interface(host)) return 0; diff --git a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp index a604adba3d..d113635624 100644 --- a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp +++ b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp @@ -15,8 +15,12 @@ static constexpr int SCE_NGS2_ERROR_INVALID_VOICE_INDEX = static_cast(0x804 static constexpr int SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA = static_cast(0x804A8430); static constexpr int SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT = static_cast(0x804A8431); static constexpr int SCE_NGS2_ERROR_UNKNOWN_WAVEFORM_FORMAT = static_cast(0x804A8432); +static constexpr int SCE_NGS2_ERROR_INVALID_PAN_UNIT_ANGLE = static_cast(0x804A8450); +static constexpr int SCE_NGS2_ERROR_INVALID_PAN_SPEAKER = static_cast(0x804A8451); static constexpr int SCE_NGS2_ERROR_INVALID_VOICE_HANDLE = static_cast(0x804A8203); static constexpr int SCE_NGS2_ERROR_NULL_PARAM_LIST = static_cast(0x804A8360); +static constexpr int SCE_NGS2_ERROR_INVALID_GEOM_LISTENER_ADDRESS = static_cast(0x804A8461); +static constexpr int SCE_NGS2_ERROR_INVALID_GEOM_FLAG = static_cast(0x804A8463); static constexpr std::uint32_t SCE_NGS2_RACK_ID_SAMPLER = 0x1000; static constexpr std::uint32_t SCE_NGS2_RACK_ID_SUBMIXER = 0x2000; diff --git a/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp b/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp index 6c08a090c1..9357c6a942 100644 --- a/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp @@ -26,6 +26,7 @@ static constexpr std::uint32_t MAX_SAMPLE_RATE = 192000; static constexpr std::uint32_t WAVEFORM_TYPE_PCM_FIRST = 0x10; static constexpr std::uint32_t WAVEFORM_TYPE_PCM_LAST = 0x1b; static constexpr std::uint32_t WAVEFORM_TYPE_VAG = 0x1c; +static constexpr std::uint32_t VAG_FRAME_SAMPLES = 28; static constexpr std::uint32_t PCM_BYTES_PER_SAMPLE[] = {1, 1, 2, 2, 3, 3, 4, 4, 4, 4, 8, 8}; static bool IsPcmType(std::uint32_t type) { @@ -43,6 +44,18 @@ static Ngs2WaveformBlock PcmBlock(std::uint32_t type, std::uint32_t channels, st return block; } +static Ngs2WaveformBlock VagBlock(std::uint32_t channels, std::uint32_t pos, std::uint32_t count) { + const std::uint32_t frameBytes = 16u * channels; + const std::uint32_t first = pos / VAG_FRAME_SAMPLES; + const std::uint32_t past = (pos + count + VAG_FRAME_SAMPLES - 1u) / VAG_FRAME_SAMPLES; + const auto offset = static_cast(static_cast(first) * frameBytes); + Ngs2WaveformBlock block{}; + block.data_offset = offset; + block.data_size = static_cast(past * frameBytes - offset); + block.num_samples = (past - first) * VAG_FRAME_SAMPLES; + return block; +} + void Ngs2Atrac9DecoderDeleter::operator()(void* handle) const { Atrac9ReleaseHandle(handle); } @@ -432,7 +445,10 @@ int APS5_VABI sceNgs2CalcWaveformBlock(const Ngs2WaveformFormat* format, uint32_ *block = PcmBlock(type, format->num_channels, sample_pos, num_samples); return SCE_NGS2_OK; } - if (type == WAVEFORM_TYPE_VAG) throw std::runtime_error("NGS2: waveform blocks of type " + Ngs2Hex(type) + " are not implemented"); + if (type == WAVEFORM_TYPE_VAG) { + *block = VagBlock(format->num_channels, sample_pos, num_samples); + return SCE_NGS2_OK; + } if (type != SCE_NGS2_WAVEFORM_TYPE_ATRAC9) return SCE_NGS2_ERROR_UNKNOWN_WAVEFORM_FORMAT; if (format->num_channels == 0 || format->num_channels > NGS2_MAX_CHANNELS || format->sample_rate == 0 || format->sample_rate > MAX_SAMPLE_RATE) { return SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT; @@ -450,6 +466,28 @@ int APS5_VABI sceNgs2CalcWaveformBlock(const Ngs2WaveformFormat* format, uint32_ return SCE_NGS2_OK; } +int APS5_VABI sceNgs2GetWaveformFrameInfo(const Ngs2WaveformFormat* format, uint32_t* out_frame_size, uint32_t* out_num_frame_samples, uint32_t* out_units_per_frame, uint32_t* out_num_delay_samples) { + if (out_frame_size == nullptr || out_num_frame_samples == nullptr || out_units_per_frame == nullptr || out_num_delay_samples == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; + if (format == nullptr || format->num_channels == 0 || format->num_channels > NGS2_MAX_CHANNELS || format->sample_rate == 0 || format->sample_rate > MAX_SAMPLE_RATE) { + return SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT; + } + if (format->waveform_type == SCE_NGS2_WAVEFORM_TYPE_PCM_I16L) { + *out_frame_size = format->num_channels * sizeof(std::int16_t); + *out_num_frame_samples = 1; + *out_units_per_frame = 1; + *out_num_delay_samples = 0; + return SCE_NGS2_OK; + } + if (format->waveform_type != SCE_NGS2_WAVEFORM_TYPE_ATRAC9) throw std::runtime_error("NGS2: the frame info of waveform type " + Ngs2Hex(format->waveform_type) + " is not implemented"); + Atrac9CodecInfo codec{}; + if (!MatchingCodecInfo(*format, codec)) return SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT; + *out_frame_size = static_cast(codec.superframeSize); + *out_num_frame_samples = static_cast(codec.frameSamples * codec.framesInSuperframe); + *out_units_per_frame = static_cast(codec.framesInSuperframe); + *out_num_delay_samples = static_cast(codec.frameSamples); + return SCE_NGS2_OK; +} + } #pragma GCC visibility pop diff --git a/core/libs/prx/libSceNgs2.native/src/Geom.cpp b/core/libs/prx/libSceNgs2.native/src/Geom.cpp index 28f5d9f10c..9b68d818ef 100644 --- a/core/libs/prx/libSceNgs2.native/src/Geom.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Geom.cpp @@ -41,9 +41,9 @@ Vector PerpendicularHint(const Vector& front) { } int Ngs2GeomCalcListener(const Ngs2GeomListenerParam* param, Ngs2GeomListenerWork* outWork, std::uint32_t flags) { - if (param == nullptr) APS5_INVALID_ARG_EX; + if (param == nullptr) return SCE_NGS2_ERROR_INVALID_GEOM_LISTENER_ADDRESS; if (outWork == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; - if (flags > 1) throw std::runtime_error("NGS2: unknown listener coordinate flags"); + if (flags > 1) return SCE_NGS2_ERROR_INVALID_GEOM_FLAG; if (!std::isfinite(param->sound_speed) || param->sound_speed <= 0.0f) APS5_INVALID_ARG_EX; const Vector position = ReadVector(param->position); ReadVector(param->velocity); diff --git a/core/libs/prx/libSceNgs2.native/src/Pan.cpp b/core/libs/prx/libSceNgs2.native/src/Pan.cpp index af88b23204..e1a4d70548 100644 --- a/core/libs/prx/libSceNgs2.native/src/Pan.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Pan.cpp @@ -55,10 +55,13 @@ static void AddBetweenSpeakers(const Ngs2PanWork& work, std::uint32_t speaker, f } int Ngs2PanInit(Ngs2PanWork* work, const float* speaker_angles, float unit_angle, uint32_t num_speakers) { - if (work == nullptr) APS5_INVALID_ARG_EX; - if (speaker_angles != nullptr && !IsUnitAngle(unit_angle)) APS5_INVALID_ARG_EX; + if (work == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; + if (speaker_angles != nullptr && !IsUnitAngle(unit_angle)) return SCE_NGS2_ERROR_INVALID_PAN_UNIT_ANGLE; const auto* layout = LayoutFor(num_speakers); - if (layout == nullptr) APS5_INVALID_ARG_EX; + if (layout == nullptr) { + if (speaker_angles == nullptr && num_speakers == 0) APS5_INVALID_ARG_EX; + return SCE_NGS2_ERROR_INVALID_PAN_SPEAKER; + } *work = {}; work->num_speakers = num_speakers; if (speaker_angles == nullptr) { diff --git a/core/libs/prx/libSceNgs2.native/src/System.cpp b/core/libs/prx/libSceNgs2.native/src/System.cpp index 99eede7a7c..2fe15a7311 100644 --- a/core/libs/prx/libSceNgs2.native/src/System.cpp +++ b/core/libs/prx/libSceNgs2.native/src/System.cpp @@ -212,6 +212,7 @@ static Ngs2SystemOption DefaultSystemOption() { option.max_grain_samples = 512; option.num_grain_samples = 256; option.sample_rate = 48000; + option.max_voice_channels = 8; return option; } @@ -422,7 +423,7 @@ int APS5_VABI sceNgs2SystemRender(uintptr_t system_handle, const Ngs2RenderBuffe } int APS5_VABI sceNgs2SystemResetOption(Ngs2SystemOption* option) { - if (option == nullptr) APS5_INVALID_ARG_EX; + if (option == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; *option = DefaultSystemOption(); return SCE_NGS2_OK; } diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp index eed5eaf1f1..bbbebab2e4 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp @@ -195,6 +195,45 @@ static void TestCalcBlock() { Require(sceNgs2CalcWaveformBlock(&silent, 0, 1, &block) == SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT); } +static void TestFrameInfo() { + const Ngs2WaveformFormat supported[] = { + {SCE_NGS2_WAVEFORM_TYPE_ATRAC9, 1, 48000, Config, 0, 0}, + {SCE_NGS2_WAVEFORM_TYPE_ATRAC9, 1, 48000, Config, 7, 9}, + }; + const Ngs2WaveformFormat rejected[] = { + {SCE_NGS2_WAVEFORM_TYPE_ATRAC9, 2, 48000, Config, 0, 0}, + {SCE_NGS2_WAVEFORM_TYPE_ATRAC9, 0, 48000, Config, 0, 0}, + {SCE_NGS2_WAVEFORM_TYPE_ATRAC9, 9, 48000, Config, 0, 0}, + {SCE_NGS2_WAVEFORM_TYPE_ATRAC9, 1, 44100, Config, 0, 0}, + {SCE_NGS2_WAVEFORM_TYPE_ATRAC9, 1, 0, Config, 0, 0}, + {SCE_NGS2_WAVEFORM_TYPE_ATRAC9, 1, 192001, Config, 0, 0}, + {SCE_NGS2_WAVEFORM_TYPE_ATRAC9, 1, 48000, 0, 0, 0}, + }; + for (const auto& format : supported) { + Ngs2WaveformBlock block{}; + Require(sceNgs2CalcWaveformBlock(&format, 0, SuperframeSamples, &block) == SCE_NGS2_OK && block.data_size == SuperframeBytes); + std::uint32_t values[4] = {0xa1, 0xa2, 0xa3, 0xa4}; + Require(sceNgs2GetWaveformFrameInfo(&format, &values[0], &values[1], &values[2], &values[3]) == SCE_NGS2_OK); + Require(values[0] == SuperframeBytes && values[1] == SuperframeSamples && values[2] == 4 && values[3] == SuperframeSamples / 4); + } + for (const auto& format : rejected) { + Ngs2WaveformBlock block{}; + const int calc = sceNgs2CalcWaveformBlock(&format, 0, SuperframeSamples, &block); + Require(calc == SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT); + std::uint32_t values[4] = {0xa1, 0xa2, 0xa3, 0xa4}; + Require(sceNgs2GetWaveformFrameInfo(&format, &values[0], &values[1], &values[2], &values[3]) == calc); + Require(values[0] == 0xa1 && values[1] == 0xa2 && values[2] == 0xa3 && values[3] == 0xa4); + } + std::uint32_t values[4] = {0xa1, 0xa2, 0xa3, 0xa4}; + Require(sceNgs2GetWaveformFrameInfo(nullptr, &values[0], &values[1], &values[2], &values[3]) == SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT); + Require(sceNgs2GetWaveformFrameInfo(&supported[0], nullptr, &values[1], &values[2], &values[3]) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); + Require(sceNgs2GetWaveformFrameInfo(&supported[0], &values[0], nullptr, &values[2], &values[3]) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); + Require(sceNgs2GetWaveformFrameInfo(&supported[0], &values[0], &values[1], nullptr, &values[3]) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); + Require(sceNgs2GetWaveformFrameInfo(&supported[0], &values[0], &values[1], &values[2], nullptr) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); + Require(sceNgs2GetWaveformFrameInfo(nullptr, nullptr, nullptr, nullptr, nullptr) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); + Require(values[0] == 0xa1 && values[1] == 0xa2 && values[2] == 0xa3 && values[3] == 0xa4); +} + static void RequirePcmBlock(std::uint32_t type, std::uint32_t channels, std::uint32_t pos, std::uint32_t count, std::uint64_t offset, std::uint64_t size) { const Ngs2WaveformFormat format{type, channels, 48000, 0, 0, 0}; Ngs2WaveformBlock block; @@ -229,6 +268,26 @@ static void TestCalcPcmBlock() { Require(sceNgs2CalcWaveformBlock(nullptr, 0, 1, &block) == SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT && block.data_size == 0 && block.num_samples == 0); } +static void RequireVagBlock(std::uint32_t channels, std::uint32_t pos, std::uint32_t count, std::uint64_t offset, std::uint64_t size, std::uint32_t samples) { + const Ngs2WaveformFormat format{0x1c, channels, 48000, 0, 0, 0}; + Ngs2WaveformBlock block; + std::memset(&block, 0xab, sizeof(block)); + Require(sceNgs2CalcWaveformBlock(&format, pos, count, &block) == SCE_NGS2_OK); + Require(block.data_offset == offset && block.data_size == size && block.num_samples == samples); + Require(block.num_repeats == 0 && block.num_skip_samples == 0 && block.reserved == 0 && block.user_data == 0); +} + +static void TestCalcVagBlock() { + RequireVagBlock(1, 0, 28, 0, 16, 28); + RequireVagBlock(1, 27, 2, 0, 32, 56); + RequireVagBlock(1, 28, 28, 16, 16, 28); + RequireVagBlock(2, 30, 1, 32, 32, 28); + RequireVagBlock(6, 56, 57, 192, 288, 84); + RequireVagBlock(1, 0, 0, 0, 0, 0); + RequireVagBlock(1, 5, 0, 0, 16, 28); + RequireVagBlock(0, 100, 50, 0, 0, 84); +} + static void Set32(std::vector& bytes, std::size_t offset, std::uint32_t value) { for (int i = 0; i < 4; ++i) bytes[offset + i] = static_cast(value >> (i * 8)); } @@ -538,7 +597,9 @@ int main() { TestParseLoops(); TestMalformedLoops(); TestCalcBlock(); + TestFrameInfo(); TestCalcPcmBlock(); + TestCalcVagBlock(); TestCorruptSuperframeAtPageEnd(); TestStreamPartitions(); TestRefill(reference); diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Geom.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Geom.cpp index 2417e360e0..ae95253a90 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Geom.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Geom.cpp @@ -115,9 +115,18 @@ static void RejectListener(const Ngs2GeomListenerParam* param, std::uint32_t fla static void TestInvalidListener() { auto param = Listener(); Require(sceNgs2GeomCalcListener(¶m, nullptr, 0) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); - RejectListener(nullptr, 0); - RejectListener(¶m, 2); - RejectListener(¶m, 0xffffffffu); + Ngs2GeomListenerWork work; + std::memset(&work, 0x5a, sizeof(work)); + unsigned char before[sizeof(work)]; + std::memcpy(before, &work, sizeof(work)); + Require(sceNgs2GeomCalcListener(nullptr, &work, 0) == static_cast(0x804A8461)); + Require(sceNgs2GeomCalcListener(nullptr, &work, 2) == static_cast(0x804A8461)); + Require(sceNgs2GeomCalcListener(nullptr, nullptr, 0) == static_cast(0x804A8461)); + Require(sceNgs2GeomCalcListener(¶m, &work, 2) == static_cast(0x804A8463)); + Require(sceNgs2GeomCalcListener(¶m, &work, 0x80000000u) == static_cast(0x804A8463)); + Require(sceNgs2GeomCalcListener(¶m, &work, 0xffffffffu) == static_cast(0x804A8463)); + Require(sceNgs2GeomCalcListener(¶m, nullptr, 2) == static_cast(0x804A8010)); + Require(std::memcmp(before, &work, sizeof(work)) == 0); for (const float value : {0.0f, -1.0f, std::numeric_limits::infinity(), std::numeric_limits::quiet_NaN()}) { param.sound_speed = value; RejectListener(¶m, 0); diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2HandleErrors.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2HandleErrors.cpp index 56246ca178..c9a0094eda 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2HandleErrors.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2HandleErrors.cpp @@ -1,5 +1,7 @@ #include "Ngs2Test.hpp" +#include +#include #include #include @@ -7,6 +9,28 @@ static void Status(int actual, int expected, const char* message) { if (actual != expected) throw std::runtime_error(message); } +static void CheckResetOption() { + Status(sceNgs2SystemResetOption(nullptr), static_cast(0x804A8010u), "null system option did not return the invalid out address error"); + constexpr std::uint64_t size = 0x90; + constexpr std::uint32_t maxGrainSamples = 512; + constexpr std::uint32_t numGrainSamples = 256; + constexpr std::uint32_t sampleRate = 48000; + constexpr std::uint32_t maxVoiceChannels = 8; + unsigned char expected[0x90] = {}; + std::memcpy(expected + 0x00, &size, sizeof(size)); + std::memcpy(expected + 0x6c, &maxGrainSamples, sizeof(maxGrainSamples)); + std::memcpy(expected + 0x70, &numGrainSamples, sizeof(numGrainSamples)); + std::memcpy(expected + 0x74, &sampleRate, sizeof(sampleRate)); + std::memcpy(expected + 0x78, &maxVoiceChannels, sizeof(maxVoiceChannels)); + static_assert(sizeof(Ngs2SystemOption) == sizeof(expected)); + Ngs2SystemOption option; + std::memset(&option, 0xAB, sizeof(option)); + Status(sceNgs2SystemResetOption(&option), SCE_NGS2_OK, "system option reset failed"); + Require(std::memcmp(&option, expected, sizeof(expected)) == 0); + Ngs2ContextBufferInfo query{}; + Status(sceNgs2SystemQueryBufferSize(&option, &query), SCE_NGS2_OK, "reset system option was rejected"); +} + int main() { try { constexpr int systemError = static_cast(0x804A8201u); @@ -39,6 +63,7 @@ int main() { Status(sceNgs2SystemDestroy(system, nullptr), SCE_NGS2_OK, "valid system destroy failed"); Status(sceNgs2SystemGetUserData(system, &value), systemError, "retired system returned the wrong error"); Require(value == sentinel); + CheckResetOption(); std::cout << "NGS2 PS5 handle errors and validation precedence checked\n"; return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Parse.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Parse.cpp index a2545f52e6..6a0c74f5b3 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Parse.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Parse.cpp @@ -113,9 +113,59 @@ static void TestParseFileBank() { std::filesystem::remove(path); } +static void TestFrameInfoPcm() { + for (std::uint32_t channels = 1; channels <= 8; channels++) { + for (const std::uint32_t rate : {1u, 22050u, 48000u, 192000u}) { + const Ngs2WaveformFormat format{SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, channels, rate, 0, 0, 0}; + std::uint32_t frameSize = 0xa1, frameSamples = 0xa2, units = 0xa3, delay = 0xa4; + Require(sceNgs2GetWaveformFrameInfo(&format, &frameSize, &frameSamples, &units, &delay) == SCE_NGS2_OK); + Require(frameSize == channels * 2 && frameSamples == 1 && units == 1 && delay == 0); + const auto file = PcmFile(channels, rate, 16, 10); + Ngs2WaveformInfo info{}; + Require(sceNgs2ParseWaveformData(file.data(), file.size(), &info) == SCE_NGS2_OK); + Require(info.audio_frame_size == frameSize && info.num_audio_frame_samples == frameSamples); + Require(info.num_audio_unit_per_frame == units && info.num_delay_samples == delay); + } + } + const Ngs2WaveformFormat ignored{SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 2, 48000, 0x12345678, 3, 5}; + std::uint32_t frameSize = 0xa1, frameSamples = 0xa2, units = 0xa3, delay = 0xa4; + Require(sceNgs2GetWaveformFrameInfo(&ignored, &frameSize, &frameSamples, &units, &delay) == SCE_NGS2_OK); + Require(frameSize == 4 && frameSamples == 1 && units == 1 && delay == 0); +} + +static void TestFrameInfoErrors() { + const Ngs2WaveformFormat pcm{SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 2, 48000, 0, 0, 0}; + std::uint32_t values[4] = {0xa1, 0xa2, 0xa3, 0xa4}; + const auto untouched = [&] { return values[0] == 0xa1 && values[1] == 0xa2 && values[2] == 0xa3 && values[3] == 0xa4; }; + Require(sceNgs2GetWaveformFrameInfo(&pcm, nullptr, &values[1], &values[2], &values[3]) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); + Require(sceNgs2GetWaveformFrameInfo(&pcm, &values[0], nullptr, &values[2], &values[3]) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); + Require(sceNgs2GetWaveformFrameInfo(&pcm, &values[0], &values[1], nullptr, &values[3]) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); + Require(sceNgs2GetWaveformFrameInfo(&pcm, &values[0], &values[1], &values[2], nullptr) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); + Require(sceNgs2GetWaveformFrameInfo(nullptr, nullptr, nullptr, nullptr, nullptr) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); + Require(sceNgs2GetWaveformFrameInfo(nullptr, &values[0], &values[1], &values[2], &values[3]) == SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT); + Require(untouched()); + const Ngs2WaveformFormat invalid[] = { + {SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 0, 48000, 0, 0, 0}, + {SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 9, 48000, 0, 0, 0}, + {SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 2, 0, 0, 0, 0}, + {SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 2, 192001, 0, 0, 0}, + {0x99, 0, 48000, 0, 0, 0}, + {SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 2, 0, 0, 0, 0}, + }; + for (const auto& format : invalid) { + Require(sceNgs2GetWaveformFrameInfo(&format, &values[0], &values[1], &values[2], &values[3]) == SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT); + Require(untouched()); + } + for (const std::uint32_t type : {0u, 0x10u, 0x11u, 0x13u, 0x14u, 0x16u, SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 0x19u, 0x41u, 0xffffffffu}) { + Require(FrameInfoThrows({type, 2, 48000, 0, 0, 0})); + } +} + int main() { TestParsePcm(); TestParseFile(); TestParseFileBank(); + TestFrameInfoPcm(); + TestFrameInfoErrors(); return 0; } diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp index 4186d67946..d76c464a6c 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -262,10 +263,32 @@ static void TestPan() { work.speaker_angles[0] = -INFINITY; Require(PanThrows(&work, &front, 1, 2)); Require(sceNgs2PanInit(&work, nullptr, 360.0f, 2) == SCE_NGS2_OK); - Require(PanInitThrows(nullptr, nullptr, 360.0f, 2)); - for (std::uint32_t count : {0u, 1u, 3u, 6u, 8u}) Require(PanInitThrows(&work, nullptr, 360.0f, count)); + Ngs2PanWork marked; + std::memset(&marked, 0x5a, sizeof(marked)); + Ngs2PanWork expected; + std::memcpy(&expected, &marked, sizeof(marked)); + const auto unchanged = [&] { return std::memcmp(&marked, &expected, sizeof(marked)) == 0; }; const float pair[] = {-30.0f, 30.0f}; - Require(PanInitThrows(&work, pair, 180.0f, 2) && !PanInitThrows(&work, pair, 2.0f * pi, 2)); + Require(sceNgs2PanInit(nullptr, nullptr, 360.0f, 2) == static_cast(0x804A8010)); + Require(sceNgs2PanInit(nullptr, pair, 180.0f, 3) == static_cast(0x804A8010)); + Require(sceNgs2PanInit(nullptr, nullptr, 100.0f, 3) == static_cast(0x804A8010)); + for (std::uint32_t count : {1u, 3u, 6u, 8u, 255u}) { + Require(sceNgs2PanInit(&marked, nullptr, 360.0f, count) == static_cast(0x804A8451)); + Require(unchanged()); + } + Require(PanInitThrows(&marked, nullptr, 360.0f, 0)); + Require(unchanged()); + for (std::uint32_t count : {0u, 1u, 3u, 6u, 8u, 255u}) { + Require(sceNgs2PanInit(&marked, pair, 360.0f, count) == static_cast(0x804A8451)); + Require(sceNgs2PanInit(&marked, pair, 2.0f * pi, count) == static_cast(0x804A8451)); + Require(unchanged()); + } + for (const float unitAngle : {180.0f, 0.0f, 359.0f, 361.0f, 90.0f, pi}) { + Require(sceNgs2PanInit(&marked, pair, unitAngle, 2) == static_cast(0x804A8450)); + Require(sceNgs2PanInit(&marked, pair, unitAngle, 3) == static_cast(0x804A8450)); + Require(unchanged()); + } + Require(sceNgs2PanInit(&work, pair, 2.0f * pi, 2) == SCE_NGS2_OK); const float invalid[] = {0.0f, NAN}; Require(PanInitThrows(&work, invalid, 360.0f, 2)); const float onUnit[] = {720.0f, 30.0f}; diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp index 95252adde2..4768cc7cb3 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp @@ -6,12 +6,14 @@ #include #include #include +#include #include extern "C" { int APS5_VABI sceNgs2ParseWaveformData(const void*, size_t, Ngs2WaveformInfo*); int APS5_VABI sceNgs2ParseWaveformFile(const char*, uint32_t, Ngs2WaveformInfo*); int APS5_VABI sceNgs2CalcWaveformBlock(const Ngs2WaveformFormat*, uint32_t, uint32_t, Ngs2WaveformBlock*); +int APS5_VABI sceNgs2GetWaveformFrameInfo(const Ngs2WaveformFormat*, uint32_t*, uint32_t*, uint32_t*, uint32_t*); int APS5_VABI sceNgs2SystemResetOption(Ngs2SystemOption*); int APS5_VABI sceNgs2SystemQueryBufferSize(const Ngs2SystemOption*, Ngs2ContextBufferInfo*); int APS5_VABI sceNgs2SystemCreate(const Ngs2SystemOption*, const Ngs2ContextBufferInfo*, uintptr_t*); @@ -60,6 +62,18 @@ inline void Check(bool value, int line) { } #define Require(value) Check((value), __LINE__) +inline bool FrameInfoThrows(const Ngs2WaveformFormat& format) { + std::uint32_t values[4] = {0xa1, 0xa2, 0xa3, 0xa4}; + bool threw = false; + try { + sceNgs2GetWaveformFrameInfo(&format, &values[0], &values[1], &values[2], &values[3]); + } catch (const std::exception&) { + threw = true; + } + Require(values[0] == 0xa1 && values[1] == 0xa2 && values[2] == 0xa3 && values[3] == 0xa4); + return threw; +} + inline constexpr std::uint32_t Grain = 8; inline std::vector> buffers; diff --git a/core/libs/prx/libSceNgs2/PendingExports.cpp b/core/libs/prx/libSceNgs2/PendingExports.cpp index 79edf98b0c..d930fb00c1 100644 --- a/core/libs/prx/libSceNgs2/PendingExports.cpp +++ b/core/libs/prx/libSceNgs2/PendingExports.cpp @@ -21,10 +21,6 @@ int APS5_VABI sceNgs2FftQuerySize() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNgs2GetWaveformFrameInfo(void* format, std::uint32_t* outFrameSize, std::uint32_t* outNumFrameSamples, std::uint32_t* outUnitsPerFrame, std::uint32_t* outNumDelaySamples) { - NotImplemented_nid_no_patch(__func__); - return 0; -} int APS5_VABI sceNgs2ParseWaveformUser(std::uintptr_t handler, std::uintptr_t userData, void* outInfo) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceNpAuth/Export.cpp b/core/libs/prx/libSceNpAuth/Export.cpp index 265c3fd4df..3289f920f0 100644 --- a/core/libs/prx/libSceNpAuth/Export.cpp +++ b/core/libs/prx/libSceNpAuth/Export.cpp @@ -84,7 +84,6 @@ int APS5_VABI sceNpAuthGetAuthorizationCodeV3(int req_id, const void* param, voi if (it == g_requests.end()) return ORBIS_NP_AUTH_ERROR_REQUEST_NOT_FOUND; if (it->second.processed) return ORBIS_NP_AUTH_ERROR_INVALID_ARGUMENT; if (it->second.aborted) { - it->second.result = ORBIS_NP_AUTH_ERROR_ABORTED; return ORBIS_NP_AUTH_ERROR_ABORTED; } it->second.processed = true; @@ -102,7 +101,6 @@ int APS5_VABI sceNpAuthGetIdTokenV3(int req_id, const void* param, void* id_toke if (it == g_requests.end()) return ORBIS_NP_AUTH_ERROR_REQUEST_NOT_FOUND; if (it->second.processed) return ORBIS_NP_AUTH_ERROR_INVALID_ARGUMENT; if (it->second.aborted) { - it->second.result = ORBIS_NP_AUTH_ERROR_ABORTED; return ORBIS_NP_AUTH_ERROR_ABORTED; } it->second.processed = true; @@ -125,7 +123,7 @@ int APS5_VABI sceNpAuthPollAsync(int req_id, int* result) { std::lock_guard lock(g_requestMutex); auto it = g_requests.find(req_id); if (it == g_requests.end()) return ORBIS_NP_AUTH_ERROR_REQUEST_NOT_FOUND; - if (!it->second.isAsync || (!it->second.processed && !it->second.aborted)) return ORBIS_NP_AUTH_ERROR_INVALID_ID; + if (!it->second.isAsync || !it->second.processed) return ORBIS_NP_AUTH_ERROR_INVALID_ID; if (!it->second.polled) { it->second.polled = true; return NP_POLL_ASYNC_RUNNING; @@ -154,7 +152,7 @@ int APS5_VABI sceNpAuthWaitAsync(int req_id, int* result) { std::lock_guard lock(g_requestMutex); auto it = g_requests.find(req_id); if (it == g_requests.end()) return ORBIS_NP_AUTH_ERROR_REQUEST_NOT_FOUND; - if (!it->second.isAsync || (!it->second.processed && !it->second.aborted)) return ORBIS_NP_AUTH_ERROR_INVALID_ID; + if (!it->second.isAsync || !it->second.processed) return ORBIS_NP_AUTH_ERROR_INVALID_ID; *result = it->second.result; return NP_POLL_ASYNC_FINISHED; } diff --git a/core/libs/prx/libSceNpCommon/PendingExports.cpp b/core/libs/prx/libSceNpCommon/PendingExports.cpp index c9d6e53ffd..d2daed1a92 100644 --- a/core/libs/prx/libSceNpCommon/PendingExports.cpp +++ b/core/libs/prx/libSceNpCommon/PendingExports.cpp @@ -115,6 +115,11 @@ int APS5_VABI sceNpCloseSema(void) { return 0; } +int APS5_VABI sceNpCmpNpId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI sceNpCmpNpIdInOrder(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceNpCppWebApi/Export.cpp b/core/libs/prx/libSceNpCppWebApi/Export.cpp index 6238d389f8..5c53984a9f 100644 --- a/core/libs/prx/libSceNpCppWebApi/Export.cpp +++ b/core/libs/prx/libSceNpCppWebApi/Export.cpp @@ -1890,4 +1890,471 @@ std::uint64_t APS5_VABI _ZNK3sce2Np9CppWebApi17TitleCloudStorage2V15Owner12getAc NotImplemented_nid_no_patch(__func__); return 0; } + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V110FriendsApi10getFriendsEiRKNS4_21ParameterToGetFriendsERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_18GetFriendsResponseEEENSA_INS8_18ResponseHeaderBaseEEEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V110FriendsApi21ParameterToGetFriends10initializeEPNS1_6Common10LibContextEPKc() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V110FriendsApi21ParameterToGetFriends9terminateEv() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V110FriendsApi21ParameterToGetFriendsC1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V110FriendsApi21ParameterToGetFriendsD1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V111PresenceApi17getBasicPresencesEiRKNS4_28ParameterToGetBasicPresencesERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_25GetBasicPresencesResponseEEENSA_INS8_18ResponseHeaderBaseEEEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V111PresenceApi28ParameterToGetBasicPresences10initializeEPNS1_6Common10LibContextEPKc() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V111PresenceApi28ParameterToGetBasicPresences9terminateEv() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V111PresenceApi28ParameterToGetBasicPresencesC1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V111PresenceApi28ParameterToGetBasicPresencesD1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V118GetFriendsResponse10getFriendsEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V124GetBlockingUsersResponse9getBlocksEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V125GetBasicPresencesResponse17getBasicPresencesEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V19BlocksApi16getBlockingUsersEiRKNS4_27ParameterToGetBlockingUsersERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_24GetBlockingUsersResponseEEENSA_INS8_18ResponseHeaderBaseEEEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V19BlocksApi27ParameterToGetBlockingUsers10initializeEPNS1_6Common10LibContextE() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V19BlocksApi27ParameterToGetBlockingUsers9terminateEv() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V19BlocksApi27ParameterToGetBlockingUsersC1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V19BlocksApi27ParameterToGetBlockingUsersD1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi15ProfanityFilter2V212ProfanityApi15filterProfanityEiRKNS4_26ParameterToFilterProfanityERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_23FilterProfanityResponseEEENSA_INS8_18ResponseHeaderBaseEEEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi15ProfanityFilter2V212ProfanityApi26ParameterToFilterProfanity10initializeEPNS1_6Common10LibContextEPKcSA_NS6_12IntrusivePtrINS3_19WebApiFilterRequestEEE() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi15ProfanityFilter2V212ProfanityApi26ParameterToFilterProfanity9terminateEv() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi15ProfanityFilter2V212ProfanityApi26ParameterToFilterProfanityC1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi15ProfanityFilter2V212ProfanityApi26ParameterToFilterProfanityD1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi15ProfanityFilter2V226WebApiFilterRequestFactory6createEPNS1_6Common10LibContextEPKcPNS5_12IntrusivePtrINS3_19WebApiFilterRequestEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi30CommunicationRestrictionStatus2V333CommunicationRestrictionStatusApi33getCommunicationRestrictionStatusEiRKNS4_44ParameterToGetCommunicationRestrictionStatusERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_38CommunicationRestrictionStatusResponseEEENSA_INS8_18ResponseHeaderBaseEEEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi30CommunicationRestrictionStatus2V333CommunicationRestrictionStatusApi44ParameterToGetCommunicationRestrictionStatus10initializeEPNS1_6Common10LibContextEm() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi30CommunicationRestrictionStatus2V333CommunicationRestrictionStatusApi44ParameterToGetCommunicationRestrictionStatus9terminateEv() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi30CommunicationRestrictionStatus2V333CommunicationRestrictionStatusApi44ParameterToGetCommunicationRestrictionStatusC1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi30CommunicationRestrictionStatus2V333CommunicationRestrictionStatusApi44ParameterToGetCommunicationRestrictionStatusD1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEE21setOnFinishedCallbackEPFvliS8_PvEPFvliSC_ESC_() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEE21setOnFinishedCallbackEPFvliS8_PvEPFvliSC_ESC_() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE21setOnFinishedCallbackEPFvliS8_PvEPFvliSC_ESC_() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEENS4_INS2_18ResponseHeaderBaseEEEE21setOnFinishedCallbackEPFvliS8_PvEPFvliSC_ESC_() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEE21setOnFinishedCallbackEPFvliS8_PvEPFvliSC_ESC_() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(void* self) { + (void)self; + return 0; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_15ProfanityFilter2V219WebApiFilterRequestEEC1ERS7_(IntrusivePtr* self, const IntrusivePtr* source) { + CopyIntrusivePtr(self, source, ReferenceCountOffset); +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_15ProfanityFilter2V219WebApiFilterRequestEEC1Ev(IntrusivePtr* self) { + *self = {}; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_15ProfanityFilter2V219WebApiFilterRequestEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, ReferenceCountOffset, __func__); +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_18ResponseHeaderBaseEEC1Ev(IntrusivePtr* self) { + *self = {}; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_18ResponseHeaderBaseEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, ReferenceCountOffset, __func__); +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6StringEEC1Ev(IntrusivePtr* self) { + *self = {}; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6StringEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, ReferenceCountOffset, __func__); +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_11UserProfile2V113BasicPresenceEEEEEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, VectorReferenceCountOffset, __func__); +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common13ConstIteratorINS2_12IntrusivePtrINS1_11UserProfile2V113BasicPresenceEEEED2Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common13ConstIteratorImED2Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEE28setResponseInformationOptionEP37SceNpWebApi2ResponseInformationOption() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEE28setResponseInformationOptionEP37SceNpWebApi2ResponseInformationOption() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE28setResponseInformationOptionEP37SceNpWebApi2ResponseInformationOption() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEENS4_INS2_18ResponseHeaderBaseEEEE28setResponseInformationOptionEP37SceNpWebApi2ResponseInformationOption() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEE28setResponseInformationOptionEP37SceNpWebApi2ResponseInformationOption() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common18ResponseHeaderBase14getHeaderValueEPKcRNS2_12IntrusivePtrINS2_6StringEEE() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_11UserProfile2V113BasicPresenceEEEE3endEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_11UserProfile2V113BasicPresenceEEEE5beginEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorImE3endEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorImE5beginEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_11UserProfile2V113BasicPresenceEEEEppEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common8IteratorImEppEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11UserProfile2V113BasicPresence12getAccountIdEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11UserProfile2V113BasicPresence12getInContextEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11UserProfile2V113BasicPresence15getOnlineStatusEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi15ProfanityFilter2V223FilterProfanityResponse10getMessageEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponse13getRestrictedEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V113BasicPresenceEEptEv(const IntrusivePtr* self) { + return self->object; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEptEv(const IntrusivePtr* self) { + return self->object; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEptEv(const IntrusivePtr* self) { + return self->object; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEptEv(const IntrusivePtr* self) { + return self->object; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEptEv(const IntrusivePtr* self) { + return self->object; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEptEv(const IntrusivePtr* self) { + return self->object; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_18ResponseHeaderBaseEE3getEv(const IntrusivePtr* self) { + return self->object; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_18ResponseHeaderBaseEEptEv(const IntrusivePtr* self) { + return self->object; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6StringEE3getEv(const IntrusivePtr* self) { + return self->object; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6StringEEptEv(const IntrusivePtr* self) { + return self->object; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_11UserProfile2V113BasicPresenceEEEEEEdeEv(const IntrusivePtr* self) { + return self->object; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_11UserProfile2V113BasicPresenceEEEEEEptEv(const IntrusivePtr* self) { + return self->object; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorImEEEdeEv(const IntrusivePtr* self) { + return self->object; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorImEEEptEv(const IntrusivePtr* self) { + return self->object; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEE18getResponseHeadersERSA_() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEE18getResponseHeadersERSA_() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE18getResponseHeadersERSA_() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEENS4_INS2_18ResponseHeaderBaseEEEE18getResponseHeadersERSA_() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEE18getResponseHeadersERSA_() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_11UserProfile2V113BasicPresenceEEEE4sizeEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common6VectorImE4sizeEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_11UserProfile2V113BasicPresenceEEEEdeEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_11UserProfile2V113BasicPresenceEEEEneERKS9_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorImEdeEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorImEneERKS4_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/core/libs/prx/libSceNpManager/Export.cpp b/core/libs/prx/libSceNpManager/Export.cpp index 0035e68915..15fd862b97 100644 --- a/core/libs/prx/libSceNpManager/Export.cpp +++ b/core/libs/prx/libSceNpManager/Export.cpp @@ -14,11 +14,13 @@ static constexpr int SCE_NP_ERROR_SIGNED_OUT = static_cast(0x80550006); static constexpr int SCE_NP_ERROR_USER_NOT_FOUND = static_cast(0x80550007); static constexpr int SCE_NP_ERROR_CALLBACK_ALREADY_REGISTERED = static_cast(0x80550008); static constexpr int SCE_NP_ERROR_CALLBACK_NOT_REGISTERED = static_cast(0x80550009); +static constexpr int SCE_NP_ERROR_INVALID_SIZE = static_cast(0x80550011); static constexpr int SCE_NP_ERROR_REQUEST_NOT_FOUND = static_cast(0x80550014); static constexpr int SCE_NP_ERROR_CALLBACK_MAX = static_cast(0x8055001D); static constexpr uint32_t NP_STATE_SIGNED_OUT = 1; static constexpr int NP_POLL_ASYNC_FINISHED = 0; static constexpr uint32_t NP_REACHABILITY_STATE_UNAVAILABLE = 0; +static constexpr uint32_t NP_GAME_PRESENCE_STATUS_OFFLINE = 0; static std::atomic g_nextRequest{1}; @@ -36,6 +38,12 @@ int AddRequest() { return reqId; } +int SignedOutRequest(int reqId) { + if (reqId == 0) return SCE_NP_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(requestMutex); + return liveRequests.count(reqId) ? SCE_NP_ERROR_SIGNED_OUT : SCE_NP_ERROR_REQUEST_NOT_FOUND; +} + using NpStateCallbackA = void (APS5_VABI *)(int userId, uint32_t state, void* userdata); using NpStateCallback = void (APS5_VABI *)(int userId, uint32_t state, NpId* npId, void* userdata); @@ -252,29 +260,31 @@ int APS5_VABI sceNpAsmTerminate(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNpCheckNpAvailabilityA(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceNpCheckNpAvailabilityA(int reqId, int userId) { + if (userId == USER_SERVICE_USER_ID_INVALID) return SCE_NP_ERROR_INVALID_ARGUMENT; + return SignedOutRequest(reqId); +} +int APS5_VABI sceNpGetAccountDateOfBirthA(int userId, void* dateOfBirth) { + if (!dateOfBirth || userId == USER_SERVICE_USER_ID_INVALID) return SCE_NP_ERROR_INVALID_ARGUMENT; + return SCE_NP_ERROR_SIGNED_OUT; +} +int APS5_VABI sceNpGetAccountLanguageA(int reqId, int userId, void* language) { + if (!language || userId == USER_SERVICE_USER_ID_INVALID) return SCE_NP_ERROR_INVALID_ARGUMENT; + return SignedOutRequest(reqId); +} +int APS5_VABI sceNpGetGamePresenceStatus(const NpOnlineId* onlineId, uint32_t* status) { + if (!onlineId || !status) return SCE_NP_ERROR_INVALID_ARGUMENT; + *status = NP_GAME_PRESENCE_STATUS_OFFLINE; return 0; } -int APS5_VABI sceNpGetAccountDateOfBirthA(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceNpGetGamePresenceStatusA(int userId, uint32_t* status) { + if (!status || userId == USER_SERVICE_USER_ID_INVALID) return SCE_NP_ERROR_INVALID_ARGUMENT; + *status = NP_GAME_PRESENCE_STATUS_OFFLINE; return 0; } -int APS5_VABI sceNpGetAccountLanguageA(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} -int APS5_VABI sceNpGetGamePresenceStatus(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} -int APS5_VABI sceNpGetGamePresenceStatusA(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} -int APS5_VABI sceNpGetParentalControlInfoA(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpGetParentalControlInfoA(int reqId, int userId, int8_t* age, void* info) { + if (!age || !info || userId == USER_SERVICE_USER_ID_INVALID) return SCE_NP_ERROR_INVALID_ARGUMENT; + return SignedOutRequest(reqId); } int APS5_VABI sceNpIdMapperAbortRequest(void) { NotImplemented_nid_no_patch(__func__); @@ -348,8 +358,9 @@ int APS5_VABI sceNpIntRegisterGamePresenceCallback(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNpIsPlusMember(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceNpIsPlusMember(int, bool* isMember) { + if (!isMember) return SCE_NP_ERROR_INVALID_ARGUMENT; + *isMember = false; return 0; } int APS5_VABI sceNpManagerIntAbortRequest(void) { @@ -871,9 +882,19 @@ int APS5_VABI sceNpRegisterStateCallback(NpStateCallback callback, void* userdat } int APS5_VABI sceNpSetContentRestriction(const NpContentRestriction* restriction) { - (void)restriction; - NotImplemented_nid_no_patch(__func__); - return 0; + struct Fields { + std::uint64_t size; + std::int8_t defaultAgeRestriction; + std::uint8_t padding[3]; + std::int32_t ageRestrictionCount; + const void* ageRestrictions; + }; + if (!restriction) return SCE_NP_ERROR_INVALID_ARGUMENT; + const auto* fields = reinterpret_cast(restriction); + if (fields->size != sizeof(Fields)) return SCE_NP_ERROR_INVALID_SIZE; + if (fields->defaultAgeRestriction < 0 || fields->ageRestrictionCount > 0x100) return SCE_NP_ERROR_INVALID_ARGUMENT; + if (fields->ageRestrictionCount > 0 && !fields->ageRestrictions) return SCE_NP_ERROR_INVALID_ARGUMENT; + return 0; } int APS5_VABI sceNpSetNpTitleId(const NpTitleId* title_id, const NpTitleSecret* title_secret) { @@ -976,6 +997,54 @@ int APS5_VABI sceNpUnknown_WxLcN76q19o(void) { int APS5_VABI sceNpCheckCallbackForLib() { + return sceNpCheckCallback(); +} + +} + +extern "C" { + +int APS5_VABI sceNpInGameMessageCreateHandle() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpInGameMessageDeleteHandle() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpInGameMessageInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpInGameMessagePrepare() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpInGameMessageSendData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpInGameMessageTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSnsFacebookCreateRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSnsFacebookDeleteRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSnsFacebookGetAccessToken() { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceNpMatching2/Export.cpp b/core/libs/prx/libSceNpMatching2/Export.cpp new file mode 100644 index 0000000000..2746971c77 --- /dev/null +++ b/core/libs/prx/libSceNpMatching2/Export.cpp @@ -0,0 +1,151 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceNpMatching2ContextStart() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2ContextStop() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2CreateContext() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2CreateJoinRoom() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2DestroyContext() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2GetRoomDataExternalList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2GetRoomDataInternal() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2GetRoomMemberDataInternal() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2GetServerId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2GetUserInfoList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2GetWorldInfoList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2Initialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2JoinRoom() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2KickoutRoomMember() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2LeaveRoom() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2RegisterContextCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2RegisterRoomEventCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2RegisterRoomMessageCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2RegisterSignalingCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2SearchRoom() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2SendRoomChatMessage() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2SendRoomMessage() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2SetDefaultRequestOptParam() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2SetRoomDataExternal() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2SetRoomDataInternal() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2SetRoomMemberDataInternal() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2SetUserInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2SignalingGetConnectionInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2Terminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceNpScoreRanking/Export.cpp b/core/libs/prx/libSceNpScoreRanking/Export.cpp new file mode 100644 index 0000000000..1bb8bc6df6 --- /dev/null +++ b/core/libs/prx/libSceNpScoreRanking/Export.cpp @@ -0,0 +1,81 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceNpScoreAbortRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpScoreCreateNpTitleCtx() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpScoreCreateRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpScoreDeleteNpTitleCtx() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpScoreDeleteRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpScoreGetBoardInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpScoreGetFriendsRanking() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpScoreGetFriendsRankingAsync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpScoreGetRankingByNpId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpScoreGetRankingByNpIdAsync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpScoreGetRankingByRange() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpScoreGetRankingByRangeAsync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpScorePollAsync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpScoreRecordScore() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpScoreRecordScoreAsync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceNpSignaling/Export.cpp b/core/libs/prx/libSceNpSignaling/Export.cpp new file mode 100644 index 0000000000..8ab2a25170 --- /dev/null +++ b/core/libs/prx/libSceNpSignaling/Export.cpp @@ -0,0 +1,46 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceNpSignalingActivateConnection() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSignalingCreateContext() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSignalingDeleteContext() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSignalingGetConnectionInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSignalingInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSignalingSetContextOption() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSignalingTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSignalingTerminateConnection() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceNpSnsFacebookDialog/Export.cpp b/core/libs/prx/libSceNpSnsFacebookDialog/Export.cpp index 58bb27df19..9a4fe0e8cd 100644 --- a/core/libs/prx/libSceNpSnsFacebookDialog/Export.cpp +++ b/core/libs/prx/libSceNpSnsFacebookDialog/Export.cpp @@ -15,4 +15,29 @@ int APS5_VABI sceNpSnsFacebookDialogGetStatus(void) { return 0; } +int APS5_VABI sceNpSnsFacebookDialogGetResult() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSnsFacebookDialogInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSnsFacebookDialogOpen() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSnsFacebookDialogTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSnsFacebookDialogUpdateStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceNpTrophy/CMakeLists.txt b/core/libs/prx/libSceNpTrophy/CMakeLists.txt new file mode 100644 index 0000000000..7301fa20ab --- /dev/null +++ b/core/libs/prx/libSceNpTrophy/CMakeLists.txt @@ -0,0 +1,16 @@ +add_library(libSceNpTrophy SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceNpTrophy PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceNpTrophy) + +if(BUILD_TESTING) + add_test_executable(guest_np_trophy_tests ${LIBS_INCLUDE_DIR}/tests/GuestNpTrophy.cpp) + target_include_directories(guest_np_trophy_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_np_trophy_tests PRIVATE libSceNpTrophy) + configure_windows_unwind(guest_np_trophy_tests) + add_test(NAME guest_np_trophy COMMAND guest_np_trophy_tests) + set_tests_properties(guest_np_trophy PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_np_trophy PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceNpTrophy/Export.cpp b/core/libs/prx/libSceNpTrophy/Export.cpp new file mode 100644 index 0000000000..aad30b5af0 --- /dev/null +++ b/core/libs/prx/libSceNpTrophy/Export.cpp @@ -0,0 +1,268 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +namespace { + +constexpr int NP_TROPHY_ERROR_INVALID_ARGUMENT = static_cast(0x80551604u); +constexpr int NP_TROPHY_ERROR_INSUFFICIENT_BUFFER = static_cast(0x80551605u); +constexpr int NP_TROPHY_ERROR_INVALID_HANDLE = static_cast(0x80551608u); +constexpr int NP_TROPHY_ERROR_INVALID_CONTEXT = static_cast(0x80551609u); +constexpr int NP_TROPHY_ERROR_INVALID_TROPHY_ID = static_cast(0x8055160au); +constexpr int NP_TROPHY_ERROR_INVALID_GROUP_ID = static_cast(0x8055160bu); +constexpr int NP_TROPHY_ERROR_TROPHY_ALREADY_UNLOCKED = static_cast(0x8055160cu); +constexpr int NP_TROPHY_ERROR_NOT_REGISTERED = static_cast(0x8055160fu); +constexpr int NP_TROPHY_ERROR_ALREADY_REGISTERED = static_cast(0x80551610u); +constexpr int NP_TROPHY_ERROR_CONTEXT_ALREADY_EXISTS = static_cast(0x80551613u); +constexpr int NP_TROPHY_ERROR_ICON_FILE_NOT_FOUND = static_cast(0x80551614u); +constexpr int NP_TROPHY_ERROR_INVALID_TRP_FILE_FORMAT = static_cast(0x80551616u); +constexpr int NP_TROPHY_ERROR_TROPHY_NOT_UNLOCKED = static_cast(0x8055161au); +constexpr int NP_TROPHY_ERROR_TITLE_CONF_NOT_INSTALLED = static_cast(0x80551626u); +constexpr int NP_TROPHY_INVALID_ID = -1; +constexpr int NP_TROPHY_NUM_MAX = 128; +constexpr std::uint32_t TrpMagic = 0xdca24d00u; +constexpr std::size_t TrpHeaderBytes = 0x60; +constexpr std::size_t TrpEntryBytes = 0x40; + +struct TrophyFlagArray { + std::uint32_t bits[NP_TROPHY_NUM_MAX / 32]; +}; + +struct TrpEntry { + std::uint64_t offset; + std::uint64_t size; +}; + +struct Context { + int userId; + std::uint32_t serviceLabel; + bool registered = false; + std::filesystem::path package; + std::map entries; + int trophies = 0; + std::filesystem::path state; + std::set unlocked; +}; + +std::mutex g_mutex; +std::map g_contexts; +std::set g_handles; +int g_nextContext = 1; +int g_nextHandle = 1; + +std::uint32_t BigEndian32(const std::uint8_t* bytes) { + return (std::uint32_t{bytes[0]} << 24) | (std::uint32_t{bytes[1]} << 16) | (std::uint32_t{bytes[2]} << 8) | bytes[3]; +} + +std::uint64_t BigEndian64(const std::uint8_t* bytes) { + return (std::uint64_t{BigEndian32(bytes)} << 32) | BigEndian32(bytes + 4); +} + +std::optional> ReadPackage(const std::filesystem::path& path) { + std::ifstream file(path, std::ios::binary); + if (!file) return std::nullopt; + std::array header{}; + if (!file.read(reinterpret_cast(header.data()), header.size()) || BigEndian32(header.data()) != TrpMagic) return std::map{}; + const auto count = BigEndian32(header.data() + 0x10); + if (BigEndian32(header.data() + 0x14) != TrpEntryBytes) return std::map{}; + std::map entries; + for (std::uint32_t i = 0; i < count; ++i) { + std::array entry{}; + if (!file.read(reinterpret_cast(entry.data()), entry.size())) return std::map{}; + const std::string name(reinterpret_cast(entry.data()), strnlen(reinterpret_cast(entry.data()), 32)); + entries[name] = {BigEndian64(entry.data() + 32), BigEndian64(entry.data() + 40)}; + } + return entries; +} + +void LoadState(Context& context) { + std::ifstream file(context.state); + int id = 0; + while (file >> id) { + if (id >= 0 && id < context.trophies) context.unlocked.insert(id); + } +} + +void SaveState(const Context& context) { + std::filesystem::create_directories(context.state.parent_path()); + const auto temporary = context.state.string() + ".tmp"; + { + std::ofstream file(temporary, std::ios::trunc); + for (const auto id : context.unlocked) file << id << '\n'; + } + std::filesystem::rename(temporary, context.state); +} + +int Lookup(int contextId, int handle, Context*& context) { + if (contextId == NP_TROPHY_INVALID_ID) return NP_TROPHY_ERROR_INVALID_CONTEXT; + if (handle == NP_TROPHY_INVALID_ID) return NP_TROPHY_ERROR_INVALID_HANDLE; + const auto found = g_contexts.find(contextId); + if (found == g_contexts.end()) return NP_TROPHY_ERROR_INVALID_CONTEXT; + if (!g_handles.contains(handle)) return NP_TROPHY_ERROR_INVALID_HANDLE; + context = &found->second; + return 0; +} + +int CopyIcon(const Context& context, const std::string& name, void* buffer, std::uint64_t* size) { + const auto entry = context.entries.find(name); + if (entry == context.entries.end()) return NP_TROPHY_ERROR_ICON_FILE_NOT_FOUND; + if (buffer == nullptr) { + *size = entry->second.size; + return 0; + } + if (*size < entry->second.size) return NP_TROPHY_ERROR_INSUFFICIENT_BUFFER; + std::ifstream file(context.package, std::ios::binary); + file.seekg(static_cast(entry->second.offset)); + if (!file.read(static_cast(buffer), static_cast(entry->second.size))) return NP_TROPHY_ERROR_INVALID_TRP_FILE_FORMAT; + *size = entry->second.size; + return 0; +} + +} + +extern "C" { + +int APS5_VABI sceNpTrophyCreateContext(int* context, int userId, std::uint32_t serviceLabel, std::uint64_t options) { + if (!context || options != 0) return NP_TROPHY_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(g_mutex); + for (const auto& [id, existing] : g_contexts) { + if (existing.userId == userId && existing.serviceLabel == serviceLabel) return NP_TROPHY_ERROR_CONTEXT_ALREADY_EXISTS; + } + const int id = g_nextContext++; + g_contexts[id] = Context{userId, serviceLabel}; + *context = id; + return 0; +} + +int APS5_VABI sceNpTrophyDestroyContext(int context) { + std::lock_guard lock(g_mutex); + return g_contexts.erase(context) == 1 ? 0 : NP_TROPHY_ERROR_INVALID_CONTEXT; +} + +int APS5_VABI sceNpTrophyCreateHandle(int* handle) { + if (!handle) return NP_TROPHY_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(g_mutex); + const int id = g_nextHandle++; + g_handles.insert(id); + *handle = id; + return 0; +} + +int APS5_VABI sceNpTrophyDestroyHandle(int handle) { + std::lock_guard lock(g_mutex); + return g_handles.erase(handle) == 1 ? 0 : NP_TROPHY_ERROR_INVALID_HANDLE; +} + +int APS5_VABI sceNpTrophyRegisterContext(int contextId, int handle, std::uint64_t options) { + if (options != 0) return NP_TROPHY_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(g_mutex); + Context* context = nullptr; + if (const int error = Lookup(contextId, handle, context)) return error; + if (context->registered) return NP_TROPHY_ERROR_ALREADY_REGISTERED; + char name[32]; + std::snprintf(name, sizeof(name), "trophy%02u.trp", context->serviceLabel); + context->package = ResolvePath_nid_no_patch((std::string("/app0/sce_sys/trophy/") + name).c_str()); + const auto entries = ReadPackage(context->package); + if (!entries) return NP_TROPHY_ERROR_TITLE_CONF_NOT_INSTALLED; + if (entries->empty()) return NP_TROPHY_ERROR_INVALID_TRP_FILE_FORMAT; + context->entries = *entries; + for (int id = 0; id < NP_TROPHY_NUM_MAX; ++id) { + char icon[16]; + std::snprintf(icon, sizeof(icon), "TROP%03d.PNG", id); + if (!context->entries.contains(icon)) break; + context->trophies = id + 1; + } + std::snprintf(name, sizeof(name), "trophy%02u.%d", context->serviceLabel, context->userId); + context->state = std::filesystem::path("_trophy") / name; + LoadState(*context); + context->registered = true; + return 0; +} + +int APS5_VABI sceNpTrophyGetTrophyUnlockState(int contextId, int handle, TrophyFlagArray* flags, std::uint32_t* count) { + if (!flags || !count) return NP_TROPHY_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(g_mutex); + Context* context = nullptr; + if (const int error = Lookup(contextId, handle, context)) return error; + if (!context->registered) return NP_TROPHY_ERROR_NOT_REGISTERED; + std::memset(flags, 0, sizeof(*flags)); + for (const auto id : context->unlocked) flags->bits[id >> 5] |= 1u << (id & 31); + *count = static_cast(context->trophies); + return 0; +} + +int APS5_VABI sceNpTrophyUnlockTrophy(int contextId, int handle, int trophyId, int* platinumId) { + if (!platinumId) return NP_TROPHY_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(g_mutex); + Context* context = nullptr; + if (const int error = Lookup(contextId, handle, context)) return error; + if (!context->registered) return NP_TROPHY_ERROR_NOT_REGISTERED; + if (trophyId < 0 || trophyId >= context->trophies) return NP_TROPHY_ERROR_INVALID_TROPHY_ID; + if (context->unlocked.contains(trophyId)) return NP_TROPHY_ERROR_TROPHY_ALREADY_UNLOCKED; + context->unlocked.insert(trophyId); + SaveState(*context); + *platinumId = NP_TROPHY_INVALID_ID; + return 0; +} + +int APS5_VABI sceNpTrophyGetGameIcon(int contextId, int handle, void* buffer, std::uint64_t* size) { + if (!size) return NP_TROPHY_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(g_mutex); + Context* context = nullptr; + if (const int error = Lookup(contextId, handle, context)) return error; + if (!context->registered) return NP_TROPHY_ERROR_NOT_REGISTERED; + return CopyIcon(*context, "ICON0.PNG", buffer, size); +} + +int APS5_VABI sceNpTrophyGetGroupIcon(int contextId, int handle, int groupId, void* buffer, std::uint64_t* size) { + if (!size) return NP_TROPHY_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(g_mutex); + Context* context = nullptr; + if (const int error = Lookup(contextId, handle, context)) return error; + if (!context->registered) return NP_TROPHY_ERROR_NOT_REGISTERED; + if (groupId < 0) return NP_TROPHY_ERROR_INVALID_GROUP_ID; + char icon[16]; + std::snprintf(icon, sizeof(icon), "GR%03d.PNG", groupId); + return CopyIcon(*context, icon, buffer, size); +} + +int APS5_VABI sceNpTrophyGetTrophyIcon(int contextId, int handle, int trophyId, void* buffer, std::uint64_t* size) { + if (!size) return NP_TROPHY_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(g_mutex); + Context* context = nullptr; + if (const int error = Lookup(contextId, handle, context)) return error; + if (!context->registered) return NP_TROPHY_ERROR_NOT_REGISTERED; + if (trophyId < 0 || trophyId >= context->trophies) return NP_TROPHY_ERROR_INVALID_TROPHY_ID; + if (!context->unlocked.contains(trophyId)) return NP_TROPHY_ERROR_TROPHY_NOT_UNLOCKED; + char icon[16]; + std::snprintf(icon, sizeof(icon), "TROP%03d.PNG", trophyId); + return CopyIcon(*context, icon, buffer, size); +} + +int APS5_VABI sceNpTrophyGetGameInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTrophyGetGroupInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTrophyGetTrophyInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceNpTrophy2/include/NpTrophy2Constants.hpp b/core/libs/prx/libSceNpTrophy2/include/NpTrophy2Constants.hpp index 6a853b4c28..58ec7f5e33 100644 --- a/core/libs/prx/libSceNpTrophy2/include/NpTrophy2Constants.hpp +++ b/core/libs/prx/libSceNpTrophy2/include/NpTrophy2Constants.hpp @@ -4,6 +4,7 @@ #include static constexpr int SCE_NP_TROPHY2_OK = 0; +static constexpr int SCE_NP_TROPHY2_ERROR_INVALID_TROPHY_ID = -2141898486; static constexpr int SCE_NP_TROPHY2_ERROR_ICON_FILE_NOT_FOUND = -2141898479; static constexpr int NP_TROPHY2_CONTEXT_DEFAULT = 1; diff --git a/core/libs/prx/libSceNpTrophy2/src/TrophyInfo.cpp b/core/libs/prx/libSceNpTrophy2/src/TrophyInfo.cpp index ce29da1513..cc32a75483 100644 --- a/core/libs/prx/libSceNpTrophy2/src/TrophyInfo.cpp +++ b/core/libs/prx/libSceNpTrophy2/src/TrophyInfo.cpp @@ -82,9 +82,7 @@ int APS5_VABI sceNpTrophy2GetTrophyInfo(int context, int handle, int trophy_id, return SCE_NP_TROPHY2_OK; } const auto trophy = package->trophies.find(trophy_id); - if (trophy == package->trophies.end()) { - throw std::invalid_argument(std::string(__func__) + ": trophy " + std::to_string(trophy_id) + " is not in " + NpTrophy2::PackagePath(owner->serviceLabel)); - } + if (trophy == package->trophies.end()) return SCE_NP_TROPHY2_ERROR_INVALID_TROPHY_ID; if (details != nullptr) fillDetails(trophy->second, details); if (data != nullptr) fillData(*owner, trophy->second, data); return SCE_NP_TROPHY2_OK; diff --git a/core/libs/prx/libSceNpTus/CMakeLists.txt b/core/libs/prx/libSceNpTus/CMakeLists.txt index 1cb8f4a676..6046093997 100644 --- a/core/libs/prx/libSceNpTus/CMakeLists.txt +++ b/core/libs/prx/libSceNpTus/CMakeLists.txt @@ -9,8 +9,14 @@ if(BUILD_TESTING) configure_windows_unwind(guest_np_tus_tests) add_test(NAME guest_np_tus COMMAND guest_np_tus_tests) set_tests_properties(guest_np_tus PROPERTIES TIMEOUT 20) + add_test_executable(guest_np_tus_offline_tests ${LIBS_INCLUDE_DIR}/tests/GuestNpTusOffline.cpp) + target_include_directories(guest_np_tus_offline_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_np_tus_offline_tests PRIVATE libSceNpTus) + configure_windows_unwind(guest_np_tus_offline_tests) + add_test(NAME guest_np_tus_offline COMMAND guest_np_tus_offline_tests) + set_tests_properties(guest_np_tus_offline PROPERTIES TIMEOUT 20) if(WIN32) - set_tests_properties(guest_np_tus PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_np_tus guest_np_tus_offline PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/prx/libSceNpTus/Export.cpp b/core/libs/prx/libSceNpTus/Export.cpp index a69e6cb5a6..da02e06801 100644 --- a/core/libs/prx/libSceNpTus/Export.cpp +++ b/core/libs/prx/libSceNpTus/Export.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -11,12 +12,24 @@ static constexpr int SCE_NP_COMMUNITY_ERROR_TOO_MANY_OBJECTS = static_cast( static constexpr int SCE_NP_COMMUNITY_ERROR_ABORTED = static_cast(0x80550707); static constexpr int SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT = static_cast(0x8055070C); static constexpr int SCE_NP_COMMUNITY_ERROR_INVALID_ID = static_cast(0x8055070E); +static constexpr int SCE_NP_COMMUNITY_ERROR_INVALID_TYPE = static_cast(0x80550711); static constexpr int SCE_NP_COMMUNITY_ERROR_TOO_MANY_SLOTID = static_cast(0x80550718); +static constexpr int SCE_NP_COMMUNITY_ERROR_TOO_MANY_NPID = static_cast(0x80550719); static constexpr int POLL_FINISHED = 0; static constexpr int POLL_RUNNING = 1; static constexpr std::size_t MAX_TITLE_CONTEXTS = 32; static constexpr std::size_t MAX_REQUESTS = 256; static constexpr int MAX_SLOTS = 64; +static constexpr int MAX_USERS = 101; +static constexpr int MAX_FRIENDS = 100; +static constexpr std::int32_t TSS_MAX_SLOT_ID = 15; +static constexpr std::uint64_t TSS_MAX_SIZE = 64 * 1024; +static constexpr std::int32_t OPETYPE_EQUAL = 1; +static constexpr std::int32_t OPETYPE_LESS_OR_EQUAL = 6; +static constexpr std::int32_t DATASTATUS_SORTTYPE_DESCENDING_DATE = 1; +static constexpr std::int32_t DATASTATUS_SORTTYPE_ASCENDING_DATE = 2; +static constexpr std::int32_t VARIABLE_SORTTYPE_DESCENDING_DATE = 1; +static constexpr std::int32_t VARIABLE_SORTTYPE_ASCENDING_VALUE = 4; namespace { struct Request { @@ -75,6 +88,16 @@ int Complete(int reqId, int asyncResult) { if (const int error = WaitOffline(reqId, &result)) return error; return result; } + +constexpr std::uint32_t INVALID_SERVICE_LABEL = 0xFFFFFFFF; + +int CreateTitleContext() { + std::lock_guard lock(tusMutex); + if (titleContexts.size() >= MAX_TITLE_CONTEXTS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_OBJECTS; + const int contextId = nextTitleContext++; + titleContexts.insert(contextId); + return contextId; +} } extern "C" { @@ -82,11 +105,7 @@ extern "C" { int APS5_VABI sceNpTusCreateNpTitleCtxA(std::uint32_t serviceLabel, int userId) { (void)serviceLabel; (void)userId; - std::lock_guard lock(tusMutex); - if (titleContexts.size() >= MAX_TITLE_CONTEXTS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_OBJECTS; - const int contextId = nextTitleContext++; - titleContexts.insert(contextId); - return contextId; + return CreateTitleContext(); } int APS5_VABI sceNpTusCreateRequest(int titleCtxId) { @@ -164,14 +183,14 @@ int APS5_VABI sceNpTusSetThreadParam(void) { return 0; } -int APS5_VABI sceNpTssCreateNpTitleCtx(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpTssCreateNpTitleCtx(std::uint32_t serviceLabel, const NpId* npId) { + if (!npId) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (serviceLabel == INVALID_SERVICE_LABEL) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return CreateTitleContext(); } -int APS5_VABI sceNpTssCreateNpTitleCtxA(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpTssCreateNpTitleCtxA(std::uint32_t serviceLabel, int userId) { + return sceNpTusCreateNpTitleCtxA(serviceLabel, userId); } int APS5_VABI sceNpTusCreateTitleCtx(void) { @@ -292,176 +311,16 @@ int APS5_VABI sceNpTusDeleteMultiSlotVariableVUser(int reqId, const NpOnlineId* return Complete(reqId, sceNpTusDeleteMultiSlotVariableVUserAsync(reqId, targetVirtualUserId, slotIds, arrayLen, option)); } -int APS5_VABI sceNpTusGetMultiUserVariable(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiUserVariableVUser(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiUserVariableAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiUserVariableVUserAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetFriendsVariable(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetFriendsVariableAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusAddAndGetVariable(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusAddAndGetVariableVUser(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusAddAndGetVariableAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusAddAndGetVariableVUserAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusTryAndSetVariable(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusTryAndSetVariableVUser(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusTryAndSetVariableAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusTryAndSetVariableVUserAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusDeleteMultiSlotData(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpTusDeleteMultiSlotDataVUser(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNpTusDeleteMultiSlotDataAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpTusDeleteMultiSlotDataVUserAsync(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNpTusSetDataAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusSetDataVUserAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetDataAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetDataVUserAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiSlotDataStatus(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiSlotDataStatusVUser(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiSlotDataStatusAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiSlotDataStatusVUserAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiUserDataStatus(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiUserDataStatusVUser(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiUserDataStatusAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiUserDataStatusVUserAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetFriendsDataStatus(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetFriendsDataStatusAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTssGetSmallStorage(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTssGetSmallStorageAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpTssGetStorage(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -477,111 +336,6 @@ int APS5_VABI sceNpTssGetDataAsync(void) { return 0; } -int APS5_VABI sceNpTusAddAndGetVariableA(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusAddAndGetVariableAVUser(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusAddAndGetVariableAVUserAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusAddAndGetVariableForCrossSave(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusAddAndGetVariableForCrossSaveAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusAddAndGetVariableForCrossSaveVUser(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusAddAndGetVariableForCrossSaveVUserAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiSlotDataStatusA(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiSlotDataStatusAAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiSlotDataStatusAVUser(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiSlotDataStatusAVUserAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiSlotDataStatusForCrossSave(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiSlotDataStatusForCrossSaveAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiSlotDataStatusForCrossSaveVUser(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiSlotDataStatusForCrossSaveVUserAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetDataA(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetDataAAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetDataAVUser(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetDataAVUserAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetDataForCrossSave(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetDataForCrossSaveAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpTusGetDataForCrossSaveVUser(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -592,86 +346,11 @@ int APS5_VABI sceNpTusGetDataForCrossSaveVUserAsync(void) { return 0; } -int APS5_VABI sceNpTusGetMultiUserVariableA(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiUserVariableAAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiUserVariableAVUser(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiUserVariableAVUserAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpTusGetMultiUserVariableForCrossSave(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNpTusGetMultiUserVariableForCrossSaveAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiUserVariableForCrossSaveVUser(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiUserVariableForCrossSaveVUserAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiUserDataStatusA(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiUserDataStatusAAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiUserDataStatusAVUser(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiUserDataStatusAVUserAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiUserDataStatusForCrossSave(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiUserDataStatusForCrossSaveAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiUserDataStatusForCrossSaveVUser(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusGetMultiUserDataStatusForCrossSaveVUserAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpTusGetFriendsVariableA(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -712,72 +391,657 @@ int APS5_VABI sceNpTusGetFriendsDataStatusForCrossSaveAsync(void) { return 0; } -int APS5_VABI sceNpTusSetDataA(void) { +int APS5_VABI sceNpTssGetSmallStorageAsync(int reqId, void* data, std::uint64_t maxSize, std::uint64_t* contentLength, void* option) { + (void)data; + (void)contentLength; + if (option) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (maxSize > TSS_MAX_SIZE) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusAddAndGetVariableAVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int64_t inVariable, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option) { + (void)inVariable; + (void)isLastChangedAuthor; + (void)isLastChangedDate; + (void)outVariableSize; + if (!targetVirtualUserId || !outVariable) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (IsEmpty(targetVirtualUserId)) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusAddAndGetVariableAsync(int reqId, const void* targetNpId, std::int32_t slotId, std::int64_t inVariable, const void* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option) { + (void)targetNpId; + (void)inVariable; + (void)isLastChangedAuthor; + (void)isLastChangedDate; + (void)outVariableSize; + if (option) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (!outVariable) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusAddAndGetVariableForCrossSaveAsync(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, std::int64_t inVariable, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option) { + (void)targetAccountId; + (void)inVariable; + (void)isLastChangedAuthor; + (void)isLastChangedDate; + (void)outVariableSize; + if (option || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (!outVariable) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusAddAndGetVariableForCrossSaveVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int64_t inVariable, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option) { + (void)inVariable; + (void)isLastChangedAuthor; + (void)isLastChangedDate; + (void)outVariableSize; + if (!targetVirtualUserId || !outVariable) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (IsEmpty(targetVirtualUserId)) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusAddAndGetVariableVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int64_t inVariable, const void* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option) { + (void)inVariable; + (void)isLastChangedAuthor; + (void)isLastChangedDate; + (void)outVariableSize; + if (option) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (!targetVirtualUserId || !outVariable) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (IsEmpty(targetVirtualUserId)) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusDeleteMultiSlotDataAAsync(int reqId, std::uint64_t targetAccountId, const std::int32_t* slotIds, int arrayLen, void* option) { + (void)targetAccountId; + if (!slotIds) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || arrayLen < 1) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayLen > MAX_SLOTS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_SLOTID; + if (std::any_of(slotIds, slotIds + arrayLen, [](std::int32_t id) { return id < 0; })) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusDeleteMultiSlotDataAsync(int reqId, const void* targetNpId, const std::int32_t* slotIds, int arrayLen, void* option) { + if (!targetNpId || !slotIds || arrayLen < 1) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayLen > MAX_SLOTS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_SLOTID; + if (std::any_of(slotIds, slotIds + arrayLen, [](std::int32_t id) { return id < 0; })) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetDataAAsync(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, void* dataStatus, std::uint64_t dataStatusSize, void* data, std::uint64_t recvSize, void* option) { + (void)targetAccountId; + (void)data; + (void)recvSize; + if (option) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (!dataStatus || dataStatusSize == 0) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetDataAVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, void* dataStatus, std::uint64_t dataStatusSize, void* data, std::uint64_t recvSize, void* option) { + (void)data; + (void)recvSize; + if (option) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (!targetVirtualUserId || !dataStatus || dataStatusSize == 0) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (IsEmpty(targetVirtualUserId)) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetDataAsync(int reqId, const void* targetNpId, std::int32_t slotId, void* dataStatus, std::uint64_t dataStatusSize, void* data, std::uint64_t recvSize, void* option) { + (void)targetNpId; + (void)data; + (void)recvSize; + if (option) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (!dataStatus || dataStatusSize == 0) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetDataForCrossSaveAsync(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, void* dataStatus, std::uint64_t dataStatusSize, void* data, std::uint64_t recvSize, void* option) { + (void)targetAccountId; + (void)data; + (void)recvSize; + if (option || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (!dataStatus || dataStatusSize == 0) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetDataVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, void* dataStatus, std::uint64_t dataStatusSize, void* data, std::uint64_t recvSize, void* option) { + (void)data; + (void)recvSize; + if (option) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (!targetVirtualUserId || !dataStatus || dataStatusSize == 0) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (IsEmpty(targetVirtualUserId)) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetFriendsDataStatusAsync(int reqId, std::int32_t slotId, std::int32_t includeSelf, std::int32_t sortType, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + (void)includeSelf; + (void)statusArraySize; + if (!statusArray) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || arrayLen < 1 || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayLen > MAX_FRIENDS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_NPID; + if (sortType < DATASTATUS_SORTTYPE_DESCENDING_DATE || sortType > DATASTATUS_SORTTYPE_ASCENDING_DATE) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetFriendsVariableAsync(int reqId, std::int32_t slotId, std::int32_t includeSelf, std::int32_t sortType, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option) { + (void)includeSelf; + (void)variableArraySize; + if (!variableArray) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || arrayLen < 1 || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayLen > MAX_FRIENDS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_NPID; + if (sortType < VARIABLE_SORTTYPE_DESCENDING_DATE || sortType > VARIABLE_SORTTYPE_ASCENDING_VALUE) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusAAsync(int reqId, std::uint64_t targetAccountId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + (void)targetAccountId; + (void)statusArraySize; + if (!slotIds || !statusArray) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || arrayLen < 1) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayLen > MAX_SLOTS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_SLOTID; + if (std::any_of(slotIds, slotIds + arrayLen, [](std::int32_t id) { return id < 0; })) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusAVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + (void)statusArraySize; + if (!targetVirtualUserId || !slotIds || !statusArray) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || arrayLen < 1) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayLen > MAX_SLOTS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_SLOTID; + if (std::any_of(slotIds, slotIds + arrayLen, [](std::int32_t id) { return id < 0; })) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (IsEmpty(targetVirtualUserId)) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusAsync(int reqId, const void* targetNpId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + (void)statusArraySize; + if (!targetNpId || !slotIds || !statusArray || arrayLen < 1) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayLen > MAX_SLOTS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_NPID; + if (std::any_of(slotIds, slotIds + arrayLen, [](std::int32_t id) { return id < 0; })) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusForCrossSaveAsync(int reqId, std::uint64_t targetAccountId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + (void)targetAccountId; + (void)statusArraySize; + if (!slotIds || !statusArray) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || arrayLen < 1) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayLen > MAX_SLOTS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_SLOTID; + if (std::any_of(slotIds, slotIds + arrayLen, [](std::int32_t id) { return id < 0; })) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusForCrossSaveVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + (void)statusArraySize; + if (!targetVirtualUserId || !slotIds || !statusArray) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || arrayLen < 1) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayLen > MAX_SLOTS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_SLOTID; + if (std::any_of(slotIds, slotIds + arrayLen, [](std::int32_t id) { return id < 0; })) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (IsEmpty(targetVirtualUserId)) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + (void)statusArraySize; + if (!targetVirtualUserId || !slotIds || !statusArray || arrayLen < 1) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayLen > MAX_SLOTS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_NPID; + if (std::any_of(slotIds, slotIds + arrayLen, [](std::int32_t id) { return id < 0; })) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (IsEmpty(targetVirtualUserId)) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusAAsync(int reqId, const std::uint64_t* targetAccountIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + (void)statusArraySize; + if (!targetAccountIdArray || !statusArray) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || arrayLen < 1 || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayLen > MAX_USERS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_NPID; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusAVUserAsync(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + (void)statusArraySize; + if (!targetVirtualUserIdArray || !statusArray) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || arrayLen < 1 || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayLen > MAX_USERS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_NPID; + for (int index = 0; index < arrayLen; ++index) { + if (IsEmpty(&targetVirtualUserIdArray[index])) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusAsync(int reqId, const void* targetNpIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + (void)statusArraySize; + if (!targetNpIdArray || !statusArray || arrayLen < 1) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayLen > MAX_USERS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_NPID; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusForCrossSaveAsync(int reqId, const std::uint64_t* targetAccountIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + (void)statusArraySize; + if (!targetAccountIdArray || !statusArray) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || arrayLen < 1 || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayLen > MAX_USERS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_NPID; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusForCrossSaveVUserAsync(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + (void)statusArraySize; + if (!targetVirtualUserIdArray || !statusArray) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || arrayLen < 1 || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayLen > MAX_USERS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_NPID; + for (int index = 0; index < arrayLen; ++index) { + if (IsEmpty(&targetVirtualUserIdArray[index])) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusVUserAsync(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + (void)statusArraySize; + if (!targetVirtualUserIdArray || !statusArray || arrayLen < 1) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayLen > MAX_USERS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_NPID; + for (int index = 0; index < arrayLen; ++index) { + if (IsEmpty(&targetVirtualUserIdArray[index])) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetMultiUserVariableAAsync(int reqId, const std::uint64_t* targetAccountIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option) { + (void)variableArraySize; + if (!targetAccountIdArray || !variableArray) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || arrayLen < 1 || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayLen > MAX_USERS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_NPID; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetMultiUserVariableAVUserAsync(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option) { + (void)variableArraySize; + if (!targetVirtualUserIdArray || !variableArray) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || arrayLen < 1 || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayLen > MAX_USERS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_NPID; + for (int index = 0; index < arrayLen; ++index) { + if (IsEmpty(&targetVirtualUserIdArray[index])) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetMultiUserVariableAsync(int reqId, const void* targetNpIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option) { + (void)variableArraySize; + if (!targetNpIdArray || !variableArray || arrayLen < 1) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayLen > MAX_USERS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_NPID; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetMultiUserVariableForCrossSaveAsync(int reqId, const std::uint64_t* targetAccountIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option) { + (void)variableArraySize; + if (!targetAccountIdArray || !variableArray) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || arrayLen < 1 || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayLen > MAX_USERS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_NPID; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetMultiUserVariableForCrossSaveVUserAsync(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option) { + (void)variableArraySize; + if (!targetVirtualUserIdArray || !variableArray) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || arrayLen < 1 || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayLen > MAX_USERS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_NPID; + for (int index = 0; index < arrayLen; ++index) { + if (IsEmpty(&targetVirtualUserIdArray[index])) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetMultiUserVariableVUserAsync(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option) { + (void)variableArraySize; + if (!targetVirtualUserIdArray || !variableArray || arrayLen < 1) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayLen > MAX_USERS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_NPID; + for (int index = 0; index < arrayLen; ++index) { + if (IsEmpty(&targetVirtualUserIdArray[index])) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusSetDataAAsync(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, std::uint64_t totalSize, std::uint64_t sendSize, const void* data, const void* info, std::uint64_t infoStructSize, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* option) { + (void)targetAccountId; + (void)info; + (void)infoStructSize; + (void)isLastChangedAuthor; + (void)isLastChangedDate; + if (option) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (!data || sendSize == 0) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (slotId < 0 || sendSize > totalSize) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusSetDataAVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::uint64_t totalSize, std::uint64_t sendSize, const void* data, const void* info, std::uint64_t infoStructSize, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* option) { + (void)info; + (void)infoStructSize; + (void)isLastChangedAuthor; + (void)isLastChangedDate; + if (option) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (!targetVirtualUserId || !data || sendSize == 0) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (slotId < 0 || sendSize > totalSize) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (IsEmpty(targetVirtualUserId)) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusSetDataAsync(int reqId, const void* targetNpId, std::int32_t slotId, std::uint64_t totalSize, std::uint64_t sendSize, const void* data, const void* info, std::uint64_t infoStructSize, const void* isLastChangedAuthor, const void* isLastChangedDate, void* option) { + (void)targetNpId; + (void)info; + (void)infoStructSize; + (void)isLastChangedAuthor; + (void)isLastChangedDate; + if (option) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (!data || sendSize == 0) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (slotId < 0 || sendSize > totalSize) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusSetDataVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::uint64_t totalSize, std::uint64_t sendSize, const void* data, const void* info, std::uint64_t infoStructSize, const void* isLastChangedAuthor, const void* isLastChangedDate, void* option) { + (void)info; + (void)infoStructSize; + (void)isLastChangedAuthor; + (void)isLastChangedDate; + if (option) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (!targetVirtualUserId || !data || sendSize == 0) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (slotId < 0 || sendSize > totalSize) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (IsEmpty(targetVirtualUserId)) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusTryAndSetVariableAAsync(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option) { + (void)targetAccountId; + (void)variable; + (void)compareValue; + (void)isLastChangedAuthor; + (void)isLastChangedDate; + (void)resultVariableSize; + if (!resultVariable) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (opeType < OPETYPE_EQUAL || opeType > OPETYPE_LESS_OR_EQUAL) return SCE_NP_COMMUNITY_ERROR_INVALID_TYPE; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusTryAndSetVariableAVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option) { + (void)variable; + (void)compareValue; + (void)isLastChangedAuthor; + (void)isLastChangedDate; + (void)resultVariableSize; + if (!targetVirtualUserId || !resultVariable) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (opeType < OPETYPE_EQUAL || opeType > OPETYPE_LESS_OR_EQUAL) return SCE_NP_COMMUNITY_ERROR_INVALID_TYPE; + if (IsEmpty(targetVirtualUserId)) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusTryAndSetVariableAsync(int reqId, const void* targetNpId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const void* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option) { + (void)variable; + (void)compareValue; + (void)isLastChangedAuthor; + (void)isLastChangedDate; + (void)resultVariableSize; + if (!targetNpId || !resultVariable) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (opeType < OPETYPE_EQUAL || opeType > OPETYPE_LESS_OR_EQUAL) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusTryAndSetVariableForCrossSaveAsync(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option) { + (void)targetAccountId; + (void)variable; + (void)compareValue; + (void)isLastChangedAuthor; + (void)isLastChangedDate; + (void)resultVariableSize; + if (!resultVariable) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (opeType < OPETYPE_EQUAL || opeType > OPETYPE_LESS_OR_EQUAL) return SCE_NP_COMMUNITY_ERROR_INVALID_TYPE; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusTryAndSetVariableForCrossSaveVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option) { + (void)variable; + (void)compareValue; + (void)isLastChangedAuthor; + (void)isLastChangedDate; + (void)resultVariableSize; + if (!targetVirtualUserId || !resultVariable) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (opeType < OPETYPE_EQUAL || opeType > OPETYPE_LESS_OR_EQUAL) return SCE_NP_COMMUNITY_ERROR_INVALID_TYPE; + if (IsEmpty(targetVirtualUserId)) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusTryAndSetVariableVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const void* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option) { + (void)variable; + (void)compareValue; + (void)isLastChangedAuthor; + (void)isLastChangedDate; + (void)resultVariableSize; + if (!targetVirtualUserId || !resultVariable) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (opeType < OPETYPE_EQUAL || opeType > OPETYPE_LESS_OR_EQUAL) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (IsEmpty(targetVirtualUserId)) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTssGetSmallStorage(int reqId, void* data, std::uint64_t maxSize, std::uint64_t* contentLength, void* option) { + return Complete(reqId, sceNpTssGetSmallStorageAsync(reqId, data, maxSize, contentLength, option)); +} + +int APS5_VABI sceNpTusAddAndGetVariable(int reqId, const void* targetNpId, std::int32_t slotId, std::int64_t inVariable, const void* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option) { + return Complete(reqId, sceNpTusAddAndGetVariableAsync(reqId, targetNpId, slotId, inVariable, isLastChangedAuthor, isLastChangedDate, outVariable, outVariableSize, option)); +} + +int APS5_VABI sceNpTusAddAndGetVariableA(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, std::int64_t inVariable, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option) { + return Complete(reqId, sceNpTusAddAndGetVariableAAsync(reqId, targetAccountId, slotId, inVariable, isLastChangedAuthor, isLastChangedDate, outVariable, outVariableSize, option)); +} + +int APS5_VABI sceNpTusAddAndGetVariableAVUser(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int64_t inVariable, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option) { + return Complete(reqId, sceNpTusAddAndGetVariableAVUserAsync(reqId, targetVirtualUserId, slotId, inVariable, isLastChangedAuthor, isLastChangedDate, outVariable, outVariableSize, option)); +} + +int APS5_VABI sceNpTusAddAndGetVariableForCrossSave(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, std::int64_t inVariable, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option) { + return Complete(reqId, sceNpTusAddAndGetVariableForCrossSaveAsync(reqId, targetAccountId, slotId, inVariable, isLastChangedAuthor, isLastChangedDate, outVariable, outVariableSize, option)); +} + +int APS5_VABI sceNpTusAddAndGetVariableForCrossSaveVUser(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int64_t inVariable, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option) { + return Complete(reqId, sceNpTusAddAndGetVariableForCrossSaveVUserAsync(reqId, targetVirtualUserId, slotId, inVariable, isLastChangedAuthor, isLastChangedDate, outVariable, outVariableSize, option)); +} + +int APS5_VABI sceNpTusAddAndGetVariableVUser(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int64_t inVariable, const void* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option) { + return Complete(reqId, sceNpTusAddAndGetVariableVUserAsync(reqId, targetVirtualUserId, slotId, inVariable, isLastChangedAuthor, isLastChangedDate, outVariable, outVariableSize, option)); +} + +int APS5_VABI sceNpTusDeleteMultiSlotData(int reqId, const void* targetNpId, const std::int32_t* slotIds, int arrayLen, void* option) { + return Complete(reqId, sceNpTusDeleteMultiSlotDataAsync(reqId, targetNpId, slotIds, arrayLen, option)); +} + +int APS5_VABI sceNpTusDeleteMultiSlotDataA(int reqId, std::uint64_t targetAccountId, const std::int32_t* slotIds, int arrayLen, void* option) { + return Complete(reqId, sceNpTusDeleteMultiSlotDataAAsync(reqId, targetAccountId, slotIds, arrayLen, option)); +} + +int APS5_VABI sceNpTusGetDataA(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, void* dataStatus, std::uint64_t dataStatusSize, void* data, std::uint64_t recvSize, void* option) { + return Complete(reqId, sceNpTusGetDataAAsync(reqId, targetAccountId, slotId, dataStatus, dataStatusSize, data, recvSize, option)); +} + +int APS5_VABI sceNpTusGetDataAVUser(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, void* dataStatus, std::uint64_t dataStatusSize, void* data, std::uint64_t recvSize, void* option) { + return Complete(reqId, sceNpTusGetDataAVUserAsync(reqId, targetVirtualUserId, slotId, dataStatus, dataStatusSize, data, recvSize, option)); +} + +int APS5_VABI sceNpTusGetDataForCrossSave(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, void* dataStatus, std::uint64_t dataStatusSize, void* data, std::uint64_t recvSize, void* option) { + return Complete(reqId, sceNpTusGetDataForCrossSaveAsync(reqId, targetAccountId, slotId, dataStatus, dataStatusSize, data, recvSize, option)); +} + +int APS5_VABI sceNpTusGetFriendsDataStatus(int reqId, std::int32_t slotId, std::int32_t includeSelf, std::int32_t sortType, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + return Complete(reqId, sceNpTusGetFriendsDataStatusAsync(reqId, slotId, includeSelf, sortType, statusArray, statusArraySize, arrayLen, option)); +} + +int APS5_VABI sceNpTusGetFriendsVariable(int reqId, std::int32_t slotId, std::int32_t includeSelf, std::int32_t sortType, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option) { + return Complete(reqId, sceNpTusGetFriendsVariableAsync(reqId, slotId, includeSelf, sortType, variableArray, variableArraySize, arrayLen, option)); +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatus(int reqId, const void* targetNpId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + return Complete(reqId, sceNpTusGetMultiSlotDataStatusAsync(reqId, targetNpId, slotIds, statusArray, statusArraySize, arrayLen, option)); +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusA(int reqId, std::uint64_t targetAccountId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + return Complete(reqId, sceNpTusGetMultiSlotDataStatusAAsync(reqId, targetAccountId, slotIds, statusArray, statusArraySize, arrayLen, option)); +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusAVUser(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + return Complete(reqId, sceNpTusGetMultiSlotDataStatusAVUserAsync(reqId, targetVirtualUserId, slotIds, statusArray, statusArraySize, arrayLen, option)); +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusForCrossSave(int reqId, std::uint64_t targetAccountId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + return Complete(reqId, sceNpTusGetMultiSlotDataStatusForCrossSaveAsync(reqId, targetAccountId, slotIds, statusArray, statusArraySize, arrayLen, option)); +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusForCrossSaveVUser(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + return Complete(reqId, sceNpTusGetMultiSlotDataStatusForCrossSaveVUserAsync(reqId, targetVirtualUserId, slotIds, statusArray, statusArraySize, arrayLen, option)); +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusVUser(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + return Complete(reqId, sceNpTusGetMultiSlotDataStatusVUserAsync(reqId, targetVirtualUserId, slotIds, statusArray, statusArraySize, arrayLen, option)); +} + +int APS5_VABI sceNpTusGetMultiUserDataStatus(int reqId, const void* targetNpIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + return Complete(reqId, sceNpTusGetMultiUserDataStatusAsync(reqId, targetNpIdArray, slotId, statusArray, statusArraySize, arrayLen, option)); +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusA(int reqId, const std::uint64_t* targetAccountIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + return Complete(reqId, sceNpTusGetMultiUserDataStatusAAsync(reqId, targetAccountIdArray, slotId, statusArray, statusArraySize, arrayLen, option)); +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusAVUser(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + return Complete(reqId, sceNpTusGetMultiUserDataStatusAVUserAsync(reqId, targetVirtualUserIdArray, slotId, statusArray, statusArraySize, arrayLen, option)); +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusForCrossSave(int reqId, const std::uint64_t* targetAccountIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + return Complete(reqId, sceNpTusGetMultiUserDataStatusForCrossSaveAsync(reqId, targetAccountIdArray, slotId, statusArray, statusArraySize, arrayLen, option)); +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusForCrossSaveVUser(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + return Complete(reqId, sceNpTusGetMultiUserDataStatusForCrossSaveVUserAsync(reqId, targetVirtualUserIdArray, slotId, statusArray, statusArraySize, arrayLen, option)); +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusVUser(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + return Complete(reqId, sceNpTusGetMultiUserDataStatusVUserAsync(reqId, targetVirtualUserIdArray, slotId, statusArray, statusArraySize, arrayLen, option)); +} + +int APS5_VABI sceNpTusGetMultiUserVariable(int reqId, const void* targetNpIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option) { + return Complete(reqId, sceNpTusGetMultiUserVariableAsync(reqId, targetNpIdArray, slotId, variableArray, variableArraySize, arrayLen, option)); +} + +int APS5_VABI sceNpTusGetMultiUserVariableA(int reqId, const std::uint64_t* targetAccountIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option) { + return Complete(reqId, sceNpTusGetMultiUserVariableAAsync(reqId, targetAccountIdArray, slotId, variableArray, variableArraySize, arrayLen, option)); +} + +int APS5_VABI sceNpTusGetMultiUserVariableAVUser(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option) { + return Complete(reqId, sceNpTusGetMultiUserVariableAVUserAsync(reqId, targetVirtualUserIdArray, slotId, variableArray, variableArraySize, arrayLen, option)); +} + +int APS5_VABI sceNpTusGetMultiUserVariableForCrossSaveVUser(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option) { + return Complete(reqId, sceNpTusGetMultiUserVariableForCrossSaveVUserAsync(reqId, targetVirtualUserIdArray, slotId, variableArray, variableArraySize, arrayLen, option)); +} + +int APS5_VABI sceNpTusGetMultiUserVariableVUser(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option) { + return Complete(reqId, sceNpTusGetMultiUserVariableVUserAsync(reqId, targetVirtualUserIdArray, slotId, variableArray, variableArraySize, arrayLen, option)); +} + +int APS5_VABI sceNpTusSetDataA(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, std::uint64_t totalSize, std::uint64_t sendSize, const void* data, const void* info, std::uint64_t infoStructSize, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* option) { + return Complete(reqId, sceNpTusSetDataAAsync(reqId, targetAccountId, slotId, totalSize, sendSize, data, info, infoStructSize, isLastChangedAuthor, isLastChangedDate, option)); +} + +int APS5_VABI sceNpTusSetDataAVUser(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::uint64_t totalSize, std::uint64_t sendSize, const void* data, const void* info, std::uint64_t infoStructSize, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* option) { + return Complete(reqId, sceNpTusSetDataAVUserAsync(reqId, targetVirtualUserId, slotId, totalSize, sendSize, data, info, infoStructSize, isLastChangedAuthor, isLastChangedDate, option)); +} + +int APS5_VABI sceNpTusTryAndSetVariable(int reqId, const void* targetNpId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const void* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option) { + return Complete(reqId, sceNpTusTryAndSetVariableAsync(reqId, targetNpId, slotId, opeType, variable, compareValue, isLastChangedAuthor, isLastChangedDate, resultVariable, resultVariableSize, option)); +} + +int APS5_VABI sceNpTusTryAndSetVariableA(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option) { + return Complete(reqId, sceNpTusTryAndSetVariableAAsync(reqId, targetAccountId, slotId, opeType, variable, compareValue, isLastChangedAuthor, isLastChangedDate, resultVariable, resultVariableSize, option)); +} + +int APS5_VABI sceNpTusTryAndSetVariableAVUser(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option) { + return Complete(reqId, sceNpTusTryAndSetVariableAVUserAsync(reqId, targetVirtualUserId, slotId, opeType, variable, compareValue, isLastChangedAuthor, isLastChangedDate, resultVariable, resultVariableSize, option)); +} + +int APS5_VABI sceNpTusTryAndSetVariableForCrossSave(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option) { + return Complete(reqId, sceNpTusTryAndSetVariableForCrossSaveAsync(reqId, targetAccountId, slotId, opeType, variable, compareValue, isLastChangedAuthor, isLastChangedDate, resultVariable, resultVariableSize, option)); +} + +int APS5_VABI sceNpTusTryAndSetVariableForCrossSaveVUser(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option) { + return Complete(reqId, sceNpTusTryAndSetVariableForCrossSaveVUserAsync(reqId, targetVirtualUserId, slotId, opeType, variable, compareValue, isLastChangedAuthor, isLastChangedDate, resultVariable, resultVariableSize, option)); +} + +int APS5_VABI sceNpTusTryAndSetVariableVUser(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const void* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option) { + return Complete(reqId, sceNpTusTryAndSetVariableVUserAsync(reqId, targetVirtualUserId, slotId, opeType, variable, compareValue, isLastChangedAuthor, isLastChangedDate, resultVariable, resultVariableSize, option)); +} + +int APS5_VABI sceNpTssGetData() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNpTusSetDataAAsync(void) { +int APS5_VABI sceNpTusCreateNpTitleCtx() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNpTusSetDataAVUser(void) { +int APS5_VABI sceNpTusGetData() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNpTusSetDataAVUserAsync(void) { +int APS5_VABI sceNpTusGetDataVUser() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNpTusDeleteMultiSlotDataA(void) { +int APS5_VABI sceNpTusGetMultiSlotVariable() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNpTusDeleteMultiSlotDataAAsync(void) { +int APS5_VABI sceNpTusGetMultiSlotVariableVUser() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNpTusTryAndSetVariableA(void) { +int APS5_VABI sceNpTusSetData() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNpTusTryAndSetVariableAAsync(void) { +int APS5_VABI sceNpTusSetDataVUser() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNpTusTryAndSetVariableAVUser(void) { +int APS5_VABI sceNpTusSetMultiSlotVariable() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNpTusTryAndSetVariableAVUserAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusTryAndSetVariableForCrossSave(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusTryAndSetVariableForCrossSaveAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusTryAndSetVariableForCrossSaveVUser(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpTusTryAndSetVariableForCrossSaveVUserAsync(void) { +int APS5_VABI sceNpTusSetMultiSlotVariableVUser() { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceNpUtility/Export.cpp b/core/libs/prx/libSceNpUtility/Export.cpp index 79f5389752..2247cdf62b 100644 --- a/core/libs/prx/libSceNpUtility/Export.cpp +++ b/core/libs/prx/libSceNpUtility/Export.cpp @@ -38,3 +38,82 @@ int APS5_VABI sceNpBandwidthTestShutdown(int context_id, void* result) { } } + +extern "C" { + +int APS5_VABI sceNpBandwidthTestInitStart() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpLookupCreateRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpLookupCreateTitleCtx() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpLookupDeleteRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpLookupDeleteTitleCtx() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpLookupNpId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWordFilterAbortRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWordFilterCensorComment() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWordFilterCreateAsyncRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWordFilterCreateRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWordFilterCreateTitleCtx() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWordFilterDeleteRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWordFilterDeleteTitleCtx() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWordFilterPollAsync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWordFilterSanitizeComment() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceNpWebApi/CMakeLists.txt b/core/libs/prx/libSceNpWebApi/CMakeLists.txt new file mode 100644 index 0000000000..0e3e193000 --- /dev/null +++ b/core/libs/prx/libSceNpWebApi/CMakeLists.txt @@ -0,0 +1,16 @@ +add_library(libSceNpWebApi SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceNpWebApi PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceNpWebApi) + +if(BUILD_TESTING) + add_test_executable(guest_np_web_api_tests ${LIBS_INCLUDE_DIR}/tests/GuestNpWebApi.cpp) + target_include_directories(guest_np_web_api_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_np_web_api_tests PRIVATE libSceNpWebApi) + configure_windows_unwind(guest_np_web_api_tests) + add_test(NAME guest_np_web_api COMMAND guest_np_web_api_tests) + set_tests_properties(guest_np_web_api PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_np_web_api PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceNpWebApi/Export.cpp b/core/libs/prx/libSceNpWebApi/Export.cpp new file mode 100644 index 0000000000..1aece4e185 --- /dev/null +++ b/core/libs/prx/libSceNpWebApi/Export.cpp @@ -0,0 +1,181 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" +#include +#include +#include +#include +#include + +static constexpr int SCE_NP_WEBAPI_ERROR_INVALID_ARGUMENT = static_cast(0x80552902); +static constexpr int SCE_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND = static_cast(0x80552904); +static constexpr int SCE_NP_WEBAPI_ERROR_PUSH_EVENT_FILTER_NOT_FOUND = static_cast(0x8055290B); +static constexpr int SCE_NP_WEBAPI_ERROR_HANDLE_NOT_FOUND = static_cast(0x8055290D); +static constexpr int SCE_NP_WEBAPI_ERROR_SERVICE_PUSH_EVENT_FILTER_NOT_FOUND = static_cast(0x8055290E); +static constexpr int SCE_NP_WEBAPI_ERROR_SIGNED_IN_USER_NOT_FOUND = static_cast(0x80552910); +static constexpr int SCE_NP_WEBAPI_ERROR_LIB_CONTEXT_MAX = static_cast(0x8055291A); + +namespace { + +struct LibraryContext { + int libHttpCtxId; + std::size_t poolSize; + std::unordered_set handles; + std::unordered_set pushEventFilters; +}; + +std::mutex contextsMutex; +std::unordered_map contexts; +int lastContext = 0; +int lastObject = 0; + +int nextObject() { + lastObject = lastObject % 0xefffffff + 1; + return lastObject; +} + +} + +extern "C" { + +int APS5_VABI sceNpWebApiAbortRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApiCreateContext() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApiCreateHandle(int libCtxId) { + std::lock_guard lock(contextsMutex); + const auto context = contexts.find(libCtxId); + if (context == contexts.end()) return SCE_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND; + const auto handle = nextObject(); + context->second.handles.insert(handle); + return handle; +} + +int APS5_VABI sceNpWebApiCreatePushEventFilter(int libCtxId, const void* filterParam, std::size_t filterParamNum) { + if (!filterParam || filterParamNum == 0) return SCE_NP_WEBAPI_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(contextsMutex); + const auto context = contexts.find(libCtxId); + if (context == contexts.end()) return SCE_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND; + const auto filter = nextObject(); + context->second.pushEventFilters.insert(filter); + return filter; +} + +int APS5_VABI sceNpWebApiCreateRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApiCreateServicePushEventFilter(int libCtxId, int handleId, const char* npServiceName, std::uint32_t npServiceLabel, const void* filterParam, std::size_t filterParamNum) { + (void)npServiceLabel; + if (!npServiceName || !filterParam || filterParamNum == 0) return SCE_NP_WEBAPI_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(contextsMutex); + const auto context = contexts.find(libCtxId); + if (context == contexts.end()) return SCE_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND; + if (!context->second.handles.contains(handleId)) return SCE_NP_WEBAPI_ERROR_HANDLE_NOT_FOUND; + return SCE_NP_WEBAPI_ERROR_SIGNED_IN_USER_NOT_FOUND; +} + +int APS5_VABI sceNpWebApiDeleteContext() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApiDeleteHandle(int libCtxId, int handleId) { + std::lock_guard lock(contextsMutex); + const auto context = contexts.find(libCtxId); + if (context == contexts.end()) return SCE_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND; + return context->second.handles.erase(handleId) != 0 ? 0 : SCE_NP_WEBAPI_ERROR_HANDLE_NOT_FOUND; +} + +int APS5_VABI sceNpWebApiDeletePushEventFilter(int libCtxId, int filterId) { + std::lock_guard lock(contextsMutex); + const auto context = contexts.find(libCtxId); + if (context == contexts.end()) return SCE_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND; + return context->second.pushEventFilters.erase(filterId) != 0 ? 0 : SCE_NP_WEBAPI_ERROR_PUSH_EVENT_FILTER_NOT_FOUND; +} + +int APS5_VABI sceNpWebApiDeleteRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApiDeleteServicePushEventFilter(int libCtxId, int filterId) { + (void)filterId; + std::lock_guard lock(contextsMutex); + if (!contexts.contains(libCtxId)) return SCE_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND; + return SCE_NP_WEBAPI_ERROR_SERVICE_PUSH_EVENT_FILTER_NOT_FOUND; +} + +int APS5_VABI sceNpWebApiGetHttpResponseHeaderValue() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApiGetHttpResponseHeaderValueLength() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApiGetHttpStatusCode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApiInitialize(int libHttpCtxId, std::size_t poolSize) { + std::lock_guard lock(contextsMutex); + for (int attempt = 0; attempt < 0x7fff; ++attempt) { + lastContext = lastContext % 0x7fff + 1; + if (contexts.contains(lastContext)) continue; + contexts[lastContext] = {libHttpCtxId, poolSize, {}, {}}; + return lastContext; + } + return SCE_NP_WEBAPI_ERROR_LIB_CONTEXT_MAX; +} + +int APS5_VABI sceNpWebApiReadData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApiRegisterPushEventCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApiRegisterServicePushEventCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApiSendRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApiTerminate(int libCtxId) { + std::lock_guard lock(contextsMutex); + return contexts.erase(libCtxId) != 0 ? 0 : SCE_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND; +} + +int APS5_VABI sceNpWebApiUnregisterPushEventCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApiUnregisterServicePushEventCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApiUtilityParseNpId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libScePad/Export.cpp b/core/libs/prx/libScePad/Export.cpp index 3a6f40f658..057d434611 100644 --- a/core/libs/prx/libScePad/Export.cpp +++ b/core/libs/prx/libScePad/Export.cpp @@ -187,8 +187,8 @@ int APS5_VABI scePadGetControllerInformation(int handle, PadControllerInformatio info->touchPadInfo.pixelDensity = 44.86f; info->touchPadInfo.resolution.x = kTouchPadWidth; info->touchPadInfo.resolution.y = kTouchPadHeight; - info->stickInfo.deadZoneLeft = 2; - info->stickInfo.deadZoneRight = 2; + info->stickInfo.deadZoneLeft = 13; + info->stickInfo.deadZoneRight = 13; info->connectionType = PAD_CONNECTION_TYPE_LOCAL; info->connectedCount = 1; info->connected = true; @@ -231,8 +231,8 @@ int APS5_VABI scePadGetExtControllerInformation(int handle, void* info) { base->touchPadInfo.pixelDensity = 44.86f; base->touchPadInfo.resolution.x = kTouchPadWidth; base->touchPadInfo.resolution.y = kTouchPadHeight; - base->stickInfo.deadZoneLeft = 2; - base->stickInfo.deadZoneRight = 2; + base->stickInfo.deadZoneLeft = 13; + base->stickInfo.deadZoneRight = 13; base->connectionType = PAD_CONNECTION_TYPE_LOCAL; base->connectedCount = 1; base->connected = true; @@ -923,18 +923,18 @@ bool APS5_VABI scePadIsValidHandle(std::int32_t handle, std::int32_t portType) { } bool APS5_VABI scePadIsDS4Connected(std::int32_t handle) { - NotImplemented_nid_no_patch(__func__); - return 0; + (void)handle; + return false; } bool APS5_VABI scePadIsBlasterConnected(std::int32_t handle) { - NotImplemented_nid_no_patch(__func__); - return 0; + (void)handle; + return false; } bool APS5_VABI scePadIsMoveConnected(std::int32_t handle) { - NotImplemented_nid_no_patch(__func__); - return 0; + (void)handle; + return false; } int APS5_VABI scePadGetBluetoothAddress(std::int32_t handle, std::uint64_t* address) { diff --git a/core/libs/prx/libScePosixForWebKit/Export.cpp b/core/libs/prx/libScePosixForWebKit/Export.cpp index ca47a2ecf7..c02c94fd7a 100644 --- a/core/libs/prx/libScePosixForWebKit/Export.cpp +++ b/core/libs/prx/libScePosixForWebKit/Export.cpp @@ -1,3 +1,4 @@ +#include "SceTypes.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libc/include/HostEntropy.hpp" #include "prx/libc/include/General.hpp" @@ -7,6 +8,15 @@ #include #include +extern "C" { +int APS5_VABI access_nid_postfix(const char*, int); +int APS5_VABI clock_gettime_nid_postfix(int, KernelTimespec*); +char* APS5_VABI getcwd_nid_postfix(char*, std::size_t); +char* APS5_VABI getenv_nid_postfix(const char*); +int APS5_VABI lstat_nid_postfix(const char*, FileStat*); +void* APS5_VABI mmap_nid_postfix(void*, std::size_t, int, int, int, std::int64_t) noexcept; +} + namespace { constexpr int FnmNomatch = 1; @@ -181,14 +191,12 @@ std::uint32_t APS5_VABI arc4random_nid_postfix() { return value; } -int APS5_VABI __wrap_access() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI __wrap_access(const char* path, int mode) { + return access_nid_postfix(path, mode); } -int APS5_VABI __wrap_clock_gettime() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI __wrap_clock_gettime(int clockId, KernelTimespec* tp) { + return clock_gettime_nid_postfix(clockId, tp); } int APS5_VABI __wrap_fchown() { @@ -196,14 +204,12 @@ int APS5_VABI __wrap_fchown() { return 0; } -int APS5_VABI __wrap_getcwd() { - NotImplemented_nid_no_patch(__func__); - return 0; +char* APS5_VABI __wrap_getcwd(char* buffer, std::size_t size) { + return getcwd_nid_postfix(buffer, size); } -int APS5_VABI __wrap_getenv() { - NotImplemented_nid_no_patch(__func__); - return 0; +char* APS5_VABI __wrap_getenv(const char* name) { + return getenv_nid_postfix(name); } int APS5_VABI __wrap_link() { @@ -211,14 +217,12 @@ int APS5_VABI __wrap_link() { return 0; } -int APS5_VABI __wrap_lstat() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI __wrap_lstat(const char* path, FileStat* sb) { + return lstat_nid_postfix(path, sb); } -int APS5_VABI __wrap_mmap() { - NotImplemented_nid_no_patch(__func__); - return 0; +void* APS5_VABI __wrap_mmap(void* address, std::size_t length, int protection, int flags, int descriptor, std::int64_t offset) { + return mmap_nid_postfix(address, length, protection, flags, descriptor, offset); } int APS5_VABI __wrap_readlink() { diff --git a/core/libs/prx/libSceSaveData.native/Export.cpp b/core/libs/prx/libSceSaveData.native/Export.cpp index afe320f439..e3ccc08dee 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -1031,3 +1031,77 @@ int APS5_VABI sceSaveDataDebugTarget() { } } + +namespace { + +struct SaveDataMountPs4 { + std::int32_t user_id; + std::int32_t pad; + const void* title_id; + const SceSaveDataDirName* dir_name; + const void* fingerprint; + std::uint64_t blocks; + std::uint32_t mount_mode; + std::uint8_t reserved[32]; +}; + +} + +extern "C" { + +int APS5_VABI sceSaveDataInitialize(const void* init) { + return sceSaveDataInitialize3(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->title_id != nullptr) NotImplemented_nid_no_patch("sceSaveDataMount of another title's save data"); + SaveDataMount3 converted{}; + converted.user_id = mount->user_id; + converted.dir_name = mount->dir_name; + converted.blocks = mount->blocks; + converted.mount_mode = mount->mount_mode; + return sceSaveDataMount3(&converted, mount_result); +} + +int APS5_VABI sceSaveDataUmount(const SaveDataMountPoint* mount_point) { + return sceSaveDataUmount2(0, mount_point); +} + +int APS5_VABI sceSaveDataSetupSaveDataMemory(std::int32_t user_id, std::size_t memory_size, const SaveDataParam* param) { + SaveDataMemorySetup2 setup{}; + setup.user_id = user_id; + setup.memory_size = memory_size; + SaveDataMemorySetupResult result{}; + if (const int rc = sceSaveDataSetupSaveDataMemory2(&setup, &result); rc != SAVE_DATA_OK) return rc; + if (param == nullptr) return SAVE_DATA_OK; + SaveDataMemorySet2 set{}; + set.user_id = user_id; + set.param = param; + return sceSaveDataSetSaveDataMemory2(&set); +} + +int APS5_VABI sceSaveDataGetSaveDataMemory(std::int32_t user_id, void* buf, std::size_t buf_size, std::int64_t offset) { + SaveDataMemoryData data{}; + data.buf = buf; + data.buf_size = buf_size; + data.offset = static_cast(offset); + SaveDataMemoryGet2 get{}; + get.user_id = user_id; + get.data = &data; + return sceSaveDataGetSaveDataMemory2(&get); +} + +int APS5_VABI sceSaveDataSetSaveDataMemory(std::int32_t user_id, void* buf, std::size_t buf_size, std::int64_t offset) { + SaveDataMemoryData data{}; + data.buf = buf; + data.buf_size = buf_size; + data.offset = static_cast(offset); + SaveDataMemorySet2 set{}; + set.user_id = user_id; + set.data = &data; + set.data_num = 1; + return sceSaveDataSetSaveDataMemory2(&set); +} + +} diff --git a/core/libs/prx/libSceScreenShot/CMakeLists.txt b/core/libs/prx/libSceScreenShot/CMakeLists.txt new file mode 100644 index 0000000000..0300d678fa --- /dev/null +++ b/core/libs/prx/libSceScreenShot/CMakeLists.txt @@ -0,0 +1,16 @@ +add_library(libSceScreenShot SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceScreenShot PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceScreenShot) + +if(BUILD_TESTING) + add_test_executable(guest_screenshot_tests ${LIBS_INCLUDE_DIR}/tests/GuestScreenShot.cpp) + target_include_directories(guest_screenshot_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_screenshot_tests PRIVATE libSceScreenShot libkernel libc) + configure_windows_unwind(guest_screenshot_tests) + add_test(NAME guest_screenshot COMMAND guest_screenshot_tests small) + add_test(NAME guest_screenshot_large_overlay COMMAND guest_screenshot_tests large) + if(WIN32) + set_tests_properties(guest_screenshot guest_screenshot_large_overlay PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceScreenShot/Export.cpp b/core/libs/prx/libSceScreenShot/Export.cpp index 3f898a1f74..dbc4cf09af 100644 --- a/core/libs/prx/libSceScreenShot/Export.cpp +++ b/core/libs/prx/libSceScreenShot/Export.cpp @@ -1,35 +1,89 @@ +#include #include #include +#include #include "SceTypes.hpp" +#include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libkernel/File/include/File.hpp" + +extern "C" int APS5_VABI sceKernelFstat(int d, FileStat* sb); + +namespace { + +constexpr int ErrorArgument = static_cast(0x80BE0001); +constexpr int ErrorOverlayFile = static_cast(0x80BE0003); +constexpr int ErrorOverlaySize = static_cast(0x80BE000E); +constexpr std::uint32_t LargeOverlaySdkVersion = 0x3500000; +constexpr std::size_t MaxOverlayPath = 0x400; +constexpr std::uint16_t FileTypeMask = 0xF000; +constexpr std::uint16_t DirectoryType = 0x4000; + +std::atomic disabled{false}; + +std::uint32_t CompiledSdkVersion() { + static const auto* processParameters = static_cast(ApplicationProcessParameters_nid_no_patch()); + std::uint32_t version; + std::memcpy(&version, processParameters + 0x10, sizeof(version)); + return version; +} + +int SetOverlayImage(const char* path, int offsetX, int offsetY) { + const bool large = CompiledSdkVersion() >= LargeOverlaySdkVersion; + const int maxX = large ? 3839 : 1919; + const int maxY = large ? 2159 : 1079; + const std::int64_t maxSize = large ? 8 * 1024 * 1024 : 2 * 1024 * 1024; + if (!path || offsetX < 0 || offsetX > maxX || offsetY < 0 || offsetY > maxY) return ErrorArgument; + if (strnlen(path, MaxOverlayPath) >= MaxOverlayPath) return ErrorArgument; + const int descriptor = sceKernelOpen(path, 0, 0); + if (descriptor < 0) return ErrorOverlayFile; + FileStat stat{}; + int result = ErrorOverlayFile; + if (sceKernelFstat(descriptor, &stat) == 0 && (stat.st_mode & FileTypeMask) != DirectoryType) { + result = stat.st_size > 0 && stat.st_size <= maxSize ? 0 : ErrorOverlaySize; + } + sceKernelClose(descriptor); + return result; +} + +} extern "C" { -int APS5_VABI sceScreenShotSetOverlayImage(const char* path, int x, int y) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceScreenShotSetOverlayImage(const char* path, int offsetX, int offsetY) { + return SetOverlayImage(path, offsetX, offsetY); } -int APS5_VABI sceScreenShotSetOverlayImageWithOrigin(const char* path, int x, int y, int origin) { - NotImplemented_nid_no_patch(__func__); - return 0; -} -int APS5_VABI sceScreenShotCapture() { - NotImplemented_nid_no_patch(__func__); - return 0; + +int APS5_VABI sceScreenShotSetOverlayImageWithOrigin(const char* path, int offsetX, int offsetY, int origin) { + if (origin < 1 || origin > 9) return ErrorArgument; + return SetOverlayImage(path, offsetX, offsetY); } + int APS5_VABI sceScreenShotDisable() { - NotImplemented_nid_no_patch(__func__); - return 0; -} -int APS5_VABI sceScreenShotDisableNotification() { - NotImplemented_nid_no_patch(__func__); + disabled = true; return 0; } + int APS5_VABI sceScreenShotEnable() { - NotImplemented_nid_no_patch(__func__); + disabled = false; return 0; } + +int APS5_VABI sceScreenShotIsDisabled(int id, int* isDisabled) { + if (id == -1 || !isDisabled) return ErrorArgument; + *isDisabled = disabled ? 1 : 0; + return 0; +} + +int APS5_VABI sceScreenShotDisableNotification() { + return 0; +} + int APS5_VABI sceScreenShotEnableNotification() { + return 0; +} + +int APS5_VABI sceScreenShotCapture() { NotImplemented_nid_no_patch(__func__); return 0; } @@ -41,10 +95,6 @@ int APS5_VABI sceScreenShotGetDrcParam() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceScreenShotIsDisabled() { - NotImplemented_nid_no_patch(__func__); - return 0; -} int APS5_VABI sceScreenShotIsVshScreenCaptureDisabled() { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceSsl/Export.cpp b/core/libs/prx/libSceSsl/Export.cpp index bfc982dcdc..ceb7654ba3 100644 --- a/core/libs/prx/libSceSsl/Export.cpp +++ b/core/libs/prx/libSceSsl/Export.cpp @@ -1337,8 +1337,7 @@ int APS5_VABI sceSslLoadRootCACert() { } int APS5_VABI sceSslRecv(int sslConnectionId, void* buf, std::size_t fulllen) { - NotImplemented_nid_no_patch(__func__); - return 0; + return sceSslRead(sslConnectionId, buf, fulllen, 0); } int APS5_VABI sceSslReuseConnection(int sslConnectionId, int sockId) { @@ -1347,8 +1346,7 @@ int APS5_VABI sceSslReuseConnection(int sslConnectionId, int sockId) { } int APS5_VABI sceSslSend(int sslConnectionId, void* msg, std::size_t fulllen) { - NotImplemented_nid_no_patch(__func__); - return 0; + return sceSslWrite(sslConnectionId, msg, fulllen, 0); } int APS5_VABI sceSslSetSslVersion() { diff --git a/core/libs/prx/libSceSystemService/CMakeLists.txt b/core/libs/prx/libSceSystemService/CMakeLists.txt index 3542172d1b..20a06f0cd9 100644 --- a/core/libs/prx/libSceSystemService/CMakeLists.txt +++ b/core/libs/prx/libSceSystemService/CMakeLists.txt @@ -22,10 +22,14 @@ if(BUILD_TESTING) configure_windows_unwind(guest_loadexec_host) find_package(Python3 COMPONENTS Interpreter QUIET) if(Python3_FOUND) + set(loadexecLibraries $ $ $ $) + if(APS5_ENABLE_TRACY) + list(APPEND loadexecLibraries $) + endif() add_test(NAME loadexec_arguments COMMAND ${Python3_EXECUTABLE} ${CMAKE_SOURCE_DIR}/core/relinker/relinker/tests/test_loadexec_arguments.py $ $ - $ $ $ $ ${CMAKE_CXX_COMPILER}) + ${loadexecLibraries} ${CMAKE_CXX_COMPILER}) set_tests_properties(loadexec_arguments PROPERTIES TIMEOUT 30) add_asan_test_options(loadexec_arguments verify_asan_link_order=0) endif() diff --git a/core/libs/prx/libSceSystemService/Export.cpp b/core/libs/prx/libSceSystemService/Export.cpp index 65eae0e1e4..3da3b74835 100644 --- a/core/libs/prx/libSceSystemService/Export.cpp +++ b/core/libs/prx/libSceSystemService/Export.cpp @@ -309,10 +309,9 @@ int APS5_VABI sceSystemServiceParamGetInt(int paramId, int* value) { case SYSTEM_SERVICE_PARAM_ID_TIME_FORMAT: *value = SYSTEM_SERVICE_PARAM_TIME_FORMAT_24HOUR; break; case SYSTEM_SERVICE_PARAM_ID_TIME_ZONE: *value = HostTimeZone().minutes; break; case SYSTEM_SERVICE_PARAM_ID_SUMMERTIME: *value = HostTimeZone().summer ? 1 : 0; break; - case SYSTEM_SERVICE_PARAM_ID_GAME_PARENTAL_LEVEL: *value = SYSTEM_SERVICE_PARAM_GAME_PARENTAL_OFF; break; - case SYSTEM_SERVICE_PARAM_ID_ENTER_BUTTON_ASSIGN: *value = SYSTEM_SERVICE_PARAM_ENTER_BUTTON_CROSS; break; + case SYSTEM_SERVICE_PARAM_ID_100: *value = 0; break; default: - if ((paramId >= 100 && paramId <= 111) || (paramId >= 200 && paramId <= 213) || paramId == 300) { + if ((paramId >= 101 && paramId <= 111) || (paramId >= 200 && paramId <= 213) || paramId == 300) { NotImplemented_nid_no_patch("sceSystemServiceParamGetInt: registry-backed parameter"); } return SYSTEM_SERVICE_ERROR_PARAMETER; @@ -852,4 +851,12 @@ int APS5_VABI sceCoredumpSequenceHandlerCancelCoredump(void) { return 0; } +int APS5_VABI sceSystemServiceIsBgmPlaying(void) { + return 0; +} + +int APS5_VABI sceShellCoreUtilIsBgmPlaying(void) { + return 0; +} + } diff --git a/core/libs/prx/libSceSystemService/PendingExports.cpp b/core/libs/prx/libSceSystemService/PendingExports.cpp index 1c37a7a66c..9fbb45de38 100644 --- a/core/libs/prx/libSceSystemService/PendingExports.cpp +++ b/core/libs/prx/libSceSystemService/PendingExports.cpp @@ -1241,11 +1241,6 @@ bool APS5_VABI sceShellCoreUtilIsBgmCpuBudgetAvailable() { return 0; } -int APS5_VABI sceShellCoreUtilIsBgmPlaying() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - bool APS5_VABI sceShellCoreUtilIsExternalStorageAppMoveInProgress() { NotImplemented_nid_no_patch(__func__); return 0; @@ -2117,11 +2112,6 @@ bool APS5_VABI sceSystemServiceIsBgmCpuBudgetAvailable() { return 0; } -int APS5_VABI sceSystemServiceIsBgmPlaying() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceSystemServiceIsEyeToEyeDistanceAdjusted(std::uint32_t userId, std::uint8_t* adjusted) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceSystemService/SystemService.hpp b/core/libs/prx/libSceSystemService/SystemService.hpp index 7b1664cc90..22f85c1123 100644 --- a/core/libs/prx/libSceSystemService/SystemService.hpp +++ b/core/libs/prx/libSceSystemService/SystemService.hpp @@ -23,6 +23,7 @@ constexpr int SYSTEM_SERVICE_PARAM_ID_TIME_ZONE = 4; constexpr int SYSTEM_SERVICE_PARAM_ID_SUMMERTIME = 5; constexpr int SYSTEM_SERVICE_PARAM_ID_SYSTEM_NAME = 6; constexpr int SYSTEM_SERVICE_PARAM_ID_GAME_PARENTAL_LEVEL = 7; +constexpr int SYSTEM_SERVICE_PARAM_ID_100 = 100; constexpr int SYSTEM_SERVICE_PARAM_ID_400 = 400; constexpr int SYSTEM_SERVICE_PARAM_ID_ENTER_BUTTON_ASSIGN = 1000; @@ -34,8 +35,6 @@ constexpr int SYSTEM_SERVICE_PARAM_LANG_ARABIC = 21; constexpr int SYSTEM_SERVICE_PARAM_LANG_UKRAINIAN = 30; constexpr int SYSTEM_SERVICE_PARAM_DATE_FORMAT_DDMMYYYY = 1; constexpr int SYSTEM_SERVICE_PARAM_TIME_FORMAT_24HOUR = 1; -constexpr int SYSTEM_SERVICE_PARAM_GAME_PARENTAL_OFF = 0; -constexpr int SYSTEM_SERVICE_PARAM_ENTER_BUTTON_CROSS = 1; inline int SystemServiceConsoleLanguage(std::string_view caller) { const char* configured = std::getenv("ANYPS5_LANGUAGE"); diff --git a/core/libs/prx/libSceUlt/Export.cpp b/core/libs/prx/libSceUlt/Export.cpp index 63344b81f8..f876cfba21 100644 --- a/core/libs/prx/libSceUlt/Export.cpp +++ b/core/libs/prx/libSceUlt/Export.cpp @@ -236,6 +236,15 @@ int APS5_VABI sceUltUlthreadOptParamInitialize(UltOptParam* optParam, std::uint3 return InitializeOptParam(optParam); } +int APS5_VABI sceUltConditionVariableOptParamInitialize(UltOptParam* optParam, std::uint32_t buildVersion) { + (void)buildVersion; + return InitializeOptParam(optParam); +} + +int APS5_VABI _sceUltConditionVariableOptParamInitialize(UltOptParam* optParam, std::uint32_t buildVersion) { + return sceUltConditionVariableOptParamInitialize(optParam, buildVersion); +} + int APS5_VABI sceUltMutexCreate(void* mutex, const char* name, void* waitingQueueResourcePool, const UltMutexOptParam* optParam, std::uint32_t buildVersion) { (void)name; (void)buildVersion; @@ -369,7 +378,9 @@ int APS5_VABI sceUltMutexUnlock(void* mutex) { return ULT_OK; } -int APS5_VABI _sceUltConditionVariableCreate(void* condition, const char*, void* mutex, const void* optParam, std::uint32_t) { +int APS5_VABI sceUltConditionVariableCreate(void* condition, const char* name, void* mutex, const void* optParam, std::uint32_t buildVersion) { + (void)name; + (void)buildVersion; if (condition == nullptr || mutex == nullptr) return ULT_ERROR_NULL; if (optParam != nullptr) throw std::runtime_error("ULT condition variable options are not implemented"); std::lock_guard lock(gMutex); @@ -385,6 +396,10 @@ int APS5_VABI _sceUltConditionVariableCreate(void* condition, const char*, void* return ULT_OK; } +int APS5_VABI _sceUltConditionVariableCreate(void* condition, const char* name, void* mutex, const void* optParam, std::uint32_t buildVersion) { + return sceUltConditionVariableCreate(condition, name, mutex, optParam, buildVersion); +} + int APS5_VABI sceUltConditionVariableDestroy(void* condition) { std::lock_guard lock(gMutex); const auto it = gConditions.find(condition); @@ -1081,20 +1096,5 @@ int APS5_VABI sceUltUlthreadFactoryInitialize() { return 0; } -int APS5_VABI sceUltConditionVariableCreate() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceUltConditionVariableOptParamInitialize() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - - -int APS5_VABI _sceUltConditionVariableOptParamInitialize() { - NotImplemented_nid_no_patch(__func__); - return 0; -} } diff --git a/core/libs/prx/libSceVdecsw/Export.cpp b/core/libs/prx/libSceVdecsw/Export.cpp index 4c70113346..d6fa384070 100644 --- a/core/libs/prx/libSceVdecsw/Export.cpp +++ b/core/libs/prx/libSceVdecsw/Export.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -19,6 +20,8 @@ constexpr int VdecswErrorInputQueueEmpty = static_cast(0x81510116u); constexpr int VdecswErrorDecodePending = static_cast(0x81510117u); constexpr std::uint32_t CodecVp9 = 2382845; constexpr std::size_t MaxQueuedPictures = 4; +constexpr std::uint32_t CodecAvc = 1; +constexpr std::uint32_t CodecHevc = 974921; struct InputData { std::uint64_t thisSize; @@ -99,10 +102,55 @@ struct AvcPictureInfo { std::uint8_t vui[44]; }; +struct HevcPictureInfo { + std::uint64_t thisSize; + bool isValid; + std::uint64_t ptsData; + std::uint64_t dtsData; + std::uint64_t attachedData; + std::uint32_t picWidthInLumaSamples; + std::uint32_t picHeightInLumaSamples; + std::uint8_t bitDepthLumaMinus8; + std::uint8_t bitDepthChromaMinus8; + std::uint8_t timingInfoPresentFlag; + std::uint32_t numUnitsInTick; + std::uint32_t timeScale; + std::uint32_t aspectRatioInfoPresentFlag; + std::uint8_t aspectRatioIdc; + std::uint16_t sarWidth; + std::uint16_t sarHeight; + std::uint8_t videoSignal[8]; + std::uint32_t picStruct; + std::uint32_t sourceScanType; + std::uint32_t duplicateFlag; + std::uint32_t conformanceWindowFlag; + std::uint32_t confWinLeftOffset; + std::uint32_t confWinRightOffset; + std::uint32_t confWinTopOffset; + std::uint32_t confWinBottomOffset; + std::uint32_t defaultDisplayWindow[5]; + std::uint8_t chromaLocation[4]; + std::uint8_t presentFlags[11]; + std::uint8_t idrPictureFlag; + std::uint8_t irapPictureFlag; + std::uint8_t generalProfileSpace; + std::uint8_t generalTierFlag; + std::uint8_t generalProfileIdc; + std::uint8_t generalSourceFlags[3]; + std::uint8_t generalLevelIdc; + std::uint8_t subLayer[9]; + std::uint8_t frameCroppingFlag; + std::uint32_t frameCropLeftOffset; + std::uint32_t frameCropRightOffset; + std::uint32_t frameCropTopOffset; + std::uint32_t frameCropBottomOffset; +}; + static_assert(sizeof(InputData) == 0x30 && sizeof(InputResult) == 0x18 && sizeof(FrameBuffer) == 0x18); static_assert(sizeof(Vp9PictureInfo) == 0x58 && offsetof(Vp9PictureInfo, ptsData) == 0x10 && offsetof(Vp9PictureInfo, profile) == 0x28 && offsetof(Vp9PictureInfo, bitDepth) == 0x38 && offsetof(Vp9PictureInfo, colorSpace) == 0x3c && offsetof(Vp9PictureInfo, renderHeight) == 0x50); static_assert(sizeof(OutputInfo) == 0x38 && offsetof(OutputInfo, isLastFrame) == 9 && offsetof(OutputInfo, pictureCount) == 0xb && offsetof(OutputInfo, isDiscardedFrame) == 0x1c); 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 Videodec2FrameBuffer { std::uint64_t thisSize; @@ -126,19 +174,6 @@ struct Videodec2OutputInfo { std::uint32_t framePitchInBytes; }; -struct Videodec2AvcPictureInfo { - std::uint64_t thisSize; - bool isValid; - std::uint64_t ptsData; - std::uint64_t dtsData; - std::uint64_t attachedData; - std::uint8_t idrPictureFlag; - std::uint8_t profileIdc; - std::uint8_t levelIdc; - std::uint32_t picWidthInLumaSamples; - std::uint32_t picHeightInLumaSamples; -}; - struct Videodec2Crop { std::uint32_t left; std::uint32_t right; @@ -146,6 +181,18 @@ struct Videodec2Crop { 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; +}; + struct DecoderMemoryInfo { std::uint64_t thisSize; std::uint64_t cpuMemorySize; @@ -171,8 +218,7 @@ int APS5_VABI sceVideodec2Decode_nid_postfix(std::uint64_t handle, const InputDa int APS5_VABI sceVideodec2Flush_nid_postfix(std::uint64_t handle, Videodec2FrameBuffer* frame, Videodec2OutputInfo* output); int APS5_VABI sceVideodec2GetVp9PictureInfo_nid_postfix(const Videodec2OutputInfo* output, Vp9PictureInfo* info); int APS5_VABI sceVideodec2Reset_nid_postfix(std::uint64_t handle); -int APS5_VABI sceVideodec2GetAvcPictureInfo_nid_postfix(const Videodec2OutputInfo* output, Videodec2AvcPictureInfo* first, Videodec2AvcPictureInfo* second); -void Videodec2PictureCrop_nid_no_patch(std::uint64_t handle, const void* frameBuffer, Videodec2Crop* crop); +void Videodec2PictureHeader_nid_no_patch(std::uint64_t handle, const void* frameBuffer, Videodec2PictureHeader* header); } namespace { @@ -187,10 +233,12 @@ struct ConsumedInput { std::uint32_t frames; }; +using PictureInfo = std::variant; + struct Picture { std::vector pixels; Videodec2OutputInfo output; - AvcPictureInfo info; + PictureInfo info; }; struct Decoder { @@ -200,7 +248,7 @@ struct Decoder { std::deque consumed; std::deque outputs; std::deque pictures; - std::map delivered; + std::map delivered; bool finalized = false; bool drained = false; std::map deliveredVp9; @@ -216,27 +264,54 @@ std::shared_ptr findDecoder(std::uint64_t handle) { return found->second; } -AvcPictureInfo pictureInfo(std::uint64_t handle, const Videodec2OutputInfo& output) { - Videodec2AvcPictureInfo source{sizeof(Videodec2AvcPictureInfo)}; - sceVideodec2GetAvcPictureInfo_nid_postfix(&output, &source, nullptr); - if (!source.isValid) throw std::runtime_error("Vdecsw: decoded picture has no picture info"); - Videodec2Crop crop{}; - Videodec2PictureCrop_nid_no_patch(handle, output.frameBuffer, &crop); +PictureInfo pictureInfo(std::uint64_t handle, const Videodec2OutputInfo& output) { + Videodec2PictureHeader 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; + if (output.codecType == CodecHevc) { + HevcPictureInfo info{}; + info.thisSize = sizeof(HevcPictureInfo); + info.isValid = true; + info.ptsData = header.pts; + info.dtsData = header.dts; + info.attachedData = header.attached; + info.picWidthInLumaSamples = output.frameWidth; + info.picHeightInLumaSamples = output.frameHeight; + info.bitDepthLumaMinus8 = header.bitDepthMinus8; + info.bitDepthChromaMinus8 = header.bitDepthMinus8; + info.conformanceWindowFlag = cropped; + info.confWinLeftOffset = crop.left / 2u; + info.confWinRightOffset = crop.right / 2u; + info.confWinTopOffset = crop.top / 2u; + info.confWinBottomOffset = crop.bottom / 2u; + info.idrPictureFlag = header.hevcIdr; + info.irapPictureFlag = header.keyFrame; + info.generalProfileIdc = static_cast(header.profile); + info.generalLevelIdc = static_cast(header.level); + info.frameCroppingFlag = cropped; + info.frameCropLeftOffset = info.confWinLeftOffset; + info.frameCropRightOffset = info.confWinRightOffset; + info.frameCropTopOffset = info.confWinTopOffset; + info.frameCropBottomOffset = info.confWinBottomOffset; + return info; + } + if (output.codecType != CodecAvc) throw std::runtime_error("Vdecsw: picture info is only implemented for H.264 and HEVC"); const auto widthInMbs = (output.frameWidth + 15u) / 16u; const auto heightInMbs = (output.frameHeight + 15u) / 16u; AvcPictureInfo info{}; info.thisSize = sizeof(AvcPictureInfo); info.isValid = true; - info.ptsData = source.ptsData; - info.dtsData = source.dtsData; - info.attachedData = source.attachedData; - info.idrPictureFlag = source.idrPictureFlag; - info.profileIdc = source.profileIdc; - info.levelIdc = source.levelIdc; + info.ptsData = header.pts; + info.dtsData = header.dts; + info.attachedData = header.attached; + info.idrPictureFlag = header.keyFrame; + info.profileIdc = static_cast(header.profile); + info.levelIdc = static_cast(header.level); info.picWidthInMbsMinus1 = widthInMbs - 1u; info.picHeightInMapUnitsMinus1 = heightInMbs - 1u; info.frameMbsOnlyFlag = 1; - info.frameCroppingFlag = crop.left != 0 || crop.right != 0 || crop.top != 0 || crop.bottom != 0; + info.frameCroppingFlag = cropped; info.frameCropLeftOffset = crop.left / 2u; info.frameCropRightOffset = crop.right / 2u; info.frameCropTopOffset = crop.top / 2u; @@ -251,6 +326,29 @@ Vp9PictureInfo vp9PictureInfo(const Videodec2OutputInfo& output) { return info; } +template +int deliveredPictureInfo(const OutputInfo* output, TInfo* info) { + if (output == nullptr || info == nullptr || info->thisSize < sizeof(std::uint64_t)) throw std::runtime_error("Vdecsw: invalid argument pointer"); + if (output->pictureCount > 1) throw std::runtime_error("Vdecsw: second field picture info is not implemented"); + if (output->pictureCount == 0) return 0; + std::vector> all; + { + std::lock_guard lock(decodersMutex); + for (const auto& [handle, decoder] : decoders) all.push_back(decoder); + } + for (const auto& decoder : all) { + std::lock_guard lock(decoder->mutex); + const auto found = decoder->delivered.find(output->frameBuffer); + if (found == decoder->delivered.end()) continue; + const auto* source = std::get_if(&found->second); + if (source == nullptr) throw std::runtime_error("Vdecsw: picture info requested for another codec"); + const auto bytes = std::min(info->thisSize, sizeof(TInfo)) - sizeof(std::uint64_t); + std::memcpy(reinterpret_cast(info) + sizeof(std::uint64_t), reinterpret_cast(source) + sizeof(std::uint64_t), bytes); + return 0; + } + throw std::runtime_error("Vdecsw: picture info requested for a frame buffer no decoder returned"); +} + void advance(std::uint64_t handle, Decoder& decoder) { while (decoder.pictures.size() < MaxQueuedPictures) { std::vector pixels(decoder.frameBytes); @@ -270,7 +368,7 @@ void advance(std::uint64_t handle, Decoder& decoder) { return; } if (output.isValid) { - const auto info = output.codecType == CodecVp9 ? AvcPictureInfo{} : pictureInfo(handle, output); + const auto info = output.codecType == CodecVp9 ? PictureInfo{} : pictureInfo(handle, output); decoder.pictures.push_back({std::move(pixels), output, info}); } } @@ -414,31 +512,12 @@ int APS5_VABI sceVdecswGetPictureInfo(const OutputInfo* output, void* first, voi } int APS5_VABI sceVdecswGetAvcPictureInfo(const OutputInfo* output, AvcPictureInfo* first, AvcPictureInfo* second) { - if (output == nullptr || first == nullptr || first->thisSize < sizeof(std::uint64_t)) throw std::runtime_error("Vdecsw: invalid argument pointer"); - if (output->pictureCount > 1) throw std::runtime_error("Vdecsw: second field picture info is not implemented"); - if (output->pictureCount == 0) return 0; static_cast(second); - std::vector> all; - { - std::lock_guard lock(decodersMutex); - for (const auto& [handle, decoder] : decoders) all.push_back(decoder); - } - for (const auto& decoder : all) { - std::lock_guard lock(decoder->mutex); - const auto found = decoder->delivered.find(output->frameBuffer); - if (found == decoder->delivered.end()) continue; - const auto bytes = std::min(first->thisSize, sizeof(AvcPictureInfo)) - sizeof(std::uint64_t); - std::memcpy(reinterpret_cast(first) + sizeof(std::uint64_t), reinterpret_cast(&found->second) + sizeof(std::uint64_t), bytes); - return 0; - } - throw std::runtime_error("Vdecsw: picture info requested for a frame buffer no decoder returned"); + return deliveredPictureInfo(output, first); } -int APS5_VABI sceVdecswGetHevcPictureInfo(const OutputInfo* output, void* picture) { - (void)output; - (void)picture; - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVdecswGetHevcPictureInfo(const OutputInfo* output, HevcPictureInfo* picture) { + return deliveredPictureInfo(output, picture); } int APS5_VABI sceVdecswGetVp9PictureInfo(const OutputInfo* output, Vp9PictureInfo* picture) { diff --git a/core/libs/prx/libSceVideoOut/CMakeLists.txt b/core/libs/prx/libSceVideoOut/CMakeLists.txt index 2b3dd27800..dee987ce6e 100644 --- a/core/libs/prx/libSceVideoOut/CMakeLists.txt +++ b/core/libs/prx/libSceVideoOut/CMakeLists.txt @@ -84,7 +84,12 @@ if(BUILD_TESTING) set_tests_properties(guest_video_out_cursor PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() - set(ANYPS5_VIDEO_OUT_TEST_DRIVER "offscreen" CACHE STRING + if(WIN32) + set(videoOutTestDriver "windows") + else() + set(videoOutTestDriver "offscreen") + endif() + set(ANYPS5_VIDEO_OUT_TEST_DRIVER "${videoOutTestDriver}" CACHE STRING "SDL video driver for VideoOut flip tests") foreach(flipMode IN ITEMS labels labels_main) @@ -133,6 +138,9 @@ if(BUILD_TESTING AND NOT WIN32) add_test(NAME video_out_frame_dump_queue COMMAND video_out_frame_dump_queue_tests) set_tests_properties(video_out_frame_dump_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") + 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") endif() if(BUILD_TESTING) diff --git a/core/libs/prx/libSceVideoOut/tests/Flip.cpp b/core/libs/prx/libSceVideoOut/tests/Flip.cpp index 59241e643b..365eddf412 100644 --- a/core/libs/prx/libSceVideoOut/tests/Flip.cpp +++ b/core/libs/prx/libSceVideoOut/tests/Flip.cpp @@ -479,10 +479,12 @@ void testControls() { check(owner->GetTriggeredEvents(&event, 1) == 1 && event.udata == &settings && sceVideoOutGetEventId(&event) == VIDEO_OUT_EVENT_VBLANK, "vblank event or updated user data missing"); check(sceVideoOutAddPreVblankStartEvent(queue, handle, &mode) == 0, "pre-vblank subscription failed"); check(sceVideoOutWaitVblank(handle) == 0, "vblank wait failed"); - KernelEvent events[2]{}; - check(owner->GetTriggeredEvents(events, 2) == 2, "pre-vblank or vblank event missing"); - const auto& preVblank = sceVideoOutGetEventId(&events[0]) == VIDEO_OUT_EVENT_PRE_VBLANK_START ? events[0] : events[1]; - check(sceVideoOutGetEventId(&preVblank) == VIDEO_OUT_EVENT_PRE_VBLANK_START && preVblank.udata == &mode, "pre-vblank event missing"); + std::array events{}; + const int collected = owner->GetTriggeredEvents(events.data(), static_cast(events.size())); + check(collected >= 2, "pre-vblank or vblank event missing"); + const auto end = events.begin() + collected; + check(std::any_of(events.begin(), end, [&](KernelEvent& item) { return sceVideoOutGetEventId(&item) == VIDEO_OUT_EVENT_VBLANK && item.udata == &settings; }), "vblank event missing"); + check(std::any_of(events.begin(), end, [&](KernelEvent& item) { return sceVideoOutGetEventId(&item) == VIDEO_OUT_EVENT_PRE_VBLANK_START && item.udata == &mode; }), "pre-vblank event missing"); check(sceVideoOutDeletePreVblankStartEvent(queue, handle) == 0, "pre-vblank unsubscription failed"); std::vector allocation(65536 + 65535); const auto storage = alignedBuffer(allocation); @@ -1052,7 +1054,9 @@ int main(int argc, char** argv) { std::fprintf(stderr, "%s\n", error.what()); try { LibcRunShutdown_nid_postfix(); } catch (const std::exception& shutdown) { std::fprintf(stderr, "shutdown: %s\n", shutdown.what()); } - if (std::string(error.what()).find("Vulkan support") != std::string::npos && !std::getenv("ANYPS5_REQUIRE_DISPLAY")) { + const std::string what = error.what(); + const bool noVulkanWindow = what.find("Vulkan support") != std::string::npos || (what.starts_with("DisplayWindow: ") && what.find("Vulkan") != std::string::npos); + if (noVulkanWindow && !std::getenv("ANYPS5_REQUIRE_DISPLAY")) { std::printf("skipped, no display or Vulkan device: %s\n", error.what()); return 77; } diff --git a/core/libs/prx/libSceVideoOut/tests/FrameDumpQueue.cpp b/core/libs/prx/libSceVideoOut/tests/FrameDumpQueue.cpp index a5105da79f..b98c07e1be 100644 --- a/core/libs/prx/libSceVideoOut/tests/FrameDumpQueue.cpp +++ b/core/libs/prx/libSceVideoOut/tests/FrameDumpQueue.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -28,11 +29,16 @@ namespace { constexpr std::uint32_t Width = 3840; constexpr std::uint32_t Height = 2160; -constexpr std::uint32_t Scale = 4; -constexpr std::uint32_t DumpWidth = Width / Scale; -constexpr std::uint32_t DumpHeight = Height / Scale; -constexpr std::size_t DumpBytes = 54 + static_cast(DumpWidth) * DumpHeight * 4; +std::uint32_t scaleFromEnvironment() { + const char* value = std::getenv("APS5_DUMP_SCALE"); + return value != nullptr ? static_cast(std::strtoul(value, nullptr, 10)) : 4u; +} +const std::uint32_t Scale = scaleFromEnvironment(); +const std::uint32_t DumpWidth = Width / Scale; +const std::uint32_t DumpHeight = Height / Scale; +const std::size_t DumpBytes = 54 + static_cast(DumpWidth) * DumpHeight * 4; constexpr int Frames = 4; +bool WaitMode = false; void check(bool condition, const char* reason) { if (!condition) throw std::runtime_error(reason); @@ -137,6 +143,13 @@ void testDumpQueue() { check(sceVideoOutRegisterBuffers2(handle, 1, 1, &linearBuffer, 1, &linearAttribute, 0, nullptr) == 0, "linear buffer registration failed"); const HeldDump first(0); const HeldDump last(Frames - 1); + std::vector firstDump; + std::thread reader; + const auto started = std::chrono::steady_clock::now(); + if (WaitMode) reader = std::thread([&] { + std::this_thread::sleep_for(std::chrono::seconds(2)); + firstDump = first.Release(); + }); std::exception_ptr failure; try { for (int frame = 0; frame < Frames; ++frame) { @@ -149,8 +162,10 @@ void testDumpQueue() { } catch (...) { failure = std::current_exception(); } - const auto firstDump = first.Release(); + if (WaitMode) reader.join(); + else firstDump = first.Release(); if (failure) std::rethrow_exception(failure); + if (WaitMode) check(std::chrono::steady_clock::now() - started >= std::chrono::milliseconds(1500), "the presenter did not wait for the writer"); checkDump(firstDump, tiledPixels); const auto lastDump = last.Release(); checkDump(lastDump, linearPixels); @@ -164,11 +179,13 @@ void testDumpQueue() { } -int main() { +int main(int argc, char** argv) { + WaitMode = argc > 1 && std::strcmp(argv[1], "wait") == 0; VideoOutTestEnvironment environment; try { + if (WaitMode) check(Scale == 1, "the wait test needs APS5_DUMP_SCALE=1"); testDumpQueue(); - std::puts("VideoOut frame dump queue test passed"); + std::puts(WaitMode ? "VideoOut frame dump wait test passed" : "VideoOut frame dump queue test passed"); return 0; } catch (const std::exception& error) { std::fprintf(stderr, "%s\n", error.what()); diff --git a/core/libs/prx/libSceVideoOut/tests/InputMapping.cpp b/core/libs/prx/libSceVideoOut/tests/InputMapping.cpp index 3497443fcc..6b0c4348ed 100644 --- a/core/libs/prx/libSceVideoOut/tests/InputMapping.cpp +++ b/core/libs/prx/libSceVideoOut/tests/InputMapping.cpp @@ -18,6 +18,14 @@ void require(bool condition, const char* message) { std::filesystem::path root; +void setConfigPath(const std::filesystem::path& path) { +#ifdef _WIN32 + require(_wputenv_s(L"ANYPS5_INPUT_CONFIG", path.wstring().c_str()) == 0, "set ANYPS5_INPUT_CONFIG"); +#else + require(::setenv("ANYPS5_INPUT_CONFIG", path.string().c_str(), 1) == 0, "set ANYPS5_INPUT_CONFIG"); +#endif +} + void useConfig(const std::string& text) { const auto path = root / "anyps5-input.ini"; { @@ -25,11 +33,7 @@ void useConfig(const std::string& text) { file << text; require(file.good(), "write the input mapping"); } -#ifdef _WIN32 - require(_putenv_s("ANYPS5_INPUT_CONFIG", path.string().c_str()) == 0, "set ANYPS5_INPUT_CONFIG"); -#else - require(::setenv("ANYPS5_INPUT_CONFIG", path.string().c_str(), 1) == 0, "set ANYPS5_INPUT_CONFIG"); -#endif + setConfigPath(path); } bool rejects(const std::string& text) { @@ -140,14 +144,6 @@ void mouseLookReject() { require(rejects("MouseDecayMs = 20\nmousedecayms = 30\n"), "a mouse setting given twice is rejected"); } -void setConfigPath(const std::filesystem::path& path) { -#ifdef _WIN32 - require(_wputenv_s(L"ANYPS5_INPUT_CONFIG", path.wstring().c_str()) == 0, "set ANYPS5_INPUT_CONFIG"); -#else - require(::setenv("ANYPS5_INPUT_CONFIG", path.string().c_str(), 1) == 0, "set ANYPS5_INPUT_CONFIG"); -#endif -} - void nonAsciiPaths() { const std::u8string name = u8"anyps5-input-\u00fc-\u65e5\u672c-" + std::u8string(root.filename().u8string()); const auto directory = root / std::filesystem::path(name); diff --git a/core/libs/prx/libSceVideodec/Export.cpp b/core/libs/prx/libSceVideodec/Export.cpp index 56919ce362..56ae352fcf 100644 --- a/core/libs/prx/libSceVideodec/Export.cpp +++ b/core/libs/prx/libSceVideodec/Export.cpp @@ -168,6 +168,10 @@ struct Decoder2Crop { }; static_assert(sizeof(ConfigInfo) == 40 && sizeof(ResourceInfo) == 56 && sizeof(Ctrl) == 24); +static_assert(offsetof(ConfigInfo, maxFrameWidth) == 20 && offsetof(ConfigInfo, videodecFlags) == 32); +static_assert(offsetof(ResourceInfo, cpuGpuMemorySize) == 24 && offsetof(ResourceInfo, maxFrameBufferSize) == 40); +static_assert(offsetof(ResourceInfo, frameBufferAlignment) == 48); +static_assert(sizeof(Decoder2ConfigInfo) == 40 && sizeof(Decoder2MemoryInfo) == 72); static_assert(sizeof(FrameBuffer) == 24 && sizeof(InputData) == 48 && sizeof(PictureInfo) == 112); static_assert(sizeof(CodecInfo) == 64 && offsetof(PictureInfo, codec) == 48); @@ -212,6 +216,20 @@ struct Geometry { Decoder2Crop crop{}; }; +Decoder2ConfigInfo MapConfig(const ConfigInfo& config) { + Decoder2ConfigInfo decoderConfig{}; + decoderConfig.thisSize = sizeof(Decoder2ConfigInfo); + decoderConfig.resourceType = 1; + decoderConfig.codecType = CodecAvc; + decoderConfig.profile = config.profile; + decoderConfig.maxLevel = config.maxLevel; + decoderConfig.maxFrameWidth = config.maxFrameWidth; + decoderConfig.maxFrameHeight = config.maxFrameHeight; + decoderConfig.maxDpbFrameCount = config.maxDpbFrameCount; + decoderConfig.decodePipelineDepth = 1; + return decoderConfig; +} + Geometry Measure(const Decoder2OutputInfo& output, const Decoder2Crop& crop) { Geometry geometry; geometry.width = output.frameWidth; @@ -266,16 +284,7 @@ extern "C" { int APS5_VABI sceVideodecCreateDecoder(const ConfigInfo* config, const ResourceInfo* resource, Ctrl* ctrl) { if (config == nullptr || resource == nullptr || ctrl == nullptr) return VideodecErrorArgumentPointer; if (config->thisSize < sizeof(ConfigInfo) || resource->thisSize < sizeof(ResourceInfo)) return VideodecErrorStructSize; - Decoder2ConfigInfo decoderConfig{}; - decoderConfig.thisSize = sizeof(Decoder2ConfigInfo); - decoderConfig.resourceType = 1; - decoderConfig.codecType = CodecAvc; - decoderConfig.profile = config->profile; - decoderConfig.maxLevel = config->maxLevel; - decoderConfig.maxFrameWidth = config->maxFrameWidth; - decoderConfig.maxFrameHeight = config->maxFrameHeight; - decoderConfig.maxDpbFrameCount = config->maxDpbFrameCount; - decoderConfig.decodePipelineDepth = 1; + const auto decoderConfig = MapConfig(*config); Decoder2MemoryInfo decoderMemory{}; decoderMemory.thisSize = sizeof(Decoder2MemoryInfo); decoderMemory.cpuMemorySize = resource->cpuMemorySize; @@ -407,8 +416,21 @@ int APS5_VABI sceVideodecMapMemory(void) { return 0; } -int APS5_VABI sceVideodecQueryResourceInfo(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceVideodecQueryResourceInfo(const ConfigInfo* config, ResourceInfo* resource) { + if (config == nullptr || resource == nullptr) return VideodecErrorArgumentPointer; + if (config->thisSize < sizeof(ConfigInfo) || resource->thisSize < sizeof(ResourceInfo)) return VideodecErrorStructSize; + const auto decoderConfig = MapConfig(*config); + Decoder2MemoryInfo decoderMemory{}; + decoderMemory.thisSize = sizeof(Decoder2MemoryInfo); + try { + sceVideodec2QueryDecoderMemoryInfo_nid_postfix(&decoderConfig, &decoderMemory); + } catch (const std::exception&) { + return VideodecErrorConfigInfo; + } + resource->cpuMemorySize = decoderMemory.cpuMemorySize; + resource->cpuGpuMemorySize = decoderMemory.cpuGpuMemorySize; + resource->maxFrameBufferSize = decoderMemory.maxFrameBufferSize; + resource->frameBufferAlignment = decoderMemory.frameBufferAlignment; return 0; } diff --git a/core/libs/prx/libSceVideodec2/Export.cpp b/core/libs/prx/libSceVideodec2/Export.cpp index db8822b8eb..583b18c0ff 100644 --- a/core/libs/prx/libSceVideodec2/Export.cpp +++ b/core/libs/prx/libSceVideodec2/Export.cpp @@ -30,6 +30,8 @@ constexpr std::uint32_t HeightAlignment = 32; constexpr std::uint32_t CodecAvc = 1; constexpr std::uint32_t CodecHevc = 974921; constexpr std::uint32_t CodecVp9 = 2382845; +constexpr int Videodec2ErrorAccessUnitSize = static_cast(0x811D010Du); +constexpr int Videodec2ErrorAccessUnitPointer = static_cast(0x811D010Eu); struct ComputeMemoryInfo { std::uint64_t thisSize; @@ -281,6 +283,20 @@ struct Picture { bool idr = false; Vp9PictureInfo vp9{}; 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; }; struct FrameDeleter { @@ -435,6 +451,16 @@ bool CarriesAvcSlice(const std::vector& unit) { return false; } +bool StartsHevcIdr(const std::vector& unit) { + for (std::size_t index = 0; index + 3 < unit.size(); ++index) { + if (unit[index] != 0 || unit[index + 1] != 0 || unit[index + 2] != 1) continue; + const auto type = (unit[index + 3] >> 1u) & 0x3fu; + if (type < 32) return type == 19 || type == 20; + index += 2; + } + return false; +} + void Receive(Decoder& decoder) { for (;;) { FramePointer frame(av_frame_alloc()); @@ -498,12 +524,16 @@ bool WriteNv12(Decoder& decoder, const AVFrame& frame, std::uint8_t* target, std return sws_scale(decoder.scaler, frame.data, frame.linesize, 0, frame.height, planes, strides) == frame.height; } -void EmitDecoded(std::uint64_t handle, Decoder& decoder, FrameBuffer* frame, OutputInfo* output) { +void ClearOutput(const Decoder& decoder, FrameBuffer* frame, OutputInfo* output) { output->isValid = false; output->isErrorFrame = false; output->pictureCount = 0; output->codecType = decoder.codec; if (frame) frame->isAccepted = false; +} + +void EmitDecoded(std::uint64_t handle, Decoder& decoder, FrameBuffer* frame, OutputInfo* output) { + ClearOutput(decoder, frame, output); if (decoder.ready.empty()) return; if (!frame || !frame->frameBuffer) throw std::runtime_error("Videodec2: missing output buffer"); const auto picture = std::move(decoder.ready.front()); @@ -520,6 +550,7 @@ void EmitDecoded(std::uint64_t handle, Decoder& decoder, FrameBuffer* frame, Out if (decoder.context->profile > 0) decoder.profile = static_cast(decoder.context->profile) & 0xffu; if (decoder.context->level > 0) decoder.level = static_cast(decoder.context->level); if (!WriteNv12(decoder, *picture, static_cast(frame->frameBuffer), frame->frameBufferSize)) throw std::runtime_error("Videodec2: NV12 conversion failed"); + timestamps.bitDepthMinus8 = decoder.sampleBytes == 2 ? 2 : 0; const auto pitch = PitchBytes(decoder.width, decoder.sampleBytes); frame->isAccepted = true; output->isValid = true; @@ -603,6 +634,10 @@ int APS5_VABI sceVideodec2Decode_nid_postfix(std::uint64_t handle, const InputDa if (!found) throw std::runtime_error("Videodec2: invalid decoder handle"); auto& decoder = *found; std::lock_guard guard(decoder.mutex); + if (!input->auData || input->auSize == 0) { + ClearOutput(decoder, frame, output); + return input->auData ? Videodec2ErrorAccessUnitSize : Videodec2ErrorAccessUnitPointer; + } if (Trace() && decoder.decoded < 3) std::fprintf(stderr, "[videodec2] Decode input=%llu frame=%llu output=%llu au=%llu pts=%llu size=%ux%u fb=%p/%llu\n", static_cast(input->thisSize), static_cast(frame ? frame->thisSize : 0), static_cast(output->thisSize), static_cast(input->auSize), static_cast(input->ptsData), decoder.width, decoder.height, frame ? frame->frameBuffer : nullptr, static_cast(frame ? frame->frameBufferSize : 0)); ++decoder.decoded; const Picture picture{input->ptsData, input->dtsData, input->attachedData}; @@ -610,7 +645,7 @@ int APS5_VABI sceVideodec2Decode_nid_postfix(std::uint64_t handle, const InputDa avcodec_flush_buffers(decoder.context); decoder.flushing = false; } - if (!input->auData || input->auSize == 0 || input->auSize > static_cast(std::numeric_limits::max())) throw std::runtime_error("Videodec2: invalid access unit"); + if (input->auSize > static_cast(std::numeric_limits::max())) throw std::runtime_error("Videodec2: invalid access unit"); if (decoder.codec == CodecVp9) { auto references = decoder.vp9References; auto colors = decoder.vp9Colors; @@ -654,7 +689,9 @@ int APS5_VABI sceVideodec2Decode_nid_postfix(std::uint64_t handle, const InputDa std::memcpy(packet->data + decoder.parameterSets.size(), unit->data(), unit->size()); decoder.parameterSets.clear(); packet->pts = decoder.nextKey; - decoder.inputs.emplace(decoder.nextKey++, picture); + auto tagged = picture; + tagged.hevcIdr = decoder.codec == CodecHevc && StartsHevcIdr(*unit); + decoder.inputs.emplace(decoder.nextKey++, tagged); Send(decoder, packet); EmitDecoded(handle, decoder, frame, output); return 0; @@ -666,7 +703,7 @@ int APS5_VABI sceVideodec2Flush_nid_postfix(std::uint64_t handle, FrameBuffer* f if (!found) throw std::runtime_error("Videodec2: invalid decoder handle"); auto& decoder = *found; std::lock_guard guard(decoder.mutex); - if (!decoder.flushing) { + if (!decoder.flushing && decoder.ready.empty() && !decoder.inputs.empty()) { Send(decoder, nullptr); decoder.flushing = true; } @@ -766,15 +803,23 @@ int APS5_VABI sceVideodec2GetVp9PictureInfo_nid_postfix(const OutputInfo* output return 0; } -void Videodec2PictureCrop_nid_no_patch(std::uint64_t handle, const void* frameBuffer, Crop* crop) { - if (!crop) throw std::runtime_error("Videodec2: invalid argument pointer"); +void Videodec2PictureHeader_nid_no_patch(std::uint64_t handle, const void* frameBuffer, PictureHeader* header) { + if (!header) 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); const auto picture = decoder.pictures.find(frameBuffer); if (picture == decoder.pictures.end()) throw std::runtime_error("Videodec2: unknown frame buffer"); - *crop = picture->second.crop; + const auto& stored = picture->second; + *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; } } diff --git a/core/libs/prx/libSceVideodec2/Unimplemented.cpp b/core/libs/prx/libSceVideodec2/Unimplemented.cpp index 1cad37fd28..b427d652ac 100644 --- a/core/libs/prx/libSceVideodec2/Unimplemented.cpp +++ b/core/libs/prx/libSceVideodec2/Unimplemented.cpp @@ -55,4 +55,7 @@ int APS5_VABI sceVideodec2GetHevcPictureInfo() { NotImplemented_nid_no_patch(__func__); } +int APS5_VABI sceVideodec2InitLibrary() { + NotImplemented_nid_no_patch(__func__); +} } diff --git a/core/libs/prx/libSceVoice/Export.cpp b/core/libs/prx/libSceVoice/Export.cpp index 707d112ea4..1b3ae4cae5 100644 --- a/core/libs/prx/libSceVoice/Export.cpp +++ b/core/libs/prx/libSceVoice/Export.cpp @@ -26,6 +26,9 @@ constexpr auto NotInitialized = static_cast(0x804e0801u); constexpr auto InvalidPort = static_cast(0x804e0804u); constexpr auto InvalidArgument = static_cast(0x804e0805u); constexpr auto PortNotFound = static_cast(0x804e0807u); +constexpr std::int32_t ThreadPriorityHighest = 256; +constexpr std::int32_t ThreadPriorityLowest = 767; +constexpr std::uint64_t ThreadAffinityAll = 0x1fff; struct Port { std::int32_t type; @@ -42,6 +45,7 @@ struct Voice { bool initialized = false; bool started = false; std::int32_t appType = 0; + bool mutedAll = false; std::deque freePorts; std::map ports; std::set> connections; @@ -56,6 +60,10 @@ Voice& State() { throw std::logic_error(std::string(function) + ": " + why); } +bool IsThreadSetting(std::int32_t priority, std::uint64_t affinity) { + return priority >= ThreadPriorityHighest && priority <= ThreadPriorityLowest && affinity != 0 && (affinity & ~ThreadAffinityAll) == 0; +} + Port& RequirePort(Voice& voice, std::uint32_t id, const char* function) { if (!voice.initialized) Fail(function, "library not initialized"); const auto found = voice.ports.find(id); @@ -65,6 +73,21 @@ Port& RequirePort(Voice& voice, std::uint32_t id, const char* function) { bool IsInput(std::int32_t type) { return type == PortInDevice || type == PortInPcm || type == PortInVoice; } +bool VoiceFrame(std::int32_t bitrate, std::uint16_t& frameSize, std::uint16_t& frameDuration) { + switch (bitrate) { + case 3850: frameSize = 20; frameDuration = 40; return true; + case 4650: frameSize = 24; frameDuration = 40; return true; + case 5700: frameSize = 15; frameDuration = 20; return true; + case 7300: frameSize = 19; frameDuration = 20; return true; + case 11800: frameSize = 15; frameDuration = 10; return true; + case 12200: frameSize = 16; frameDuration = 10; return true; + case 14400: frameSize = 27; frameDuration = 15; return true; + case 16000: frameSize = 20; frameDuration = 10; return true; + case 22533: frameSize = 43; frameDuration = 15; return true; + default: return false; + } +} + } extern "C" { @@ -90,7 +113,10 @@ int APS5_VABI sceVoiceSetThreadsParams(const VoiceThreadsParams* params) { if (voice.started) return static_cast(0x804e080bu); if (params->mode == 0) return 0; if (params->mode != 1) return InvalidArgument; - Fail(__func__, "worker priority and affinity are not implemented"); + if (!IsThreadSetting(params->firstPriority, params->firstAffinity) || !IsThreadSetting(params->secondPriority, params->secondAffinity) || !IsThreadSetting(params->thirdPriority, params->thirdAffinity)) { + Fail(__func__, "the result for a priority outside 256-767 or an affinity mask outside 0x1fff is unknown"); + } + return 0; } int APS5_VABI sceVoiceStart(const VoiceStartParam* param) { @@ -121,6 +147,7 @@ int APS5_VABI sceVoiceEnd_nid_postfix(void) { voice.connections.clear(); voice.started = false; voice.appType = 0; + voice.mutedAll = false; voice.initialized = false; return 0; } @@ -138,18 +165,7 @@ int APS5_VABI sceVoiceCreatePort(std::uint32_t* portId, const VoicePortParam* pa if (type == PortInPcm || type == PortOutPcm) { if (param->pcmaudio.sample_rate != 16000 || param->pcmaudio.data_type != 0) return InvalidArgument; } else if (type == PortInVoice || type == PortOutVoice) { - switch (param->voice.bitrate) { - case 3850: frameSize = 20; frameDuration = 40; break; - case 4650: frameSize = 24; frameDuration = 40; break; - case 5700: frameSize = 15; frameDuration = 20; break; - case 7300: frameSize = 19; frameDuration = 20; break; - case 11800: frameSize = 15; frameDuration = 10; break; - case 12200: frameSize = 16; frameDuration = 10; break; - case 14400: frameSize = 27; frameDuration = 15; break; - case 16000: frameSize = 20; frameDuration = 10; break; - case 22533: frameSize = 43; frameDuration = 15; break; - default: return InvalidArgument; - } + if (!VoiceFrame(param->voice.bitrate, frameSize, frameDuration)) return InvalidArgument; } constexpr auto resourceFull = static_cast(0x804e0808u); if (voice.freePorts.empty()) return resourceFull; @@ -338,7 +354,7 @@ int APS5_VABI sceVoiceGetMuteFlag(std::uint32_t portId, bool* muted) { if (!muted) return InvalidArgument; const auto found = voice.ports.find(portId); if (found == voice.ports.end()) return PortNotFound; - *muted = found->second.muted; + *muted = found->second.muted || voice.mutedAll; return 0; } @@ -446,18 +462,51 @@ int APS5_VABI sceVoiceVADSetVersion(void) { return 0; } -int APS5_VABI sceVoiceSetMuteFlagAll(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceVoiceSetMuteFlagAll(bool muted) { + auto& voice = State(); + std::lock_guard lock(voice.mutex); + if (!voice.initialized) return NotInitialized; + voice.mutedAll = muted; return 0; } -int APS5_VABI sceVoiceSetBitRate(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceVoiceSetBitRate(std::uint32_t portId, std::uint32_t bitrate) { + auto& voice = State(); + std::lock_guard lock(voice.mutex); + if (!voice.initialized) return NotInitialized; + if (portId > 127) return InvalidPort; + const auto found = voice.ports.find(portId); + if (found == voice.ports.end()) return PortNotFound; + auto& port = found->second; + if (port.type != PortInVoice && port.type != PortOutVoice) Fail(__func__, "port does not carry encoded voice"); + std::uint16_t frameSize = 0; + std::uint16_t frameDuration = 0; + if (bitrate > INT32_MAX || !VoiceFrame(static_cast(bitrate), frameSize, frameDuration)) Fail(__func__, "unsupported bitrate " + std::to_string(bitrate)); + port.bitrate = bitrate; + port.frameSize = frameSize; return 0; } -int APS5_VABI sceVoiceUpdatePort(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceVoiceUpdatePort(std::uint32_t portId, const VoicePortParam* param) { + auto& voice = State(); + std::lock_guard lock(voice.mutex); + if (!voice.initialized) return NotInitialized; + if (portId > 127) return InvalidPort; + if (!param) Fail(__func__, "null port parameters"); + const auto found = voice.ports.find(portId); + if (found == voice.ports.end()) return PortNotFound; + auto& port = found->second; + if (param->port_type != port.type) Fail(__func__, "port type " + std::to_string(param->port_type) + " differs from the port's " + std::to_string(port.type)); + std::uint16_t frameSize = port.frameSize; + std::uint16_t frameDuration = 0; + const bool voicePort = port.type == PortInVoice || port.type == PortOutVoice; + if (voicePort && !VoiceFrame(param->voice.bitrate, frameSize, frameDuration)) Fail(__func__, "unsupported bitrate " + std::to_string(param->voice.bitrate)); + port.muted = param->mute != 0; + port.volume = std::isfinite(param->volume) ? param->volume : 0.0f; + if (voicePort) { + port.bitrate = static_cast(param->voice.bitrate); + port.frameSize = frameSize; + } return 0; } diff --git a/core/libs/prx/libSceVorbisDec/CMakeLists.txt b/core/libs/prx/libSceVorbisDec/CMakeLists.txt new file mode 100644 index 0000000000..0da3f0a43e --- /dev/null +++ b/core/libs/prx/libSceVorbisDec/CMakeLists.txt @@ -0,0 +1,77 @@ +enable_language(C) + +set(OGG_DIR ${CMAKE_SOURCE_DIR}/3rdparty/ogg) +set(VORBIS_DIR ${CMAKE_SOURCE_DIR}/3rdparty/vorbis) +set(OGG_GENERATED_DIR ${CMAKE_CURRENT_BINARY_DIR}/ogg/include) + +include(CheckIncludeFiles) +check_include_files(inttypes.h INCLUDE_INTTYPES_H) +check_include_files(stdint.h INCLUDE_STDINT_H) +check_include_files(sys/types.h INCLUDE_SYS_TYPES_H) +set(SIZE16 int16_t) +set(USIZE16 uint16_t) +set(SIZE32 int32_t) +set(USIZE32 uint32_t) +set(SIZE64 int64_t) +set(USIZE64 uint64_t) +configure_file(${OGG_DIR}/include/ogg/config_types.h.in ${OGG_GENERATED_DIR}/ogg/config_types.h @ONLY) + +add_library(ogg STATIC EXCLUDE_FROM_ALL + ${OGG_DIR}/src/bitwise.c + ${OGG_DIR}/src/framing.c +) +target_include_directories(ogg PUBLIC ${OGG_DIR}/include ${OGG_GENERATED_DIR}) + +add_library(vorbis STATIC EXCLUDE_FROM_ALL + ${VORBIS_DIR}/lib/analysis.c + ${VORBIS_DIR}/lib/bitrate.c + ${VORBIS_DIR}/lib/block.c + ${VORBIS_DIR}/lib/codebook.c + ${VORBIS_DIR}/lib/envelope.c + ${VORBIS_DIR}/lib/floor0.c + ${VORBIS_DIR}/lib/floor1.c + ${VORBIS_DIR}/lib/info.c + ${VORBIS_DIR}/lib/lookup.c + ${VORBIS_DIR}/lib/lpc.c + ${VORBIS_DIR}/lib/lsp.c + ${VORBIS_DIR}/lib/mapping0.c + ${VORBIS_DIR}/lib/mdct.c + ${VORBIS_DIR}/lib/psy.c + ${VORBIS_DIR}/lib/registry.c + ${VORBIS_DIR}/lib/res0.c + ${VORBIS_DIR}/lib/sharedbook.c + ${VORBIS_DIR}/lib/smallft.c + ${VORBIS_DIR}/lib/synthesis.c + ${VORBIS_DIR}/lib/window.c +) +target_include_directories(vorbis PUBLIC ${VORBIS_DIR}/include PRIVATE ${VORBIS_DIR}/lib) +target_link_libraries(vorbis PUBLIC ogg) +if(NOT WIN32) + target_link_libraries(vorbis PRIVATE m) +endif() + +add_library(vorbisfile STATIC EXCLUDE_FROM_ALL ${VORBIS_DIR}/lib/vorbisfile.c) +target_include_directories(vorbisfile PRIVATE ${VORBIS_DIR}/lib) +target_link_libraries(vorbisfile PUBLIC vorbis) +set_target_properties(ogg vorbis vorbisfile PROPERTIES POSITION_INDEPENDENT_CODE ON C_VISIBILITY_PRESET hidden) +if(MINGW) + foreach(library IN ITEMS ogg vorbis vorbisfile) + target_compile_options(${library} PRIVATE -fexceptions) + endforeach() +endif() + +add_library(libSceVorbisDec SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceVorbisDec PRIVATE ${LIBS_INCLUDE_DIR}) +target_compile_definitions(libSceVorbisDec PRIVATE OV_EXCLUDE_STATIC_CALLBACKS) +target_link_libraries(libSceVorbisDec PRIVATE vorbisfile vorbis ogg) +if(MINGW OR CMAKE_SYSTEM_NAME STREQUAL "Linux") + target_link_options(libSceVorbisDec PRIVATE LINKER:--exclude-libs,ALL) +endif() +configure_windows_unwind(libSceVorbisDec) + +if(BUILD_TESTING) + add_library(vorbisenc STATIC EXCLUDE_FROM_ALL ${VORBIS_DIR}/lib/vorbisenc.c) + target_include_directories(vorbisenc PRIVATE ${VORBIS_DIR}/lib) + target_link_libraries(vorbisenc PUBLIC vorbis) + set_target_properties(vorbisenc PROPERTIES POSITION_INDEPENDENT_CODE ON C_VISIBILITY_PRESET hidden) +endif() diff --git a/core/libs/prx/libSceVorbisDec/Export.cpp b/core/libs/prx/libSceVorbisDec/Export.cpp index 44ae4fcc5b..b6b60cadf5 100644 --- a/core/libs/prx/libSceVorbisDec/Export.cpp +++ b/core/libs/prx/libSceVorbisDec/Export.cpp @@ -1,33 +1,202 @@ +#include "prx/libSceVorbisDec/include/GuestVorbisFile.hpp" +#include "prx/libc/include/General.hpp" + +#include + #include #include -#include "SceTypes.hpp" -#include "prx/libc/include/General.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace VorbisDec; + +static_assert(sizeof(vorbis_comment) == sizeof(GuestComment)); +static_assert(offsetof(vorbis_comment, user_comments) == offsetof(GuestComment, user_comments)); +static_assert(offsetof(vorbis_comment, comment_lengths) == offsetof(GuestComment, comment_lengths)); +static_assert(offsetof(vorbis_comment, comments) == offsetof(GuestComment, comments)); +static_assert(offsetof(vorbis_comment, vendor) == offsetof(GuestComment, vendor)); +static_assert(SEEK_SET == 0 && SEEK_CUR == 1 && SEEK_END == 2); + +namespace { + +struct Handle { + OggVorbis_File file{}; + void* datasource = nullptr; + GuestCallbacks callbacks{}; + std::vector infos; + bool tellOutOfRange = false; + + ~Handle() { + file.datasource = nullptr; + ov_clear(&file); + } +}; + +struct Table { + std::mutex mutex; + std::unordered_map> handles; +}; + +Table& Handles() { + static auto* table = new Table; + return *table; +} + +Handle* Find(const void* vf) { + auto& table = Handles(); + const std::lock_guard lock(table.mutex); + const auto found = table.handles.find(vf); + return found == table.handles.end() ? nullptr : found->second.get(); +} + +std::unique_ptr Take(const void* vf) { + auto& table = Handles(); + const std::lock_guard lock(table.mutex); + const auto found = table.handles.find(vf); + if (found == table.handles.end()) return nullptr; + auto handle = std::move(found->second); + table.handles.erase(found); + return handle; +} + +void Store(const void* vf, std::unique_ptr handle) { + auto& table = Handles(); + const std::lock_guard lock(table.mutex); + table.handles[vf] = std::move(handle); +} + +void SyncInfos(Handle& handle) { + for (std::size_t link = 0; link < handle.infos.size(); ++link) { + const vorbis_info& info = handle.file.vi[link]; + handle.infos[link] = {info.version, info.channels, info.rate, info.bitrate_upper, info.bitrate_nominal, info.bitrate_lower, info.bitrate_window, info.codec_setup}; + } +} + +void RequireFile(const void* vf, const char* function) { + if (!vf) throw std::invalid_argument(std::string(function) + ": a null OggVorbis_File is not implemented"); +} + +template +auto WithHandle(const void* vf, const char* function, TCall call) { + RequireFile(vf, function); + if (Handle* handle = Find(vf)) { + const auto result = call(*handle); + SyncInfos(*handle); + return result; + } + Handle closed; + return call(closed); +} + +std::size_t ReadSource(void* buffer, std::size_t size, std::size_t count, void* source) { + const auto* handle = static_cast(source); + return handle->callbacks.read_func(buffer, size, count, handle->datasource); +} + +int SeekSource(void* source, ogg_int64_t offset, int whence) { + const auto* handle = static_cast(source); + return handle->callbacks.seek_func(handle->datasource, offset, whence); +} + +int CloseSource(void* source) { + const auto* handle = static_cast(source); + return handle->callbacks.close_func(handle->datasource); +} + +long TellSource(void* source) { + auto* handle = static_cast(source); + const std::int64_t position = handle->callbacks.tell_func(handle->datasource); + if (position < std::numeric_limits::min() || position > std::numeric_limits::max()) { + handle->tellOutOfRange = true; + return -1; + } + return static_cast(position); +} + +struct FilterContext { + GuestFilter filter; + void* param; +}; + +void RunFilter(float** pcm, long channels, long samples, void* context) { + const auto* filter = static_cast(context); + filter->filter(pcm, channels, samples, filter->param); +} + +} extern "C" { -int APS5_VABI sceVorbisDecBitrate(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceVorbisDecOpenCallbacks(void* datasource, void* vf, const char* initial, std::int64_t ibytes, GuestCallbacks callbacks) { + RequireFile(vf, __func__); + if (initial && (ibytes < std::numeric_limits::min() || ibytes > std::numeric_limits::max())) { + throw std::runtime_error(std::string(__func__) + ": an initial byte count outside the host long range is not implemented"); + } + Take(vf).reset(); + auto handle = std::make_unique(); + handle->datasource = datasource; + handle->callbacks = callbacks; + const ov_callbacks host{ + callbacks.read_func ? ReadSource : nullptr, + callbacks.seek_func ? SeekSource : nullptr, + callbacks.close_func ? CloseSource : nullptr, + callbacks.tell_func ? TellSource : nullptr, + }; + const int result = ov_open_callbacks(datasource ? handle.get() : nullptr, &handle->file, initial, initial ? static_cast(ibytes) : 0, host); + if (handle->tellOutOfRange) { + throw std::runtime_error(std::string(__func__) + ": a stream length outside the host long range is not implemented"); + } + if (result != 0) return result; + handle->infos.resize(static_cast(handle->file.links)); + SyncInfos(*handle); + Store(vf, std::move(handle)); return 0; } -int APS5_VABI sceVorbisDecBitrateInstant(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVorbisDecClear(void* vf) { + const std::unique_ptr handle = Take(vf); + if (!handle) { + Handle closed; + return ov_clear(&closed.file); + } + void* const source = handle->file.datasource; + handle->file.datasource = nullptr; + const int result = ov_clear(&handle->file); + if (source && handle->callbacks.close_func) handle->callbacks.close_func(handle->datasource); + return result; } -int APS5_VABI sceVorbisDecClear(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int64_t APS5_VABI sceVorbisDecBitrate(void* vf, int link) { + return WithHandle(vf, __func__, [&](Handle& handle) -> std::int64_t { return ov_bitrate(&handle.file, link); }); } -int APS5_VABI sceVorbisDecComment(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int64_t APS5_VABI sceVorbisDecBitrateInstant(void* vf) { + return WithHandle(vf, __func__, [](Handle& handle) -> std::int64_t { return ov_bitrate_instant(&handle.file); }); } -int APS5_VABI sceVorbisDecCrossLap(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +GuestComment* APS5_VABI sceVorbisDecComment(void* vf, int link) { + return WithHandle(vf, __func__, [&](Handle& handle) { return reinterpret_cast(ov_comment(&handle.file, link)); }); +} + +int APS5_VABI sceVorbisDecCrossLap(void* first, void* second) { + if (first != second) RequireFile(first, __func__); + Handle* from = Find(first); + if (first != second && from) RequireFile(second, __func__); + Handle* to = second == first ? from : Find(second); + Handle closedFrom; + Handle closedTo; + OggVorbis_File* fromFile = from ? &from->file : &closedFrom.file; + OggVorbis_File* toFile = second == first ? fromFile : to ? &to->file : &closedTo.file; + const int result = ov_crosslap(fromFile, toFile); + if (from) SyncInfos(*from); + if (to) SyncInfos(*to); + return result; } int APS5_VABI sceVorbisDecGetDecodePriority(void) { @@ -40,99 +209,84 @@ int APS5_VABI sceVorbisDecGetLastDecodeError(void) { return 0; } -int APS5_VABI sceVorbisDecHalfrate(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVorbisDecHalfrate(void* vf, int flag) { + return WithHandle(vf, __func__, [&](Handle& handle) { return ov_halfrate(&handle.file, flag); }); } -int APS5_VABI sceVorbisDecHalfrateP(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVorbisDecHalfrateP(void* vf) { + return WithHandle(vf, __func__, [](Handle& handle) { return ov_halfrate_p(&handle.file); }); } -int APS5_VABI sceVorbisDecInfo(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +GuestInfo* APS5_VABI sceVorbisDecInfo(void* vf, int link) { + return WithHandle(vf, __func__, [&](Handle& handle) -> GuestInfo* { + const vorbis_info* info = ov_info(&handle.file, link); + return info ? &handle.infos.at(static_cast(info - handle.file.vi)) : nullptr; + }); } -int APS5_VABI sceVorbisDecOpenCallbacks(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVorbisDecPcmSeek(void* vf, std::int64_t position) { + return WithHandle(vf, __func__, [&](Handle& handle) { return ov_pcm_seek(&handle.file, position); }); } -int APS5_VABI sceVorbisDecPcmSeek(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVorbisDecPcmSeekLap(void* vf, std::int64_t position) { + return WithHandle(vf, __func__, [&](Handle& handle) { return ov_pcm_seek_lap(&handle.file, position); }); } -int APS5_VABI sceVorbisDecPcmSeekLap(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVorbisDecPcmSeekPage(void* vf, std::int64_t position) { + return WithHandle(vf, __func__, [&](Handle& handle) { return ov_pcm_seek_page(&handle.file, position); }); } -int APS5_VABI sceVorbisDecPcmSeekPage(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVorbisDecPcmSeekPageLap(void* vf, std::int64_t position) { + return WithHandle(vf, __func__, [&](Handle& handle) { return ov_pcm_seek_page_lap(&handle.file, position); }); } -int APS5_VABI sceVorbisDecPcmSeekPageLap(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int64_t APS5_VABI sceVorbisDecPcmTell(void* vf) { + return WithHandle(vf, __func__, [](Handle& handle) -> std::int64_t { return ov_pcm_tell(&handle.file); }); } -int APS5_VABI sceVorbisDecPcmTell(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int64_t APS5_VABI sceVorbisDecPcmTotal(void* vf, int link) { + return WithHandle(vf, __func__, [&](Handle& handle) -> std::int64_t { return ov_pcm_total(&handle.file, link); }); } -int APS5_VABI sceVorbisDecPcmTotal(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVorbisDecRawSeek(void* vf, std::int64_t position) { + return WithHandle(vf, __func__, [&](Handle& handle) { return ov_raw_seek(&handle.file, position); }); } -int APS5_VABI sceVorbisDecRawSeek(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVorbisDecRawSeekLap(void* vf, std::int64_t position) { + return WithHandle(vf, __func__, [&](Handle& handle) { return ov_raw_seek_lap(&handle.file, position); }); } -int APS5_VABI sceVorbisDecRawSeekLap(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int64_t APS5_VABI sceVorbisDecRawTell(void* vf) { + return WithHandle(vf, __func__, [](Handle& handle) -> std::int64_t { return ov_raw_tell(&handle.file); }); } -int APS5_VABI sceVorbisDecRawTell(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int64_t APS5_VABI sceVorbisDecRawTotal(void* vf, int link) { + return WithHandle(vf, __func__, [&](Handle& handle) -> std::int64_t { return ov_raw_total(&handle.file, link); }); } -int APS5_VABI sceVorbisDecRawTotal(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int64_t APS5_VABI sceVorbisDecRead(void* vf, char* buffer, int length, int bigEndian, int word, int isSigned, int* bitstream) { + return WithHandle(vf, __func__, [&](Handle& handle) -> std::int64_t { + return ov_read(&handle.file, buffer, length, bigEndian, word, isSigned, bitstream); + }); } -int APS5_VABI sceVorbisDecRead(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int64_t APS5_VABI sceVorbisDecReadFilter(void* vf, char* buffer, int length, int bigEndian, int word, int isSigned, int* bitstream, GuestFilter filter, void* filterParam) { + FilterContext context{filter, filterParam}; + return WithHandle(vf, __func__, [&](Handle& handle) -> std::int64_t { + return ov_read_filter(&handle.file, buffer, length, bigEndian, word, isSigned, bitstream, filter ? RunFilter : nullptr, &context); + }); } -int APS5_VABI sceVorbisDecReadFilter(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int64_t APS5_VABI sceVorbisDecReadFloat(void* vf, float*** pcmChannels, int samples, int* bitstream) { + return WithHandle(vf, __func__, [&](Handle& handle) -> std::int64_t { return ov_read_float(&handle.file, pcmChannels, samples, bitstream); }); } -int APS5_VABI sceVorbisDecReadFloat(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int64_t APS5_VABI sceVorbisDecSeekable(void* vf) { + return WithHandle(vf, __func__, [](Handle& handle) -> std::int64_t { return ov_seekable(&handle.file); }); } -int APS5_VABI sceVorbisDecSeekable(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceVorbisDecSerialNumber(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int64_t APS5_VABI sceVorbisDecSerialNumber(void* vf, int link) { + return WithHandle(vf, __func__, [&](Handle& handle) -> std::int64_t { return ov_serialnumber(&handle.file, link); }); } int APS5_VABI sceVorbisDecSetDecodePriority(void) { @@ -140,39 +294,32 @@ int APS5_VABI sceVorbisDecSetDecodePriority(void) { return 0; } -int APS5_VABI sceVorbisDecStreams(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int64_t APS5_VABI sceVorbisDecStreams(void* vf) { + return WithHandle(vf, __func__, [](Handle& handle) -> std::int64_t { return ov_streams(&handle.file); }); } -int APS5_VABI sceVorbisDecTimeSeek(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVorbisDecTimeSeek(void* vf, double position) { + return WithHandle(vf, __func__, [&](Handle& handle) { return ov_time_seek(&handle.file, position); }); } -int APS5_VABI sceVorbisDecTimeSeekLap(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVorbisDecTimeSeekLap(void* vf, double position) { + return WithHandle(vf, __func__, [&](Handle& handle) { return ov_time_seek_lap(&handle.file, position); }); } -int APS5_VABI sceVorbisDecTimeSeekPage(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVorbisDecTimeSeekPage(void* vf, double position) { + return WithHandle(vf, __func__, [&](Handle& handle) { return ov_time_seek_page(&handle.file, position); }); } -int APS5_VABI sceVorbisDecTimeSeekPageLap(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVorbisDecTimeSeekPageLap(void* vf, double position) { + return WithHandle(vf, __func__, [&](Handle& handle) { return ov_time_seek_page_lap(&handle.file, position); }); } -int APS5_VABI sceVorbisDecTimeTell(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +double APS5_VABI sceVorbisDecTimeTell(void* vf) { + return WithHandle(vf, __func__, [](Handle& handle) { return ov_time_tell(&handle.file); }); } -int APS5_VABI sceVorbisDecTimeTotal(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +double APS5_VABI sceVorbisDecTimeTotal(void* vf, int link) { + return WithHandle(vf, __func__, [&](Handle& handle) { return ov_time_total(&handle.file, link); }); } } diff --git a/core/libs/prx/libSceVorbisDec/include/GuestVorbisFile.hpp b/core/libs/prx/libSceVorbisDec/include/GuestVorbisFile.hpp new file mode 100644 index 0000000000..9da5f3e1d5 --- /dev/null +++ b/core/libs/prx/libSceVorbisDec/include/GuestVorbisFile.hpp @@ -0,0 +1,46 @@ +#ifndef CORE_LIBS_PRX_LIBSCEVORBISDEC_INCLUDE_GUESTVORBISFILE_HPP +#define CORE_LIBS_PRX_LIBSCEVORBISDEC_INCLUDE_GUESTVORBISFILE_HPP + +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include + +namespace VorbisDec { + +struct GuestCallbacks { + std::size_t (APS5_VABI *read_func)(void*, std::size_t, std::size_t, void*); + int (APS5_VABI *seek_func)(void*, std::int64_t, int); + int (APS5_VABI *close_func)(void*); + std::int64_t (APS5_VABI *tell_func)(void*); +}; + +struct GuestInfo { + std::int32_t version; + std::int32_t channels; + std::int64_t rate; + std::int64_t bitrate_upper; + std::int64_t bitrate_nominal; + std::int64_t bitrate_lower; + std::int64_t bitrate_window; + void* codec_setup; +}; + +struct GuestComment { + char** user_comments; + std::int32_t* comment_lengths; + std::int32_t comments; + char* vendor; +}; + +using GuestFilter = void (APS5_VABI *)(float**, std::int64_t, std::int64_t, void*); + +static_assert(sizeof(GuestCallbacks) == 32); +static_assert(offsetof(GuestInfo, rate) == 8 && offsetof(GuestInfo, bitrate_window) == 40 && offsetof(GuestInfo, codec_setup) == 48); +static_assert(sizeof(GuestInfo) == 56); +static_assert(offsetof(GuestComment, comment_lengths) == 8 && offsetof(GuestComment, comments) == 16 && offsetof(GuestComment, vendor) == 24); +static_assert(sizeof(GuestComment) == 32); + +} + +#endif diff --git a/core/libs/prx/libSceWebBrowserDialog/CMakeLists.txt b/core/libs/prx/libSceWebBrowserDialog/CMakeLists.txt index 2c498e5df1..641f60463e 100644 --- a/core/libs/prx/libSceWebBrowserDialog/CMakeLists.txt +++ b/core/libs/prx/libSceWebBrowserDialog/CMakeLists.txt @@ -12,3 +12,14 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endif() + +if(BUILD_TESTING) + add_test_executable(guest_web_browser_dialog_cookies_tests ${LIBS_INCLUDE_DIR}/tests/GuestWebBrowserDialogCookies.cpp) + target_include_directories(guest_web_browser_dialog_cookies_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_web_browser_dialog_cookies_tests PRIVATE libSceWebBrowserDialog) + add_test(NAME guest_web_browser_dialog_cookies COMMAND guest_web_browser_dialog_cookies_tests) + if(WIN32) + set_tests_properties(guest_web_browser_dialog_cookies PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceWebBrowserDialog/Export.cpp b/core/libs/prx/libSceWebBrowserDialog/Export.cpp index 8c720fbab1..3b8080508c 100644 --- a/core/libs/prx/libSceWebBrowserDialog/Export.cpp +++ b/core/libs/prx/libSceWebBrowserDialog/Export.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include "SceTypes.hpp" #include "prx/libSceCommonDialog/CommonDialog.hpp" #include "prx/libc/include/General.hpp" @@ -24,6 +25,7 @@ static_assert(sizeof(WebBrowserDialogResetCookieParam) == 264); namespace { std::atomic g_status{0}; +std::atomic g_open{false}; constexpr int COMMON_DIALOG_STATUS_NONE = 0; constexpr int COMMON_DIALOG_STATUS_RUNNING = 2; @@ -31,10 +33,23 @@ constexpr int COMMON_DIALOG_STATUS_FINISHED = 3; constexpr int COMMON_DIALOG_RESULT_USER_CANCELED = 1; constexpr int COMMON_DIALOG_ERROR_NOT_INITIALIZED = static_cast(0x80B80003u); constexpr int COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast(0x80B80005u); +constexpr int COMMON_DIALOG_ERROR_INVALID_STATE = static_cast(0x80B80006u); constexpr int COMMON_DIALOG_ERROR_BUSY = static_cast(0x80B80007u); constexpr int COMMON_DIALOG_ERROR_NOT_RUNNING = static_cast(0x80B8000Bu); constexpr int COMMON_DIALOG_ERROR_PARAMETER = static_cast(0x80B8000Au); constexpr int COMMON_DIALOG_ERROR_ARG_NULL = static_cast(0x80B8000Du); + +bool ReservedClear(const std::uint8_t (&reserved)[256]) { + for (const auto byte : reserved) + if (byte != 0) return false; + return true; +} + +bool LengthWithin(const char* text, std::size_t limit) { + if (!text) return false; + const auto length = strnlen(text, limit); + return length != 0 && length < limit; +} } extern "C" { @@ -47,14 +62,21 @@ int APS5_VABI sceWebBrowserDialogInitialize(void) { int APS5_VABI sceWebBrowserDialogTerminate(void) { int expected = 1; - if (g_status.compare_exchange_strong(expected, COMMON_DIALOG_STATUS_NONE)) return 0; + if (g_status.compare_exchange_strong(expected, COMMON_DIALOG_STATUS_NONE)) { + g_open = false; + return 0; + } expected = COMMON_DIALOG_STATUS_FINISHED; - if (g_status.compare_exchange_strong(expected, COMMON_DIALOG_STATUS_NONE)) return 0; + if (g_status.compare_exchange_strong(expected, COMMON_DIALOG_STATUS_NONE)) { + g_open = false; + return 0; + } if (g_status.load() == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; return COMMON_DIALOG_ERROR_BUSY; } int APS5_VABI sceWebBrowserDialogClose(void) { + g_open = false; const int status = g_status.load(); if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; if (status != COMMON_DIALOG_STATUS_RUNNING) return COMMON_DIALOG_ERROR_NOT_RUNNING; @@ -88,16 +110,24 @@ int APS5_VABI sceWebBrowserDialogOpen(const void* param) { if (status == COMMON_DIALOG_STATUS_RUNNING) return COMMON_DIALOG_ERROR_BUSY; if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; g_status = COMMON_DIALOG_STATUS_FINISHED; + g_open = true; return 0; } int APS5_VABI sceWebBrowserDialogUpdateStatus(void) { - return g_status.load(); + const int status = g_status.load(); + if (status == COMMON_DIALOG_STATUS_FINISHED) g_open = false; + return status; } int APS5_VABI sceWebBrowserDialogSetCookie(const WebBrowserDialogCookieParam* param) { - NotImplemented_nid_no_patch(__func__); + if (g_status.load() == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (g_open.load()) return COMMON_DIALOG_ERROR_INVALID_STATE; + if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + if (param->size != sizeof(WebBrowserDialogCookieParam) || !LengthWithin(param->address, 0x800) || !LengthWithin(param->cookie, 0x1000) || + !ReservedClear(param->reserved)) + return COMMON_DIALOG_ERROR_PARAMETER; return 0; } @@ -118,11 +148,15 @@ int APS5_VABI sceWebBrowserDialogOpenForPredeterminedContent(const void* param, if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; if (status == COMMON_DIALOG_STATUS_RUNNING) return COMMON_DIALOG_ERROR_BUSY; g_status = COMMON_DIALOG_STATUS_FINISHED; + g_open = true; return 0; } int APS5_VABI sceWebBrowserDialogResetCookie(const WebBrowserDialogResetCookieParam* param) { - NotImplemented_nid_no_patch(__func__); + if (g_status.load() == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + if (g_open.load()) return COMMON_DIALOG_ERROR_INVALID_STATE; + if (param->size != sizeof(WebBrowserDialogResetCookieParam) || !ReservedClear(param->reserved)) return COMMON_DIALOG_ERROR_PARAMETER; return 0; } diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index f06b05d6af..b3dc056483 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -62,6 +62,7 @@ target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/HostEntropy.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Backtrace.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/DinkumwareAtomics.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/DinkumwareInternals.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Time.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/TimeExtras.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/AtomicBuiltins.cpp @@ -526,6 +527,15 @@ if(BUILD_TESTING) set_tests_properties(guest_dinkumware_atomics PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_dinkumware_internals_tests ${LIBS_INCLUDE_DIR}/tests/GuestDinkumwareInternals.cpp) + target_include_directories(guest_dinkumware_internals_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_dinkumware_internals_tests PRIVATE libc) + configure_windows_unwind(guest_dinkumware_internals_tests) + add_test(NAME guest_dinkumware_internals COMMAND guest_dinkumware_internals_tests) + if(WIN32) + set_tests_properties(guest_dinkumware_internals PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_hostname_tests ${LIBS_INCLUDE_DIR}/tests/GuestHostname.cpp) target_include_directories(guest_hostname_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_hostname_tests PRIVATE libc) @@ -679,6 +689,17 @@ if(BUILD_TESTING) set_tests_properties(guest_call_trace_abort_race PROPERTIES TIMEOUT 20 PASS_REGULAR_EXPRESSION "last traced system library calls[^~]*#102 thread [0-9]+ libTraced.prx Integers from[^~]*most called traced imports \\(102 calls in total\\):[^~]* 101 libTraced.prx Integers[^~]* 1 libOther.prx Blocks") endif() + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + add_test_executable(guest_call_trace_fixed_base_tests ${LIBS_INCLUDE_DIR}/tests/GuestCallTrace.cpp) + target_include_directories(guest_call_trace_fixed_base_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_call_trace_fixed_base_tests PRIVATE libc) + set_target_properties(guest_call_trace_fixed_base_tests PROPERTIES POSITION_INDEPENDENT_CODE OFF) + target_compile_options(guest_call_trace_fixed_base_tests PRIVATE -fno-pie) + target_link_options(guest_call_trace_fixed_base_tests PRIVATE -no-pie -fuse-ld=bfd) + add_test(NAME guest_call_trace_fixed_base_abort COMMAND guest_call_trace_fixed_base_tests abort) + set_tests_properties(guest_call_trace_fixed_base_abort PROPERTIES TIMEOUT 20 PASS_REGULAR_EXPRESSION + "#1 thread [0-9]+ libTraced.prx Integers from 0x00000000004[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f] guest_call_trace_fixed_base_tests\\+0x[0-9a-f]?[0-9a-f]?[0-9a-f]?[0-9a-f]?[0-9a-f][^0-9a-f]") + endif() endif() add_test_executable(guest_syslog_tests ${LIBS_INCLUDE_DIR}/tests/GuestSyslog.cpp) target_include_directories(guest_syslog_tests PRIVATE ${LIBS_INCLUDE_DIR}) @@ -1041,7 +1062,7 @@ if(MINGW) target_include_directories(guest_message_destructor_abi_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_message_destructor_abi_tests PRIVATE libc) configure_windows_unwind(guest_message_destructor_abi_tests) - foreach(mode IN ITEMS control decoy zero release regex-decoy regex-zero + foreach(mode IN ITEMS control hybrid decoy zero release regex-decoy regex-zero runtime-decoy runtime-zero doraise-decoy doraise-zero unshared-decoy unshared-zero regex-unshared-decoy regex-unshared-zero) add_test(NAME guest_message_destructor_${mode} COMMAND guest_message_destructor_abi_tests ${mode}) diff --git a/core/libs/prx/libc/include/FileStream.hpp b/core/libs/prx/libc/include/FileStream.hpp index 1b9954d873..3f1d90eb88 100644 --- a/core/libs/prx/libc/include/FileStream.hpp +++ b/core/libs/prx/libc/include/FileStream.hpp @@ -23,6 +23,8 @@ struct UcrtStreamState { int count; long flags; }; +extern "C" void LibcRegisterDirectoryStreams_nid_no_patch(int (*open)(const wchar_t* path), void (*forget)(int descriptor)); +extern "C" void ForgetDirectoryStream_nid_no_patch(int descriptor); #else #include #endif @@ -66,6 +68,7 @@ class FileStream { bool _dynamic; bool guestError = false; bool randomDevice = false; + bool directory = false; public: explicit FileStream(std::FILE* handle, bool dynamic = false) : _handle(handle), _dynamic(dynamic) { @@ -130,6 +133,14 @@ public: return randomDevice; } + void MarkDirectory() { + directory = true; + } + + bool IsDirectory() const { + return directory; + } + bool Reopen(const std::filesystem::path& filename, const char* mode, [[maybe_unused]] std::FILE* (*open)(const std::filesystem::path&, const std::string&) = nullptr) { auto* previous = GetHandle(); @@ -140,6 +151,7 @@ public: std::wstring wideMode; for (const char character : std::string_view(mode)) wideMode.push_back(static_cast(character)); const int replaced = previous ? _fileno(previous) : -1; + if (std::exchange(directory, false)) ForgetDirectoryStream_nid_no_patch(replaced); auto* const standard = HostStandardHandle_nid_no_patch(replaced); GuestFileLockForget_nid_no_patch(replaced); if (open && !filename.empty()) { @@ -200,18 +212,21 @@ public: _guest.descriptor = descriptor >= 0 && descriptor <= 32767 ? static_cast(descriptor) : -1; return true; } + void PublishStatus(bool endOfFile, bool error) { + __atomic_store_n(&_guest.readRemaining, 0, __ATOMIC_RELAXED); + __atomic_store_n(&_guest.writeRemaining, 0, __ATOMIC_RELAXED); + auto old = __atomic_load_n(&_guest.flags, __ATOMIC_RELAXED); + std::int16_t next; + do next = static_cast((old & ~0x60) | (endOfFile ? 0x20 : 0) | (error ? 0x40 : 0)); + while (!__atomic_compare_exchange_n(&_guest.flags, &old, next, false, __ATOMIC_RELAXED, __ATOMIC_RELAXED)); + } void SyncStatus() { if (!_handle) return; - _guest.readRemaining = 0; - _guest.writeRemaining = 0; - _guest.flags = static_cast((_guest.flags & ~0x60) | - (std::feof(GetHandle()) ? 0x20 : 0) | (std::ferror(GetHandle()) || guestError ? 0x40 : 0)); + PublishStatus(std::feof(GetHandle()) != 0, std::ferror(GetHandle()) != 0 || guestError); } void SyncUnlockedStatus() { if (!_handle) return; - _guest.readRemaining = 0; - _guest.writeRemaining = 0; #ifdef _WIN32 const bool endOfFile = std::feof(GetHandle()) != 0; const bool error = std::ferror(GetHandle()) != 0; @@ -219,8 +234,7 @@ public: const bool endOfFile = ::feof_unlocked(GetHandle()) != 0; const bool error = ::ferror_unlocked(GetHandle()) != 0; #endif - _guest.flags = static_cast((_guest.flags & ~0x60) | - (endOfFile ? 0x20 : 0) | (error || guestError ? 0x40 : 0)); + PublishStatus(endOfFile, error || guestError); } bool SwitchToReading() { @@ -260,6 +274,10 @@ public: #endif } + void KeepDirectoryReadError(bool failed) { + if (failed && directory && (_guest.flags & 0x40) != 0) errno = 21; + } + void SetError() { guestError = true; SyncStatus(); @@ -276,6 +294,7 @@ public: GetHandle(); #ifdef _WIN32 GuestFileLockForget_nid_no_patch(::_fileno(_handle)); + if (std::exchange(directory, false)) ForgetDirectoryStream_nid_no_patch(::_fileno(_handle)); #endif const int result = std::fclose(std::exchange(_handle, nullptr)); _guest.flags = 0; diff --git a/core/libs/prx/libc/include/RenameDot.hpp b/core/libs/prx/libc/include/RenameDot.hpp index 4364a80d39..a792ac7f32 100644 --- a/core/libs/prx/libc/include/RenameDot.hpp +++ b/core/libs/prx/libc/include/RenameDot.hpp @@ -5,6 +5,13 @@ #include #include "FilesystemError.hpp" +inline std::string TrimTrailingSeparators_nid_no_patch(std::string path) { + const auto separator = [](char character) { return character == '/' || character == '\\'; }; + const std::size_t root = std::filesystem::path(path).root_path().native().size(); + while (path.size() > root && path.size() > 1 && separator(path.back())) path.pop_back(); + return path; +} + inline int RenameDotError_nid_no_patch(const char* path) { std::size_t length = std::char_traits::length(path); const auto separator = [](char character) { return character == '/' || character == '\\'; }; @@ -15,7 +22,7 @@ inline int RenameDotError_nid_no_patch(const char* path) { const bool dot = name == 1 && path[start] == '.'; const bool dotDot = name == 2 && path[start] == '.' && path[start + 1] == '.'; if (!dot && !dotDot) return 0; - const std::string parent = start == 0 ? std::string(".") : std::string(path, start); + const std::string parent = start == 0 ? std::string(".") : TrimTrailingSeparators_nid_no_patch(std::string(path, start)); std::filesystem::path resolved; if (const int error = ResolvePathError_nid_no_patch(parent.c_str(), resolved)) return error; std::error_code status; diff --git a/core/libs/prx/libc/include/SonyCrt.hpp b/core/libs/prx/libc/include/SonyCrt.hpp index cf1879e2c0..db53cd3d54 100644 --- a/core/libs/prx/libc/include/SonyCrt.hpp +++ b/core/libs/prx/libc/include/SonyCrt.hpp @@ -138,20 +138,38 @@ inline void* SonySetConstraintHandlerS(void* handler) { } inline int SonyWcscpyS(std::uint16_t* dest, std::size_t size, const std::uint16_t* src) { - if (!dest || !src || size == 0) { - SonyInvokeConstraintHandler("wcscpy_s", dest, EINVAL); - return EINVAL; + constexpr int GuestEinval = 22; + constexpr int GuestErange = 34; + constexpr std::size_t rsizeMax = (SIZE_MAX >> 1) / sizeof(std::uint16_t); + if (!dest || size == 0) { + SonyInvokeConstraintHandler("wcscpy_s", dest, GuestEinval); + return GuestEinval; } - std::size_t i = 0; - while (i + 1 < size && src[i] != 0) { - dest[i] = src[i]; - ++i; + if (size > rsizeMax) { + SonyInvokeConstraintHandler("wcscpy_s", dest, GuestErange); + return GuestErange; } - dest[i] = 0; - if (src[i] != 0) { - SonyInvokeConstraintHandler("wcscpy_s", dest, ERANGE); - return ERANGE; + if (!src) { + dest[0] = 0; + SonyInvokeConstraintHandler("wcscpy_s", dest, GuestEinval); + return GuestEinval; } + std::size_t length = 0; + while (length < size && src[length] != 0) ++length; + if (length >= size) { + dest[0] = 0; + SonyInvokeConstraintHandler("wcscpy_s", dest, GuestErange); + return GuestErange; + } + const auto destination = reinterpret_cast(dest); + const auto source = reinterpret_cast(src); + const auto distance = destination < source ? source - destination : destination - source; + if (length != 0 && distance < (length + 1) * sizeof(std::uint16_t)) { + dest[0] = 0; + SonyInvokeConstraintHandler("wcscpy_s", dest, GuestEinval); + return GuestEinval; + } + std::memcpy(dest, src, (length + 1) * sizeof(std::uint16_t)); return 0; } diff --git a/core/libs/prx/libc/src/DinkumwareInternals.cpp b/core/libs/prx/libc/src/DinkumwareInternals.cpp new file mode 100644 index 0000000000..163a7362fa --- /dev/null +++ b/core/libs/prx/libc/src/DinkumwareInternals.cpp @@ -0,0 +1,96 @@ +#include +#include +#include +#include +#include +#include + +#include "prx/libc/include/General.hpp" + +extern "C" { +int* APS5_VABI __error_nid_postfix(); +double APS5_VABI strtod_nid_postfix(const char* str, char** endptr); +long long APS5_VABI strtoll_nid_postfix(const char* str, char** endptr, int base); +double APS5_VABI sin_nid_postfix(double x); +double APS5_VABI cos_nid_postfix(double x); +float APS5_VABI sinf_nid_postfix(float x); +float APS5_VABI cosf_nid_postfix(float x); +long double APS5_VABI sinl_nid_postfix(long double x); +long double APS5_VABI cosl_nid_postfix(long double x); +double APS5_VABI log_nid_postfix(double x); +double APS5_VABI log10_nid_postfix(double x); +double APS5_VABI log2_nid_postfix(double x); +float APS5_VABI logf_nid_postfix(float x); +float APS5_VABI log10f_nid_postfix(float x); +float APS5_VABI log2f_nid_postfix(float x); +} + +namespace { + +constexpr long long ExponentLimit = 1000000000; +constexpr int RangeError = 34; + +bool Hexadecimal(const char* text) { + while (std::isspace(static_cast(*text))) ++text; + if (*text == '+' || *text == '-') ++text; + return text[0] == '0' && (text[1] == 'x' || text[1] == 'X'); +} + +} + +extern "C" { + +double APS5_VABI _Stod_nid_postfix(const char* text, char** end, std::int64_t pten) { + int& guestErrno = *__error_nid_postfix(); + const int saved = guestErrno; + guestErrno = 0; + char* stop = nullptr; + const double value = strtod_nid_postfix(text, &stop); + if (end != nullptr) *end = stop; + const bool overflowed = std::isinf(value) && guestErrno == RangeError; + if (pten == 0 || stop == text || !(std::isfinite(value) || overflowed)) { + if (guestErrno == 0) guestErrno = saved; + return value; + } + if (Hexadecimal(text)) NotImplemented_nid_no_patch("_Stod (hexadecimal text with a power-of-ten scale)"); + std::string number(text, static_cast(stop)); + long long exponent = 0; + const auto marker = number.find_first_of("eE"); + if (marker != std::string::npos) { + exponent = std::strtoll(number.c_str() + marker + 1, nullptr, 10); + number.resize(marker); + } + exponent = std::clamp(exponent, -ExponentLimit, ExponentLimit) + std::clamp(pten, -ExponentLimit, ExponentLimit); + number += 'e'; + number += std::to_string(exponent); + guestErrno = saved; + return strtod_nid_postfix(number.c_str(), nullptr); +} + +long long APS5_VABI _Stoll_nid_postfix(const char* text, char** end, int base) { + return strtoll_nid_postfix(text, end, base); +} + +double APS5_VABI _Sin_nid_postfix(double x, unsigned int quadrant) { + return quadrant != 0 ? cos_nid_postfix(x) : sin_nid_postfix(x); +} + +float APS5_VABI _FSin_nid_postfix(float x, unsigned int quadrant) { + return quadrant != 0 ? cosf_nid_postfix(x) : sinf_nid_postfix(x); +} + +long double APS5_VABI _LSin_nid_postfix(long double x, unsigned int quadrant) { + return quadrant != 0 ? cosl_nid_postfix(x) : sinl_nid_postfix(x); +} + +double APS5_VABI _Log_nid_postfix(double x, int base) { + if (base > 0) return log10_nid_postfix(x); + return base == 0 ? log_nid_postfix(x) : log2_nid_postfix(x); +} + +float APS5_VABI _FLog_nid_postfix(float x, int base) { + if (base > 0) return log10f_nid_postfix(x); + return base == 0 ? logf_nid_postfix(x) : log2f_nid_postfix(x); +} + +} diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index 410fa6f309..6187b36c04 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -65,6 +65,19 @@ static std::optional ParseFileMode(const char* mode) { #ifdef _WIN32 static constexpr DWORD AppendOnlyAccess = FILE_GENERIC_WRITE & ~FILE_WRITE_DATA; + +static std::optional HostOpenName(const std::filesystem::path& path) { + auto filename = path.native(); + if (!filename.starts_with(L"\\\\?\\") && !filename.starts_with(L"\\\\.\\")) { + std::error_code pathError; + auto absolutePath = std::filesystem::absolute(path, pathError); + if (pathError) { errno = pathError.default_error_condition().value(); return std::nullopt; } + const auto absolute = absolutePath.lexically_normal().make_preferred().native(); + if (absolute.size() >= MAX_PATH) + filename = absolute.starts_with(L"\\\\") ? L"\\\\?\\UNC\\" + absolute.substr(2) : L"\\\\?\\" + absolute; + } + return filename; +} #endif static std::FILE* OpenNative(const std::filesystem::path& path, const std::string& nativeMode) { @@ -76,16 +89,9 @@ static std::FILE* OpenNative(const std::filesystem::path& path, const std::strin const DWORD disposition = access == 'r' ? OPEN_EXISTING : nativeMode.find('x') != std::string::npos ? CREATE_NEW : access == 'w' ? CREATE_ALWAYS : OPEN_ALWAYS; SECURITY_ATTRIBUTES security{sizeof(SECURITY_ATTRIBUTES), nullptr, TRUE}; - auto filename = path.native(); - if (!filename.starts_with(L"\\\\?\\") && !filename.starts_with(L"\\\\.\\")) { - std::error_code pathError; - auto absolutePath = std::filesystem::absolute(path, pathError); - if (pathError) { errno = pathError.default_error_condition().value(); return nullptr; } - const auto absolute = absolutePath.lexically_normal().make_preferred().native(); - if (absolute.size() >= MAX_PATH) - filename = absolute.starts_with(L"\\\\") ? L"\\\\?\\UNC\\" + absolute.substr(2) : L"\\\\?\\" + absolute; - } - const auto handle = CreateFileW(filename.c_str(), desired, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, &security, + const auto filename = HostOpenName(path); + if (!filename) return nullptr; + const auto handle = CreateFileW(filename->c_str(), desired, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, &security, disposition, FILE_ATTRIBUTE_NORMAL, nullptr); if (handle == INVALID_HANDLE_VALUE) { errno = WindowsOpenError_nid_no_patch(GetLastError()); @@ -307,6 +313,47 @@ static int OpenFailure(const std::filesystem::path& path, bool writes, int hostE return writes && error == 13 && std::filesystem::is_directory(path, status) ? 21 : error; } +#ifdef _WIN32 +static int (*g_openDirectoryDescriptor)(const wchar_t*) = nullptr; +static void (*g_forgetDirectoryDescriptor)(int) = nullptr; + +void LibcRegisterDirectoryStreams_nid_no_patch(int (*open)(const wchar_t*), void (*forget)(int)) { + g_openDirectoryDescriptor = open; + g_forgetDirectoryDescriptor = forget; +} + +void ForgetDirectoryStream_nid_no_patch(int descriptor) { + if (!g_forgetDirectoryDescriptor) throw std::runtime_error("ForgetDirectoryStream: libkernel has not registered its directory descriptors"); + g_forgetDirectoryDescriptor(descriptor); +} + +static FileStream* OpenDirectoryStream(const std::filesystem::path& path) { + if (!g_openDirectoryDescriptor) throw std::runtime_error("fopen: libkernel has not registered its directory descriptors"); + const auto filename = HostOpenName(path); + if (!filename) { errno = GuestErrnoFromHost_nid_no_patch(errno); return nullptr; } + const auto release = [](void* pointer) { ::operator delete(pointer); }; + std::unique_ptr storage(::operator new(sizeof(FileStream)), release); + const int descriptor = g_openDirectoryDescriptor(filename->c_str()); + if (descriptor < 0) { + const int error = GuestErrnoFromHost_nid_no_patch(errno); + errno = error == 2 ? NotFoundError_nid_no_patch(path) : error; + return nullptr; + } + auto* native = ::_fdopen(descriptor, "rb"); + if (!native) { + const int error = GuestErrnoFromHost_nid_no_patch(errno); + ForgetDirectoryStream_nid_no_patch(descriptor); + ::_close(descriptor); + errno = error; + return nullptr; + } + auto* stream = new (storage.get()) FileStream(native, true); + storage.release(); + stream->MarkDirectory(); + return stream; +} +#endif + FileStream* APS5_VABI freopen_nid_postfix(const char* filename, const char* mode, FileStream* stream) { if (!stream || !mode) { errno = 22; return nullptr; } if (!filename) { errno = 45; return nullptr; } // Mode-only reopening is not supported. @@ -370,7 +417,13 @@ FileStream* APS5_VABI fopen_nid_postfix(const char* filename, const char* mode) if (const int error = ResolvePathError_nid_no_patch(filename, fpath)) { errno = error; return nullptr; } std::unique_ptr handle(OpenNative(fpath, parsed->native), std::fclose); if (!handle) { - errno = OpenFailure(fpath, parsed->writes, errno); + const int hostError = errno; +#ifdef _WIN32 + std::error_code status; + if (!parsed->writes && GuestErrnoFromHost_nid_no_patch(hostError) == 13 && std::filesystem::is_directory(fpath, status)) + return OpenDirectoryStream(fpath); +#endif + errno = OpenFailure(fpath, parsed->writes, hostError); if (errno == 2) errno = NotFoundError_nid_no_patch(fpath); return nullptr; } @@ -428,11 +481,11 @@ FileStream* APS5_VABI _ZSt7_FiopenPKcNSt5_IosbIiE9_OpenmodeEi_nid_postfix(const } size_t APS5_VABI fread_nid_postfix(void* buffer, size_t size, size_t count, FileStream* stream) { + if (size != 0 && count > std::numeric_limits::max() / size) { errno = EINVAL; return 0; } if (stream && !stream->IsOpen()) { errno = 9; return 0; } auto* handle = GetStreamHandle(stream); if (size == 0 || count == 0) return 0; if (!buffer) throw std::runtime_error("fread: null buffer"); - if (count > std::numeric_limits::max() / size) throw std::overflow_error("fread: buffer size overflow"); const auto bytes = size * count; if (bytes > std::numeric_limits::max() - reinterpret_cast(buffer)) throw std::overflow_error("fread: buffer address overflow"); const GuestArena::HostWrite destination(buffer, bytes); @@ -445,6 +498,7 @@ size_t APS5_VABI fread_nid_postfix(void* buffer, size_t size, size_t count, File if (!stream->SwitchToReading()) return 0; const auto result = std::fread(buffer, size, count, handle); stream->SyncStatus(); + stream->KeepDirectoryReadError(result < count); return result; } @@ -529,7 +583,7 @@ int APS5_VABI fputs_nid_postfix(const char* str, FileStream* stream) { int APS5_VABI fflush_nid_postfix(FileStream* stream) { if (stream && !stream->IsOpen()) { errno = 9; return -1; } std::FILE* native = stream ? GetStreamHandle(stream) : nullptr; - if (stream && stream->IsRandomDevice()) return 0; + if (stream && (stream->IsRandomDevice() || stream->IsDirectory())) return 0; #ifdef __linux__ if (native && !__fwriting(native)) return 0; #endif diff --git a/core/libs/prx/libc/src/Formatting.cpp b/core/libs/prx/libc/src/Formatting.cpp index 07fc5468a8..5a35df68eb 100644 --- a/core/libs/prx/libc/src/Formatting.cpp +++ b/core/libs/prx/libc/src/Formatting.cpp @@ -361,6 +361,13 @@ int APS5_VABI fprintf_nid_postfix(FileStream* stream, const char* format, ...) { return result; } +static bool SizeOverflowsInt(char* buffer, size_t size) { + if (size == 0 || size - 1 <= static_cast(INT_MAX)) return false; + errno = 84; + if (buffer != nullptr) *buffer = '\0'; + return true; +} + int APS5_VABI fscanf_nid_postfix(FileStream* stream, const char* format, ...) { if (stream && !stream->IsOpen()) { errno = 9; return -1; } auto* native = GetNativeStream(stream); @@ -378,6 +385,7 @@ int APS5_VABI fscanf_nid_postfix(FileStream* stream, const char* format, ...) { va_end(args); #endif stream->SyncStatus(); + stream->KeepDirectoryReadError(result == -1); return result; } @@ -402,6 +410,7 @@ int APS5_VABI libc_printf_nid_postfix(const char* format, ...) { } int APS5_VABI snprintf_nid_postfix(char* buffer, size_t size, const char* format, ...) { + if (SizeOverflowsInt(buffer, size)) return -1; __builtin_sysv_va_list args; __builtin_sysv_va_start(args, format); const int result = LibcDetail::FormatWindows(buffer, size, format, args); @@ -438,6 +447,7 @@ int APS5_VABI libc_printf_nid_postfix(const char* format, ...) { } int APS5_VABI snprintf_nid_postfix(char* buffer, size_t size, const char* format, ...) { + if (SizeOverflowsInt(buffer, size)) return -1; std::va_list args; va_start(args, format); const int result = RequiresGuestFormatting(format) ? LibcDetail::FormatWindows(buffer, size, format, args) : std::vsnprintf(buffer, size, format, args); @@ -540,6 +550,7 @@ int APS5_VABI vsprintf_nid_postfix(char* str, const char* format, VaList* args) } int APS5_VABI vsnprintf_nid_postfix(char* str, size_t size, const char* format, VaList* c) { + if (SizeOverflowsInt(str, size)) return -1; #ifdef _WIN32 return LibcDetail::FormatWindows(str, size, format, c); #else @@ -656,6 +667,7 @@ int APS5_VABI vfscanf_nid_postfix(FileStream* stream, const char* format, VaList va_end(copy); #endif stream->SyncStatus(); + stream->KeepDirectoryReadError(result == -1); return result; } diff --git a/core/libs/prx/libc/src/LocaleSupport.cpp b/core/libs/prx/libc/src/LocaleSupport.cpp index a4388c8dfe..fb1f8042ab 100644 --- a/core/libs/prx/libc/src/LocaleSupport.cpp +++ b/core/libs/prx/libc/src/LocaleSupport.cpp @@ -298,6 +298,9 @@ std::uint64_t _ZNSt7codecvtIcc9_MbstatetE2idE_nid_postfix = 0; std::uintptr_t _ZTVSt7codecvtIcc9_MbstatetE_nid_postfix[16] {}; std::uint64_t _ZNSt7num_putIcSt19ostreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix = 0; std::uintptr_t _ZTVSt7num_putIcSt19ostreambuf_iteratorIcSt11char_traitsIcEEE_nid_postfix[12] {}; +std::uint64_t _ZNSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix = 0; +std::uintptr_t _ZTVSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE_nid_postfix[17] {}; +std::uint64_t _ZNSt8time_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix = 0; std::streamoff _ZSt7_BADOFF_nid_postfix = -1; std::fpos_t _ZSt4_Fpz_nid_postfix {}; diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index bfed7a8a4d..37616b7be3 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -750,6 +750,26 @@ double APS5_VABI _Sinh_nid_postfix(double x, double y) { return SonySinh(x, y); } +double APS5_VABI _Cosh_nid_postfix(double x, double y) { + if (!std::isfinite(x) || !std::isfinite(y)) return y * std::cosh(x); + if (y == 0.0) return std::copysign(0.0, y); + constexpr double Ln2High = 0x1.62e42fefa0000p-1; + constexpr double Ln2Low = 0x1.cf79abc9e3b3ap-40; + constexpr double InverseLn2 = 0x1.71547652b82fep+0; + const double magnitude = std::abs(x); + if (magnitude <= (std::numeric_limits::max_exponent - 1) * (Ln2High + Ln2Low)) + return y * std::cosh(x); + int scaleExponent; + const double scale = std::frexp(y, &scaleExponent); + constexpr int MaximumExponentSpan = std::numeric_limits::max_exponent - + std::numeric_limits::min_exponent + std::numeric_limits::digits + 4; + if (magnitude > MaximumExponentSpan * (Ln2High + Ln2Low)) + return std::scalbn(scale, INT_MAX); + const int exponent = static_cast(magnitude * InverseLn2); + const double reduced = std::fma(-exponent, Ln2Low, std::fma(-exponent, Ln2High, magnitude)); + return std::scalbn(scale * (0.5 * std::exp(reduced)), scaleExponent + exponent); +} + int APS5_VABI _Fltrounds_nid_postfix(void) { return SonyFltrounds(); } diff --git a/core/libs/prx/libc/src/ScanningWide.cpp b/core/libs/prx/libc/src/ScanningWide.cpp index 0b95de6e78..759e475c90 100644 --- a/core/libs/prx/libc/src/ScanningWide.cpp +++ b/core/libs/prx/libc/src/ScanningWide.cpp @@ -127,8 +127,9 @@ Fit FloatFit(std::string_view text) { if (digits == 0) return Fit::Invalid; if (Lower(text[index]) != (hex ? 'p' : 'e')) return Fit::Invalid; ++index; - if (index < text.size() && (text[index] == '+' || text[index] == '-')) ++index; - if (index == text.size()) return Fit::Prefix; + if (index == text.size()) return Fit::Valid; + if (text[index] == '+' || text[index] == '-') ++index; + if (index == text.size()) return Fit::Valid; for (; index < text.size(); ++index) { if (!IsDecimal(text[index])) return Fit::Invalid; } @@ -256,7 +257,7 @@ private: } Outcome Literal(char16_t expected) { - if (input.Peek() < 0) return Outcome::Input; + if (input.Peek() < 0) return Outcome::Matching; if (input.Peek() != expected) return Outcome::Matching; input.Advance(); return Outcome::Done; @@ -303,6 +304,7 @@ private: if (*format == u'%') { ++format; SkipSpace(); + if (input.Peek() < 0) return Outcome::Input; return Literal(u'%'); } const Directive directive = Parse(); diff --git a/core/libs/prx/libc/src/StandardStreams.cpp b/core/libs/prx/libc/src/StandardStreams.cpp index 03cdaed4cd..2cd656f01f 100644 --- a/core/libs/prx/libc/src/StandardStreams.cpp +++ b/core/libs/prx/libc/src/StandardStreams.cpp @@ -39,6 +39,7 @@ int APS5_VABI fgetc_nid_postfix(FileStream* stream) { if (!stream->SwitchToReading()) return -1; const int result = std::fgetc(native); stream->SyncStatus(); + stream->KeepDirectoryReadError(result == -1); return result; } std::int32_t APS5_VABI fgetwc_nid_postfix(FileStream* stream) { @@ -94,6 +95,7 @@ int APS5_VABI getc_unlocked_nid_postfix(FileStream* stream) { const int result = ::getc_unlocked(native); #endif stream->SyncUnlockedStatus(); + stream->KeepDirectoryReadError(result == -1); return result; } int APS5_VABI __srget_nid_postfix(FileStream* stream) { return fgetc_nid_postfix(stream); } @@ -140,6 +142,7 @@ char* APS5_VABI fgets_nid_postfix(char* buffer, int size, FileStream* stream) { if (!stream->SwitchToReading()) return nullptr; auto* result = std::fgets(buffer, size, native); stream->SyncStatus(); + stream->KeepDirectoryReadError(result == nullptr); return result; } int APS5_VABI feof_nid_postfix(FileStream* stream) { diff --git a/core/libs/prx/libc/src/Strings.cpp b/core/libs/prx/libc/src/Strings.cpp index 55ee19e5b1..b3d173267a 100644 --- a/core/libs/prx/libc/src/Strings.cpp +++ b/core/libs/prx/libc/src/Strings.cpp @@ -467,6 +467,12 @@ int APS5_VABI strncpy_s_nid_postfix(char* dest, size_t destsz, const char* src, dest[0] = '\0'; return GuestErange; } + const auto destination = reinterpret_cast(dest); + const auto source = reinterpret_cast(src); + if (destination < source + (length + 1) && source < destination + length + 1) { + dest[0] = '\0'; + return GuestEinval; + } std::memcpy(dest, src, length); dest[length] = '\0'; return 0; @@ -494,6 +500,12 @@ int APS5_VABI strncat_s_nid_postfix(char* dest, size_t destsz, const char* src, dest[0] = '\0'; return GuestErange; } + const auto destination = reinterpret_cast(dest); + const auto source = reinterpret_cast(src); + if (destination < source + (length + 1) && source < destination + used + length + 1) { + dest[0] = '\0'; + return GuestEinval; + } std::memcpy(dest + used, src, length); dest[used + length] = '\0'; return 0; diff --git a/core/libs/prx/libc/src/exception/Unwind.cpp b/core/libs/prx/libc/src/exception/Unwind.cpp index c86389adfe..94226226e0 100644 --- a/core/libs/prx/libc/src/exception/Unwind.cpp +++ b/core/libs/prx/libc/src/exception/Unwind.cpp @@ -105,14 +105,21 @@ extern "C" void _Unwind_DeleteException(_Unwind_Exception*); #endif namespace LibcUnwind { -_Unwind_Reason_Code CallPersonality(Word personality, _Unwind_Action actions, _Unwind_Exception* exception, _Unwind_Context* context) { #ifdef _WIN32 - const auto* code = reinterpret_cast(personality); +static Word ResolveImport(Word address) { + const auto* code = reinterpret_cast(address); if (code[0] == 0xff && code[1] == 0x25) { std::int32_t displacement; - std::memcpy(&displacement, code + 2, 4); - std::memcpy(&personality, code + 6 + displacement, sizeof(personality)); + std::memcpy(&displacement, code + 2, sizeof(displacement)); + std::memcpy(&address, code + 6 + displacement, sizeof(address)); } + return address; +} +#endif + +_Unwind_Reason_Code CallPersonality(Word personality, _Unwind_Action actions, _Unwind_Exception* exception, _Unwind_Context* context) { +#ifdef _WIN32 + personality = ResolveImport(personality); #endif if (personality == reinterpret_cast(__gxx_personality_v0_nid_postfix) || personality == reinterpret_cast(__gxx_personality_v0)) return __gxx_personality_v0_nid_postfix(1, actions, exception->exception_class, exception, context); @@ -257,14 +264,20 @@ const IMAGE_NT_HEADERS64* ImageHeaders(Word pc, const Byte*& base) { return nt->Signature == IMAGE_NT_SIGNATURE ? nt : nullptr; } -bool VirtualUnwind(const _Unwind_Context& context, _Unwind_Context& caller) { +bool VirtualUnwind(_Unwind_Context& context, Frame& frame) { const Word pc = context.registers[16]; const Byte* base = nullptr; const auto* nt = ImageHeaders(pc, base); if (!nt) return false; const auto* sections = IMAGE_FIRST_SECTION(nt); - for (unsigned i = 0; i < nt->FileHeader.NumberOfSections; ++i) - if (std::memcmp(sections[i].Name, ".ehmeta", 8) == 0 || std::memcmp(sections[i].Name, ".ehfram", 8) == 0) return false; + const PEXCEPTION_ROUTINE* personalities = nullptr; + bool dwarf = false; + for (unsigned i = 0; i < nt->FileHeader.NumberOfSections; ++i) { + if (std::memcmp(sections[i].Name, ".ehmeta", 8) == 0 || std::memcmp(sections[i].Name, ".ehfram", 8) == 0) dwarf = true; + if (std::memcmp(sections[i].Name, ".sehgcc", 8) == 0 && sections[i].Misc.VirtualSize >= 2 * sizeof(PEXCEPTION_ROUTINE)) + personalities = reinterpret_cast(base + sections[i].VirtualAddress); + } + if (dwarf && !personalities) return false; DWORD64 imageBase = 0; auto* function = RtlLookupFunctionEntry(pc, &imageBase, nullptr); if (!function) return false; @@ -275,11 +288,27 @@ bool VirtualUnwind(const _Unwind_Context& context, _Unwind_Context& caller) { for (unsigned i = 0; i < 16; ++i) std::memcpy(&state.FltSave.XmmRegisters[i], context.vectorRegisters[i], 16); void* handlerData = nullptr; DWORD64 establisher = 0; - if (RtlVirtualUnwind(UNW_FLAG_EHANDLER | UNW_FLAG_UHANDLER, imageBase, pc, function, &state, &handlerData, &establisher, nullptr)) return false; + const auto handler = RtlVirtualUnwind(UNW_FLAG_EHANDLER | UNW_FLAG_UHANDLER, imageBase, pc, function, &state, &handlerData, &establisher, nullptr); + frame.start = imageBase + function->BeginAddress; + if (handler) { + if (!personalities) return false; + const Word resolved = ResolveImport(reinterpret_cast(handler)); + if (resolved == ResolveImport(reinterpret_cast(personalities[0]))) frame.personality = reinterpret_cast(__gxx_personality_v0_nid_postfix); + else if (resolved == ResolveImport(reinterpret_cast(personalities[1]))) frame.personality = reinterpret_cast(__gcc_personality_v0_nid_postfix); + else return false; + frame.lsda = reinterpret_cast(handlerData); + } + auto& caller = frame.caller; caller = context; for (unsigned i = 0; i < 17; ++i) caller.registers[i] = *registers[i]; for (unsigned i = 0; i < 16; ++i) std::memcpy(caller.vectorRegisters[i], &state.FltSave.XmmRegisters[i], 16); caller.signalFrame = false; + context.cfa = caller.registers[7]; + context.region = frame.start; + context.lsda = frame.lsda; + context.personality = frame.personality; + context.textBase = imageBase; + context.dataBase = 0; return true; } #endif @@ -414,10 +443,8 @@ bool GetRules(_Unwind_Context& context, Frame& frame, Rules& rules) { if (!DecodeFrame(context, frame)) { #ifdef _WIN32 frame = {}; - if (!VirtualUnwind(context, frame.caller)) return false; + if (!VirtualUnwind(context, frame)) return false; frame.foreign = true; - context.cfa = frame.caller.registers[7]; - context.region = context.lsda = context.personality = context.textBase = context.dataBase = 0; return true; #else return false; diff --git a/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp b/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp index 7b4cc729ed..1d031c17a1 100644 --- a/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp +++ b/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp @@ -235,6 +235,14 @@ constexpr DWORD GccException = 0x20474343u; constexpr std::uint64_t GccExceptionClass = 0x474e5543432b2b00ull; constexpr std::uint64_t CxaExceptionTypeOffset = 80; +PWSTR ThreadDescription(HANDLE thread) { + using GetDescription = HRESULT (WINAPI *)(HANDLE, PWSTR*); + static const auto getDescription = reinterpret_cast(GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "GetThreadDescription")); + PWSTR description = nullptr; + if (!getDescription || FAILED(getDescription(thread, &description))) return nullptr; + return description; +} + std::uint64_t FrameWithoutUnwindData(const CONTEXT& start) { const HMODULE runtime[] = {GetModuleHandleA("KERNELBASE.dll"), GetModuleHandleA("libgcc_s_seh-1.dll"), GetModuleHandleA("libstdc++-6.dll")}; CONTEXT context = start; @@ -363,8 +371,7 @@ LONG WINAPI ReportCrash(EXCEPTION_POINTERS* info) { const auto* context = info->ContextRecord; char line[MAX_PATH + 64]; char threadName[128] = ""; - PWSTR description = nullptr; - if (SUCCEEDED(GetThreadDescription(GetCurrentThread(), &description)) && description) { + if (PWSTR description = ThreadDescription(GetCurrentThread())) { WideCharToMultiByte(CP_UTF8, 0, description, -1, threadName, sizeof(threadName), nullptr, nullptr); LocalFree(description); } @@ -582,8 +589,7 @@ void ReportAllThreads() { const std::size_t candidateCount = haveContext && frameCount < 2 ? CaptureStackCandidates(context.Rsp, candidates, sizeof(candidates) / sizeof(candidates[0])) : 0; ResumeThread(thread); char name[128] = ""; - PWSTR description = nullptr; - if (SUCCEEDED(GetThreadDescription(thread, &description)) && description) { + if (PWSTR description = ThreadDescription(thread)) { WideCharToMultiByte(CP_UTF8, 0, description, -1, name, sizeof(name), nullptr, nullptr); LocalFree(description); } diff --git a/core/libs/prx/libc/src/specifics/windows/SehPersonalities.cpp b/core/libs/prx/libc/src/specifics/windows/SehPersonalities.cpp new file mode 100644 index 0000000000..cd4c856063 --- /dev/null +++ b/core/libs/prx/libc/src/specifics/windows/SehPersonalities.cpp @@ -0,0 +1,11 @@ +#include + +extern "C" { +EXCEPTION_DISPOSITION __gxx_personality_seh0(EXCEPTION_RECORD*, void*, CONTEXT*, void*); +EXCEPTION_DISPOSITION __gcc_personality_seh0(EXCEPTION_RECORD*, void*, CONTEXT*, void*); + +[[gnu::section(".sehgcc"), gnu::used]] extern const PEXCEPTION_ROUTINE LibcSehPersonalities_nid_no_patch[] { + __gxx_personality_seh0, + __gcc_personality_seh0, +}; +} diff --git a/core/libs/prx/libc/src/specifics/x86_64/CallTrace.cpp b/core/libs/prx/libc/src/specifics/x86_64/CallTrace.cpp index 96d6c872a9..c81b91a39a 100644 --- a/core/libs/prx/libc/src/specifics/x86_64/CallTrace.cpp +++ b/core/libs/prx/libc/src/specifics/x86_64/CallTrace.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -117,6 +118,7 @@ struct ModuleSegment { struct ModuleMap { std::size_t count; + std::uint64_t pageSize; ModuleSegment segments[256]; }; @@ -135,16 +137,20 @@ void SnapshotModules() { if (changes == g_moduleChanges) return; g_moduleChanges = changes; auto* map = new ModuleMap{}; + map->pageSize = static_cast(sysconf(_SC_PAGESIZE)); dl_iterate_phdr([](dl_phdr_info* info, std::size_t, void* data) { auto& modules = *static_cast(data); const char* path = info->dlpi_name != nullptr && *info->dlpi_name != '\0' ? info->dlpi_name : program_invocation_short_name; const char* slash = std::strrchr(path, '/'); const char* name = slash == nullptr ? path : slash + 1; + std::uint64_t first = std::numeric_limits::max(); + for (int index = 0; index < info->dlpi_phnum; ++index) + if (info->dlpi_phdr[index].p_type == PT_LOAD) first = std::min(first, info->dlpi_phdr[index].p_vaddr & ~(modules.pageSize - 1)); for (int index = 0; index < info->dlpi_phnum && modules.count < std::size(modules.segments); ++index) { const auto& header = info->dlpi_phdr[index]; if (header.p_type != PT_LOAD || header.p_memsz == 0) continue; auto& segment = modules.segments[modules.count++]; - segment.base = info->dlpi_addr; + segment.base = info->dlpi_addr + first; segment.start = info->dlpi_addr + header.p_vaddr; segment.end = segment.start + header.p_memsz; std::strncpy(segment.name, name, sizeof(segment.name) - 1); diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 635db8b212..1b9c5d5348 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -1033,7 +1033,7 @@ if(BUILD_TESTING) target_link_libraries(guest_raise_exception_tests PRIVATE libkernel libc) configure_windows_unwind(guest_raise_exception_tests) add_test(NAME guest_raise_exception COMMAND guest_raise_exception_tests) - set_tests_properties(guest_raise_exception PROPERTIES TIMEOUT 30) + set_tests_properties(guest_raise_exception PROPERTIES TIMEOUT 60) add_asan_test_options(guest_raise_exception detect_stack_use_after_return=0) if(WIN32) set_tests_properties(guest_raise_exception PROPERTIES ENVIRONMENT_MODIFICATION @@ -1706,6 +1706,13 @@ if(BUILD_TESTING) set_tests_properties(guest_directory_rewind PROPERTIES TIMEOUT 15 ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + add_test_executable(guest_directory_link_type_tests ${LIBS_INCLUDE_DIR}/tests/GuestDirectoryLinkTypes.cpp) + target_include_directories(guest_directory_link_type_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_directory_link_type_tests PRIVATE libkernel libc) + add_test(NAME guest_directory_link_types COMMAND guest_directory_link_type_tests) + set_tests_properties(guest_directory_link_types PROPERTIES TIMEOUT 15) + endif() add_test_executable(guest_directory_entry_tests ${LIBS_INCLUDE_DIR}/tests/GuestDirectoryEntries.cpp) target_include_directories(guest_directory_entry_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_directory_entry_tests PRIVATE libkernel libc) @@ -1836,7 +1843,7 @@ if(BUILD_TESTING) target_include_directories(guest_file_lock_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_file_lock_tests PRIVATE libkernel libc) configure_windows_unwind(guest_file_lock_tests) - foreach(fileLockCase IN ITEMS duplicate lifetime conversion threads streams process) + foreach(fileLockCase IN ITEMS duplicate lifetime conversion threads streams process directory) add_test(NAME guest_file_lock_${fileLockCase} COMMAND guest_file_lock_tests ${fileLockCase}) set_tests_properties(guest_file_lock_${fileLockCase} PROPERTIES TIMEOUT 20 ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") diff --git a/core/libs/prx/libkernel/Equeue/src/EventAccessors.cpp b/core/libs/prx/libkernel/Equeue/src/EventAccessors.cpp index e72bfe3900..fb42c6e073 100644 --- a/core/libs/prx/libkernel/Equeue/src/EventAccessors.cpp +++ b/core/libs/prx/libkernel/Equeue/src/EventAccessors.cpp @@ -21,7 +21,13 @@ intptr_t APS5_VABI sceKernelGetEventData(const KernelEvent* ev) { int APS5_VABI sceKernelGetEventError(const KernelEvent* ev) { const KernelEvent& event = requireEvent(ev, __func__); - return (event.flags & EV_ERROR) != 0 ? static_cast(event.data) : 0; + if (event.fflags == 0) { + return 0; + } + if (event.fflags > 0xFFFFu) { + throw std::runtime_error(std::string(__func__) + ": fflags " + std::to_string(event.fflags) + " above 0xFFFF"); + } + return static_cast(0x80020000u | event.fflags); } intptr_t APS5_VABI sceKernelGetEventFflags(const KernelEvent* ev) { diff --git a/core/libs/prx/libkernel/Equeue/src/FileEvent.cpp b/core/libs/prx/libkernel/Equeue/src/FileEvent.cpp index 04fe98539f..cd24ff30d0 100644 --- a/core/libs/prx/libkernel/Equeue/src/FileEvent.cpp +++ b/core/libs/prx/libkernel/Equeue/src/FileEvent.cpp @@ -165,6 +165,16 @@ struct Snapshot { }; bool TakeSnapshot(HANDLE handle, Snapshot& snapshot) { + std::wstring path(MAX_PATH, L'\0'); + auto length = ::GetFinalPathNameByHandleW(handle, path.data(), static_cast(path.size()), FILE_NAME_NORMALIZED); + if (length >= path.size()) { + path.resize(length); + length = ::GetFinalPathNameByHandleW(handle, path.data(), length, FILE_NAME_NORMALIZED); + } + if (length != 0 && length < path.size()) { + path.resize(length); + snapshot.path = std::move(path); + } FILE_BASIC_INFO basic{}; FILE_STANDARD_INFO standard{}; if (!::GetFileInformationByHandleEx(handle, FileBasicInfo, &basic, sizeof(basic)) || @@ -178,16 +188,6 @@ bool TakeSnapshot(HANDLE handle, Snapshot& snapshot) { snapshot.creation = basic.CreationTime.QuadPart; snapshot.lastWrite = basic.LastWriteTime.QuadPart; snapshot.change = basic.ChangeTime.QuadPart; - std::wstring path(MAX_PATH, L'\0'); - auto length = ::GetFinalPathNameByHandleW(handle, path.data(), static_cast(path.size()), FILE_NAME_NORMALIZED); - if (length >= path.size()) { - path.resize(length); - length = ::GetFinalPathNameByHandleW(handle, path.data(), length, FILE_NAME_NORMALIZED); - } - if (length != 0 && length < path.size()) { - path.resize(length); - snapshot.path = std::move(path); - } return true; } diff --git a/core/libs/prx/libkernel/File/include/DescriptorControl.hpp b/core/libs/prx/libkernel/File/include/DescriptorControl.hpp index 341324638c..4358f8e67d 100644 --- a/core/libs/prx/libkernel/File/include/DescriptorControl.hpp +++ b/core/libs/prx/libkernel/File/include/DescriptorControl.hpp @@ -17,7 +17,10 @@ void ShareDescriptor(int from, int to); void ForgetDescriptor(int fd); bool Appends(int fd); bool IsVnode(int fd); +bool IsCharacterDevice(int fd); } +extern "C" bool DescriptorHasNoVnode_nid_no_patch(int fd); + #endif diff --git a/core/libs/prx/libkernel/File/include/WindowsSymbolicLink.hpp b/core/libs/prx/libkernel/File/include/WindowsSymbolicLink.hpp index cdc089e731..5d3e3bb584 100644 --- a/core/libs/prx/libkernel/File/include/WindowsSymbolicLink.hpp +++ b/core/libs/prx/libkernel/File/include/WindowsSymbolicLink.hpp @@ -47,6 +47,20 @@ class WindowsSymbolicLink { } public: + static bool IsNameSurrogate(const std::filesystem::path& native, const char* operation) { + const HANDLE handle = CreateFileW(native.c_str(), FILE_READ_ATTRIBUTES, 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) + throw std::system_error(static_cast(GetLastError()), std::system_category(), std::string(operation) + ": CreateFileW"); + FILE_ATTRIBUTE_TAG_INFO information{}; + const BOOL read = GetFileInformationByHandleEx(handle, FileAttributeTagInfo, &information, sizeof(information)); + const DWORD code = GetLastError(); + CloseHandle(handle); + if (!read) + throw std::system_error(static_cast(code), std::system_category(), std::string(operation) + ": GetFileInformationByHandleEx"); + return (information.FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0 && IsReparseTagNameSurrogate(information.ReparseTag); + } + static bool Read(const std::filesystem::path& native, std::string& target, const char* operation) { const HANDLE handle = CreateFileW(native.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); diff --git a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp index a83f899801..d9cf12e8bc 100644 --- a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp +++ b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp @@ -3,6 +3,7 @@ #include "prx/libkernel/File/include/File.hpp" #include "prx/libkernel/File/include/RandomDevice.hpp" #include "prx/libkernel/File/include/ArchivedFile.hpp" +#include "prx/libkernel/Socket/include/SocketRuntime.hpp" #include "prx/libc/include/GuestArena.hpp" #include "prx/libc/include/FileLock.hpp" #include "prx/libc/include/general/VabiMacros.hpp" @@ -583,6 +584,16 @@ bool Appends(int fd) { return found != g_entries.end() && found->second.status && (*found->second.status & Append) != 0; } +bool IsCharacterDevice(int fd) { + if (fd < 0) return false; +#ifdef _WIN32 + return DescriptorIsOpen_nid_no_patch(fd) && !GuestSockets::IsOpen(fd) && !File::DirectoryDescriptorPath(fd) && ::GetFileType(WindowsDescriptorHandle_nid_no_patch(fd)) == FILE_TYPE_CHAR; +#else + struct stat status{}; + return ::fstat(fd, &status) == 0 && S_ISCHR(status.st_mode); +#endif +} + bool IsVnode(int fd) { if (fd < 0 || File::IsRandomDevice(fd)) return false; if (File::IsArchivedFile(fd)) return true; @@ -596,3 +607,7 @@ bool IsVnode(int fd) { } } + +extern "C" bool DescriptorHasNoVnode_nid_no_patch(int fd) { + return !File::IsVnode(fd) && !File::IsCharacterDevice(fd) && !File::IsRandomDevice(fd); +} diff --git a/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp b/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp index 7727bb9000..e140a6b0f4 100644 --- a/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp +++ b/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp @@ -8,6 +8,7 @@ #ifdef _WIN32 +#include "prx/libc/include/FileStream.hpp" #include "prx/libc/include/General.hpp" #include @@ -63,6 +64,19 @@ void Load(DirectoryState& state, int fd) { } } +int OpenStreamDirectory(const wchar_t* path) { + return File::OpenDirectoryDescriptor(path); +} + +struct StreamRegistration { + StreamRegistration() { LibcRegisterDirectoryStreams_nid_no_patch(&OpenStreamDirectory, &File::ForgetDirectoryDescriptor); } + ~StreamRegistration() { LibcRegisterDirectoryStreams_nid_no_patch(nullptr, nullptr); } + StreamRegistration(const StreamRegistration&) = delete; + StreamRegistration& operator=(const StreamRegistration&) = delete; +}; + +const StreamRegistration g_streamRegistration; + } namespace File { diff --git a/core/libs/prx/libkernel/File/src/NativeStat.cpp b/core/libs/prx/libkernel/File/src/NativeStat.cpp index 75e7d82619..1fda5aabf7 100644 --- a/core/libs/prx/libkernel/File/src/NativeStat.cpp +++ b/core/libs/prx/libkernel/File/src/NativeStat.cpp @@ -53,15 +53,20 @@ static int DoFstat(int fd, NativeStat* st) { static int DoLstat(const std::filesystem::path& p, NativeStat* st) { const auto attributes = GetFileAttributesW(p.c_str()); if (attributes == INVALID_FILE_ATTRIBUTES || (attributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0) return DoStat(p, st); + if (!File::WindowsSymbolicLink::IsNameSurrogate(p, "lstat")) return DoStat(p, st); std::string target; - if (!File::WindowsSymbolicLink::Read(p, target, "lstat") && !File::WindowsSymbolicLink::ReadJunction(p, target, "lstat")) - NotImplemented_nid_no_patch("lstat of a Windows reparse point other than a symbolic link"); + const bool link = File::WindowsSymbolicLink::Read(p, target, "lstat") || File::WindowsSymbolicLink::ReadJunction(p, target, "lstat"); const auto handle = CreateFileW(p.c_str(), FILE_READ_ATTRIBUTES, 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 File::WindowsFileTime::Failure(GetLastError()); const int result = ReadHandleInfo(handle, st); CloseHandle(handle); if (result != 0) return result; + if (!link) { + st->st_mode = (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0 ? (_S_IFDIR | 0111) : _S_IFREG; + st->st_mode |= (attributes & FILE_ATTRIBUTE_READONLY) != 0 ? 0444 : 0666; + return 0; + } st->st_mode = SymbolicLinkMode; st->st_size = static_cast<__int64>(target.size()); st->isSymbolicLink = true; diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index 0cd332ef01..417c79bd96 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -2,6 +2,7 @@ #include "prx/libkernel/File/include/NativeStat.hpp" #include "prx/libc/include/General.hpp" #include "prx/libc/include/FilesystemError.hpp" +#include "prx/libc/include/RenameDot.hpp" #include "prx/libc/include/GuestArena.hpp" #include "prx/libkernel/DirectMemory/DirectMemory.hpp" #include "prx/libkernel/File/include/File.hpp" @@ -352,6 +353,18 @@ std::int64_t APS5_VABI sceKernelWrite(int d, const void* buf, std::size_t nbytes return static_cast(n); } +static bool SeekOverflows(int d, std::int64_t offset, int whence) { + const std::int64_t current = NativeLseek(d, 0, 1); + if (current < 0) return false; + std::int64_t base = current; + if (whence == 2) { + base = NativeLseek(d, 0, 2); + NativeLseek(d, current, 0); + if (base < 0) return false; + } + return base > std::numeric_limits::max() - offset; +} + std::int64_t APS5_VABI sceKernelLseek(int d, std::int64_t offset, int whence) { if (GuestSockets::IsOpen(d)) return SCE_KERNEL_ERROR_ESPIPE; if (whence < 0 || whence > 2) { @@ -365,7 +378,11 @@ std::int64_t APS5_VABI sceKernelLseek(int d, std::int64_t offset, int whence) { } #endif std::int64_t result = NativeLseek(d, offset, whence); - if (result < 0) return SceIoErrorFromErrno(errno); + if (result < 0) { + const int error = errno; + if (error == EINVAL && offset > 0 && (whence == 1 || whence == 2) && SeekOverflows(d, offset, whence)) return SceIoErrorFromErrno(EOVERFLOW); + return SceIoErrorFromErrno(error); + } return result; } @@ -423,6 +440,7 @@ int StatInDirectory_nid_no_patch(int directory, const char* path, FileStat* sb, int APS5_VABI sceKernelUnlink(const char* path) { if (path == nullptr) return SceErrorFromErrno(GuestEfault); if (*path == '\0') return SceErrorFromErrno(2); + if (const int dot = RenameDotError_nid_no_patch(path)) return SceKernelError(dot); std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native)) return SceKernelError(error); constexpr int GuestEperm = 1; diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 2aece3834c..fa57cf492d 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -865,6 +865,9 @@ int APS5_VABI flock_nid_postfix(int d, int operation) { #endif const int type = operation & 8 ? 8 : operation & 2 ? 2 : operation & 1 ? 1 : 0; if (type == 0) return PosixFailure(GUEST_EBADF); +#ifdef _WIN32 + if (File::IsDirectoryDescriptor(d)) return 0; +#endif if (NativeFlock(d, type | (operation & 4)) != 0) { #ifdef _WIN32 const auto error = ::GetLastError(); @@ -1135,6 +1138,35 @@ int APS5_VABI sceKernelGetdents(int fd, char* buf, int nbytes) { #else +static bool InsideApp0(int fd) { + const int savedErrno = errno; + const bool inside = [fd] { + const auto directory = File::DirectoryDescriptorPath(fd); + if (!directory) return false; + std::error_code error; + const auto app0 = std::filesystem::weakly_canonical(ResolveDiskPath_nid_no_patch("/app0"), error); + if (error || app0.empty()) return false; + const auto relative = directory->lexically_relative(app0); + return !relative.empty() && *relative.begin() != ".."; + }(); + errno = savedErrno; + return inside; +} + +static unsigned char GuestEntryType(int fd, const struct dirent64& entry, bool followLinks) { + if (entry.d_type != DT_LNK && entry.d_type != DT_UNKNOWN) return entry.d_type; + if (entry.d_type == DT_LNK && !followLinks) return DT_LNK; + const int savedErrno = errno; + struct stat target{}; + const bool found = ::fstatat(fd, entry.d_name, &target, followLinks ? 0 : AT_SYMLINK_NOFOLLOW) == 0; + errno = savedErrno; + if (!found) return entry.d_type; + if (S_ISDIR(target.st_mode)) return DT_DIR; + if (S_ISREG(target.st_mode)) return DT_REG; + if (S_ISLNK(target.st_mode)) return DT_LNK; + return entry.d_type; +} + int APS5_VABI sceKernelGetdirentries(int fd, char* buf, int nbytes, int64_t* basep) { if (GuestSockets::IsOpen(fd)) return SceErrorFromErrno(GUEST_EINVAL); File::RefuseArchivedFile(fd, "getdirentries"); @@ -1156,6 +1188,7 @@ int APS5_VABI sceKernelGetdirentries(int fd, char* buf, int nbytes, int64_t* bas if (read < 0) return SceErrorFromErrno(errno); std::size_t written = 0; off_t resume = base; + const bool followLinks = InsideApp0(fd); for (long offset = 0; offset < read;) { const auto* entry = reinterpret_cast(host.data() + offset); const auto nameLength = std::strlen(entry->d_name); @@ -1172,7 +1205,7 @@ int APS5_VABI sceKernelGetdirentries(int fd, char* buf, int nbytes, int64_t* bas const auto nameBytes = static_cast(nameLength); std::memcpy(out, &fileNumber, sizeof(fileNumber)); std::memcpy(out + 4, &recordLength, sizeof(recordLength)); - out[6] = static_cast(entry->d_type); + out[6] = static_cast(GuestEntryType(fd, *entry, followLinks)); out[7] = static_cast(nameBytes); std::memcpy(out + GuestHeaderBytes, entry->d_name, nameLength); written += record; @@ -1463,6 +1496,7 @@ int APS5_VABI sceKernelRename(const char* from, const char* to) { int APS5_VABI sceKernelRmdir(const char* path) { if (!path) return SceErrorFromErrno(GUEST_EFAULT); if (*path == '\0') return SceErrorFromErrno(GUEST_ENOENT); + if (const int dot = RenameDotError_nid_no_patch(path)) return SceKernelError(dot); std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native)) return SceKernelError(error); std::error_code error; @@ -1472,7 +1506,6 @@ int APS5_VABI sceKernelRmdir(const char* path) { const bool blockedByFile = HasNonDirectoryPrefix(native) || std::filesystem::exists(WithoutTrailingSeparator(native), error); return SceErrorFromErrno(blockedByFile ? GUEST_ENOTDIR : GUEST_ENOENT); } - if (FinalPathComponentIsDot_nid_no_patch(path)) return SceErrorFromErrno(GUEST_EINVAL); if (!std::filesystem::is_empty(native, error)) return SceErrorFromErrno(GUEST_ENOTEMPTY); if (!RemoveEmptyDirectory(native)) return SceErrorFromErrno(GUEST_EIO); RecordWrittenPath_nid_no_patch(native); diff --git a/core/libs/prx/libkernel/Pthread/include/Pthread.hpp b/core/libs/prx/libkernel/Pthread/include/Pthread.hpp index 0cef7ba498..c4442bd188 100644 --- a/core/libs/prx/libkernel/Pthread/include/Pthread.hpp +++ b/core/libs/prx/libkernel/Pthread/include/Pthread.hpp @@ -182,5 +182,7 @@ std::unique_lock LockLiveGuestThread(const PthreadPrivate* thread); bool LiveGuestThreadExists(std::int64_t tid); bool GetLiveGuestThreadAffinity_nid_no_patch(std::int64_t tid, KernelCpumask& mask); bool SetLiveGuestThreadAffinity_nid_no_patch(std::int64_t tid, KernelCpumask mask); +extern "C" std::size_t GetDefaultStackSize_nid_no_patch(); +extern "C" bool SetDefaultStackSize_nid_no_patch(std::size_t size); #endif diff --git a/core/libs/prx/libkernel/Pthread/src/Attr.cpp b/core/libs/prx/libkernel/Pthread/src/Attr.cpp index ce6ccac93c..0b3b82078c 100644 --- a/core/libs/prx/libkernel/Pthread/src/Attr.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Attr.cpp @@ -12,6 +12,18 @@ static constexpr int DETACH_JOINABLE = 0; static constexpr int DETACH_DETACHED = 1; static constexpr int SCHED_FIFO_PS5 = 1; +static std::atomic defaultStackSize{DEFAULT_STACK_SIZE}; + +std::size_t GetDefaultStackSize_nid_no_patch() { + return defaultStackSize.load(std::memory_order_relaxed); +} + +bool SetDefaultStackSize_nid_no_patch(std::size_t size) { + if (size <= MIN_STACK_SIZE) return false; + defaultStackSize.store(size, std::memory_order_relaxed); + return true; +} + #ifdef _WIN32 #include #include @@ -23,7 +35,7 @@ int APS5_VABI scePthreadAttrInit(PthreadAttr* attr) { if (!attr) throw std::runtime_error(std::string(__func__) + ": null attr"); auto* p = new (std::nothrow) PthreadAttrPrivate{}; if (!p) return SCE_KERNEL_ERROR_ENOMEM; - p->_stacksize = DEFAULT_STACK_SIZE; + p->_stacksize = GetDefaultStackSize_nid_no_patch(); p->_detachstate = DETACH_JOINABLE; p->_schedpriority = 700; p->_schedpolicy = SCHED_FIFO_PS5; diff --git a/core/libs/prx/libkernel/Pthread/src/Cond.cpp b/core/libs/prx/libkernel/Pthread/src/Cond.cpp index 609acd98aa..a3a3087bf7 100644 --- a/core/libs/prx/libkernel/Pthread/src/Cond.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Cond.cpp @@ -19,7 +19,10 @@ namespace { constexpr int sceTimedOut = static_cast(0x8002003cu); -std::mutex condInitializationMutex; +std::mutex& InitializationMutex() { + static std::mutex mutex; + return mutex; +} PthreadCond destroyedCond() { return reinterpret_cast(std::uintptr_t{2}); @@ -36,7 +39,7 @@ PthreadMutex adaptiveInitializer() { std::shared_ptr resolveCond(PthreadCond* cond, int* clockId = nullptr) { if (!cond) throw std::invalid_argument("Condition variable pointer is null"); - std::lock_guard lock(condInitializationMutex); + std::lock_guard lock(InitializationMutex()); std::atomic_ref slot(*cond); auto current = slot.load(std::memory_order_acquire); if (current == destroyedCond()) @@ -161,7 +164,7 @@ int APS5_VABI scePthreadCondInit(PthreadCond* cond, const PthreadCondattr* attr, auto replacement = std::make_unique(); if (attr && *attr) replacement->_clockid = (*attr)->_clockid; - std::lock_guard lock(condInitializationMutex); + std::lock_guard lock(InitializationMutex()); std::atomic_ref(*cond).store(replacement.release(), std::memory_order_release); return 0; } @@ -169,7 +172,7 @@ int APS5_VABI scePthreadCondInit(PthreadCond* cond, const PthreadCondattr* attr, int APS5_VABI scePthreadCondDestroy(PthreadCond* cond) { if (!cond) throw std::invalid_argument("Condition variable pointer is null"); - std::lock_guard lock(condInitializationMutex); + std::lock_guard lock(InitializationMutex()); if (*cond == destroyedCond()) return SCE_KERNEL_ERROR_EINVAL; delete *cond; diff --git a/core/libs/prx/libkernel/Pthread/src/Mutex.cpp b/core/libs/prx/libkernel/Pthread/src/Mutex.cpp index dccc979442..d7bc4013f8 100644 --- a/core/libs/prx/libkernel/Pthread/src/Mutex.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Mutex.cpp @@ -16,7 +16,10 @@ constexpr int sceNotPermitted = static_cast(0x80020001u); constexpr int sceDeadlock = static_cast(0x8002000bu); constexpr int sceBusy = static_cast(0x80020010u); constexpr int sceTimedOut = static_cast(0x8002003cu); -std::mutex initializationMutex; +std::mutex& InitializationMutex() { + static std::mutex mutex; + return mutex; +} PthreadMutex destroyedMutex() { return reinterpret_cast(std::uintptr_t{2}); @@ -36,7 +39,7 @@ PthreadMutex resolveMutex(PthreadMutex* mutex, bool initialize) { std::atomic_ref slot(*mutex); if (const auto current = slot.load(std::memory_order_acquire); isInitializedMutex(current)) return current; - std::lock_guard lock(initializationMutex); + std::lock_guard lock(InitializationMutex()); const auto current = slot.load(std::memory_order_acquire); if (current == destroyedMutex()) return nullptr; @@ -162,7 +165,7 @@ int APS5_VABI scePthreadMutexInit(PthreadMutex* mutex, const PthreadMutexattr* a auto replacement = std::make_unique(); if (attr && *attr) replacement->SetType((*attr)->type); - std::lock_guard lock(initializationMutex); + std::lock_guard lock(InitializationMutex()); std::atomic_ref(*mutex).store(replacement.release(), std::memory_order_release); return 0; } @@ -170,7 +173,7 @@ int APS5_VABI scePthreadMutexInit(PthreadMutex* mutex, const PthreadMutexattr* a int APS5_VABI scePthreadMutexDestroy(PthreadMutex* mutex) { if (!mutex) throw std::invalid_argument("Mutex pointer is null"); - std::lock_guard lock(initializationMutex); + std::lock_guard lock(InitializationMutex()); if (*mutex == destroyedMutex()) return SCE_KERNEL_ERROR_EINVAL; if (*mutex == adaptiveInitializer()) { diff --git a/core/libs/prx/libkernel/Pthread/src/Thread.cpp b/core/libs/prx/libkernel/Pthread/src/Thread.cpp index 8bc51e38db..aa76de407d 100644 --- a/core/libs/prx/libkernel/Pthread/src/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Thread.cpp @@ -35,7 +35,6 @@ static constexpr int SCE_OK = 0; -static constexpr std::size_t DEFAULT_STACK_SIZE = 1u << 20; static constexpr std::size_t HOST_STACK_MARGIN = 1u << 20; static constexpr int DETACH_DETACHED = 1; static constexpr std::size_t THREAD_NAME_CAPACITY = 32; @@ -75,9 +74,9 @@ static GuestThreadState& CurrentGuestState() { return state; #endif } -static std::mutex stackLock; -static std::map> liveStacks; -static std::map protectedStackPages; +static std::mutex& stackLock = *new std::mutex(); +static auto& liveStacks = *new std::map>(); +static auto& protectedStackPages = *new std::map(); #if defined(__linux__) struct GuestStackMapping { std::uintptr_t mappingEnd; @@ -85,7 +84,7 @@ struct GuestStackMapping { std::uintptr_t stackEnd; }; static std::mutex guestStackMappingsLock; -static std::map guestStackMappings; +static auto& guestStackMappings = *new std::map(); static void RegisterGuestStackMapping(PthreadPrivate* thread) { const auto mappingStart = reinterpret_cast(thread->guestStack); @@ -825,7 +824,7 @@ int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, Pthread bool detached = false; if (attr && *attr) detached = ((*attr)->_detachstate == DETACH_DETACHED); p->_detached = detached; - p->stackSize = attr ? (*attr)->_stacksize : DEFAULT_STACK_SIZE; + p->stackSize = attr ? (*attr)->_stacksize : GetDefaultStackSize_nid_no_patch(); if (attr) { if ((*attr)->_inheritsched != GUEST_INHERIT_SCHED) p->scope = (*attr)->_scope; diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 3b412c4a5a..22a664494c 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -106,7 +106,9 @@ int ConnectError() { } int SendError(int type) { #ifdef _WIN32 - if (type == 2 && WSAGetLastError() == WSAENOTCONN) return 39; + const int error = WSAGetLastError(); + if (type == 2 && error == WSAENOTCONN) return 39; + if (type == 1 && (error == WSAECONNABORTED || error == WSAECONNRESET)) return 32; #else static_cast(type); #endif @@ -312,6 +314,21 @@ bool ZeroPortConnect(const Socket& socket, const void* address, std::uint32_t le Fail(49); return true; } +constexpr std::int64_t NotAnswered = -2; +std::int64_t ZeroPortSend(const Socket& socket, const sockaddr_storage& destination, std::uint64_t length) { + if (socket.type != 2 || socket.family != 2 || destination.ss_family != AF_INET) return NotAnswered; + const auto& remote = reinterpret_cast(destination); + if (remote.sin_port != 0) return NotAnswered; + sockaddr_storage peer{}; + socklen_t peerSize = sizeof(peer); + if (::getpeername(socket.value, reinterpret_cast(&peer), &peerSize) == 0) return NotAnswered; + sockaddr_in local{}; + socklen_t localSize = sizeof(local); + const bool bound = ::getsockname(socket.value, reinterpret_cast(&local), &localSize) == 0 + && local.sin_port != 0 && local.sin_addr.s_addr != htonl(INADDR_ANY); + if (bound && remote.sin_addr.s_addr != htonl(INADDR_ANY) && remote.sin_addr.s_addr != htonl(INADDR_BROADCAST)) return static_cast(length); + return Fail(49); +} int Disconnect(const Socket& socket) { sockaddr_storage peer{}; socklen_t peerSize = sizeof(peer); @@ -538,6 +555,16 @@ constexpr short GuestPollWritable = 0x4; constexpr short GuestPollVnodeReady = GuestPollReadable | GuestPollWritable; constexpr short GuestPollHangUp = 0x10; constexpr short GuestPollError = 0x8; +#ifdef _WIN32 +short ClosedStreamEvents(int descriptor, short revents, short requested) { + if ((revents & (POLLHUP | POLLERR)) != POLLHUP) return revents; + const auto socket = Find(descriptor); + if (!socket || socket->type != 1) return revents; + revents = static_cast(revents | (requested & POLLRDNORM)); + std::lock_guard lock(socket->modeMutex); + return socket->sendShut ? revents : static_cast(revents & ~POLLHUP); +} +#endif [[noreturn]] void ThrowReadEndWritable(int descriptor) { throw std::runtime_error("poll: POLLOUT on the read end of pipe descriptor " + std::to_string(descriptor) + " is not supported"); } @@ -1375,6 +1402,7 @@ std::int64_t APS5_VABI sendto_nid_postfix(int descriptor, const void* buffer, st socklen_t size; if (!Address(address, addressLength, native, size)) return -1; if (native.ss_family != (socket->family == 2 ? AF_INET : AF_INET6)) return Fail(47); + if (const auto answered = ZeroPortSend(*socket, native, length); answered != NotAnswered) return answered; result = static_cast(::sendto(socket->value, static_cast(buffer), static_cast(length), SocketSendFlags(*socket, flags), reinterpret_cast(&native), size)); } @@ -1742,6 +1770,9 @@ int APS5_VABI poll_nid_postfix(GuestPollDescriptor* descriptors, std::uint32_t c } const auto collect = [&](std::size_t i, bool pipe) { auto& entry = descriptors[owners[i]]; +#ifdef _WIN32 + if (!pipe) native[i].revents = ClosedStreamEvents(entry.descriptor, native[i].revents, native[i].events); +#endif for (const auto& flag : RequestFlags) if ((entry.events & flag.guest) && (native[i].revents & flag.native)) entry.revents = static_cast(entry.revents | flag.guest); diff --git a/core/libs/prx/libkernel/System/src/Coredump.cpp b/core/libs/prx/libkernel/System/src/Coredump.cpp index f618d1e2ea..f6502ea438 100644 --- a/core/libs/prx/libkernel/System/src/Coredump.cpp +++ b/core/libs/prx/libkernel/System/src/Coredump.cpp @@ -68,7 +68,7 @@ int APS5_VABI sceCoredumpAttachMemoryRegionAsUserFile(std::uint32_t userValue, c } int APS5_VABI sceCoredumpSetUserDataType(std::int32_t type) { - NotImplemented_nid_no_patch("Uxqkdta7wEg"); + (void)type; return 0; } diff --git a/core/libs/prx/libkernel/System/src/Exception.cpp b/core/libs/prx/libkernel/System/src/Exception.cpp index b7eb4f65f7..e3a768008f 100644 --- a/core/libs/prx/libkernel/System/src/Exception.cpp +++ b/core/libs/prx/libkernel/System/src/Exception.cpp @@ -426,7 +426,9 @@ bool RaiseOn(Pthread thread, GuestExceptionHandler handler, int signum) { delivery.context.ContextFlags = CONTEXT_FULL | CONTEXT_SEGMENTS; if (!GetThreadContext(native, &delivery.context)) { ResumeThread(native); - throw std::runtime_error("sceKernelRaiseException: cannot read the target thread context"); + if (std::chrono::steady_clock::now() >= retryDeadline) throw std::runtime_error("sceKernelRaiseException: cannot read the target thread context"); + SwitchToThread(); + continue; } if (thread->waitCount.load(std::memory_order_seq_cst) > 0) { const bool accepted = QueueUserAPC(WaitingEntry, native, reinterpret_cast(queued.get())) != 0; diff --git a/core/libs/prx/libkernel/System/src/Process.cpp b/core/libs/prx/libkernel/System/src/Process.cpp index 348464a177..3ea5497d80 100644 --- a/core/libs/prx/libkernel/System/src/Process.cpp +++ b/core/libs/prx/libkernel/System/src/Process.cpp @@ -8,6 +8,12 @@ #else #include #include +#ifdef __linux__ +#include +#elif defined(__APPLE__) +#include +#include +#endif #endif #if defined(__linux__) && defined(__x86_64__) #include @@ -18,6 +24,7 @@ #include "prx/libc/include/General.hpp" #include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libc/include/HostEntropy.hpp" +#include "prx/libc/include/GuestArena.hpp" #include "prx/libc/include/Shutdown.hpp" #include "prx/libkernel/AppMetadata/include/AppMetadata.hpp" #include "prx/libkernel/DirectMemory/DirectMemory.hpp" @@ -46,6 +53,7 @@ extern "C" int* APS5_VABI __error_nid_postfix(); extern "C" int APS5_VABI stat_nid_postfix(const char* path, FileStat* sb); +extern "C" int APS5_VABI sceKernelVirtualQuery(const void* addr, int flags, VirtualQueryInfo* info, std::uint64_t info_size); namespace { @@ -59,6 +67,86 @@ std::mutex supplementaryGroupsMutex; std::vector supplementaryGroups{0}; constexpr unsigned freebsdWaitOptions = 0x8000003Fu; +bool StoreGuestId(std::uint32_t* output) { + if (output == nullptr) return true; + const auto address = reinterpret_cast(output); + if (address > std::numeric_limits::max() - sizeof(*output)) return false; + const auto end = address + sizeof(*output); + for (auto cursor = address; cursor < end;) { + VirtualQueryInfo info{}; + if (sceKernelVirtualQuery(reinterpret_cast(cursor), 0, &info, sizeof(info)) != 0 || + info.end <= cursor || (info.protection & 2) == 0) + return false; + cursor = std::min(end, info.end); + } + const GuestArena::HostWrite guard(output, sizeof(*output)); + if (!guard.Open()) return false; + constexpr std::uint32_t zero = 0; +#ifdef _WIN32 + SIZE_T written = 0; + return WriteProcessMemory(GetCurrentProcess(), output, &zero, sizeof(zero), &written) != 0 && written == sizeof(zero); +#elif defined(__linux__) + iovec local{const_cast(&zero), sizeof(zero)}; + iovec remote{output, sizeof(zero)}; + return ::process_vm_writev(::getpid(), &local, 1, &remote, 1, 0) == static_cast(sizeof(zero)); +#elif defined(__APPLE__) + return ::mach_vm_write(::mach_task_self(), reinterpret_cast(output), + reinterpret_cast(const_cast(&zero)), sizeof(zero)) == KERN_SUCCESS; +#else + *output = zero; + return true; +#endif +} + +bool GuestRange(const void* pointer, std::size_t bytes) { + const auto start = reinterpret_cast(pointer); + return start != 0 && bytes <= std::numeric_limits::max() - start; +} + +bool ReadGuest(void* destination, const void* source, std::size_t bytes) { + if (bytes == 0) return true; + if (!GuestRange(source, bytes)) 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("credentials: 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("credentials: checked guest reads are not implemented on this host"); +#endif +} + +bool WriteGuest(void* destination, const void* source, std::size_t bytes) { + if (bytes == 0) return true; + if (!GuestRange(destination, bytes)) return false; + const GuestArena::HostWrite guard(destination, bytes); + if (!guard.Open()) return false; +#ifdef _WIN32 + SIZE_T copied = 0; + return WriteProcessMemory(GetCurrentProcess(), destination, source, bytes, &copied) != 0 && copied == bytes; +#elif defined(__linux__) + iovec local{const_cast(source), bytes}; + iovec remote{destination, bytes}; + const auto copied = ::process_vm_writev(::getpid(), &local, 1, &remote, 1, 0); + if (copied < 0 && errno != EFAULT) throw std::runtime_error("credentials: process_vm_writev failed with errno " + std::to_string(errno)); + return copied >= 0 && static_cast(copied) == bytes; +#elif defined(__APPLE__) + return ::mach_vm_write(::mach_task_self(), reinterpret_cast(destination), + reinterpret_cast(source), static_cast(bytes)) == KERN_SUCCESS; +#else + throw std::runtime_error("credentials: checked guest writes are not implemented on this host"); +#endif +} + std::atomic gpoBits{0}; constexpr std::array openPsId{'A', 'n', 'y', 'P', 'S', '5', 'O', 'p', 'e', 'n', 'P', 's', 'I', 'd', 0, 1}; @@ -314,6 +402,22 @@ int APS5_VABI issetugid_nid_postfix(void) { return 0; } +int APS5_VABI getresuid_nid_postfix(std::uint32_t* ruid, std::uint32_t* euid, std::uint32_t* suid) { + if (!StoreGuestId(ruid) || !StoreGuestId(euid) || !StoreGuestId(suid)) { + *__error_nid_postfix() = errnoBadAddress; + return -1; + } + return 0; +} + +int APS5_VABI getresgid_nid_postfix(std::uint32_t* rgid, std::uint32_t* egid, std::uint32_t* sgid) { + if (!StoreGuestId(rgid) || !StoreGuestId(egid) || !StoreGuestId(sgid)) { + *__error_nid_postfix() = errnoBadAddress; + return -1; + } + return 0; +} + void APS5_VABI exit_nid_postfix(int code) { LibcExit_nid_no_patch(code); } @@ -770,6 +874,28 @@ int APS5_VABI setregid_nid_postfix(std::uint32_t rgid, std::uint32_t egid) { return 0; } +int APS5_VABI setresuid_nid_postfix(std::uint32_t ruid, std::uint32_t euid, std::uint32_t suid) { + constexpr std::uint32_t unchanged = 0xffffffffu; + if (ruid != 0 && ruid != unchanged) + throw std::runtime_error("setresuid: unsupported real user id " + std::to_string(ruid)); + if (euid != 0 && euid != unchanged) + throw std::runtime_error("setresuid: unsupported effective user id " + std::to_string(euid)); + if (suid != 0 && suid != unchanged) + throw std::runtime_error("setresuid: unsupported saved user id " + std::to_string(suid)); + return 0; +} + +int APS5_VABI setresgid_nid_postfix(std::uint32_t rgid, std::uint32_t egid, std::uint32_t sgid) { + constexpr std::uint32_t unchanged = 0xffffffffu; + if (rgid != 0 && rgid != unchanged) + throw std::runtime_error("setresgid: unsupported real group id " + std::to_string(rgid)); + if (egid != 0 && egid != unchanged) + throw std::runtime_error("setresgid: unsupported effective group id " + std::to_string(egid)); + if (sgid != 0 && sgid != unchanged) + throw std::runtime_error("setresgid: unsupported saved group id " + std::to_string(sgid)); + return 0; +} + int APS5_VABI setgroups_nid_postfix(int count, const std::uint32_t* groups) { if (count < 0 || count > freebsdGroupsMax) { *__error_nid_postfix() = errnoInvalidArgument; @@ -779,14 +905,22 @@ int APS5_VABI setgroups_nid_postfix(int count, const std::uint32_t* groups) { *__error_nid_postfix() = errnoBadAddress; return -1; } - if (count > 0 && groups[0] != 0) - throw std::runtime_error("setgroups: unsupported effective group id " + std::to_string(groups[0])); + std::vector requested; + if (count > 0) { + requested.resize(static_cast(count)); + if (!ReadGuest(requested.data(), groups, requested.size() * sizeof(requested[0]))) { + *__error_nid_postfix() = errnoBadAddress; + return -1; + } + } + if (count > 0 && requested[0] != 0) + throw std::runtime_error("setgroups: unsupported effective group id " + std::to_string(requested[0])); std::lock_guard lock(supplementaryGroupsMutex); if (count == 0) { supplementaryGroups.resize(1); return 0; } - supplementaryGroups.assign(groups, groups + count); + supplementaryGroups.swap(requested); return 0; } @@ -802,7 +936,10 @@ int APS5_VABI getgroups_nid_postfix(int capacity, std::uint32_t* groups) { *__error_nid_postfix() = errnoBadAddress; return -1; } - std::copy(supplementaryGroups.begin(), supplementaryGroups.end(), groups); + if (!WriteGuest(groups, supplementaryGroups.data(), supplementaryGroups.size() * sizeof(supplementaryGroups[0]))) { + *__error_nid_postfix() = errnoBadAddress; + return -1; + } return count; } diff --git a/core/libs/prx/libkernel/System/src/Signals.cpp b/core/libs/prx/libkernel/System/src/Signals.cpp index 773a688b23..da332603c9 100644 --- a/core/libs/prx/libkernel/System/src/Signals.cpp +++ b/core/libs/prx/libkernel/System/src/Signals.cpp @@ -32,6 +32,8 @@ struct GuestSigaction { }; static_assert(sizeof(GuestSigaction) == 32 && offsetof(GuestSigaction, flags) == 8 && offsetof(GuestSigaction, mask) == 12); +extern "C" GuestSignalSet _sigintr_nid_postfix{}; + namespace { using GuestHandler = void (APS5_VABI *)(int); constexpr int MaxSignal = 128; @@ -483,7 +485,7 @@ GuestHandler APS5_VABI signal_nid_postfix(int guest, GuestHandler handler) { const auto invalid = reinterpret_cast(static_cast(-1)); if (guest < 1 || guest > MaxSignal || ((guest == GuestSigkill || guest == GuestSigstop) && handler != nullptr)) { *__error_nid_postfix() = 22; return invalid; } std::lock_guard lock(registration); - const GuestSigaction action{reinterpret_cast(handler), SaRestart, {}}; + const GuestSigaction action{reinterpret_cast(handler), SignalInSet(_sigintr_nid_postfix, guest) ? 0 : SaRestart, {}}; if (!Install(guest, action)) { *__error_nid_postfix() = 22; return invalid; } const auto previous = reinterpret_cast(dispositions[guest].handler); dispositions[guest] = action; diff --git a/core/libs/prx/libkernel/Unimplemented.cpp b/core/libs/prx/libkernel/Unimplemented.cpp index 3dd75f72d8..617dc506f1 100644 --- a/core/libs/prx/libkernel/Unimplemented.cpp +++ b/core/libs/prx/libkernel/Unimplemented.cpp @@ -133,3 +133,17 @@ int APS5_VABI sceKernelGetProcessAbiType() { } } + +extern "C" { + +int APS5_VABI __Ux86_64_setcontext() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sigreturn() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libkernel_sys/Export.Discovery.cpp b/core/libs/prx/libkernel_sys/Export.Discovery.cpp index 8b3da583e4..f64683effc 100644 --- a/core/libs/prx/libkernel_sys/Export.Discovery.cpp +++ b/core/libs/prx/libkernel_sys/Export.Discovery.cpp @@ -98,11 +98,6 @@ int APS5_VABI fchownat_nid_postfix(void) { return 0; } -int APS5_VABI fstat_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI futimesat_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -313,11 +308,6 @@ int APS5_VABI lchflags_nid_postfix(void) { return 0; } -int APS5_VABI lchmod_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI lchown_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -483,11 +473,6 @@ int APS5_VABI pthread_rwlockattr_settype_np_nid_postfix(void) { return 0; } -int APS5_VABI pthread_set_defaultstacksize_np_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI pthread_set_user_context_np_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -2428,16 +2413,6 @@ int APS5_VABI scePthreadSetBesteffort(void) { return 0; } -int APS5_VABI scePthreadSetDefaultstacksize(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI scePthreadSetName(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI scePthreadSetconcurrency(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 82158449b6..a046b93a98 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -21,8 +21,10 @@ #include "prx/libc/include/General.hpp" #include "prx/libc/include/FilesystemError.hpp" #include "prx/libkernel/File/include/DirectoryDescriptor.hpp" +#include "prx/libkernel/File/include/DescriptorControl.hpp" #include "prx/libc/include/CreationMask.hpp" #include "prx/libkernel/File/include/FileFlags.hpp" +#include "prx/libkernel/KernelErrors.hpp" #ifdef _WIN32 #ifndef NOMINMAX #define NOMINMAX @@ -153,10 +155,8 @@ std::int64_t ReadNativeLink(const std::filesystem::path& native, char* buffer, s const DWORD attributes = GetFileAttributesW(native.wstring().c_str()); if (attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0) { std::string target; - if (!File::WindowsSymbolicLink::Read(native, target, "readlink") && !File::WindowsSymbolicLink::ReadJunction(native, target, "readlink")) { - NotImplemented_nid_no_patch("readlink of a Windows reparse point other than a symbolic link"); - return -1; - } + if (!File::WindowsSymbolicLink::Read(native, target, "readlink") && !File::WindowsSymbolicLink::ReadJunction(native, target, "readlink")) + return Fail(GuestEinval); return CopyLinkTarget(target, buffer, size); } #endif @@ -274,6 +274,12 @@ int APS5_VABI fstatfs_nid_postfix(int d, void* buf) { return _fstatfs_nid_postfix(d, buf); } +std::int64_t APS5_VABI _fstat_nid_postfix(int d, FileStat* sb); + +int APS5_VABI fstat_nid_postfix(int d, FileStat* sb) { + return static_cast(_fstat_nid_postfix(d, sb)); +} + std::int64_t APS5_VABI readlink_nid_postfix(const char* path, char* buffer, std::size_t size) { if (size > static_cast(std::numeric_limits::max())) return Fail(GuestEinval); if (path == nullptr) return Fail(GuestEfault); @@ -335,6 +341,35 @@ std::int64_t APS5_VABI pathconf_nid_postfix(const char* path, int name) { } } +#ifdef _WIN32 +static int FollowLinkPath(const std::filesystem::path& source, std::filesystem::path& target) { + target = source; + const DWORD attributes = GetFileAttributesW(source.c_str()); + if (attributes == INVALID_FILE_ATTRIBUTES || (attributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0) return 0; + const HANDLE handle = CreateFileW(source.c_str(), 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, + OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); + if (handle == INVALID_HANDLE_VALUE) { + const DWORD code = GetLastError(); + if (code == ERROR_FILE_NOT_FOUND || code == ERROR_PATH_NOT_FOUND) return GuestEnoent; + if (code == ERROR_CANT_RESOLVE_FILENAME) return GuestEloop; + throw std::system_error(static_cast(code), std::system_category(), "link: CreateFileW"); + } + std::wstring name(MAX_PATH, L'\0'); + DWORD length = GetFinalPathNameByHandleW(handle, name.data(), static_cast(name.size()), FILE_NAME_NORMALIZED | VOLUME_NAME_DOS); + if (length >= name.size()) { + name.resize(length); + length = GetFinalPathNameByHandleW(handle, name.data(), static_cast(name.size()), FILE_NAME_NORMALIZED | VOLUME_NAME_DOS); + } + const DWORD code = GetLastError(); + CloseHandle(handle); + if (length == 0 || length >= name.size()) + throw std::system_error(static_cast(code), std::system_category(), "link: GetFinalPathNameByHandleW"); + name.resize(length); + target = std::filesystem::path(name); + return 0; +} +#endif + int APS5_VABI link_nid_postfix(const char* existing, const char* created) { if (existing == nullptr || created == nullptr) return static_cast(Fail(GuestEfault)); if (*existing == '\0' || *created == '\0') return static_cast(Fail(GuestEnoent)); @@ -347,7 +382,13 @@ int APS5_VABI link_nid_postfix(const char* existing, const char* created) { if (std::filesystem::is_directory(status)) return static_cast(Fail(GuestEperm)); if (std::filesystem::exists(std::filesystem::symlink_status(destination, error))) return static_cast(Fail(GuestEexist)); if (!std::filesystem::is_directory(destination.parent_path(), error)) return static_cast(Fail(HasNonDirectoryPrefix(destination) ? GuestEnotdir : GuestEnoent)); +#ifdef _WIN32 + std::filesystem::path target; + if (const int followError = FollowLinkPath(source, target)) return static_cast(Fail(followError)); + if (std::filesystem::is_directory(target, error)) return static_cast(Fail(GuestEperm)); +#else const auto target = std::filesystem::is_symlink(source, error) ? std::filesystem::canonical(source, error) : source; +#endif if (!error) std::filesystem::create_hard_link(target, destination, error); if (error == std::errc::cross_device_link) return static_cast(Fail(GuestExdev)); if (error) throw std::system_error(error, "link"); @@ -358,6 +399,7 @@ int APS5_VABI link_nid_postfix(const char* existing, const char* created) { int APS5_VABI fchdir_nid_postfix(int fd) { int error = 0; const char* path = GetDirectoryDescriptorPath_nid_no_patch(fd, &error); + if (path == nullptr && error == GuestEnotdir && DescriptorHasNoVnode_nid_no_patch(fd)) return static_cast(Fail(GuestEinval)); if (path == nullptr) return static_cast(Fail(error)); #ifndef _WIN32 if (::faccessat(fd, ".", X_OK, AT_EACCESS) != 0) return static_cast(Fail(errno)); @@ -386,6 +428,7 @@ int APS5_VABI chroot_nid_postfix(const char* path) { int APS5_VABI symlink_nid_postfix(const char* target, const char* created) { if (target == nullptr || created == nullptr) return static_cast(Fail(GuestEfault)); + if (std::strlen(target) >= GuestPathMax) return static_cast(Fail(GuestEnametoolong)); if (*target == '\0' || *created == '\0') return static_cast(Fail(GuestEnoent)); const std::filesystem::path contents(target); if (contents.has_root_name() || contents.has_root_directory()) { @@ -532,6 +575,23 @@ int APS5_VABI fchmodat_nid_postfix(int fd, const char* path, int mode, int flag) return 0; } +int APS5_VABI lchmod_nid_postfix(const char* path, std::uint16_t mode) { +#ifndef __APPLE__ + std::filesystem::path native; + if (path != nullptr && *path != '\0' && ResolvePathError_nid_no_patch(path, native) == 0) { +#ifdef _WIN32 + const DWORD attributes = GetFileAttributesW(native.wstring().c_str()); + const bool symbolic = attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0; +#else + std::error_code status; + const bool symbolic = std::filesystem::is_symlink(std::filesystem::symlink_status(native, status)); +#endif + if (symbolic) NotImplemented_nid_no_patch("lchmod: the mode of a symbolic link on a host that cannot change it"); + } +#endif + return fchmodat_nid_postfix(GuestAtFdcwd, path, mode, GuestAtSymlinkNofollow); +} + int APS5_VABI fchown_nid_postfix(int fd, std::uint32_t uid, std::uint32_t gid) { constexpr std::uint32_t Unchanged = 0xFFFFFFFFu; if (uid != 0 && uid != Unchanged) NotImplemented_nid_no_patch("fchown: an owner other than the guest user"); @@ -576,8 +636,8 @@ int APS5_VABI unlinkat_nid_postfix(int fd, const char* path, int flag) { while (!native.has_filename() && native.has_relative_path()) native = native.parent_path(); std::error_code statusError; const auto symlinkStatus = std::filesystem::symlink_status(native, statusError); + if (IsDotOrDotDot(path)) return static_cast(Fail(GuestEinval)); if (flag & GuestAtRemovedir) { - if (IsDotOrDotDot(path)) return static_cast(Fail(GuestEinval)); if (std::filesystem::is_symlink(symlinkStatus)) return static_cast(Fail(GuestEnotdir)); if (!std::filesystem::is_directory(symlinkStatus)) { if (HasNonDirectoryPrefix(native) || std::filesystem::exists(symlinkStatus)) @@ -690,4 +750,20 @@ int APS5_VABI renameat_nid_postfix(int oldfd, const char* from, int newfd, const return 0; } + +bool SetDefaultStackSize_nid_no_patch(std::size_t size); +void APS5_VABI pthread_set_name_np_nid_postfix(void* thread, const char* name); + +int APS5_VABI pthread_set_defaultstacksize_np_nid_postfix(std::size_t size) { + return SetDefaultStackSize_nid_no_patch(size) ? 0 : GuestEinval; +} + +int APS5_VABI scePthreadSetDefaultstacksize(std::size_t size) { + return SetDefaultStackSize_nid_no_patch(size) ? 0 : SceKernelError(GuestEinval); +} + +void APS5_VABI scePthreadSetName(void* thread, const char* name) { + pthread_set_name_np_nid_postfix(thread, name); +} + } diff --git a/core/libs/tests/FileStreams.cpp b/core/libs/tests/FileStreams.cpp index f53419748c..2e11f4dfb2 100644 --- a/core/libs/tests/FileStreams.cpp +++ b/core/libs/tests/FileStreams.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -222,6 +223,11 @@ int main(int argc, char** argv) { ExpectException([&] { fputs_nid_postfix(nullptr, stream); }); ExpectException([&] { fwrite_nid_postfix(nullptr, 1, sizeof(payload), stream); }); errno = 0; + Require(fread_nid_postfix(buffer.data(), 2, SIZE_MAX / 2 + 1, stream) == 0 && errno == EINVAL && ferror_nid_postfix(stream) == 0); + errno = 0; + Require(fread_nid_postfix(buffer.data(), 2, SIZE_MAX / 2 + 1, nullptr) == 0 && errno == EINVAL); + ExpectException([&] { fwrite_nid_postfix(buffer.data(), SIZE_MAX / 2 + 1, 2, stream); }); + errno = 0; Require(fseek_nid_postfix(stream, 0, -1) == -1); Require(errno == EINVAL); Require(fclose_nid_postfix(stream) == 0); @@ -234,18 +240,11 @@ int main(int argc, char** argv) { errno = 0; Require(fopen_nid_postfix(argv[1], "rb") == nullptr); Require(errno == ENOENT); -#ifdef _WIN32 - const std::string directoryMode = "rb"; - constexpr int directoryError = EACCES; -#else - const std::string directoryMode = "wb"; - constexpr int directoryError = EISDIR; -#endif const std::string directoryName = std::string(argv[1]) + ".dir"; Require(std::filesystem::create_directory(directoryName)); errno = 0; - Require(fopen_nid_postfix(directoryName.c_str(), directoryMode.c_str()) == nullptr); - Require(errno == directoryError); + Require(fopen_nid_postfix(directoryName.c_str(), "wb") == nullptr); + Require(errno == EISDIR); Require(std::filesystem::remove(directoryName)); const std::u8string unicodeName = u8"セーブé.tmp"; const std::string guestName(unicodeName.begin(), unicodeName.end()); diff --git a/core/libs/tests/Formatting.cpp b/core/libs/tests/Formatting.cpp index f22d7880dd..937f348f17 100644 --- a/core/libs/tests/Formatting.cpp +++ b/core/libs/tests/Formatting.cpp @@ -3,6 +3,7 @@ #include "prx/libc/include/GuestHeap.hpp" #include #include +#include #include #include #include @@ -84,6 +85,34 @@ static int APS5_VABI PrintList(const char* format, ...) { return result; } +static void CheckSizeLimits() { + using Formatter = int (APS5_VABI*)(char*, size_t, const char*, ...); + for (const Formatter format : {&snprintf_nid_postfix, &FormatList}) { + char output[8]; + for (const std::size_t oversized : {static_cast(INT_MAX) + 2, static_cast(SIZE_MAX)}) { + for (const bool wide : {false, true}) { + std::memset(output, 'Z', sizeof(output)); + int consumed = -1; + *__error_nid_postfix() = 0; + const int result = wide ? format(output, oversized, "%ls%n", u"x", &consumed) + : format(output, oversized, "x%n", &consumed); + Require(result == -1 && *__error_nid_postfix() == 84 && output[0] == 0 && consumed == -1); + Require(std::all_of(output + 1, output + sizeof(output), [](char value) { return value == 'Z'; })); + } + } + for (const std::size_t capacity : {std::size_t{0}, std::size_t{1}, static_cast(INT_MAX), static_cast(INT_MAX) + 1}) { + std::memset(output, 'Z', sizeof(output)); + *__error_nid_postfix() = 35; + Require(format(output, capacity, "x") == 1 && *__error_nid_postfix() == 35); + Require(output[0] == (capacity == 0 ? 'Z' : capacity == 1 ? '\0' : 'x')); + const std::size_t written = capacity < 2 ? capacity : 2; + if (capacity > 1) Require(output[1] == 0); + Require(std::all_of(output + written, output + sizeof(output), [](char value) { return value == 'Z'; })); + } + Require(format(nullptr, 0, "x") == 1); + } +} + static bool CheckWidePrecision() { const char16_t input[] = u"A\u00e9\u20ac\U0001f600Z"; const char* expected[] = {"", "A", "A", "A\xc3\xa9", "A\xc3\xa9", "A\xc3\xa9", "A\xc3\xa9\xe2\x82\xac", @@ -368,4 +397,4 @@ static void CheckAllocatingFormat() { free_nid_postfix(allocated); } -int main() { RunChecks(); CheckAllocatingFormat(); return CheckWidePrecision() ? 0 : 1; } +int main() { CheckSizeLimits(); RunChecks(); CheckAllocatingFormat(); return CheckWidePrecision() ? 0 : 1; } diff --git a/core/libs/tests/GuestAjm.cpp b/core/libs/tests/GuestAjm.cpp index 017b6304a8..4fca98867a 100644 --- a/core/libs/tests/GuestAjm.cpp +++ b/core/libs/tests/GuestAjm.cpp @@ -391,6 +391,21 @@ void TestMp3ParseFrame() { Require(sceAjmDecMp3ParseFrame(reservedSampleRate, 4, 0, &frame) == invalidParameter); const std::uint8_t mpeg25Above64Kbps[4] = {0xFF, 0xE3, 0x98, 0xC0}; Require(sceAjmDecMp3ParseFrame(mpeg25Above64Kbps, 4, 0, &frame) == invalidParameter); + for (const std::uint8_t layer : {0u, 2u, 3u}) { + const std::uint8_t header[4] = {0xFF, static_cast(0xF9u | (layer << 1u)), 0x90, 0x04}; + if (layer == 0) { + std::memset(&frame, 0xA5, sizeof(frame)); + const auto previous = frame; + Require(sceAjmDecMp3ParseFrame(header, sizeof(header), 0, &frame) == invalidParameter); + Require(std::memcmp(&frame, &previous, sizeof(frame)) == 0); + } else { + Require(ParsesTo(header, sizeof(header), 417, 2, 1152, 128000, 44100)); + } + std::memset(&frame, 0xA5, sizeof(frame)); + const auto previous = frame; + Require(sceAjmDecMp3ParseFrame(header, sizeof(header), 1, &frame) == invalidParameter); + Require(std::memcmp(&frame, &previous, sizeof(frame)) == 0); + } } const std::uint8_t LAME_MPEG1_STEREO_VBR[] = { @@ -457,14 +472,17 @@ bool OflParsesTo(const std::vector& stream, std::uint32_t streamSi return frame.num_frames == frames && frame.encoder_delay == delay && frame.total_samples == total && frame.ofl_type == type; } -bool OflThrows(const std::vector& stream) { - AjmDecMp3ParseFrame frame{}; - try { - sceAjmDecMp3ParseFrame(stream.data(), static_cast(stream.size()), 1, &frame); - } catch (const std::runtime_error&) { - return true; +std::vector ProtectedMp3Frame(std::vector frame, bool tagAfterCrc) { + frame[1] &= 0xFE; + if (tagAfterCrc) { + const auto size = frame.size(); + frame.insert(frame.begin() + 4, {0xA5, 0x5A}); + frame.resize(size); + } else { + frame[4] = 0xA5; + frame[5] = 0x5A; } - return false; + return frame; } void TestMp3ParseOfl() { @@ -481,21 +499,75 @@ void TestMp3ParseOfl() { const auto fgh = Mp3Frame(FGH_FRAME, sizeof(FGH_FRAME), 417); Require(OflParsesTo(fgh, 417, 0, 1105, 441000, 3)); Require(OflParsesTo(Mp3Frame(FGH_BAD_CRC_FRAME, sizeof(FGH_BAD_CRC_FRAME), 417), 417, 0, 0, 0, 0)); + const auto marker = std::find(fgh.begin() + 36, fgh.end(), 0xB4u); + Require(marker != fgh.end() && static_cast(fgh.end() - marker) >= 10); + std::vector misleadingFgh(417, 0); + std::copy_n(fgh.begin(), 4, misleadingFgh.begin()); + const auto markerOffset = static_cast(marker - fgh.begin()); + std::copy_n(fgh.begin() + static_cast(markerOffset), 10, misleadingFgh.begin() + 36); + misleadingFgh[45] ^= 0x01u; + std::copy_n(fgh.begin() + static_cast(markerOffset), 10, misleadingFgh.begin() + 48); + Require(OflParsesTo(misleadingFgh, 417, 0, 0, 0, 0)); auto vbri = Mp3Frame(VBRI_FRAME, sizeof(VBRI_FRAME), 417); Require(OflParsesTo(vbri, 417, 0, 576, 0, 2)); vbri.insert(vbri.end(), fgh.begin(), fgh.end()); Require(OflParsesTo(vbri, 834, 0, 576 + 1105, 441000, 4)); Require(OflParsesTo(vbri, 480, 0, 576, 0, 2)); - auto protectedFrame = stereo; - protectedFrame[1] = 0xFA; - Require(OflThrows(protectedFrame)); - auto layer2 = stereo; - layer2[1] = 0xFD; - Require(OflThrows(layer2)); + for (const bool tagAfterCrc : {false, true}) { + const auto protectedStereo = ProtectedMp3Frame(stereo, tagAfterCrc); + Require(OflParsesTo(protectedStereo, 417, 10, 1152 + 576 + 529, 10000, 1)); + const auto protectedMono = ProtectedMp3Frame(mono, tagAfterCrc); + Require(OflParsesTo(protectedMono, 576, 10, 1152 + 576 + 529, 10000, 1)); + const auto protectedMpeg2 = ProtectedMp3Frame(mpeg2, tagAfterCrc); + Require(OflParsesTo(protectedMpeg2, 208, 20, 576 + 576 + 529, 10000, 1)); + } + auto protectedNonzeroSideInfo = ProtectedMp3Frame(stereo, true); + protectedNonzeroSideInfo[8] = 0x00; + protectedNonzeroSideInfo[9] = 0x10; + std::move_backward(protectedNonzeroSideInfo.begin() + 38, protectedNonzeroSideInfo.end() - 2, protectedNonzeroSideInfo.end()); + protectedNonzeroSideInfo[38] = 0; + protectedNonzeroSideInfo[39] = 0; + Require(OflParsesTo(protectedNonzeroSideInfo, 417, 10, 1152 + 576 + 529, 10000, 1)); + const auto protectedFgh = ProtectedMp3Frame(fgh, true); + Require(OflParsesTo(protectedFgh, 417, 0, 1105, 441000, 3)); auto monoVbri = Mp3Frame(VBRI_FRAME, sizeof(VBRI_FRAME), 417); monoVbri[3] = 0xC4; - Require(OflThrows(monoVbri)); + Require(OflParsesTo(monoVbri, 417, 0, 576, 0, 2)); + for (const std::uint8_t version : {0xF3u, 0xE3u}) { + for (const std::uint8_t mode : {0x04u, 0xC4u}) { + auto otherVbri = Mp3Frame(VBRI_FRAME, sizeof(VBRI_FRAME), version == 0xF3 ? 208 : 417); + otherVbri[1] = version; + otherVbri[2] = 0x80; + otherVbri[3] = mode; + Require(OflParsesTo(otherVbri, static_cast(otherVbri.size()), 0, 576, 0, 2)); + } + } + const auto protectedVbri = ProtectedMp3Frame(monoVbri, false); + Require(OflParsesTo(protectedVbri, 417, 0, 576, 0, 2)); + auto monoVbriAndFgh = monoVbri; + monoVbriAndFgh.insert(monoVbriAndFgh.end(), protectedFgh.begin(), protectedFgh.end()); + Require(OflParsesTo(monoVbriAndFgh, 834, 0, 576 + 1105, 441000, 4)); +} + +void TestMp3MetadataBounds() { + const auto stereo = Mp3Frame(LAME_MPEG1_STEREO_VBR, sizeof(LAME_MPEG1_STEREO_VBR), 417); + const auto protectedStereo = ProtectedMp3Frame(stereo, true); + const auto protectedLame = ProtectedMp3Frame(stereo, false); + auto vbri = Mp3Frame(VBRI_FRAME, sizeof(VBRI_FRAME), 417); + vbri[3] = 0xC4; + for (const auto& complete : {stereo, protectedStereo, protectedLame, vbri}) { + for (std::size_t length = 0; length <= complete.size(); ++length) { + const std::vector prefix(complete.begin(), complete.begin() + length); + AjmDecMp3ParseFrame frame{}; + const int result = sceAjmDecMp3ParseFrame(prefix.data(), static_cast(prefix.size()), 1, &frame); + Require(result == (length < 4 ? static_cast(0x80930005) : 0)); + if (length >= 4) Require(frame.frame_size == 417); + } + } + std::vector chain; + for (std::size_t i = 0; i < 4096; ++i) chain.insert(chain.end(), vbri.begin(), vbri.end()); + Require(OflParsesTo(chain, static_cast(chain.size()), 0, 576, 0, 2)); } struct GaplessDecode { @@ -1933,6 +2005,7 @@ int main() { Require(nullBuffersThrow && batchInfo.offset == 0); TestMp3ParseFrame(); TestMp3ParseOfl(); + TestMp3MetadataBounds(); TestMp3(context); TestOpus(context); TestOpus(context, 1); diff --git a/core/libs/tests/GuestAjmWVorbis.cpp b/core/libs/tests/GuestAjmWVorbis.cpp new file mode 100644 index 0000000000..4344e98e00 --- /dev/null +++ b/core/libs/tests/GuestAjmWVorbis.cpp @@ -0,0 +1,107 @@ +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include +#include +#include +#include +#include + +struct HeaderPacketParam { + std::uint64_t size; + std::uint32_t channels; + std::uint32_t sampleRate; + std::int32_t bytesPerSecond; + std::int32_t blockSize0Log2; + std::int32_t blockSize1Log2; + std::uint32_t reserved; + const std::uint8_t* setupPacket; + std::uint64_t setupPacketSize; + std::uint8_t* output; + std::uint64_t outputSize; + std::uint64_t writtenSize; +}; +static_assert(sizeof(HeaderPacketParam) == 0x48); + +extern "C" int APS5_VABI sceAjmDecWVorbisCreateHeaderPacket(HeaderPacketParam* param); + +namespace { + +constexpr int InvalidParameter = static_cast(0x8A6E0001); +constexpr int InvalidSize = static_cast(0x8A6E0002); +constexpr int OutputTooSmall = static_cast(0x8A6E0003); +constexpr int InvalidSetup = static_cast(0x8A6E0004); + +void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "%s\n", message); + std::abort(); +} + +constexpr std::array Setup {5, 'v', 'o', 'r', 'b', 'i', 's', 9, 0, 0xAA, 0xBB, 0xCC}; + +HeaderPacketParam Valid(std::uint8_t* output, std::size_t outputSize) { + return {sizeof(HeaderPacketParam), 2, 48000, 16000, 8, 11, 0, Setup.data(), Setup.size(), output, outputSize, 0}; +} + +int Create(HeaderPacketParam param) { return sceAjmDecWVorbisCreateHeaderPacket(¶m); } + +void TestValidation() { + std::array output {}; + Require(sceAjmDecWVorbisCreateHeaderPacket(nullptr) == InvalidParameter, "a null parameter is refused"); + auto param = Valid(output.data(), output.size()); + param.size = 0x40; + Require(Create(param) == InvalidSize, "a wrong structure size is refused"); + param = Valid(output.data(), output.size()); + param.bytesPerSecond = -1; + Require(Create(param) == InvalidParameter, "a negative byte rate is refused"); + param = Valid(output.data(), output.size()); + param.blockSize0Log2 = 5; + Require(Create(param) == InvalidParameter, "a block size below 64 is refused"); + param.blockSize0Log2 = 38; + param.blockSize1Log2 = 38; + Require(Create(param) == 0, "block size exponents are taken modulo 32 as the console's shift does"); + param = Valid(output.data(), output.size()); + param.blockSize1Log2 = 7; + Require(Create(param) == InvalidParameter, "block size 1 below block size 0 is refused"); + param.blockSize1Log2 = 14; + Require(Create(param) == InvalidParameter, "a block size above 8192 is refused"); + param = Valid(output.data(), output.size()); + param.setupPacket = nullptr; + param.setupPacketSize = 0; + Require(Create(param) == InvalidParameter, "a null setup packet is refused before its size"); + param = Valid(output.data(), output.size()); + param.setupPacketSize = 8; + Require(Create(param) == InvalidSetup, "a setup packet below 9 bytes is refused"); + param = Valid(nullptr, 0); + Require(Create(param) == InvalidParameter, "a null output is refused before its size"); + param = Valid(output.data(), 9); + Require(Create(param) == OutputTooSmall, "an output below 10 bytes is refused"); + param = Valid(output.data(), 56 + Setup.size() - 1); + Require(Create(param) == OutputTooSmall, "an output that cannot hold the setup packet is refused"); +} + +void TestPacket() { + std::array output {}; + output.fill(0xEE); + auto param = Valid(output.data(), output.size()); + Require(sceAjmDecWVorbisCreateHeaderPacket(¶m) == 0, "a valid header packet is built"); + Require(param.writtenSize == 56 + Setup.size(), "the written size covers the table, both headers and the setup packet"); + constexpr std::uint8_t Expected[] { + 0, 0, 30, 0, 16, 0, 9, 0, 3, 0, + 1, 'v', 'o', 'r', 'b', 'i', 's', 0, 0, 0, 0, 2, 0x80, 0xBB, 0, 0, 0, 0, 0, 0, 0x00, 0xF4, 0x01, 0x00, 0, 0, 0, 0, 0xB8, 1, + 3, 'v', 'o', 'r', 'b', 'i', 's', 0, 0, 0, 0, 0, 0, 0, 0, 1, + }; + static_assert(sizeof(Expected) == 56); + Require(std::memcmp(output.data(), Expected, sizeof(Expected)) == 0, "the table, identification and comment headers match"); + Require(std::memcmp(output.data() + 56, Setup.data(), Setup.size()) == 0, "the setup packet follows unchanged"); + Require(output[56 + Setup.size()] == 0xEE, "nothing is written past the packet"); +} + +} + +int main() { + TestValidation(); + TestPacket(); + std::puts("guest ajm wvorbis header packet: PASS"); +} diff --git a/core/libs/tests/GuestAmpr.cpp b/core/libs/tests/GuestAmpr.cpp index f6017f152f..b17776bcd9 100644 --- a/core/libs/tests/GuestAmpr.cpp +++ b/core/libs/tests/GuestAmpr.cpp @@ -37,6 +37,10 @@ int APS5_VABI sceAmprCommandBufferSetMarkerWithColor(Apr::CommandBufferObject*, std::uint64_t APS5_VABI sceAmprMeasureCommandSizePushMarker(const char*); std::uint64_t APS5_VABI sceAmprMeasureCommandSizePushMarkerWithColor(const char*, std::uint32_t); std::uint64_t APS5_VABI sceAmprMeasureCommandSizePopMarker(); +std::uint64_t APS5_VABI sceAmprMeasureCommandSizeWriteAddressOnCompletion(); +std::uint64_t APS5_VABI sceAmprMeasureCommandSizeWriteCounterOnCompletion(); +std::uint64_t APS5_VABI sceAmprMeasureCommandSizeWriteKernelEventQueueOnCompletion(); +void* APS5_VABI sceAmprCommandBufferGetBufferBaseAddress(const Apr::CommandBufferObject*); std::uint64_t APS5_VABI sceAmprMeasureCommandSizeSetMarker(const char*); std::uint64_t APS5_VABI sceAmprMeasureCommandSizeSetMarkerWithColor(const char*, std::uint32_t); int APS5_VABI sceKernelAprSubmitCommandBuffer(const Apr::CommandBufferObject*, std::uint32_t); @@ -582,6 +586,10 @@ void TestVersionedCounters() { void TestClearBuffer() { Recorder recorder; + Require(sceAmprMeasureCommandSizeWriteAddressOnCompletion() == sizeof(Apr::WriteAddressCommand)); + Require(sceAmprMeasureCommandSizeWriteCounterOnCompletion() == sizeof(Apr::WriteCounterCommand)); + Require(sceAmprMeasureCommandSizeWriteKernelEventQueueOnCompletion() == sizeof(Apr::WriteKernelEventQueueCommand)); + Require(sceAmprCommandBufferGetBufferBaseAddress(&recorder.buffer) == recorder.buffer.base); Require(sceAmprCommandBufferNop(&recorder.buffer, 1) == 0); Require(recorder.Offset() != 0 && recorder.Commands() == 1); Require(sceAmprCommandBufferClearBuffer(&recorder.buffer) == recorder.memory.data()); diff --git a/core/libs/tests/GuestAppContent.cpp b/core/libs/tests/GuestAppContent.cpp index 468467e521..42be4b89b2 100644 --- a/core/libs/tests/GuestAppContent.cpp +++ b/core/libs/tests/GuestAppContent.cpp @@ -15,6 +15,7 @@ int APS5_VABI sceAppContentAddcontUnmount(const AppContentMountPoint*); int APS5_VABI sceAppContentAddcontEnqueueDownload(uint32_t, const NpUnifiedEntitlementLabel*); int APS5_VABI sceAppContentAddcontDelete(uint32_t, const NpUnifiedEntitlementLabel*); int APS5_VABI sceAppContentGetAddcontDownloadProgress(uint32_t, const NpUnifiedEntitlementLabel*, void*); +int APS5_VABI sceAppContentTemporaryDataMount(AppContentMountPoint*); int APS5_VABI sceAppContentTemporaryDataMount2(uint32_t, AppContentMountPoint*); int APS5_VABI sceAppContentTemporaryDataUnmount(const AppContentMountPoint*); int APS5_VABI sceAppContentTemporaryDataFormat(const AppContentMountPoint*); @@ -191,6 +192,17 @@ int main() { Require(sceAppContentTemporaryDataMount2(1, &mountPoint) == 0); Require(std::filesystem::is_empty(backing)); Require(sceAppContentTemporaryDataUnmount(&mountPoint) == 0); + Require(sceAppContentTemporaryDataMount(nullptr) == ErrorParameter); + { std::ofstream file(backing / "remove.txt"); file << "format"; } + std::memset(&mountPoint, 0x5a, sizeof(mountPoint)); + Require(sceAppContentTemporaryDataMount(&mountPoint) == 0); + Require(std::strcmp(mountPoint.data, "/temp0") == 0); + Require(std::filesystem::is_empty(backing)); + Require(sceAppContentTemporaryDataMount(&second) == ErrorBusy); + Require(sceAppContentTemporaryDataMount2(0, &second) == ErrorBusy); + Require(sceAppContentTemporaryDataUnmount(&mountPoint) == 0); + Require(sceAppContentTemporaryDataMount(&mountPoint) == 0); + Require(sceAppContentTemporaryDataUnmount(&mountPoint) == 0); unsigned char info[24]; std::memset(info, 0x5a, sizeof(info)); unsigned char untouchedInfo[24]; diff --git a/core/libs/tests/GuestAudio3dSpeakerArray.cpp b/core/libs/tests/GuestAudio3dSpeakerArray.cpp new file mode 100644 index 0000000000..7ef0c9942e --- /dev/null +++ b/core/libs/tests/GuestAudio3dSpeakerArray.cpp @@ -0,0 +1,75 @@ +#include "SceTypes.hpp" +#include +#include +#include +#include +#include + +struct Audio3dSpeakerArrayParameters { + const AudioOut2Position* positions; + std::uint32_t numSpeakers; + std::uint8_t is3d; + void* memory; + std::size_t memorySize; +}; + +extern "C" { +size_t APS5_VABI sceAudio3dGetSpeakerArrayMemorySize(std::uint32_t num_speakers, std::uint8_t is_3d); +int APS5_VABI sceAudio3dCreateSpeakerArray(AudioOut2SpeakerArrayHandle* handle, const Audio3dSpeakerArrayParameters* parameters); +int APS5_VABI sceAudio3dDeleteSpeakerArray(AudioOut2SpeakerArrayHandle handle); +int APS5_VABI sceAudio3dGetSpeakerArrayMixCoefficients(AudioOut2SpeakerArrayHandle handle, AudioOut2Position pos, float spread, float* coefficients, std::uint32_t num_coefficients); +int APS5_VABI sceAudio3dGetSpeakerArrayMixCoefficients2(AudioOut2SpeakerArrayHandle handle, AudioOut2Position pos, float spread, float* coefficients, std::uint32_t num_coefficients, std::uint8_t height_aware, float downmix_spread_radius); +size_t APS5_VABI sceAudioOut2GetSpeakerArrayMemorySize(std::uint32_t num_speakers, std::uint8_t is_3d, std::uint8_t is_ambisonics); +} + +namespace { + +constexpr int INVALID_PARAMETER = static_cast(0x80EA0004); + +void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "%s\n", message); + std::abort(); +} + +template +void RequireThrows(F f, const char* message) { + bool threw = false; + try { + f(); + } catch (const std::exception&) { + threw = true; + } + Require(threw, message); +} + +} + +int main() { + Require(sceAudio3dGetSpeakerArrayMemorySize(4, 1) == sceAudioOut2GetSpeakerArrayMemorySize(4, 1, 0), "memory size forwards to AudioOut2 without ambisonics"); + + const AudioOut2Position positions[4] = {{1, 1, 0}, {1, -1, 0}, {-1, 1, 0}, {-1, -1, 0}}; + alignas(16) unsigned char memory[1024] = {}; + const std::size_t size = sceAudio3dGetSpeakerArrayMemorySize(4, 0); + Require(size <= sizeof(memory), "speaker array fits the test buffer"); + const Audio3dSpeakerArrayParameters parameters{positions, 4, 0, memory, size}; + + AudioOut2SpeakerArrayHandle handle = nullptr; + Require(sceAudio3dCreateSpeakerArray(nullptr, ¶meters) == INVALID_PARAMETER, "null handle is an invalid parameter"); + Require(sceAudio3dCreateSpeakerArray(&handle, nullptr) == INVALID_PARAMETER, "null parameters are an invalid parameter"); + Require(handle == nullptr, "a rejected create leaves the handle"); + Require(sceAudio3dCreateSpeakerArray(&handle, ¶meters) == 0, "create succeeds without ambisonics parameters"); + Require(handle != nullptr, "create stores the handle"); + + float coefficients[4] = {-1, -1, -1, -1}; + Require(sceAudio3dGetSpeakerArrayMixCoefficients(handle, AudioOut2Position{1, 0, 0}, 0.0f, coefficients, 4) == 0, "mix coefficients succeed"); + Require(coefficients[2] == 0.0f && coefficients[3] == 0.0f, "mix coefficients are written"); + float coefficients2[4] = {-1, -1, -1, -1}; + Require(sceAudio3dGetSpeakerArrayMixCoefficients2(handle, AudioOut2Position{1, 0, 0}, 0.0f, coefficients2, 4, 1, 0.5f) == 0, "mix coefficients 2 succeed"); + Require(coefficients2[3] == 0.0f, "mix coefficients 2 are written"); + RequireThrows([&] { sceAudio3dGetSpeakerArrayMixCoefficients(handle, AudioOut2Position{1, 0, 0}, 0.0f, coefficients, 3); }, "a coefficient count that differs from the array is rejected"); + + Require(sceAudio3dDeleteSpeakerArray(handle) == 0, "delete succeeds"); + RequireThrows([&] { sceAudio3dDeleteSpeakerArray(handle); }, "a deleted array is no longer valid"); + return 0; +} diff --git a/core/libs/tests/GuestAudioIn.cpp b/core/libs/tests/GuestAudioIn.cpp index 05b2eac559..15caefd1ec 100644 --- a/core/libs/tests/GuestAudioIn.cpp +++ b/core/libs/tests/GuestAudioIn.cpp @@ -16,6 +16,10 @@ int APS5_VABI sceAudioInInit(); int APS5_VABI sceAudioInOpen(int, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t); int APS5_VABI sceAudioInHqOpen(int, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t); int APS5_VABI sceAudioInAsyncOpen(int, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t); +int APS5_VABI sceAudioInOpenEx(int, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t); +int APS5_VABI sceAudioInHqOpenEx(int, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t); +int APS5_VABI sceAudioInSetCompressorPreGain(); +int APS5_VABI sceAudioInSetNoiseGateThreshold(); int32_t APS5_VABI sceAudioInIsPadEmbeddedMic(int32_t); int32_t APS5_VABI sceAudioInGetGain(int32_t, float*); int32_t APS5_VABI sceAudioInGetRerouteCount(int32_t); @@ -48,6 +52,21 @@ namespace { constexpr int user = 0x10000000; +void TestOpenEx() { + constexpr int invalidSize = static_cast(0x80260102); + constexpr int invalidFreq = static_cast(0x80260103); + constexpr int invalidType = static_cast(0x80260104); + Require(sceAudioInOpenEx(user, 2, 0, 7, 256, 48000, 2) == invalidType); + Require(sceAudioInOpenEx(user, 0, 0, 7, 512, 48000, 2) == invalidSize); + Require(sceAudioInOpenEx(user, 0, 0, 7, 256, 44100, 2) == invalidFreq); + Require(sceAudioInHqOpenEx(user, 0, 0, 7, 128, 44100, 2) == invalidFreq); + int port = sceAudioInOpenEx(user, 0, 0, 7, 256, 48000, 2); + Require(port > 0 && sceAudioInClose(port) == 0); + port = sceAudioInHqOpenEx(user, 0, 0, 7, 256, 16000, 2); + Require(port > 0 && sceAudioInClose(port) == 0); + Require(sceAudioInSetCompressorPreGain() == 0 && sceAudioInSetNoiseGateThreshold() == 0); +} + void TestValidation() { constexpr int invalidHandle = static_cast(0x80260101); constexpr int invalidSize = static_cast(0x80260102); @@ -99,7 +118,10 @@ void TestValidation() { port = sceAudioInAsyncOpen(user, 0, 0, 128, 48000, 0x10); gain = 0.5f; Require(port > 0 && sceAudioInGetGain(port, &gain) == 0 && gain == 1.0f && sceAudioInGetRerouteCount(port) == 0 && sceAudioInClose(port) == 0); - Require(ReportsNotImplemented("sceAudioInIsPadEmbeddedMic", [] { sceAudioInIsPadEmbeddedMic(1); })); + Require(sceAudioInIsPadEmbeddedMic(1) == static_cast(0x80260101)); + const int mic = sceAudioInOpen(user, 0, 0, 256, 48000, 0x10); + Require(mic > 0 && sceAudioInIsPadEmbeddedMic(mic) == 0 && sceAudioInClose(mic) == 0); + Require(sceAudioInIsPadEmbeddedMic(mic) == static_cast(0x80260101)); } void InputSound(int handle, std::vector& block, int samples = 256) { @@ -119,6 +141,7 @@ void TestCapture() { SetEnvironment("SDL_DISKAUDIOFILEIN", path.string()); TestValidation(); + TestOpenEx(); const int handle = sceAudioInOpen(user, 0, 0, 256, 48000, 2); Require(handle > 0 && sceAudioInGetSilentState(handle) == 0); std::vector block(256 * 2 + 1, 0x5555); @@ -147,6 +170,7 @@ void TestNoDevice() { SetEnvironment("SDL_AUDIODRIVER", "none"); TestValidation(); + TestOpenEx(); const int handle = sceAudioInOpen(user, 0, 0, 256, 16000, 0x12); Require(handle > 0 && sceAudioInGetSilentState(handle) == 1); Require(sceAudioInGetSilentState(handle + 1) == invalidHandle); diff --git a/core/libs/tests/GuestAudioPropagation.cpp b/core/libs/tests/GuestAudioPropagation.cpp index 0f060fd0a5..bc5db1f6a6 100644 --- a/core/libs/tests/GuestAudioPropagation.cpp +++ b/core/libs/tests/GuestAudioPropagation.cpp @@ -7,10 +7,15 @@ #include #include +#ifdef _WIN32 +#include +#else +#include +#endif + extern "C" { int APS5_VABI sceAudioPropagationPathGetNumPoints(); int APS5_VABI sceAudioPropagationSystemLock(); -int APS5_VABI sceAudioPropagationMaterialInit(); } namespace { @@ -34,6 +39,16 @@ void RequireThrows(TCall call, const char* message) { Require(false, message); } +void* Resolve(const char* name) { +#ifdef _WIN32 + const HMODULE module = GetModuleHandleA("libSceAudioPropagation.prx"); + Require(module != nullptr, "library module"); + return reinterpret_cast(GetProcAddress(module, name)); +#else + return dlsym(RTLD_DEFAULT, name); +#endif +} + AudioPropagationSystemMemory Memory() { AudioPropagationSystemMemory memory{}; memory.desc = {AudioPropagation::SystemMemoryId, AudioPropagation::SystemMemorySize}; @@ -294,7 +309,11 @@ int main() { RequireThrows([&] { sceAudioPropagationPathGetNumPoints(); }, "path get num points throw"); RequireThrows([&] { sceAudioPropagationSystemLock(); }, "system lock throw"); - RequireThrows([&] { sceAudioPropagationMaterialInit(); }, "material init throw"); + + Require(Resolve("sceAudioPropagationSystemCreate") != nullptr && Resolve("sceAudioPropagationSourceSetAudioPath") != nullptr, "export lookup"); + for (const char* name : {"sceAudioPropagationAudioPathInit", "sceAudioPropagationAudioPathPointInit", "sceAudioPropagationMaterialInit", "sceAudioPropagationPortalSettingsInit", "sceAudioPropagationRayInit", "sceAudioPropagationSourceRenderInfoInit", "sceAudioPropagationSourceSetAudioPathsParamInit", "sceAudioPropagationSystemMemoryInit", "sceAudioPropagationSystemOptionInit", "sceAudioPropagationSystem"}) { + Require(Resolve(name) == nullptr, name); + } std::puts("AudioPropagation tests passed"); return 0; diff --git a/core/libs/tests/GuestCommonDialog.cpp b/core/libs/tests/GuestCommonDialog.cpp index 1638caf9ec..e5b9cd993b 100644 --- a/core/libs/tests/GuestCommonDialog.cpp +++ b/core/libs/tests/GuestCommonDialog.cpp @@ -13,12 +13,15 @@ void Require(bool value) { if (!value) std::abort(); } +constexpr int kErrAlreadySystemInitialized = static_cast(0x80B80002); + } int main() { Require(!sceCommonDialogIsUsed()); Require(sceCommonDialogInitialize() == COMMON_DIALOG_OK); Require(!sceCommonDialogIsUsed()); - Require(sceCommonDialogInitialize() == COMMON_DIALOG_ERROR_ALREADY_INITIALIZED); + Require(sceCommonDialogInitialize() == kErrAlreadySystemInitialized); + Require(sceCommonDialogInitialize() == kErrAlreadySystemInitialized); Require(!sceCommonDialogIsUsed()); } diff --git a/core/libs/tests/GuestContentSearch.cpp b/core/libs/tests/GuestContentSearch.cpp index e9facbccc1..5676436c77 100644 --- a/core/libs/tests/GuestContentSearch.cpp +++ b/core/libs/tests/GuestContentSearch.cpp @@ -15,12 +15,34 @@ int APS5_VABI sceContentSearchOpenMetadataByContentId(std::int64_t contentId, st int APS5_VABI sceContentSearchCloseMetadata(std::int32_t metadataId); int APS5_VABI sceContentSearchGetMetadataFieldInfo(std::int32_t metadataId, const char* field, std::int32_t* metadataType, std::int32_t* size); int APS5_VABI sceContentSearchGetMetadataValue(std::int32_t metadataId, const char* field, void* value); +int APS5_VABI sceContentBinderEnd(void); +int APS5_VABI sceContentBinderGetNumOfDifference(void); +int APS5_VABI sceContentBinderInitialize(void); +int APS5_VABI sceContentBinderIsUpdated(void); +int APS5_VABI sceContentBinderReset(void); +int APS5_VABI sceContentBinderSearchDifference(void); +int APS5_VABI sceContentBinderStart(void); +int APS5_VABI sceContentBinderTerminate(void); +int APS5_VABI sceContentSearchGetApplicationLastUpdateId(void); +int APS5_VABI sceContentSearchGetMyApplicationIndex(void); +int APS5_VABI sceContentSearchGetNumOfApplication(void); +int APS5_VABI sceContentSearchSearchApplication(void); } namespace { void Require(bool value) { if (!value) std::abort(); } +template +bool ThrowsRuntimeError(TAction action) { + try { + action(); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + template bool Throws(TAction action) { try { @@ -136,4 +158,18 @@ int main() { Require(sceContentSearchTerm() == 0); Require(sceContentSearchGetContentLastUpdateId(&updateId) == ErrorNotInitialized); Require(updateId == -1); + constexpr int binderResult = static_cast(0x809D000F); + Require(sceContentBinderInitialize() == binderResult); + Require(sceContentBinderStart() == binderResult); + Require(sceContentBinderReset() == binderResult); + Require(sceContentBinderIsUpdated() == binderResult); + Require(sceContentBinderGetNumOfDifference() == binderResult); + Require(sceContentBinderSearchDifference() == binderResult); + Require(sceContentBinderEnd() == binderResult); + Require(sceContentBinderTerminate() == binderResult); + + Require(ThrowsRuntimeError([] { sceContentSearchGetApplicationLastUpdateId(); })); + Require(ThrowsRuntimeError([] { sceContentSearchGetMyApplicationIndex(); })); + Require(ThrowsRuntimeError([] { sceContentSearchGetNumOfApplication(); })); + Require(ThrowsRuntimeError([] { sceContentSearchSearchApplication(); })); } diff --git a/core/libs/tests/GuestCoredumpUserData.cpp b/core/libs/tests/GuestCoredumpUserData.cpp index 3b7c6c3817..d0baa9fd12 100644 --- a/core/libs/tests/GuestCoredumpUserData.cpp +++ b/core/libs/tests/GuestCoredumpUserData.cpp @@ -15,6 +15,7 @@ int64_t APS5_VABI sceCoredumpWriteUserData(const void* data, size_t size); void APS5_VABI sceCoredumpDebugTextOut(const char* str, int len); int APS5_VABI sceCoredumpAttachUserFile(uint32_t userValue, const char* path); int APS5_VABI sceCoredumpAttachMemoryRegionAsUserFile(uint32_t userValue, const void* mem, std::int32_t size, const char* name); +int APS5_VABI sceCoredumpSetUserDataType(std::int32_t type); } namespace { @@ -77,6 +78,7 @@ int main() { Require(sceCoredumpAttachUserFile(0x40000000u, "") == NotInCoredumpHandler); Require(NotInCoredumpHandler < 0); + Require(sceCoredumpSetUserDataType(0) == 0); RequireWritesRefused(region); RequireAttachesRefused(region); sceCoredumpDebugTextOut(text, static_cast(sizeof(text) - 1)); diff --git a/core/libs/tests/GuestCredentials.cpp b/core/libs/tests/GuestCredentials.cpp index 887f69c727..ff69af902b 100644 --- a/core/libs/tests/GuestCredentials.cpp +++ b/core/libs/tests/GuestCredentials.cpp @@ -11,6 +11,8 @@ int APS5_VABI setuid_nid_postfix(std::uint32_t); int APS5_VABI setgid_nid_postfix(std::uint32_t); int APS5_VABI setreuid_nid_postfix(std::uint32_t, std::uint32_t); int APS5_VABI setregid_nid_postfix(std::uint32_t, std::uint32_t); +int APS5_VABI setresuid_nid_postfix(std::uint32_t, std::uint32_t, std::uint32_t); +int APS5_VABI setresgid_nid_postfix(std::uint32_t, std::uint32_t, std::uint32_t); int APS5_VABI setgroups_nid_postfix(int, const std::uint32_t*); int APS5_VABI getgroups_nid_postfix(int, std::uint32_t*); int APS5_VABI getuid_nid_postfix(void); @@ -18,6 +20,8 @@ int APS5_VABI geteuid_nid_postfix(void); int APS5_VABI getgid_nid_postfix(void); int APS5_VABI getegid_nid_postfix(void); int APS5_VABI issetugid_nid_postfix(void); +int APS5_VABI getresuid_nid_postfix(std::uint32_t*, std::uint32_t*, std::uint32_t*); +int APS5_VABI getresgid_nid_postfix(std::uint32_t*, std::uint32_t*, std::uint32_t*); int* APS5_VABI __error_nid_postfix(); } @@ -43,6 +47,17 @@ static bool ThrowsRe(SetReId function, std::uint32_t r, std::uint32_t e) { return false; } +using SetResId = int (APS5_VABI *)(std::uint32_t, std::uint32_t, std::uint32_t); + +static bool ThrowsRes(SetResId function, std::uint32_t r, std::uint32_t e, std::uint32_t s) { + try { + function(r, e, s); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + constexpr int GuestEfault = 14; constexpr int GuestEinval = 22; constexpr std::uint32_t Unchanged = 0xffffffffu; @@ -68,6 +83,32 @@ int main() { Require(Throws(setgid_nid_postfix, Unchanged)); Require(getuid_nid_postfix() == 0 && geteuid_nid_postfix() == 0); Require(getgid_nid_postfix() == 0 && getegid_nid_postfix() == 0 && issetugid_nid_postfix() == 0); + + std::uint32_t ruid = 99, euid = 99, suid = 99; + *__error_nid_postfix() = 13; + Require(getresuid_nid_postfix(&ruid, &euid, &suid) == 0); + Require(ruid == 0 && euid == 0 && suid == 0); + Require(*__error_nid_postfix() == 13); + ruid = 99; + euid = 99; + suid = 99; + Require(getresuid_nid_postfix(&ruid, nullptr, nullptr) == 0 && ruid == 0); + Require(getresuid_nid_postfix(nullptr, &euid, nullptr) == 0 && euid == 0); + Require(getresuid_nid_postfix(nullptr, nullptr, &suid) == 0 && suid == 0); + Require(getresuid_nid_postfix(nullptr, nullptr, nullptr) == 0); + + std::uint32_t rgid = 99, egid = 99, sgid = 99; + *__error_nid_postfix() = 13; + Require(getresgid_nid_postfix(&rgid, &egid, &sgid) == 0); + Require(rgid == 0 && egid == 0 && sgid == 0); + Require(*__error_nid_postfix() == 13); + rgid = 99; + egid = 99; + sgid = 99; + Require(getresgid_nid_postfix(&rgid, nullptr, nullptr) == 0 && rgid == 0); + Require(getresgid_nid_postfix(nullptr, &egid, nullptr) == 0 && egid == 0); + Require(getresgid_nid_postfix(nullptr, nullptr, &sgid) == 0 && sgid == 0); + Require(getresgid_nid_postfix(nullptr, nullptr, nullptr) == 0); Require(setreuid_nid_postfix(Unchanged, Unchanged) == 0); Require(setreuid_nid_postfix(0, Unchanged) == 0); Require(setreuid_nid_postfix(Unchanged, 0) == 0); @@ -82,10 +123,44 @@ int main() { Require(ThrowsRe(setregid_nid_postfix, 1000, 0)); Require(ThrowsRe(setregid_nid_postfix, 0, 1000)); + Require(setresuid_nid_postfix(Unchanged, Unchanged, Unchanged) == 0); + Require(setresuid_nid_postfix(0, Unchanged, Unchanged) == 0); + Require(setresuid_nid_postfix(Unchanged, 0, Unchanged) == 0); + Require(setresuid_nid_postfix(Unchanged, Unchanged, 0) == 0); + Require(setresuid_nid_postfix(0, 0, 0) == 0); + Require(ThrowsRes(setresuid_nid_postfix, 1000, 0, 0)); + Require(ThrowsRes(setresuid_nid_postfix, 0, 1000, 0)); + Require(ThrowsRes(setresuid_nid_postfix, 0, 0, 1000)); + Require(ThrowsRes(setresuid_nid_postfix, 1000, Unchanged, Unchanged)); + Require(ThrowsRes(setresuid_nid_postfix, Unchanged, 1000, Unchanged)); + Require(ThrowsRes(setresuid_nid_postfix, Unchanged, Unchanged, 1000)); + + Require(setresgid_nid_postfix(Unchanged, Unchanged, Unchanged) == 0); + Require(setresgid_nid_postfix(0, Unchanged, Unchanged) == 0); + Require(setresgid_nid_postfix(Unchanged, 0, Unchanged) == 0); + Require(setresgid_nid_postfix(Unchanged, Unchanged, 0) == 0); + Require(setresgid_nid_postfix(0, 0, 0) == 0); + Require(ThrowsRes(setresgid_nid_postfix, 1000, 0, 0)); + Require(ThrowsRes(setresgid_nid_postfix, 0, 1000, 0)); + Require(ThrowsRes(setresgid_nid_postfix, 0, 0, 1000)); + Require(ThrowsRes(setresgid_nid_postfix, 1000, Unchanged, Unchanged)); + Require(ThrowsRes(setresgid_nid_postfix, Unchanged, 1000, Unchanged)); + Require(ThrowsRes(setresgid_nid_postfix, Unchanged, Unchanged, 1000)); + std::array groups{}; Require(getgroups_nid_postfix(0, nullptr) == 1); Require(getgroups_nid_postfix(static_cast(groups.size()), groups.data()) == 1 && groups[0] == 0); Require(groups[0] == static_cast(getegid_nid_postfix())); + *__error_nid_postfix() = 13; + RequireFailure(setgroups_nid_postfix(1, reinterpret_cast(1)), GuestEfault); + *__error_nid_postfix() = 13; + RequireFailure(getgroups_nid_postfix(1, reinterpret_cast(1)), GuestEfault); + Require(getgroups_nid_postfix(0, nullptr) == 1); + + *__error_nid_postfix() = 13; + RequireFailure(getresuid_nid_postfix(reinterpret_cast(1), nullptr, nullptr), GuestEfault); + *__error_nid_postfix() = 13; + RequireFailure(getresgid_nid_postfix(nullptr, reinterpret_cast(1), nullptr), GuestEfault); const std::array wanted{0, 5, 7}; Require(setgroups_nid_postfix(static_cast(wanted.size()), wanted.data()) == 0); diff --git a/core/libs/tests/GuestCryptoMemcmp.cpp b/core/libs/tests/GuestCryptoMemcmp.cpp new file mode 100644 index 0000000000..578b4a4645 --- /dev/null +++ b/core/libs/tests/GuestCryptoMemcmp.cpp @@ -0,0 +1,125 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#else +#include +#include +#endif + +extern "C" int APS5_VABI CRYPTO_memcmp_nid_postfix(const void*, const void*, std::size_t); + +namespace { + +void Require(bool value) { + if (!value) { + std::fprintf(stderr, "CRYPTO_memcmp check failed\n"); + std::abort(); + } +} + +class GuardedPage { +public: + explicit GuardedPage(bool different) { +#ifdef _WIN32 + SYSTEM_INFO info{}; + GetSystemInfo(&info); + pageSize = info.dwPageSize; + memory = static_cast(VirtualAlloc(nullptr, pageSize * 2, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + Require(memory != nullptr); + DWORD previous; + Require(VirtualProtect(memory + pageSize, pageSize, PAGE_NOACCESS, &previous) != 0); +#else + const long nativePageSize = sysconf(_SC_PAGESIZE); + Require(nativePageSize > 0); + pageSize = static_cast(nativePageSize); + void* allocation = mmap(nullptr, pageSize * 2, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(allocation != MAP_FAILED); + memory = static_cast(allocation); + Require(mprotect(memory + pageSize, pageSize, PROT_NONE) == 0); +#endif + std::memset(memory, 0xA5, pageSize); + if (different) memory[pageSize - 1] ^= 0xFF; +#ifdef _WIN32 + Require(VirtualProtect(memory, pageSize, PAGE_READONLY, &previous) != 0); +#else + Require(mprotect(memory, pageSize, PROT_READ) == 0); +#endif + } + + GuardedPage(const GuardedPage&) = delete; + GuardedPage& operator=(const GuardedPage&) = delete; + + ~GuardedPage() { +#ifdef _WIN32 + VirtualFree(memory, 0, MEM_RELEASE); +#else + munmap(memory, pageSize * 2); +#endif + } + + const unsigned char* Tail(std::size_t length) const { + Require(length <= pageSize); + return memory + pageSize - length; + } + +private: + unsigned char* memory = nullptr; + std::size_t pageSize = 0; +}; + +} + +int main() { + Require(CRYPTO_memcmp_nid_postfix(nullptr, nullptr, 0) == 0); + unsigned char byte = 0xA5; + Require(CRYPTO_memcmp_nid_postfix(nullptr, &byte, 0) == 0); + Require(CRYPTO_memcmp_nid_postfix(&byte, nullptr, 0) == 0); + for (unsigned int left = 0; left < 256; ++left) { + for (unsigned int right = 0; right < 256; ++right) { + const unsigned char a = static_cast(left); + const unsigned char b = static_cast(right); + Require((CRYPTO_memcmp_nid_postfix(&a, &b, 1) == 0) == (left == right)); + } + } + std::array first; + for (std::size_t index = 0; index < first.size(); ++index) first[index] = static_cast(index * 37); + auto second = first; + const auto original = first; + Require(CRYPTO_memcmp_nid_postfix(first.data(), first.data(), first.size()) == 0); + Require(CRYPTO_memcmp_nid_postfix(first.data() + 1, second.data() + 1, 65) == 0); + for (std::size_t index = 1; index <= 65; ++index) { + second[index] ^= 0x80; + const auto changed = second; + Require(CRYPTO_memcmp_nid_postfix(first.data() + 1, second.data() + 1, 65) != 0); + Require(CRYPTO_memcmp_nid_postfix(second.data() + 1, first.data() + 1, 65) != 0); + Require(first == original && second == changed); + second[index] ^= 0x80; + } + second[32] ^= 0x80; + Require(CRYPTO_memcmp_nid_postfix(first.data(), second.data(), 32) == 0); + Require(CRYPTO_memcmp_nid_postfix(first.data(), second.data(), 33) != 0); + second = first; + second[1] ^= 0x80; + second[65] ^= 0x80; + Require(CRYPTO_memcmp_nid_postfix(first.data(), second.data(), first.size()) != 0); + first.fill(0xCC); + Require(CRYPTO_memcmp_nid_postfix(first.data(), first.data() + 1, first.size() - 1) == 0); + first[32] = 0xCD; + const auto overlap = first; + Require(CRYPTO_memcmp_nid_postfix(first.data(), first.data() + 1, first.size() - 1) != 0); + Require(first == overlap); + const GuardedPage equal(false); + const GuardedPage other(false); + const GuardedPage different(true); + for (const std::size_t length : {0, 1, 2, 7, 15, 16, 17, 31, 32, 33, 63, 64, 65, 127, 128, 129}) { + Require(CRYPTO_memcmp_nid_postfix(equal.Tail(length), other.Tail(length), length) == 0); + Require((CRYPTO_memcmp_nid_postfix(equal.Tail(length), different.Tail(length), length) == 0) == (length == 0)); + } +} diff --git a/core/libs/tests/GuestDinkumwareInternals.cpp b/core/libs/tests/GuestDinkumwareInternals.cpp new file mode 100644 index 0000000000..aa660dabdf --- /dev/null +++ b/core/libs/tests/GuestDinkumwareInternals.cpp @@ -0,0 +1,88 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include + +extern "C" { +int* APS5_VABI __error_nid_postfix(); +double APS5_VABI _Stod_nid_postfix(const char* text, char** end, std::int64_t pten); +long long APS5_VABI _Stoll_nid_postfix(const char* text, char** end, int base); +double APS5_VABI _Sin_nid_postfix(double x, unsigned int quadrant); +float APS5_VABI _FSin_nid_postfix(float x, unsigned int quadrant); +long double APS5_VABI _LSin_nid_postfix(long double x, unsigned int quadrant); +double APS5_VABI _Log_nid_postfix(double x, int base); +float APS5_VABI _FLog_nid_postfix(float x, int base); +} + +namespace { + +constexpr int RangeError = 34; + +void Require(bool condition, const char* message) { + if (!condition) { + std::fprintf(stderr, "Dinkumware internals: %s\n", message); + std::exit(1); + } +} + +void CheckStod(const char* text, std::int64_t pten, double expected, long consumed, int error, const char* message) { + *__error_nid_postfix() = 0; + char* end = nullptr; + const double value = _Stod_nid_postfix(text, &end, pten); + const bool same = std::isnan(expected) ? std::isnan(value) : value == expected && std::signbit(value) == std::signbit(expected); + Require(same && end - text == consumed && *__error_nid_postfix() == error, message); +} + +void TestStod() { + CheckStod("1.5", 0, 1.5, 3, 0, "unscaled decimal"); + CheckStod("1.5", 1, 15.0, 3, 0, "decimal scaled up"); + CheckStod("12", -2, 0.12, 2, 0, "decimal scaled down"); + CheckStod(" -2.25e3xyz", 300, -0x1.a3f7f806a8135p+1007, 9, 0, "the scale is applied before rounding"); + CheckStod("1e-400", 300, 1e-100, 6, 0, "an underflowing text scaled back into range"); + CheckStod("1e-400", 0, 0.0, 6, RangeError, "an unscaled underflow"); + CheckStod("1e300", 100, HUGE_VAL, 5, RangeError, "a scale that overflows"); + CheckStod("1e300", 8, 1e308, 5, 0, "a scale up to the largest decades"); + CheckStod("5e-324", -1, 0.0, 6, RangeError, "a scale that underflows to zero"); + CheckStod("1", 2147483647, HUGE_VAL, 1, RangeError, "the largest scale"); + CheckStod("5", std::int64_t{1} << 40, HUGE_VAL, 1, RangeError, "a scale above 32 bits"); + CheckStod("1e400", -100, 1e300, 5, 0, "an overflowing text scaled back into range"); + CheckStod("0.01e41", -2, 1e37, 7, 0, "a negative scale from leading fraction zeros"); + CheckStod("1e-2147483648", -1, 0.0, 13, RangeError, "the smallest exponent and scale"); + CheckStod("-0", 5, -0.0, 2, 0, "negative zero keeps its sign"); + CheckStod("infinity", 3, HUGE_VAL, 8, 0, "infinity is not scaled"); + CheckStod("nan", 3, NAN, 3, 0, "NaN is not scaled"); + CheckStod("1e", 2, 100.0, 1, 0, "an exponent marker without digits"); + CheckStod(".5", 2, 50.0, 2, 0, "a fraction without integer digits"); + CheckStod("0.3", -1, 0.03, 3, 0, "0.3 scaled down rounds as 0.03"); + CheckStod("123456789012345678901234567890", -20, 1234567890.1234567, 30, 0, "a long digit string"); + CheckStod("abc", 4, 0.0, 0, 0, "no number"); + CheckStod("0x1.8p0", 0, 1.5, 7, 0, "unscaled hexadecimal"); +} + +void TestStoll() { + char* end = nullptr; + *__error_nid_postfix() = 0; + Require(_Stoll_nid_postfix("-0x1F", &end, 0) == -31 && std::strcmp(end, "") == 0, "_Stoll base detection"); + Require(_Stoll_nid_postfix("+42abc", &end, 10) == 42 && std::strcmp(end, "abc") == 0, "_Stoll stops at the first non-digit"); + Require(_Stoll_nid_postfix(" z", &end, 36) == 35, "_Stoll base 36"); + Require(*__error_nid_postfix() == 0, "_Stoll set errno without an overflow"); + Require(_Stoll_nid_postfix("9223372036854775808", &end, 10) == 9223372036854775807ll && *__error_nid_postfix() == RangeError, "_Stoll saturates"); +} + +void TestDispatch() { + Require(_Sin_nid_postfix(0.5, 0) == std::sin(0.5) && _Sin_nid_postfix(0.5, 1) == std::cos(0.5) && _Sin_nid_postfix(0.5, 7) == std::cos(0.5), "_Sin"); + Require(_FSin_nid_postfix(0.5f, 0) == std::sin(0.5f) && _FSin_nid_postfix(0.5f, 2) == std::cos(0.5f), "_FSin"); + Require(_LSin_nid_postfix(0.5L, 0) == std::sin(0.5L) && _LSin_nid_postfix(0.5L, 3) == std::cos(0.5L), "_LSin"); + Require(_Log_nid_postfix(8.0, 0) == std::log(8.0) && _Log_nid_postfix(1000.0, 1) == 3.0 && _Log_nid_postfix(1000.0, 5) == 3.0 && _Log_nid_postfix(8.0, -1) == 3.0, "_Log"); + Require(_FLog_nid_postfix(8.0f, 0) == std::log(8.0f) && _FLog_nid_postfix(100.0f, 1) == 2.0f && _FLog_nid_postfix(8.0f, -3) == 3.0f, "_FLog"); +} + +} + +int main() { + TestStod(); + TestStoll(); + TestDispatch(); +} diff --git a/core/libs/tests/GuestDirectoryLinkTypes.cpp b/core/libs/tests/GuestDirectoryLinkTypes.cpp new file mode 100644 index 0000000000..344b0c3a1c --- /dev/null +++ b/core/libs/tests/GuestDirectoryLinkTypes.cpp @@ -0,0 +1,80 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/File/include/FileFlags.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceKernelOpen(const char*, int, std::uint16_t); +int APS5_VABI sceKernelClose(int); +int APS5_VABI sceKernelGetdents(int, char*, int); +} + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "Directory link type check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +static constexpr std::uint8_t TypeDirectory = 4; +static constexpr std::uint8_t TypeRegular = 8; +static constexpr std::uint8_t TypeLink = 10; + +static std::map Types(const char* path) { + const int directory = sceKernelOpen(path, SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); + Require(directory >= 0); + std::array buffer{}; + std::map types; + for (;;) { + const int read = sceKernelGetdents(directory, buffer.data(), static_cast(buffer.size())); + Require(read >= 0); + if (read == 0) break; + for (int offset = 0; offset < read;) { + std::uint16_t recordLength = 0; + std::memcpy(&recordLength, buffer.data() + offset + 4, sizeof(recordLength)); + Require(recordLength != 0 && offset + recordLength <= read); + types[std::string(buffer.data() + offset + 8)] = static_cast(buffer[offset + 6]); + offset += recordLength; + } + } + Require(sceKernelClose(directory) == 0); + return types; +} + +int main() { + const auto root = std::filesystem::temp_directory_path() / + ("anyps5-directory-link-types-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + Require(std::filesystem::create_directories(root / "app0" / "real")); + Require(std::filesystem::create_directories(root / "dump" / "packed")); + Require(std::filesystem::create_directories(root / "data")); + { std::ofstream stream(root / "dump" / "eboot.bin", std::ios::binary); stream << "x"; } + std::filesystem::create_directory_symlink(root / "dump" / "packed", root / "app0" / "packed"); + std::filesystem::create_symlink(root / "dump" / "eboot.bin", root / "app0" / "eboot.bin"); + std::filesystem::create_symlink(root / "dump" / "missing", root / "app0" / "dangling"); + std::filesystem::create_directory_symlink(root / "dump" / "packed", root / "data" / "saved"); + std::filesystem::current_path(root); + + auto app0 = Types("/app0"); + Require(app0.contains("real") && app0["real"] == TypeDirectory); + Require(app0.contains("packed") && app0["packed"] == TypeDirectory); + Require(app0.contains("eboot.bin") && app0["eboot.bin"] == TypeRegular); + Require(app0.contains("dangling") && app0["dangling"] == TypeLink); + + auto data = Types("/data"); + Require(data.contains("saved") && data["saved"] == TypeLink); + + std::error_code error; + std::filesystem::current_path(std::filesystem::temp_directory_path(), error); + std::filesystem::remove_all(root, error); + std::puts("directory link type tests passed"); + return 0; +} diff --git a/core/libs/tests/GuestEqueueEvents.cpp b/core/libs/tests/GuestEqueueEvents.cpp index 8e97329913..c0858dc39a 100644 --- a/core/libs/tests/GuestEqueueEvents.cpp +++ b/core/libs/tests/GuestEqueueEvents.cpp @@ -36,6 +36,7 @@ static constexpr int EVFILT_USER = -11; static constexpr int EVFILT_HRTIMER = -15; static constexpr int EVFILT_AMPR = -25; static constexpr std::uint16_t EV_ERROR = 0x4000; +static constexpr std::uint16_t EV_EOF = 0x8000; static void Require(bool value) { if (!value) std::abort(); } @@ -49,6 +50,15 @@ static bool RejectsNull(TResult (APS5_VABI *accessor)(const KernelEvent*)) { return false; } +static bool RejectsWideFflags(const KernelEvent* ev) { + try { + sceKernelGetEventError(ev); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + static void SleepAtLeast(std::chrono::steady_clock::duration duration) { const auto start = std::chrono::steady_clock::now(); while (std::chrono::steady_clock::now() - start < duration) { @@ -291,12 +301,30 @@ int main() { Require(sceKernelGetEventFflags(&rawEvent) == static_cast(0x80000001LL)); Require(sceKernelGetEventData(&rawEvent) == -5); Require(sceKernelGetEventUserData(&rawEvent) == nullptr); - Require(sceKernelGetEventError(&rawEvent) == 0); - rawEvent.flags = static_cast(~EV_ERROR); - Require(sceKernelGetEventError(&rawEvent) == 0); + Require(RejectsWideFflags(&rawEvent)); + rawEvent.fflags = 0; rawEvent.flags = EV_ERROR; rawEvent.data = 22; - Require(sceKernelGetEventError(&rawEvent) == 22); + Require(sceKernelGetEventError(&rawEvent) == 0); + rawEvent.data = -1; + Require(sceKernelGetEventError(&rawEvent) == 0); + rawEvent.data = 0x80020016; + Require(sceKernelGetEventError(&rawEvent) == 0); + rawEvent.fflags = 5; + rawEvent.data = 0; + Require(sceKernelGetEventError(&rawEvent) == static_cast(0x80020005)); + rawEvent.data = 22; + Require(sceKernelGetEventError(&rawEvent) == static_cast(0x80020005)); + rawEvent.flags = 0; + rawEvent.fflags = 7; + Require(sceKernelGetEventError(&rawEvent) == static_cast(0x80020007)); + rawEvent.flags = EV_EOF; + rawEvent.fflags = 3; + rawEvent.data = 9; + Require(sceKernelGetEventError(&rawEvent) == static_cast(0x80020003)); + rawEvent.flags = static_cast(~EV_ERROR); + rawEvent.fflags = 0xFFFF; + Require(sceKernelGetEventError(&rawEvent) == static_cast(0x8002FFFF)); Require(RejectsNull(sceKernelGetEventData)); Require(RejectsNull(sceKernelGetEventError)); diff --git a/core/libs/tests/GuestErrorDialog.cpp b/core/libs/tests/GuestErrorDialog.cpp index 106819b6a8..51779896a1 100644 --- a/core/libs/tests/GuestErrorDialog.cpp +++ b/core/libs/tests/GuestErrorDialog.cpp @@ -9,6 +9,8 @@ int APS5_VABI sceErrorDialogUpdateStatus(void); int APS5_VABI sceErrorDialogGetStatus(void); int APS5_VABI sceErrorDialogClose(void); int APS5_VABI sceErrorDialogTerminate(void); +int APS5_VABI sceErrorDialogOpenDetail(void); +int APS5_VABI sceErrorDialogOpenWithReport(void); } namespace { @@ -20,6 +22,7 @@ constexpr int kStatusFinished = 3; constexpr int kErrNotInitialized = static_cast(0x80ED0001); constexpr int kErrAlreadyInitialized = static_cast(0x80ED0002); constexpr int kErrParam = static_cast(0x80ED0003); +constexpr int kErrUnexpectedFatal = static_cast(0x80ED0004); constexpr int kErrInvalidState = static_cast(0x80ED0005); struct ErrorDialogParam { @@ -68,4 +71,6 @@ int main() { Require(sceErrorDialogGetStatus() == 0); Require(sceErrorDialogUpdateStatus() == 0); Require(sceErrorDialogClose() == kErrInvalidState); + Require(sceErrorDialogOpenDetail() == kErrUnexpectedFatal); + Require(sceErrorDialogOpenWithReport() == kErrUnexpectedFatal); } diff --git a/core/libs/tests/GuestFileLock.cpp b/core/libs/tests/GuestFileLock.cpp index 3fb7ac7b71..4bb5835820 100644 --- a/core/libs/tests/GuestFileLock.cpp +++ b/core/libs/tests/GuestFileLock.cpp @@ -296,6 +296,23 @@ static void Processes() { Require(ChildTry(fixtures, 2) == 0); } +static void Directories() { + const std::filesystem::path directory = std::filesystem::temp_directory_path() / ("anyps5-flock-dir-" + std::to_string(::GetCurrentProcessId())); + std::filesystem::create_directories(directory); + const std::string path = directory.string(); + const int descriptor = open_nid_postfix(path.c_str(), 0x20000, 0); + Require(descriptor >= 0); + Require(flock_nid_postfix(descriptor, 1 | 4) == 0); + Require(flock_nid_postfix(descriptor, 2 | 4) == 0); + Require(flock_nid_postfix(descriptor, 2) == 0); + Require(flock_nid_postfix(descriptor, 8) == 0); + Require(flock_nid_postfix(descriptor, 0) == -1 && *__error_nid_postfix() == 9); + Require(flock_nid_postfix(descriptor, 4) == -1 && *__error_nid_postfix() == 9); + Require(close_nid_postfix(descriptor) == 0); + std::error_code ignored; + std::filesystem::remove_all(directory, ignored); +} + int main(int argc, char** argv) { if (argc >= 4 && std::string(argv[1]) == "child") { int busy = 0; @@ -320,5 +337,6 @@ int main(int argc, char** argv) { else if (mode == "threads") Threads(); else if (mode == "streams") Streams(); else if (mode == "process") Processes(); + else if (mode == "directory") Directories(); else std::abort(); } diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index 4d7f42b692..d759f1051d 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -2,10 +2,12 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libc/include/General.hpp" #include "prx/libc/include/FileStream.hpp" +#include "prx/libc/include/RenameDot.hpp" #include "prx/libkernel/File/include/File.hpp" #include "prx/libkernel/File/include/NativeStat.hpp" #include "prx/libkernel/KernelErrors.hpp" #include +#include #include #include #include @@ -21,6 +23,7 @@ #ifdef _WIN32 #include #include +#include #else #include #endif @@ -85,6 +88,13 @@ FileStream* APS5_VABI freopen_nid_postfix(const char*, const char*, FileStream*) std::size_t APS5_VABI fread_nid_postfix(void*, std::size_t, std::size_t, FileStream*); int APS5_VABI fclose_nid_postfix(FileStream*); int APS5_VABI fgetc_nid_postfix(FileStream*); +int APS5_VABI fileno_nid_postfix(FileStream*); +char* APS5_VABI fgets_nid_postfix(char*, int, FileStream*); +int APS5_VABI fscanf_nid_postfix(FileStream*, const char*, ...); +int APS5_VABI fputc_nid_postfix(int, FileStream*); +int APS5_VABI fseek_nid_postfix(FileStream*, std::int64_t, int); +std::int64_t APS5_VABI ftell_nid_postfix(FileStream*); +void APS5_VABI clearerr_nid_postfix(FileStream*); int APS5_VABI feof_nid_postfix(FileStream*); int APS5_VABI ferror_nid_postfix(FileStream*); int APS5_VABI setvbuf_nid_postfix(FileStream*, char*, int, std::size_t); @@ -250,6 +260,29 @@ static void TestParentTraversal() { Require(stat_nid_postfix("/parent-test-mount/missing/.", &status) == -1 && *__error_nid_postfix() == 2); Require(stat_nid_postfix("/parent-test-mount/existing/.", &status) == 0 && (status.st_mode & 0170000) == 0040000); Require(access_nid_postfix("/parent-test-mount/existing/./", 0) == 0); + Require(TrimTrailingSeparators_nid_no_patch("/") == "/"); + Require(TrimTrailingSeparators_nid_no_patch("/mount/dir//") == "/mount/dir"); + Require(TrimTrailingSeparators_nid_no_patch("dir/") == "dir"); +#ifdef _WIN32 + Require(TrimTrailingSeparators_nid_no_patch("C:\\") == "C:\\"); + Require(TrimTrailingSeparators_nid_no_patch("C:/") == "C:/"); + Require(TrimTrailingSeparators_nid_no_patch("C:\\dir\\") == "C:\\dir"); + Require(TrimTrailingSeparators_nid_no_patch("C:\\\\") == "C:\\"); + Require(TrimTrailingSeparators_nid_no_patch("C:\\folder\\\\") == "C:\\folder"); +#endif + std::filesystem::create_directory(root / "dot-victim"); + for (const char* name : {"/parent-test-mount/dot-victim/.", "/parent-test-mount/dot-victim/..", "/parent-test-mount/dot-victim/./", "/parent-test-mount/dot-victim/../"}) { + Require(unlink_nid_postfix(name) == -1 && *__error_nid_postfix() == 22); + Require(rmdir_nid_postfix(name) == -1 && *__error_nid_postfix() == 22); + Require(sceKernelUnlink(name) == static_cast(0x80020016u)); + Require(sceKernelRmdir(name) == static_cast(0x80020016u)); + } + Require(unlink_nid_postfix("/parent-test-mount/present.txt/.") == -1 && *__error_nid_postfix() == 20); + Require(rename_nid_postfix("/parent-test-mount/present.txt/.", "/parent-test-mount/dot-renamed") == -1 && *__error_nid_postfix() == 20); + Require(rename_nid_postfix("/parent-test-mount/present.txt/..", "/parent-test-mount/dot-renamed") == -1 && *__error_nid_postfix() == 20); + Require(rmdir_nid_postfix("/parent-test-mount/missing/..") == -1 && *__error_nid_postfix() == 2); + Require(std::filesystem::is_directory(root / "dot-victim") && std::filesystem::is_regular_file(root / "present.txt")); + std::filesystem::remove(root / "dot-victim"); #ifndef _WIN32 for (const char* name : {"slash-unlink", "slash-remove", "slash-rename", "slash-open", "slash-kernel"}) std::ofstream(root / name) << "kept"; Require(unlink_nid_postfix("/parent-test-mount/slash-unlink/") == -1 && *__error_nid_postfix() == 20); @@ -606,6 +639,45 @@ int main() { for (const char* openMode : {"w", "wb", "r+", "a"}) { Require(fopen_nid_postfix(root.string().c_str(), openMode) == nullptr && *__error_nid_postfix() == 21); } + { + const auto listed = root / "listed.txt"; + std::ofstream(listed, std::ios::binary) << "listed"; + for (const char* openMode : {"r", "rb", "re"}) { + auto* directoryStream = fopen_nid_postfix(root.string().c_str(), openMode); + Require(directoryStream != nullptr); + const int directoryDescriptor = fileno_nid_postfix(directoryStream); + FileStat directoryStatus{}; + Require(directoryDescriptor >= 0 && sceKernelFstat(directoryDescriptor, &directoryStatus) == 0 && (directoryStatus.st_mode & 0170000) == 0040000); + Require(feof_nid_postfix(directoryStream) == 0 && ferror_nid_postfix(directoryStream) == 0 && fflush_nid_postfix(directoryStream) == 0); + char unread[8] = {}; + int scanned = 0; + *__error_nid_postfix() = 0; + Require(fread_nid_postfix(unread, 1, sizeof(unread), directoryStream) == 0 && *__error_nid_postfix() == 21); + Require(ferror_nid_postfix(directoryStream) != 0 && feof_nid_postfix(directoryStream) == 0); + clearerr_nid_postfix(directoryStream); + Require(ferror_nid_postfix(directoryStream) == 0); + *__error_nid_postfix() = 0; + Require(fgetc_nid_postfix(directoryStream) == -1 && *__error_nid_postfix() == 21 && ferror_nid_postfix(directoryStream) != 0); + clearerr_nid_postfix(directoryStream); + *__error_nid_postfix() = 0; + Require(fgets_nid_postfix(unread, sizeof(unread), directoryStream) == nullptr && *__error_nid_postfix() == 21 && ferror_nid_postfix(directoryStream) != 0); + clearerr_nid_postfix(directoryStream); + *__error_nid_postfix() = 0; + Require(fscanf_nid_postfix(directoryStream, "%d", &scanned) == -1 && *__error_nid_postfix() == 21 && ferror_nid_postfix(directoryStream) != 0); + clearerr_nid_postfix(directoryStream); + Require(fseek_nid_postfix(directoryStream, 0, 0) == 0 && ftell_nid_postfix(directoryStream) == 0); + *__error_nid_postfix() = 0; + Require(fputc_nid_postfix('x', directoryStream) == -1 && *__error_nid_postfix() == 9 && ferror_nid_postfix(directoryStream) != 0); + Require(fclose_nid_postfix(directoryStream) == 0); +#ifdef _WIN32 + const int reused = open_nid_postfix(listed.string().c_str(), 0, 0); + char text[6]{}; + Require(reused == directoryDescriptor && read_nid_postfix(reused, text, sizeof(text)) == 6 && std::memcmp(text, "listed", 6) == 0); + Require(close_nid_postfix(reused) == 0); +#endif + } + Require(std::filesystem::remove(listed)); + } Require(mkdir_nid_postfix((root / "missing" / "child").string().c_str(), 0700) == -1 && *__error_nid_postfix() == 2); Require(mkdir_nid_postfix(nullptr, 0700) == -1 && *__error_nid_postfix() == 14); const auto file = root / "file.txt"; @@ -1046,6 +1118,42 @@ int main() { const int followedJunction = sceKernelOpen(junction.string().c_str(), 0, 0); Require(followedJunction >= 0 && sceKernelClose(followedJunction) == 0); Require(std::filesystem::remove(junction) && std::filesystem::remove(junctionTarget)); + const auto tagged = root / "tagged.bin"; + { std::ofstream stream(tagged, std::ios::binary); stream << std::string(1u << 18, 'a'); } + const HANDLE taggedHandle = CreateFileW(tagged.c_str(), GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, + FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr); + Require(taggedHandle != INVALID_HANDLE_VALUE); + std::array reparseData{}; + REPARSE_GUID_DATA_BUFFER reparseHeader{}; + reparseHeader.ReparseTag = 0x00000ABC; + reparseHeader.ReparseDataLength = 4; + reparseHeader.ReparseGuid = {0x6a1e3d0b, 0x1f6c, 0x4c1e, {0x9b, 0x52, 0x3d, 0x8e, 0x41, 0x07, 0x5c, 0x2a}}; + std::memcpy(reparseData.data(), &reparseHeader, REPARSE_GUID_DATA_BUFFER_HEADER_SIZE); + DWORD returned = 0; + const bool taggedSet = DeviceIoControl(taggedHandle, FSCTL_SET_REPARSE_POINT, reparseData.data(), + static_cast(reparseData.size()), nullptr, 0, &returned, nullptr) != 0; + if (!taggedSet) Require(GetLastError() == ERROR_PRIVILEGE_NOT_HELD || GetLastError() == ERROR_ACCESS_DENIED); + Require(CloseHandle(taggedHandle) != 0); + if (!taggedSet) { + Require(std::filesystem::remove(tagged)); + std::fprintf(stderr, "Windows reparse point checks skipped: setting a reparse point on a file needs rights this user lacks\n"); + } else { + Require((GetFileAttributesW(tagged.c_str()) & FILE_ATTRIBUTE_REPARSE_POINT) != 0); + FileStat taggedStatus{}; + FileStat taggedFollowed{}; + const int taggedResult = lstat_nid_postfix(tagged.string().c_str(), &taggedStatus); + const int taggedError = *__error_nid_postfix(); + Require(stat_nid_postfix(tagged.string().c_str(), &taggedFollowed) == taggedResult); + Require(taggedResult == 0 ? (taggedStatus.st_ino == taggedFollowed.st_ino && taggedStatus.st_mode == taggedFollowed.st_mode) + : *__error_nid_postfix() == taggedError); + const HANDLE taggedCleanup = CreateFileW(tagged.c_str(), GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, + FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr); + Require(taggedCleanup != INVALID_HANDLE_VALUE); + reparseHeader.ReparseDataLength = 0; + Require(DeviceIoControl(taggedCleanup, FSCTL_DELETE_REPARSE_POINT, &reparseHeader, REPARSE_GUID_DATA_BUFFER_HEADER_SIZE, + nullptr, 0, &returned, nullptr) != 0); + Require(CloseHandle(taggedCleanup) != 0 && std::filesystem::remove(tagged)); + } #endif FileStat identity{}; FileStat byDescriptor{}; @@ -1109,6 +1217,14 @@ int main() { Require(pread_nid_postfix(locked, nullptr, 1, 0) == -1 && *__error_nid_postfix() == 14); Require(close_nid_postfix(transferPipe[0]) == 0 && close_nid_postfix(transferPipe[1]) == 0); } + Require(lseek_nid_postfix(locked, 4, 0) == 4); + Require(lseek_nid_postfix(locked, INT64_MAX, 1) == -1 && *__error_nid_postfix() == 84); + Require(lseek_nid_postfix(locked, 0, 1) == 4); + Require(lseek_nid_postfix(locked, -1, 0) == -1 && *__error_nid_postfix() == 22); + const std::int64_t lockedSize = lseek_nid_postfix(locked, 0, 2); + Require(lockedSize >= 0); + Require(lseek_nid_postfix(locked, INT64_MAX - lockedSize + 1, 2) == -1 && *__error_nid_postfix() == 84); + Require(lseek_nid_postfix(locked, 0, 1) == lockedSize); Require(flock_nid_postfix(-1, 2 | 4) == -1 && *__error_nid_postfix() == 9); Require(flock_nid_postfix(0x7fffffff, 1) == -1 && *__error_nid_postfix() == 9); int flockPipe[2] = {-1, -1}; diff --git a/core/libs/tests/GuestFont.cpp b/core/libs/tests/GuestFont.cpp index 1ca28f3228..5fc1104e79 100644 --- a/core/libs/tests/GuestFont.cpp +++ b/core/libs/tests/GuestFont.cpp @@ -1,4 +1,5 @@ #include "prx/libSceFont/include/FontDriver.hpp" +#include "prx/libc/include/General.hpp" #include "tests/FontFixture.hpp" #include #include @@ -7,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -70,6 +72,7 @@ int APS5_VABI sceFontFtSupportOpenTypeTtf(FontLibrary library); int APS5_VABI sceFontFtSupportPcf(FontLibrary library); int APS5_VABI sceFontFtSupportPfr(FontLibrary library); int APS5_VABI sceFontFtSupportTrueType(FontLibrary library); +int APS5_VABI sceFontFtSupportTrueTypeGx(FontLibrary library); int APS5_VABI sceFontFtSupportType1(FontLibrary library); int APS5_VABI sceFontFtSupportType42(FontLibrary library); int APS5_VABI sceFontFtSupportWinFonts(FontLibrary library); @@ -89,9 +92,9 @@ static void Check(bool value, int line) { static void SetFontDirectory(const std::filesystem::path& directory) { #ifdef _WIN32 - Require(_putenv_s("ANYPS5_SYSTEM_FONTS", directory.string().c_str()) == 0); + Require(_wputenv_s(L"ANYPS5_SYSTEM_FONTS", directory.wstring().c_str()) == 0); #else - Require(::setenv("ANYPS5_SYSTEM_FONTS", directory.string().c_str(), 1) == 0); + Require(::setenv("ANYPS5_SYSTEM_FONTS", directory.c_str(), 1) == 0); #endif } @@ -220,10 +223,10 @@ int main() { FontLibrary library = nullptr; Require(sceFontCreateLibrary(&memory, nullptr, &library) == SCE_FONT_ERROR_INVALID_PARAMETER && library == nullptr); Require(sceFontCreateLibrary(&memory, sceFontSelectLibraryFt(0), &library) == SCE_FONT_OK && library != nullptr); - Require(sceFontFtSupportBdf(library) == 0 && sceFontFtSupportCid(library) == 0 && sceFontFtSupportOpenType(library) == 0 && sceFontFtSupportOpenTypeOtf(library) == 0 && sceFontFtSupportOpenTypeTtf(library) == 0 && sceFontFtSupportPcf(library) == 0 && sceFontFtSupportPfr(library) == 0 && sceFontFtSupportTrueType(library) == 0 && sceFontFtSupportType1(library) == 0 && sceFontFtSupportType42(library) == 0 && sceFontFtSupportWinFonts(library) == 0); + Require(sceFontFtSupportBdf(library) == 0 && sceFontFtSupportCid(library) == 0 && sceFontFtSupportOpenType(library) == 0 && sceFontFtSupportOpenTypeOtf(library) == 0 && sceFontFtSupportOpenTypeTtf(library) == 0 && sceFontFtSupportPcf(library) == 0 && sceFontFtSupportPfr(library) == 0 && sceFontFtSupportTrueType(library) == 0 && sceFontFtSupportTrueTypeGx(library) == 0 && sceFontFtSupportType1(library) == 0 && sceFontFtSupportType42(library) == 0 && sceFontFtSupportWinFonts(library) == 0); const auto* nativeLibrary = static_cast(library); const auto savedLock = nativeLibrary->lock_word; - for (auto support : {sceFontFtSupportBdf, sceFontFtSupportCid, sceFontFtSupportOpenType, sceFontFtSupportOpenTypeOtf, sceFontFtSupportOpenTypeTtf, sceFontFtSupportPcf, sceFontFtSupportPfr, sceFontFtSupportTrueType, sceFontFtSupportType1, sceFontFtSupportType42, sceFontFtSupportWinFonts}) { + for (auto support : {sceFontFtSupportBdf, sceFontFtSupportCid, sceFontFtSupportOpenType, sceFontFtSupportOpenTypeOtf, sceFontFtSupportOpenTypeTtf, sceFontFtSupportPcf, sceFontFtSupportPfr, sceFontFtSupportTrueType, sceFontFtSupportTrueTypeGx, sceFontFtSupportType1, sceFontFtSupportType42, sceFontFtSupportWinFonts}) { Font::FontLibNative invalidLibrary{}; Require(support(nullptr) == SCE_FONT_ERROR_INVALID_LIBRARY); Require(support(&invalidLibrary) == SCE_FONT_ERROR_INVALID_LIBRARY); @@ -311,6 +314,20 @@ int main() { Require(sceFontOpenFontMemory(library, notAFont, sizeof(notAFont), nullptr, &font) == SCE_FONT_ERROR_NO_SUPPORT_FORMAT && font == nullptr); const std::vector fontData = EmptyGlyphFont(); + const auto fileFont = fontRoot / std::filesystem::path(u8"external-a\u00e7\u00e3o-\u65e5\u672c") / "font.ttf"; + std::filesystem::create_directories(fileFont.parent_path()); + { + std::ofstream file(fileFont, std::ios::binary); + file.write(reinterpret_cast(fontData.data()), static_cast(fontData.size())); + Require(static_cast(file)); + } + const std::u8string fontRootText = fontRoot.u8string(); + AddPathAlias_nid_no_patch("/font-test", reinterpret_cast(fontRootText.c_str())); + const std::u8string fileFontText = u8"/font-test/external-a\u00e7\u00e3o-\u65e5\u672c/font.ttf"; + FontHandle fileHandle = nullptr; + Require(sceFontOpenFontFile(library, reinterpret_cast(fileFontText.c_str()), 1, nullptr, &fileHandle) == SCE_FONT_OK && fileHandle != nullptr); + Require(sceFontCloseFont(fileHandle) == SCE_FONT_OK); + RemovePathAlias_nid_no_patch("/font-test"); Require(sceFontOpenFontMemory(library, fontData.data(), static_cast(fontData.size()), nullptr, &font) == SCE_FONT_OK && font != nullptr); int setting = -1; Require(sceFontGetScriptLanguage(font, 3, &setting) == SCE_FONT_ERROR_INVALID_PARAMETER && setting == 0); diff --git a/core/libs/tests/GuestFreeType.cpp b/core/libs/tests/GuestFreeType.cpp index 8e5c7e7d0f..6e427fd2fe 100644 --- a/core/libs/tests/GuestFreeType.cpp +++ b/core/libs/tests/GuestFreeType.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -33,6 +34,7 @@ std::uint32_t APS5_VABI FT_Get_Char_Index_nid_postfix(GuestFaceRec*, std::uint64 int APS5_VABI FT_Load_Glyph_nid_postfix(GuestFaceRec*, std::uint32_t, std::int32_t); int APS5_VABI FT_Load_Char_nid_postfix(GuestFaceRec*, std::uint64_t, std::int32_t); int APS5_VABI FT_Render_Glyph_nid_postfix(GuestGlyphSlotRec*, std::int32_t); +int APS5_VABI FT_Set_Char_Size_nid_postfix(GuestFaceRec*, std::int64_t, std::int64_t, std::uint32_t, std::uint32_t); } static void Check(bool value, int line) { @@ -89,6 +91,15 @@ static void SlotMatches(const GuestGlyphSlotRec* slot, FT_GlyphSlot host) { Require(slot->lsb_delta == host->lsb_delta && slot->rsb_delta == host->rsb_delta && slot->control_len == host->control_len); } +static void SizeMatches(const GuestFaceRec* face, FT_Face host) { + const auto& actual = face->size->metrics; + const auto& expected = host->size->metrics; + Require(actual.x_ppem == expected.x_ppem && actual.y_ppem == expected.y_ppem); + Require(actual.x_scale == expected.x_scale && actual.y_scale == expected.y_scale); + Require(actual.ascender == expected.ascender && actual.descender == expected.descender); + Require(actual.height == expected.height && actual.max_advance == expected.max_advance); +} + struct Finalizers { std::vector calls; }; @@ -191,6 +202,8 @@ int main() { Require(FT_New_Memory_Face_nid_postfix(library, font.data(), fontSize, -1, &probe) == 0 && probe != nullptr); Require(probe->num_faces == nativeProbe->num_faces && probe->face_index == nativeProbe->face_index); Require((probe->glyph == nullptr) == (nativeProbe->glyph == nullptr) && (probe->size == nullptr) == (nativeProbe->size == nullptr)); + Require(FT_Set_Char_Size_nid_postfix(probe, std::numeric_limits::max(), 0, 0, 0) == + FT_Set_Char_Size(nativeProbe, 64, 64, 72, 72)); Finalizers nativeProbeEvents; Finalizers guestProbeEvents; nativeProbe->generic = {&nativeProbeEvents, NativeFaceFinalizer}; @@ -302,6 +315,64 @@ int main() { slot->outline.points = original; Require(FT_Render_Glyph_nid_postfix(slot, FT_RENDER_MODE_NORMAL) == 0 && slot->bitmap_left == 10); + { + struct Request { + std::int64_t width; + std::int64_t height; + std::uint32_t horizontal; + std::uint32_t vertical; + }; + const std::array requests{ + Request{0, 0, 0, 0}, Request{0, 12 * 64, 0, 0}, Request{12 * 64, 0, 0, 0}, + Request{12 * 64, 18 * 64, 96, 144}, Request{12 * 64 + 32, 12 * 64 + 32, 72, 72}, + Request{1, 63, 72, 72}, Request{-1, -100, 0, 0}, Request{0, -100, 96, 0}, + Request{12 * 64, 18 * 64, 0, 144}, Request{12 * 64, 18 * 64, 96, 0}, + Request{65535ll * 64, 64, 72, 72}, Request{65536ll * 64, 64, 72, 72}, + Request{64, 65536ll * 64, 72, 72}, + Request{18 * 64, 12 * 64, 72, 72} + }; + std::size_t successes = 0; + std::size_t failures = 0; + for (const auto& request : requests) { + std::array previousSlot{}; + std::memcpy(previousSlot.data(), face->glyph, previousSlot.size()); + const auto expected = FT_Set_Char_Size(host, static_cast(request.width), static_cast(request.height), + request.horizontal, request.vertical); + Require(FT_Set_Char_Size_nid_postfix(face, request.width, request.height, request.horizontal, request.vertical) == expected); + Require(std::memcmp(previousSlot.data(), face->glyph, previousSlot.size()) == 0); + expected == 0 ? ++successes : ++failures; + SizeMatches(face, host); + Require(FT_Load_Char_nid_postfix(face, 'A', FT_LOAD_DEFAULT) == FT_Load_Char(host, 'A', FT_LOAD_DEFAULT)); + SlotMatches(face->glyph, host->glyph); + } + Require(successes != 0 && failures != 0); + for (const auto value : std::array{std::numeric_limits::min(), -0x80000001ll}) { + Require(FT_Set_Char_Size_nid_postfix(face, value, 0, 0, 0) == FT_Set_Char_Size(host, 64, 64, 0, 0)); + SizeMatches(face, host); + Require(FT_Set_Char_Size_nid_postfix(face, 0, value, 0, 0) == 0); + SizeMatches(face, host); + } + for (const auto value : {std::numeric_limits::min(), std::numeric_limits::max()}) { + Require(FT_Set_Char_Size_nid_postfix(nullptr, value, value, 0, 0) == FT_Err_Invalid_Face_Handle); + } + if constexpr (sizeof(FT_F26Dot6) < sizeof(std::int64_t)) { + for (const auto value : std::array{0x7FFFFFFFll, 0x80000000ll, 0x100000040ll, std::numeric_limits::max()}) { + for (const bool width : {false, true}) { + Require(ThrowsWith([&] { FT_Set_Char_Size_nid_postfix(face, width ? value : 64, width ? 64 : value, 72, 72); }, + "FT_Set_Char_Size: a character size or resolution outside the host arithmetic range is not implemented")); + SizeMatches(face, host); + } + } + Require(ThrowsWith([&] { FT_Set_Char_Size_nid_postfix(face, 64, 64, std::numeric_limits::max(), 72); }, + "FT_Set_Char_Size: a character size or resolution outside the host arithmetic range is not implemented")); + SizeMatches(face, host); + } + Require(FT_Set_Char_Size_nid_postfix(memoryFace, 0, 48 * 64, 0, 144) == 0); + Require(memoryFace->size->metrics.y_ppem == 96 && face->size->metrics.y_ppem == 1); + Require(FT_Set_Pixel_Sizes_nid_postfix(face, 0, 100) == 0 && FT_Set_Pixel_Sizes(host, 0, 100) == 0); + std::printf("FT_Set_Char_Size: %zu native request comparisons, %zu successes, %zu errors\n", requests.size(), successes, failures); + } + Finalizers nativeDone; Finalizers guestDone; SetFinalizers(host, nativeDone); diff --git a/core/libs/tests/GuestFreeTypeNames.cpp b/core/libs/tests/GuestFreeTypeNames.cpp new file mode 100644 index 0000000000..a6aa3b7673 --- /dev/null +++ b/core/libs/tests/GuestFreeTypeNames.cpp @@ -0,0 +1,128 @@ +#include "FontFixture.hpp" +#include "prx/libSceFreeType/include/GuestObjects.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace FreeType; + +extern "C" { +int APS5_VABI FT_Get_Glyph_Name_nid_postfix(GuestFaceRec*, std::uint32_t, void*, std::uint32_t); +std::uint32_t APS5_VABI FT_Get_Name_Index_nid_postfix(GuestFaceRec*, const char*); +const char* APS5_VABI FT_Get_Postscript_Name_nid_postfix(GuestFaceRec*); +} + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "FreeType names check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +static std::vector NamedFont(const std::string& postscript, const std::string& first, const std::string& second) { + std::vector post(32, 0); + post[1] = 2; + for (const int value : {3, 0, 258, 259}) FontFixture::Put16(post, value); + for (const auto* name : {&first, &second}) { + post.push_back(static_cast(name->size())); + post.insert(post.end(), name->begin(), name->end()); + } + auto names = FontFixture::Words({0, 1, 18, 3, 1, 0x0409, 6, static_cast(postscript.size() * 2), 0}); + for (const unsigned char ch : postscript) FontFixture::Put16(names, ch); + return FontFixture::BuildFont(3, {{"post", post}, {"name", names}}); +} + +struct FacePair { + std::vector bytes; + GuestFace guest; + FT_Face oracle = nullptr; + + FacePair(GuestLibrary& library, FT_Library nativeLibrary, std::vector font) : bytes(std::move(font)) { + guest.library = &library; + Require(FT_New_Memory_Face(library.host, bytes.data(), static_cast(bytes.size()), 0, &guest.host) == 0); + Require(FT_New_Memory_Face(nativeLibrary, bytes.data(), static_cast(bytes.size()), 0, &oracle) == 0); + SyncFace(guest); + } + + ~FacePair() { + Require(FT_Done_Face(guest.host) == 0); + Require(FT_Done_Face(oracle) == 0); + } +}; + +static void CompareGlyph(GuestFaceRec* guest, FT_Face native, std::uint32_t index, std::uint32_t size) { + std::array expected; + expected.fill('?'); + auto actual = expected; + const FT_Error error = FT_Get_Glyph_Name(native, index, expected.data() + 3, size); + Require(FT_Get_Glyph_Name_nid_postfix(guest, index, actual.data() + 3, size) == error); + Require(actual == expected); +} + +static void CheckFace(FacePair& face, bool names) { + Require((FT_HAS_GLYPH_NAMES(face.oracle) != 0) == names); + for (const std::uint32_t index : {0u, 1u, 2u, 3u, 0x7FFFFFFFu}) { + for (const std::uint32_t size : {0u, 1u, 2u, 5u, 24u}) CompareGlyph(&face.guest.rec, face.oracle, index, size); + Require(FT_Get_Glyph_Name_nid_postfix(&face.guest.rec, index, nullptr, 24) == FT_Get_Glyph_Name(face.oracle, index, nullptr, 24)); + } + for (const std::uint32_t index : {0x80000000u, 0xFFFFFFFFu}) { + for (const std::uint32_t size : {0u, 1u, 24u}) { + std::array expected; + expected.fill('?'); + auto actual = expected; + if (size) expected[3] = '\0'; + Require(FT_Get_Glyph_Name_nid_postfix(&face.guest.rec, index, actual.data() + 3, size) == + (size ? FT_Err_Invalid_Glyph_Index : FT_Err_Invalid_Argument)); + Require(actual == expected); + } + } + for (const char* name : {".notdef", "alpha.custom", "beta", "gamma.custom", "delta", "ALPHA.CUSTOM", "missing", ""}) { + Require(FT_Get_Name_Index_nid_postfix(&face.guest.rec, name) == FT_Get_Name_Index(face.oracle, name)); + } + Require(FT_Get_Name_Index_nid_postfix(&face.guest.rec, nullptr) == 0); + const char* actual = FT_Get_Postscript_Name_nid_postfix(&face.guest.rec); + const char* expected = FT_Get_Postscript_Name(face.oracle); + Require((actual == nullptr) == (expected == nullptr)); + Require(!actual || std::strcmp(actual, expected) == 0); + Require(actual == FT_Get_Postscript_Name(face.guest.host)); +} + +int main() { + GuestLibrary library{}; + FT_Library nativeLibrary = nullptr; + Require(FT_Init_FreeType(&library.host) == 0); + Require(FT_Init_FreeType(&nativeLibrary) == 0); + { + FacePair first(library, nativeLibrary, NamedFont("NameFixture-A", "alpha.custom", "beta")); + FacePair second(library, nativeLibrary, NamedFont("NameFixture-B", "gamma.custom", "delta")); + std::vector post(32, 0); + post[1] = 3; + FacePair unnamed(library, nativeLibrary, FontFixture::BuildFont(3, {{"post", post}})); + CheckFace(first, true); + Require(FT_Get_Name_Index_nid_postfix(&first.guest.rec, "alpha.custom") == 1); + Require(FT_Get_Name_Index_nid_postfix(&first.guest.rec, "beta") == 2); + const char* retained = FT_Get_Postscript_Name_nid_postfix(&first.guest.rec); + Require(retained && std::strcmp(retained, "NameFixture-A") == 0); + CheckFace(second, true); + Require(FT_Get_Name_Index_nid_postfix(&second.guest.rec, "gamma.custom") == 1); + Require(FT_Get_Name_Index_nid_postfix(&second.guest.rec, "delta") == 2); + CheckFace(unnamed, false); + Require(FT_Get_Postscript_Name_nid_postfix(&unnamed.guest.rec) == nullptr); + Require(FT_Get_Postscript_Name_nid_postfix(&first.guest.rec) == retained && std::strcmp(retained, "NameFixture-A") == 0); + for (const std::uint32_t size : {0u, 1u, 24u}) CompareGlyph(nullptr, nullptr, 0, size); + Require(FT_Get_Glyph_Name_nid_postfix(nullptr, 0, nullptr, 0) == FT_Err_Invalid_Face_Handle); + Require(FT_Get_Name_Index_nid_postfix(nullptr, "alpha.custom") == 0); + Require(FT_Get_Name_Index_nid_postfix(nullptr, nullptr) == 0); + Require(FT_Get_Postscript_Name_nid_postfix(nullptr) == nullptr); + } + Require(FT_Done_FreeType(library.host) == 0); + Require(FT_Done_FreeType(nativeLibrary) == 0); +} diff --git a/core/libs/tests/GuestFreeTypeSfntNames.cpp b/core/libs/tests/GuestFreeTypeSfntNames.cpp new file mode 100644 index 0000000000..12affd6cbd --- /dev/null +++ b/core/libs/tests/GuestFreeTypeSfntNames.cpp @@ -0,0 +1,144 @@ +#include "FontFixture.hpp" +#include "prx/libSceFreeType/include/GuestObjects.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" + +#include FT_SFNT_NAMES_H + +#include +#include +#include +#include +#include +#include +#include + +using namespace FreeType; + +extern "C" { +std::uint32_t APS5_VABI FT_Get_Sfnt_Name_Count_nid_postfix(GuestFaceRec*); +int APS5_VABI FT_Get_Sfnt_Name_nid_postfix(GuestFaceRec*, std::uint32_t, GuestSfntName*); +} + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "FreeType SFNT names check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +struct NameRecord { + std::uint16_t platform; + std::uint16_t encoding; + std::uint16_t language; + std::uint16_t name; + std::vector bytes; +}; + +static std::vector NamedFont(const std::vector& records) { + auto table = FontFixture::Words({0, static_cast(records.size()), 6 + 12 * static_cast(records.size())}); + int offset = 0; + for (const auto& record : records) { + for (const int value : {static_cast(record.platform), static_cast(record.encoding), static_cast(record.language), + static_cast(record.name), static_cast(record.bytes.size()), offset}) FontFixture::Put16(table, value); + offset += static_cast(record.bytes.size()); + } + for (const auto& record : records) table.insert(table.end(), record.bytes.begin(), record.bytes.end()); + return FontFixture::BuildFont(3, {{"name", table}}); +} + +struct FacePair { + std::vector bytes; + GuestFace guest; + FT_Face oracle = nullptr; + + FacePair(GuestLibrary& library, FT_Library nativeLibrary, std::vector font) : bytes(std::move(font)) { + guest.library = &library; + Require(FT_New_Memory_Face(library.host, bytes.data(), static_cast(bytes.size()), 0, &guest.host) == 0); + Require(FT_New_Memory_Face(nativeLibrary, bytes.data(), static_cast(bytes.size()), 0, &oracle) == 0); + SyncFace(guest); + } + + ~FacePair() { + Require(FT_Done_Face(guest.host) == 0); + Require(FT_Done_Face(oracle) == 0); + } +}; + +static GuestSfntName Query(FacePair& face, std::uint32_t index, const NameRecord& record) { + struct GuardedName { + std::array before; + GuestSfntName value; + std::array after; + } actual; + std::memset(&actual, 0xA5, sizeof(actual)); + FT_SfntName expected{}; + Require(FT_Get_Sfnt_Name(face.oracle, index, &expected) == 0); + Require(FT_Get_Sfnt_Name_nid_postfix(&face.guest.rec, index, &actual.value) == 0); + for (const auto byte : actual.before) Require(byte == 0xA5); + for (const auto byte : actual.after) Require(byte == 0xA5); + const auto& name = actual.value; + Require(name.platform_id == expected.platform_id && name.platform_id == record.platform); + Require(name.encoding_id == expected.encoding_id && name.encoding_id == record.encoding); + Require(name.language_id == expected.language_id && name.language_id == record.language); + Require(name.name_id == expected.name_id && name.name_id == record.name); + Require(name.string_len == expected.string_len && name.string_len == record.bytes.size()); + Require(std::memcmp(name.string, expected.string, name.string_len) == 0); + Require(std::memcmp(name.string, record.bytes.data(), name.string_len) == 0); + FT_SfntName native{}; + Require(FT_Get_Sfnt_Name(face.guest.host, index, &native) == 0 && name.string == native.string); + return name; +} + +static void InvalidQuery(GuestFaceRec* guest, FT_Face native, std::uint32_t index) { + GuestSfntName actual; + std::memset(&actual, 0xA5, sizeof(actual)); + std::array before; + std::memcpy(before.data(), &actual, sizeof(actual)); + FT_SfntName expected{}; + const FT_Error error = FT_Get_Sfnt_Name(native, index, &expected); + Require(error != 0); + Require(FT_Get_Sfnt_Name_nid_postfix(guest, index, &actual) == error); + Require(std::memcmp(&actual, before.data(), sizeof(actual)) == 0); + Require(FT_Get_Sfnt_Name_nid_postfix(guest, index, nullptr) == FT_Get_Sfnt_Name(native, index, nullptr)); +} + +int main() { + GuestLibrary library{}; + FT_Library nativeLibrary = nullptr; + Require(FT_Init_FreeType(&library.host) == 0); + Require(FT_Init_FreeType(&nativeLibrary) == 0); + { + const std::vector records = { + {1, 0, 0, 1, {'F', 'a', 'm', 'i', 'l', 'y'}}, + {3, 1, 0x0409, 6, {0, 'S', 0, 'f', 0, 'n', 0, 't'}}, + {3, 10, 0x0411, 256, {0, 'A', 0, 0, 0, 'B'}} + }; + FacePair first(library, nativeLibrary, NamedFont(records)); + FacePair empty(library, nativeLibrary, FontFixture::BuildFont(3, {})); + Require(FT_Get_Sfnt_Name_Count_nid_postfix(&first.guest.rec) == records.size()); + Require(FT_Get_Sfnt_Name_Count_nid_postfix(&first.guest.rec) == FT_Get_Sfnt_Name_Count(first.oracle)); + std::vector retained; + for (std::uint32_t index = 0; index < records.size(); ++index) retained.push_back(Query(first, index, records[index])); + { + const std::vector other = {{3, 1, 0x0409, 6, {0, 'O', 0, 't', 0, 'h', 0, 'e', 0, 'r'}}}; + FacePair second(library, nativeLibrary, NamedFont(other)); + Require(FT_Get_Sfnt_Name_Count_nid_postfix(&second.guest.rec) == other.size()); + Query(second, 0, other[0]); + } + for (std::uint32_t index = 0; index < records.size(); ++index) { + Require(Query(first, index, records[index]).string == retained[index].string); + Require(std::memcmp(retained[index].string, records[index].bytes.data(), retained[index].string_len) == 0); + Require(FT_Get_Sfnt_Name_nid_postfix(&first.guest.rec, index, nullptr) == FT_Get_Sfnt_Name(first.oracle, index, nullptr)); + } + Require(FT_Get_Sfnt_Name_Count_nid_postfix(&empty.guest.rec) == 0); + Require(FT_Get_Sfnt_Name_Count_nid_postfix(nullptr) == 0); + for (const std::uint32_t index : {0u, 3u, 0x80000000u, 0xFFFFFFFFu}) { + if (index >= records.size()) InvalidQuery(&first.guest.rec, first.oracle, index); + InvalidQuery(&empty.guest.rec, empty.oracle, index); + InvalidQuery(nullptr, nullptr, index); + } + } + Require(FT_Done_FreeType(library.host) == 0); + Require(FT_Done_FreeType(nativeLibrary) == 0); +} diff --git a/core/libs/tests/GuestFstatFault.cpp b/core/libs/tests/GuestFstatFault.cpp index 4f0aa66bb9..06524f7df8 100644 --- a/core/libs/tests/GuestFstatFault.cpp +++ b/core/libs/tests/GuestFstatFault.cpp @@ -60,10 +60,8 @@ int main() { Require(close_nid_postfix(sockets[0]) == 0); Require(close_nid_postfix(sockets[1]) == 0); Require(close_nid_postfix(file) == 0); -#ifndef _WIN32 RequireFailure(file, GuestEbadf); RequireFailure(-1, GuestEbadf); -#endif std::filesystem::remove_all(root); } diff --git a/core/libs/tests/GuestFstatfs.cpp b/core/libs/tests/GuestFstatfs.cpp index 0c86546ccb..26e06de298 100644 --- a/core/libs/tests/GuestFstatfs.cpp +++ b/core/libs/tests/GuestFstatfs.cpp @@ -124,9 +124,7 @@ int main() { RequireFailure(-1, GuestEbadf); Require(close_nid_postfix(file) == 0); -#ifndef _WIN32 RequireFailure(file, GuestEbadf); -#endif std::filesystem::remove_all(root); } diff --git a/core/libs/tests/GuestGameLiveStreaming.cpp b/core/libs/tests/GuestGameLiveStreaming.cpp index 8f6f879c8a..22950d430e 100644 --- a/core/libs/tests/GuestGameLiveStreaming.cpp +++ b/core/libs/tests/GuestGameLiveStreaming.cpp @@ -1,4 +1,5 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include #include @@ -24,6 +25,7 @@ int APS5_VABI sceGameLiveStreamingTerminate(void); int APS5_VABI sceGameLiveStreamingGetCurrentStatus2(GameLiveStreamingStatus2* status); struct GameLiveStreamingProgramInfo; int APS5_VABI sceGameLiveStreamingGetProgramInfo(GameLiveStreamingProgramInfo*); +int APS5_VABI sceGameLiveStreamingEnableLiveStreaming(bool isEnableLiveStreaming); } namespace { @@ -32,6 +34,15 @@ void Require(bool value) { if (!value) std::abort(); } constexpr std::int32_t kInvalidUserId = -1; +bool EnableThrowsNotInitialized(bool isEnableLiveStreaming) { + try { + sceGameLiveStreamingEnableLiveStreaming(isEnableLiveStreaming); + } catch (const std::logic_error&) { + return true; + } + return false; +} + } int main() { @@ -40,8 +51,10 @@ int main() { constexpr int notInitialized = static_cast(0x80A00004); Require(sceGameLiveStreamingTerminate() == notInitialized); Require(sceGameLiveStreamingGetCurrentStatus2(nullptr) == notInitialized); + Require(EnableThrowsNotInitialized(true)); for (const std::size_t size : {0u, 1u, 0x3fffu, 0x4001u, 0x100000u}) Require(sceGameLiveStreamingInitialize(size) == invalidArgument); Require(sceGameLiveStreamingInitialize(0x4000) == 0); + Require(sceGameLiveStreamingEnableLiveStreaming(true) == 0); Require(sceGameLiveStreamingInitialize(0) == invalidArgument); Require(sceGameLiveStreamingInitialize(0x4000) == alreadyInitialized); Require(sceGameLiveStreamingGetCurrentStatus2(nullptr) == invalidArgument); @@ -62,17 +75,22 @@ int main() { Require(byte == 0); } - bool threwProgramInfo = false; - try { - sceGameLiveStreamingGetProgramInfo(nullptr); - } catch (const std::runtime_error&) { - threwProgramInfo = true; + Require(sceGameLiveStreamingGetProgramInfo(nullptr) == static_cast(0x80A00002)); + std::array programInfo{}; + programInfo.fill(0x7f); + Require(sceGameLiveStreamingGetProgramInfo(reinterpret_cast(programInfo.data())) == 0); + for (std::size_t i = 0; i < programInfo.size(); ++i) { + Require(programInfo[i] == (i < 0x134C ? 0 : 0x7f)); } - Require(threwProgramInfo); + Require(sceGameLiveStreamingEnableLiveStreaming(false) == 0); Require(sceGameLiveStreamingTerminate() == 0); + Require(EnableThrowsNotInitialized(false)); std::memset(&status, 0x7f, sizeof(status)); Require(sceGameLiveStreamingGetCurrentStatus2(&status) == notInitialized); for (const auto value : std::as_bytes(std::span(&status, 1))) Require(value == std::byte{0x7f}); + programInfo.fill(0x7f); + Require(sceGameLiveStreamingGetProgramInfo(reinterpret_cast(programInfo.data())) == notInitialized); + for (const std::uint8_t byte : programInfo) Require(byte == 0x7f); Require(sceGameLiveStreamingTerminate() == notInitialized); } diff --git a/core/libs/tests/GuestGnmNeoRetail.cpp b/core/libs/tests/GuestGnmNeoRetail.cpp new file mode 100644 index 0000000000..29148976fa --- /dev/null +++ b/core/libs/tests/GuestGnmNeoRetail.cpp @@ -0,0 +1,131 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include + +extern "C" { +int APS5_VABI sceGnmDebugModuleReset(void); +int APS5_VABI sceGnmDebugReset(void); +int APS5_VABI sceGnmDriverInternalRetrieveGnmInterface(void); +int APS5_VABI sceGnmDriverInternalRetrieveGnmInterfaceForGpuDebugger(void); +int APS5_VABI sceGnmDriverInternalRetrieveGnmInterfaceForReplay(void); +int APS5_VABI sceGnmDriverInternalRetrieveGnmInterfaceForResourceRegistration(void); +int APS5_VABI sceGnmDriverInternalRetrieveGnmInterfaceForValidation(void); +int APS5_VABI sceGnmDriverTraceInProgress(void); +int APS5_VABI sceGnmFindResources(void); +int APS5_VABI sceGnmFindResourcesPublic(void); +int APS5_VABI sceGnmGetDbgGcHandle(void); +int APS5_VABI sceGnmGetOwnerName(void); +int APS5_VABI sceGnmGetPhysicalCounterFromVirtualized(void); +int APS5_VABI sceGnmGetResourceBaseAddressAndSizeInBytes(void); +int APS5_VABI sceGnmGetResourceName(void); +int APS5_VABI sceGnmGetResourceRegistrationBuffers(void); +int APS5_VABI sceGnmGetResourceShaderGuid(void); +int APS5_VABI sceGnmGetResourceType(void); +int APS5_VABI sceGnmGetResourceUserData(void); +int APS5_VABI sceGnmQueryResourceRegistrationUserMemoryRequirements(void); +int APS5_VABI sceGnmRegisterGdsResource(void); +int APS5_VABI sceGnmRegisterOwner(void); +int APS5_VABI sceGnmRegisterOwnerForSystem(void); +int APS5_VABI sceGnmRegisterResource(void); +int APS5_VABI sceGnmSdmaClose(void); +int APS5_VABI sceGnmSdmaConstFill(void); +int APS5_VABI sceGnmSdmaCopyLinear(void); +int APS5_VABI sceGnmSdmaCopyTiled(void); +int APS5_VABI sceGnmSdmaCopyWindow(void); +int APS5_VABI sceGnmSdmaFlush(void); +int APS5_VABI sceGnmSdmaGetMinCmdSize(void); +int APS5_VABI sceGnmSdmaOpen(void); +int APS5_VABI sceGnmSetResourceRegistrationUserMemory(void); +int APS5_VABI sceGnmSetResourceUserData(void); +int APS5_VABI sceGnmSetSpiEnableSqCounters(void); +int APS5_VABI sceGnmSetSpiEnableSqCountersForUnitInstance(void); +int APS5_VABI sceGnmUnregisterAllResourcesForOwner(void); +int APS5_VABI sceGnmUnregisterOwnerAndResources(void); +int APS5_VABI sceGnmUnregisterResource(void); +int APS5_VABI sceGnmValidateDisableDiagnostics(void); +int APS5_VABI sceGnmValidateDisableDiagnostics2(void); +int APS5_VABI sceGnmValidateDispatchCommandBuffers(void); +int APS5_VABI sceGnmValidateDrawCommandBuffers(void); +int APS5_VABI sceGnmValidateGetDiagnosticInfo(void); +int APS5_VABI sceGnmValidateGetDiagnostics(void); +int APS5_VABI sceGnmValidateOnSubmitEnabled(void); +int APS5_VABI sceGnmValidateResetState(void); +int APS5_VABI sceGnmValidationRegisterMemoryCheckCallback(void); +} + +namespace { + +constexpr int Failure = static_cast(0x8EEE00FFu); +constexpr int NoInterface = static_cast(0x80000000u); +constexpr int ValidationNotEnabled = static_cast(0x80D13FFFu); + +using Query = int (APS5_VABI *)(void); + +void RequireAll(int expected, std::initializer_list queries) { + for (const auto query : queries) { + if (query() != expected) std::abort(); + } +} + +} // namespace + +int main() { + RequireAll(Failure, { + sceGnmDebugModuleReset, + sceGnmDebugReset, + sceGnmFindResources, + sceGnmFindResourcesPublic, + sceGnmGetOwnerName, + sceGnmGetResourceBaseAddressAndSizeInBytes, + sceGnmGetResourceName, + sceGnmGetResourceRegistrationBuffers, + sceGnmGetResourceShaderGuid, + sceGnmGetResourceType, + sceGnmGetResourceUserData, + sceGnmQueryResourceRegistrationUserMemoryRequirements, + sceGnmRegisterGdsResource, + sceGnmRegisterOwner, + sceGnmRegisterOwnerForSystem, + sceGnmRegisterResource, + sceGnmSetResourceRegistrationUserMemory, + sceGnmSetResourceUserData, + sceGnmUnregisterAllResourcesForOwner, + sceGnmUnregisterOwnerAndResources, + sceGnmUnregisterResource, + sceGnmSdmaClose, + sceGnmSdmaConstFill, + sceGnmSdmaCopyLinear, + sceGnmSdmaCopyTiled, + sceGnmSdmaCopyWindow, + sceGnmSdmaFlush, + sceGnmSdmaGetMinCmdSize, + sceGnmSdmaOpen, + sceGnmGetPhysicalCounterFromVirtualized, + sceGnmSetSpiEnableSqCounters, + sceGnmSetSpiEnableSqCountersForUnitInstance, + }); + RequireAll(NoInterface, { + sceGnmDriverInternalRetrieveGnmInterface, + sceGnmDriverInternalRetrieveGnmInterfaceForGpuDebugger, + sceGnmDriverInternalRetrieveGnmInterfaceForReplay, + sceGnmDriverInternalRetrieveGnmInterfaceForResourceRegistration, + sceGnmDriverInternalRetrieveGnmInterfaceForValidation, + }); + RequireAll(0, { + sceGnmDriverTraceInProgress, + sceGnmValidateOnSubmitEnabled, + }); + RequireAll(-1, { + sceGnmGetDbgGcHandle, + }); + RequireAll(ValidationNotEnabled, { + sceGnmValidateDisableDiagnostics, + sceGnmValidateDisableDiagnostics2, + sceGnmValidateDispatchCommandBuffers, + sceGnmValidateDrawCommandBuffers, + sceGnmValidateGetDiagnosticInfo, + sceGnmValidateGetDiagnostics, + sceGnmValidateResetState, + sceGnmValidationRegisterMemoryCheckCallback, + }); +} diff --git a/core/libs/tests/GuestHmd2Gaze.cpp b/core/libs/tests/GuestHmd2Gaze.cpp new file mode 100644 index 0000000000..e3cce92206 --- /dev/null +++ b/core/libs/tests/GuestHmd2Gaze.cpp @@ -0,0 +1,106 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include + +struct GazeParam { + std::uint32_t size; + std::int32_t handle; + std::uint64_t reserved; +}; + +extern "C" { +std::int32_t APS5_VABI sceHmd2Initialize(const void* param); +std::int32_t APS5_VABI sceHmd2GazeGetResult(const GazeParam* param, void* result); +std::int32_t APS5_VABI sceHmd2GazeGetResultForFoveatedRendering(const GazeParam* param, void* result); +} + +namespace { + +constexpr std::int32_t InvalidHandle = static_cast(0x8a720003); +constexpr std::int32_t ParameterNull = static_cast(0x8a720008); +constexpr std::int32_t ParameterInvalid = static_cast(0x8a720009); + +void Require(bool value, const char* what) { + if (value) return; + std::fprintf(stderr, "FAIL: %s\n", what); + std::abort(); +} + +template +T Read(const unsigned char* bytes, std::size_t offset) { + T value; + std::memcpy(&value, bytes + offset, sizeof(value)); + return value; +} + +bool NegativeZero(float value) { + return value == 0.0f && std::signbit(value); +} + +void CheckGazeResult() { + GazeParam param {sizeof(GazeParam), 0x0e010000, 0}; + unsigned char result[0x38]; + std::memset(result, 0x5a, sizeof(result)); + const std::uint32_t size = 0x38; + std::memcpy(result, &size, sizeof(size)); + + Require(sceHmd2GazeGetResult(nullptr, result) == ParameterNull, "null param"); + Require(sceHmd2GazeGetResult(¶m, nullptr) == ParameterNull, "null result"); + GazeParam wrongParam = param; + wrongParam.size = 0x18; + Require(sceHmd2GazeGetResult(&wrongParam, result) == ParameterInvalid, "param size"); + const std::uint32_t wrongSize = 0x18; + std::memcpy(result, &wrongSize, sizeof(wrongSize)); + Require(sceHmd2GazeGetResult(¶m, result) == ParameterInvalid, "result size"); + Require(Read(result, 4) == 0x5a5a5a5a, "result untouched after a size error"); + + std::memcpy(result, &size, sizeof(size)); + Require(sceHmd2GazeGetResult(¶m, result) == InvalidHandle, "unopened handle"); + Require(Read(result, 0) == 0x38, "size kept"); + Require(Read(result, 4) == -1, "+4 is -1"); + for (const std::size_t offset : {0x8u, 0xcu, 0x10u, 0x20u, 0x24u, 0x28u, 0x2cu, 0x30u, 0x34u}) + Require(Read(result, offset) == 0, "cleared word"); + Require(NegativeZero(Read(result, 0x14)), "+0x14 is -0.0"); + Require(Read(result, 0x18) == 0, "+0x18 is 0.0"); + Require(Read(result, 0x1c) == -1.0f, "+0x1c is -1.0"); + + GazeParam zero {sizeof(GazeParam), 0, 0}; + Require(sceHmd2GazeGetResult(&zero, result) == InvalidHandle, "handle 0"); +} + +void CheckFoveatedResult() { + GazeParam param {sizeof(GazeParam), 0x0e010000, 0}; + unsigned char result[0x18]; + std::memset(result, 0x5a, sizeof(result)); + const std::uint32_t size = 0x18; + std::memcpy(result, &size, sizeof(size)); + + Require(sceHmd2GazeGetResultForFoveatedRendering(nullptr, result) == ParameterNull, "foveated null param"); + Require(sceHmd2GazeGetResultForFoveatedRendering(¶m, nullptr) == ParameterNull, "foveated null result"); + unsigned char large[0x38] = {}; + const std::uint32_t largeSize = 0x38; + std::memcpy(large, &largeSize, sizeof(largeSize)); + Require(sceHmd2GazeGetResultForFoveatedRendering(¶m, large) == ParameterInvalid, "foveated result size"); + + Require(sceHmd2GazeGetResultForFoveatedRendering(¶m, result) == InvalidHandle, "foveated unopened handle"); + Require(Read(result, 0) == 0x18, "foveated size kept"); + Require(Read(result, 4) == -1, "foveated +4 is -1"); + Require(NegativeZero(Read(result, 8)), "foveated +8 is -0.0"); + Require(Read(result, 0xc) == 0, "foveated +0xc is 0.0"); + Require(Read(result, 0x10) == -1.0f, "foveated +0x10 is -1.0"); + Require(Read(result, 0x14) == 0x5a5a5a5a, "foveated +0x14 untouched"); +} + +} + +int main() { + Require(sceHmd2Initialize(nullptr) != 0, "initialize reports no headset"); + CheckGazeResult(); + CheckFoveatedResult(); + std::puts("guest Hmd2 gaze results without a headset: PASS"); + return 0; +} diff --git a/core/libs/tests/GuestHttp.cpp b/core/libs/tests/GuestHttp.cpp index d38bbf2c63..487e6d6b76 100644 --- a/core/libs/tests/GuestHttp.cpp +++ b/core/libs/tests/GuestHttp.cpp @@ -63,15 +63,174 @@ static void CheckUriParseCharacters() { Require(Equal(element.scheme, "aZ09+.-") && element.port == 0); for (unsigned value = 128; value <= 255; ++value) { const std::string byte(1, static_cast(value)); - const std::string urls[] = { - "http://" + byte + "/", "http://[" + byte + "]/", byte + "://host/", - "a" + byte + "://host/", "http://host:" + byte + "/" - }; - for (const auto& url : urls) { + for (const auto& url : {"http://[" + byte + "]/", "http://host:" + byte + "/"}) { std::size_t required = 0; Require(sceHttpUriParse(nullptr, url.c_str(), nullptr, &required, 0) == static_cast(0x80433060)); Require(sceHttpUriParse(&element, url.c_str(), pool, nullptr, sizeof(pool)) == static_cast(0x80433060)); } + Require(sceHttpUriParse(&element, ("http://" + byte + "/").c_str(), pool, nullptr, sizeof(pool)) == 0); + Require(Equal(element.hostname, "") && element.path == byte + "/"); + Require(sceHttpUriParse(&element, (byte + "://host/").c_str(), pool, nullptr, sizeof(pool)) == 0); + Require(element.opaque && Equal(element.scheme, "") && Equal(element.hostname, "") && element.path == byte + "://host/"); + Require(sceHttpUriParse(&element, ("a" + byte + "://host/").c_str(), pool, nullptr, sizeof(pool)) == 0); + Require(Equal(element.scheme, "") && Equal(element.hostname, "a") && element.path == byte + "://host/"); + } +} + +struct UriParseCase { + const char* url; + int opaque; + std::uint16_t port; + const char* scheme; + const char* username; + const char* password; + const char* hostname; + const char* path; + const char* query; + const char* fragment; + std::size_t pathSpace = 0; +}; + +static void CheckUriParseAsMeasured() { + constexpr UriParseCase cases[] = { + {"http://bad host/", 0, 80, "http", "", "", "bad", " host/", "", ""}, + {"http://ex%41mple.com/", 0, 80, "http", "", "", "ex", "%41mple.com/", "", ""}, + {"http://h\t/", 0, 80, "http", "", "", "h", "\t/", "", ""}, + {"not a url", 1, 0, "", "", "", "not", " a url", "", ""}, + {"example.com/a", 1, 0, "", "", "", "example.com", "/a", "", ""}, + {"mailto:x", 1, 0, "mailto", "", "", "x", "", "", ""}, + {"abc:def", 1, 0, "abc", "", "", "def", "", "", ""}, + {"http:", 1, 80, "http", "", "", "", "", "", ""}, + {"https:h", 1, 443, "https", "", "", "h", "", "", ""}, + {"//host/x", 0, 0, "", "", "", "host", "/x", "", ""}, + {"", 1, 0, "", "", "", "", "", "", ""}, + {"?q", 1, 0, "", "", "", "", "", "?q", ""}, + {"http://host:65536/", 0, 0, "http", "", "", "host", "/", "", ""}, + {"http://h:65535", 0, 65535, "http", "", "", "h", "", "", ""}, + {"http://h:0080/", 0, 80, "http", "", "", "h", "/", "", ""}, + {"http://h:00000/", 0, 0, "http", "", "", "h", "/", "", ""}, + {"http://host:/", 0, 80, "http", "", "", "host", "/", "", ""}, + {"http://[::1]x/", 0, 80, "http", "", "", "::1", "x/", "", ""}, + {"http://[::1]]/", 0, 80, "http", "", "", "::1", "]/", "", ""}, + {"http://[::1]:/", 0, 80, "http", "", "", "::1", "/", "", ""}, + {"http://[]/", 0, 80, "http", "", "", "", "/", "", ""}, + {"http://[.]/", 0, 80, "http", "", "", ".", "/", "", ""}, + {"http://u@h@i/", 0, 80, "http", "u", "", "h", "@i/", "", ""}, + {"http://u s@h/", 0, 80, "http", "", "", "u", " s@h/", "", ""}, + {"http://u:p:q@h/", 0, 80, "http", "u", "p:q", "h", "/", "", ""}, + {"http://u%41@h/", 0, 80, "http", "u%41", "", "h", "/", "", ""}, + {"http://h@/", 0, 80, "http", "h", "", "", "/", "", ""}, + {"http://[::1]@h/", 0, 80, "http", "", "", "::1", "@h/", "", ""}, + {"http://u@[::1]:8/", 0, 8, "http", "u", "", "::1", "/", "", ""}, + {".@h/", 1, 0, "", ".", "", "h", "/", "", ""}, + {"http://.a/", 0, 80, "http", "", "", "", ".a/", "", ""}, + {"http://-a/", 0, 80, "http", "", "", "-a", "/", "", ""}, + {"http://a..b/", 0, 80, "http", "", "", "a..b", "/", "", ""}, + {"a.", 1, 0, "", "", "", "a.", "", "", ""}, + {"http://h/a/..", 0, 80, "http", "", "", "h", "/a/..", "", ""}, + {"http://h/a/.", 0, 80, "http", "", "", "h", "/a/.", "", ""}, + {"http://h/a/b/../../..", 0, 80, "http", "", "", "h", "/..", "", "", 13}, + {"http://h/../../a", 0, 80, "http", "", "", "h", "/a", "", "", 8}, + {"http://h/a//b/../c", 0, 80, "http", "", "", "h", "/a//c", "", "", 10}, + {"http://h/a/.../../b", 0, 80, "http", "", "", "h", "/a/b", "", "", 11}, + {"http://h/./a/../b/", 0, 80, "http", "", "", "h", "/b/", "", "", 10}, + {"./a", 1, 0, "", "", "", "", "./a", "", ""}, + {"a/../b", 1, 0, "", "", "", "a", "/b", "", "", 5}, + {"1x://h/", 1, 0, "", "", "", "1x", "//h/", "", ""}, + {"-x://h/", 0, 0, "-x", "", "", "h", "/", "", ""}, + {"s_x://h/p", 1, 0, "", "", "", "s_x", "//h/p", "", ""}, + {"s:x://h/p", 1, 0, "s", "", "", "x", "//h/p", "", ""}, + {"s@x://h/p", 1, 0, "", "s", "", "x", "//h/p", "", ""}, + {"httpx://h/", 0, 80, "httpx", "", "", "h", "/", "", ""}, + {"httpsx://h/", 0, 443, "httpsx", "", "", "h", "/", "", ""}, + {"HTTPx://h/", 0, 80, "HTTPx", "", "", "h", "/", "", ""}, + {"htt://h/", 0, 0, "htt", "", "", "h", "/", "", ""}, + {"ws://h/", 0, 0, "ws", "", "", "h", "/", "", ""}, + {"https://h:80/", 0, 80, "https", "", "", "h", "/", "", ""}, + {"http://h/p?q#f#g", 0, 80, "http", "", "", "h", "/p", "?q", "#f#g"}, + {"http://h/p#f?q", 0, 80, "http", "", "", "h", "/p", "", "#f?q"}, + {"http://h?", 0, 80, "http", "", "", "h", "", "?", ""}, + }; + char pool[256]; + for (const auto& row : cases) { + SceHttpUriElement element{}; + std::size_t required = 0; + Require(sceHttpUriParse(nullptr, row.url, nullptr, &required, 0) == 0); + const std::size_t size = std::strlen(row.scheme) + std::strlen(row.username) + std::strlen(row.password) + std::strlen(row.hostname) + + (row.pathSpace != 0 ? row.pathSpace : std::strlen(row.path)) + std::strlen(row.query) + std::strlen(row.fragment) + 7; + Require(required == size); + required = 77; + Require(sceHttpUriParse(&element, row.url, pool, &required, size - 1) == static_cast(0x80431022) && required == 77); + Require(sceHttpUriParse(&element, row.url, pool, &required, sizeof(pool)) == 0 && required == size); + Require(element.opaque == row.opaque && element.port == row.port); + Require(Equal(element.scheme, row.scheme) && Equal(element.username, row.username) && Equal(element.password, row.password)); + Require(Equal(element.hostname, row.hostname) && Equal(element.path, row.path)); + Require(Equal(element.query, row.query) && Equal(element.fragment, row.fragment)); + } + for (const char* url : {"http://[::1/", "http://[a b]/", "http://[a%b]/", "http://[a@b]/", "http://host:80a/", "http://h:65537/", "http://h:99999/", + "http://h:000080/", "http://h:80?q", "http://h:80#f", "http://h:80 /", "http://h: 80/", "http://h:+80/", "http://h:-1/", + "http://h:80:81/", "http://[::1]:8a/", "http://a:b/p"}) { + std::size_t required = 77; + Require(sceHttpUriParse(nullptr, url, nullptr, &required, 0) == static_cast(0x80433060) && required == 77); + } +} + +struct UriMergeCase { + const char* base; + const char* relative; + std::size_t sizeOnly; + const char* merged; +}; + +static void CheckUriMergeAsMeasured() { + constexpr UriMergeCase cases[] = { + {"http://user:pw@example.com:8080/a/b/c?q=1#f", "?x", 0x84, "http://user:pw@example.com:8080/a/b/?x"}, + {"http://user:pw@example.com:8080/a/b/c?q=1#f", "#y", 0x84, "http://user:pw@example.com:8080/a/b/#y"}, + {"http://user:pw@example.com:8080/a/b/c?q=1#f", "../../../../d", 0x9a, "http://user:pw@example.com:8080/a/b/../../../../d"}, + {"http://user:pw@example.com:8080/a/b/c?q=1#f", "http:d", 0x8c, "http://user:pw@example.com:8080/a/b/http:d"}, + {"http://user:pw@example.com:8080/a/b/c?q=1#f", "//other.com/x", 0x9a, "//other.com/x"}, + {"http://example.com", "../d", 0x44, "http://example.com/../d"}, + {"http://example.com/a/./b/../c", "d", 0x5f, "http://example.com/a/d"}, + {"not a url", "d", 0x26, "not a url/d"}, + {"example.com/a", "b", 0x32, "example.com/b"}, + {"http://[::1]:80/a/b", "c", 0x3c, "http://::1/a/c"}, + {"http://ex ample.com/a", "c", 0x47, "http://ex ample.com/c"}, + {"http://example.com/a", "//bad host/x", 0x5a, "//bad host/x"}, + {"//host/a/b", "c", 0x27, "//host/a/c"}, + {"/a/b", "c", 0x17, "/a/c"}, + {"a/b", "c", 0x14, "a/c"}, + {"", "c", 0xb, "/c"}, + {"", "", 0x9, "/"}, + {"mailto:x@y", "c", 0x27, "mailto:x@y/c"}, + {"http://h", "?q", 0x22, "http://h/?q"}, + {"https://h:443/a", "c", 0x31, "https://h/c"}, + {"http://h:0/a", "c", 0x2a, "http://h/c"}, + {"http://h:65536/a", "c", 0x32, "http://h/c"}, + {"http://:p@h/a", "c", 0x2d, "http://:p@h/c"}, + {"http://bad host/", "./x", 0x3c, "http://bad host/./x"}, + {"http://h/a", "http://bad host/", 0x44, "http://bad host/"}, + {"http://h/a", "HTTPS://Other.com/", 0x48, "HTTPS://Other.com/"}, + {"http://h/a", "//o", 0x2a, "//o"}, + {"http://h/a", "x:y", 0x2a, "http://h/x:y"}, + {"http://h/a", "d e", 0x2a, "http://h/d e"}, + {"http://h/a", "..", 0x28, "http://h/.."}, + {"http://h/a/./b/../c", "../d", 0x47, "http://h/a/../d"}, + {"http://h/a?q#f", "", 0x30, "http://h/"}, + {"not a url", "//x/y", 0x2e, "//x/y"}, + }; + char merged[512]; + for (const auto& row : cases) { + std::size_t required = 0; + Require(sceHttpUriMerge(nullptr, row.base, row.relative, &required, 0, 0) == 0 && required == row.sizeOnly); + std::memset(merged, 'Z', sizeof(merged)); + required = 77; + Require(sceHttpUriMerge(merged, row.base, row.relative, &required, row.sizeOnly - 1, 0) == static_cast(0x80431022)); + Require(merged[0] == 'Z'); + Require(sceHttpUriMerge(merged, row.base, row.relative, &required, sizeof(merged), 0) == 0 && Equal(merged, row.merged)); + } + for (const char* base : {"http://[::1/a", "http://h:80a/a"}) { + std::size_t required = 77; + Require(sceHttpUriMerge(merged, base, "c", &required, sizeof(merged), 0) == static_cast(0x80433060) && required == 77); } } @@ -297,7 +456,7 @@ int main() { const char* url = "https://user:secret@example.com:8443/a/./b/../c?x=1&y=2#top"; std::size_t required = 0; Require(sceHttpUriParse(nullptr, url, nullptr, &required, 0) == 0); - Require(required == 6 + 5 + 7 + 12 + 5 + 9 + 5); + Require(required == 6 + 5 + 7 + 12 + 12 + 9 + 5); char pool[256]; SceHttpUriElement element; @@ -307,6 +466,7 @@ int main() { Require(Equal(element.scheme, "https") && Equal(element.username, "user") && Equal(element.password, "secret")); Require(Equal(element.hostname, "example.com") && element.port == 8443); Require(Equal(element.path, "/a/c") && Equal(element.query, "?x=1&y=2") && Equal(element.fragment, "#top")); + Require(element.path == pool + 30 && element.query == pool + 42 && element.fragment == pool + 51); char built[256]; Require(sceHttpUriBuild(nullptr, &required, 0, &element, buildAll) == 0); @@ -353,10 +513,11 @@ int main() { Require(sceHttpUriParse(&element, nullptr, pool, nullptr, sizeof(pool)) == invalidUrl); Require(sceHttpUriParse(nullptr, url, nullptr, nullptr, 0) == invalidValue); - Require(sceHttpUriParse(nullptr, "http://host:65536/", nullptr, &required, 0) == invalidUrl); + Require(sceHttpUriParse(nullptr, "http://host:65537/", nullptr, &required, 0) == invalidUrl); Require(sceHttpUriParse(nullptr, "http://host:80a/", nullptr, &required, 0) == invalidUrl); Require(sceHttpUriParse(nullptr, "http://[::1/", nullptr, &required, 0) == invalidUrl); - Require(sceHttpUriParse(nullptr, "http://bad host/", nullptr, &required, 0) == invalidUrl); + Require(sceHttpUriParse(nullptr, "http://host:80?q", nullptr, &required, 0) == invalidUrl); + CheckUriParseAsMeasured(); Require(sceHttpUriBuild(built, nullptr, sizeof(built), nullptr, buildAll) == invalidUrl); Require(sceHttpUriBuild(nullptr, nullptr, 0, &element, buildAll) == invalidValue); @@ -503,9 +664,10 @@ int main() { Require(sceHttpUriMerge(merged, base, nullptr, &required, sizeof(merged), 0) == invalidValue); Require(sceHttpUriMerge(merged, base, "./x", &required, sizeof(merged), 1) == invalidValue); Require(sceHttpUriMerge(nullptr, nullptr, nullptr, &required, 0, 1) == invalidValue); - Require(sceHttpUriMerge(merged, "http://bad host/", "./x", &required, sizeof(merged), 0) == invalidUrl); - Require(sceHttpUriMerge(merged, base, "http://bad host/", &required, sizeof(merged), 0) == invalidUrl); + Require(sceHttpUriMerge(merged, "http://[::1/", "./x", &required, sizeof(merged), 0) == invalidUrl); + Require(sceHttpUriMerge(merged, "http://h:80a/a", "c", &required, sizeof(merged), 0) == invalidUrl); Require(required == 123); + CheckUriMergeAsMeasured(); constexpr int beforeInit = static_cast(0x80431001); constexpr int aborted = static_cast(0x80431080); diff --git a/core/libs/tests/GuestHttpOptions.cpp b/core/libs/tests/GuestHttpOptions.cpp index bfa0a25748..5944662205 100644 --- a/core/libs/tests/GuestHttpOptions.cpp +++ b/core/libs/tests/GuestHttpOptions.cpp @@ -16,6 +16,8 @@ int APS5_VABI sceHttpCreateRequestWithURL(int, int, const char*, std::uint64_t); int APS5_VABI sceHttpDeleteRequest(int); int APS5_VABI sceHttpSetAutoRedirect(int, int); int APS5_VABI sceHttpGetAutoRedirect(int, int*); +int APS5_VABI sceHttpSetAuthEnabled(int, int); +int APS5_VABI sceHttpGetAuthEnabled(int, int*); int APS5_VABI sceHttpSetNonblock(int, int); int APS5_VABI sceHttpGetNonblock(int, int*); int APS5_VABI sceHttpTrySetNonblock(int, int); @@ -32,6 +34,9 @@ int APS5_VABI sceHttpCacheRedirectedConnectionEnabled(int, int); int APS5_VABI sceHttpsEnableOptionPrivate(int, std::uint32_t); int APS5_VABI sceHttpsDisableOptionPrivate(int, std::uint32_t); int APS5_VABI sceHttpAbortRequestForce(int); +int APS5_VABI sceHttpSetProxy(int, int, int, const char*, std::uint16_t); +int APS5_VABI sceHttpAddRequestHeader(int, const char*, const char*, std::uint32_t); +int APS5_VABI sceHttpRemoveRequestHeader(int, const char*); } namespace { @@ -72,6 +77,23 @@ void CheckAutoRedirect(int context) { Require(sceHttpDeleteConnection(connection) == 0 && sceHttpDeleteTemplate(pattern) == 0, "delete failed"); } +void CheckAuthEnabled(int context) { + const int pattern = sceHttpCreateTemplate(context, "anyps5-test", 2, 0); + Require(Get(sceHttpGetAuthEnabled, pattern) == 0, "authentication is enabled by default"); + Require(sceHttpSetAuthEnabled(pattern, 1) == 0 && Get(sceHttpGetAuthEnabled, pattern) == 1, "sceHttpGetAuthEnabled did not return the set value"); + const int connection = sceHttpCreateConnectionWithURL(pattern, "http://example.com/", 1); + const int request = sceHttpCreateRequestWithURL(connection, 0, "http://example.com/a", 0); + Require(Get(sceHttpGetAuthEnabled, connection) == 1 && Get(sceHttpGetAuthEnabled, request) == 1, "a child did not inherit its parent's authentication option"); + Require(sceHttpSetAuthEnabled(request, 0) == 0 && Get(sceHttpGetAuthEnabled, request) == 0, "authentication could not be disabled on a request"); + Require(Get(sceHttpGetAuthEnabled, connection) == 1 && Get(sceHttpGetAuthEnabled, pattern) == 1, "setting a request changed its parent options"); + int value = -1; + Require(sceHttpGetAuthEnabled(request, nullptr) == InvalidValue, "a null authentication output was not refused"); + Require(sceHttpGetAuthEnabled(UnknownId, nullptr) == InvalidValue, "the authentication output pointer is not checked before the id"); + Require(sceHttpGetAuthEnabled(UnknownId, &value) == InvalidId && sceHttpSetAuthEnabled(UnknownId, 1) == InvalidId, "an unknown id was not refused for authentication options"); + Require(sceHttpDeleteRequest(request) == 0 && sceHttpGetAuthEnabled(request, &value) == InvalidId, "a deleted request still has authentication options"); + Require(sceHttpDeleteConnection(connection) == 0 && sceHttpDeleteTemplate(pattern) == 0, "delete failed"); +} + void CheckNonblock(int context) { const int pattern = sceHttpCreateTemplate(context, "anyps5-test", 2, 0); Require(Get(sceHttpGetNonblock, pattern) == 0 && Get(sceHttpTryGetNonblock, pattern) == 0, "a template is nonblocking by default"); @@ -144,11 +166,13 @@ void CheckTermEndsOptions() { int value = -1; HttpEpollHandle bound = nullptr; Require(sceHttpGetAutoRedirect(pattern, &value) == BeforeInit && sceHttpSetAutoRedirect(pattern, 0) == BeforeInit, "the redirect option outlived the last sceHttpTerm"); + Require(sceHttpGetAuthEnabled(pattern, &value) == BeforeInit && sceHttpSetAuthEnabled(pattern, 0) == BeforeInit, "the authentication option outlived the last sceHttpTerm"); Require(sceHttpGetNonblock(connection, &value) == BeforeInit && sceHttpTryGetNonblock(connection, &value) == BeforeInit, "the nonblocking getters work after the last sceHttpTerm"); Require(sceHttpSetNonblock(connection, 1) == BeforeInit && sceHttpTrySetNonblock(connection, 1) == BeforeInit, "the nonblocking setters work after the last sceHttpTerm"); Require(sceHttpGetAcceptEncodingGZIPEnabled(pattern, &value) == BeforeInit && sceHttpSetAcceptEncodingGZIPEnabled(pattern, 1) == BeforeInit, "the gzip option outlived the last sceHttpTerm"); Require(sceHttpGetEpoll(connection, &bound, nullptr) == BeforeInit && sceHttpUnsetEpoll(connection) == BeforeInit, "the epoll binding outlived the last sceHttpTerm"); Require(sceHttpCacheRedirectedConnectionEnabled(connection, 1) == BeforeInit, "sceHttpCacheRedirectedConnectionEnabled works after the last sceHttpTerm"); + Require(sceHttpSetProxy(connection, 1, 0, "proxy.example", 3128) == BeforeInit, "sceHttpSetProxy works after the last sceHttpTerm"); Require(sceHttpsEnableOptionPrivate(connection, 0x4000u) == BeforeInit && sceHttpsDisableOptionPrivate(connection, 0x4000u) == BeforeInit, "the private SSL options work after the last sceHttpTerm"); Require(sceHttpGetNonblock(connection, nullptr) == BeforeInit, "a null output is checked before initialisation"); Require(value == -1 && bound == nullptr, "a refused getter wrote its output"); @@ -161,6 +185,8 @@ void CheckUnobservedOptions(int context) { const int pattern = sceHttpCreateTemplate(context, "anyps5-test", 2, 0); const int connection = sceHttpCreateConnectionWithURL(pattern, "http://example.com/", 1); const int request = sceHttpCreateRequestWithURL(connection, 0, "http://example.com/a", 0); + Require(sceHttpAddRequestHeader(request, "X-Test", "value", 0) == 0 && sceHttpRemoveRequestHeader(request, "X-Test") == 0, "offline request header calls failed"); + Require(sceHttpRemoveRequestHeader(UnknownId, nullptr) == 0, "offline header removal started validating arguments"); Require(sceHttpCacheRedirectedConnectionEnabled(pattern, 0) == 0 && sceHttpCacheRedirectedConnectionEnabled(request, 1) == 0, "sceHttpCacheRedirectedConnectionEnabled refused a live object"); Require(sceHttpCacheRedirectedConnectionEnabled(UnknownId, 1) == InvalidId, "sceHttpCacheRedirectedConnectionEnabled took an unknown id"); for (const auto option : {sceHttpsEnableOptionPrivate, sceHttpsDisableOptionPrivate}) { @@ -170,6 +196,12 @@ void CheckUnobservedOptions(int context) { Require(option(UnknownId, 0x0200u) == InvalidValue, "the SSL flags are not checked before the id"); Require(option(UnknownId, 0x01u) == InvalidId, "a private SSL option took an unknown id"); } + for (const int id : {pattern, connection, request}) { + Require(sceHttpSetProxy(id, 1, 0, "proxy.example", 3128) == 0, "sceHttpSetProxy refused a live object"); + } + Require(sceHttpSetProxy(connection, 0, 0, nullptr, 0) == InvalidValue, "sceHttpSetProxy took a null host"); + Require(sceHttpSetProxy(UnknownId, 0, 0, nullptr, 0) == InvalidValue, "the proxy host is not checked before the id"); + Require(sceHttpSetProxy(UnknownId, 1, 1, "proxy.example", 8080) == InvalidId, "sceHttpSetProxy took an unknown id"); Require(sceHttpAbortRequestForce(request) == 0, "sceHttpAbortRequestForce failed on a live request"); Require(sceHttpDeleteRequest(request) == 0 && sceHttpDeleteConnection(connection) == 0 && sceHttpDeleteTemplate(pattern) == 0, "delete failed"); } @@ -180,6 +212,7 @@ int main() { const int context = sceHttpInit_nid_postfix(1, 1, 0x10000); Require(context > 0, "sceHttpInit failed"); CheckAutoRedirect(context); + CheckAuthEnabled(context); CheckNonblock(context); CheckAcceptEncodingGzip(context); CheckEpoll(context); diff --git a/core/libs/tests/GuestImeDialog.cpp b/core/libs/tests/GuestImeDialog.cpp index 574398c192..756a809fd7 100644 --- a/core/libs/tests/GuestImeDialog.cpp +++ b/core/libs/tests/GuestImeDialog.cpp @@ -14,6 +14,9 @@ int APS5_VABI sceImeDialogGetResult(Result*); int APS5_VABI sceImeDialogAbort(void); int APS5_VABI sceImeDialogTerm(void); int APS5_VABI sceImeDialogForceClose(void); +int APS5_VABI sceImeDialogGetCurrentStarState(std::int64_t); +int APS5_VABI sceImeDialogInitInternal(const Param*, const ExtendedParam*); +int APS5_VABI sceImeDialogSetPanelPosition(std::int32_t, std::int32_t); } namespace { @@ -25,6 +28,7 @@ constexpr std::uint32_t EndStatusOk = 0; constexpr std::uint32_t EndStatusUserCanceled = 1; constexpr std::uint32_t EndStatusAborted = 2; constexpr int ErrorBusy = static_cast(0x80bc0001u); +constexpr int ErrorSuspending = static_cast(0x80bc0009u); constexpr int ErrorNotOpened = static_cast(0x80bc0002u); constexpr int ErrorNotInUse = static_cast(0x80bc0107u); constexpr int ErrorInvalidMaxTextLength = static_cast(0x80bc0016u); @@ -186,4 +190,21 @@ int main() { Require(sceImeDialogInit(&noBuffer, nullptr) == ErrorInvalidInputTextBuffer); Require(sceImeDialogGetStatus() == StatusNone); Require(!ImeCapturesKeyboard_nid_postfix()); + + std::uint32_t star = 0xa5a5a5a5u; + const auto starAddress = static_cast(reinterpret_cast(&star)); + Require(sceImeDialogGetCurrentStarState(starAddress) == ErrorNotInUse && star == 0xa5a5a5a5u); + Require(sceImeDialogSetPanelPosition(640, 360) == ErrorNotInUse); + Require(sceImeDialogInitInternal(nullptr, nullptr) == ErrorInvalidAddress); + Require(sceImeDialogInitInternal(&empty, nullptr) == ErrorInvalidMaxTextLength); + Require(sceImeDialogGetStatus() == StatusNone); + Require(sceImeDialogInitInternal(¶m, nullptr) == 0); + Require(sceImeDialogGetStatus() == StatusRunning && ImeCapturesKeyboard_nid_postfix()); + Require(sceImeDialogInitInternal(¶m, nullptr) == ErrorBusy); + Require(sceImeDialogGetCurrentStarState(0) == ErrorInvalidAddress); + Require(sceImeDialogGetCurrentStarState(starAddress) == 0 && star == 0); + Require(sceImeDialogSetPanelPosition(640, 360) == ErrorSuspending); + Require(sceImeDialogAbort() == 0); + Require(sceImeDialogSetPanelPosition(0, 0) == ErrorSuspending); + Close(); } diff --git a/core/libs/tests/GuestJson.cpp b/core/libs/tests/GuestJson.cpp index c20ffcbd83..7c59af4225 100644 --- a/core/libs/tests/GuestJson.cpp +++ b/core/libs/tests/GuestJson.cpp @@ -1,5 +1,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include +#include +#include #include #include #include @@ -7,6 +9,7 @@ #include #include #include +#include #include struct Value { void* node; }; @@ -1374,7 +1377,78 @@ static void ValueOperatorBool() { _ZN3sce4Json5ValueD1Ev(&cleared); } -int main() { +static int NumericLocale() { + const std::string previous = std::setlocale(LC_NUMERIC, nullptr); + const char* selected = nullptr; + for (const char* name : {"xx_COMMA", "de_DE.UTF-8", "de_DE.utf8", "de-DE", "German_Germany.1252", "fr_FR.UTF-8"}) { + if (std::setlocale(LC_NUMERIC, name) && std::strcmp(std::localeconv()->decimal_point, ",") == 0) { + selected = name; + break; + } + } + if (!selected) { + std::setlocale(LC_NUMERIC, previous.c_str()); + std::fprintf(stderr, "No decimal-comma numeric locale is installed\n"); + return 77; + } + std::printf("JSON numeric locale: %s\n", selected); + int failures = 0; + const auto check = [&](bool success, const char* operation, const char* actual) { + if (!success) { + std::fprintf(stderr, "%s failed: %s\n", operation, actual); + ++failures; + } + }; + for (const auto number : {1.5, -0.125, 1.25e30, std::numeric_limits::max(), + std::numeric_limits::min(), std::numeric_limits::denorm_min()}) { + Value value{}; + _ZN3sce4Json5ValueC1Ed(&value, number); + const auto text = Serialize(value); + check(text.find(',') == std::string::npos, "Decimal-point serialization", text.c_str()); + Value parsed{}; + _ZN3sce4Json5ValueC1Ev(&parsed); + const int result = _ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&parsed, text.c_str(), text.size()); + check(result == 0 && *_ZNK3sce4Json5Value7getRealEv(&parsed) == number, "Numeric round trip", text.c_str()); + _ZN3sce4Json5ValueD1Ev(&parsed); + _ZN3sce4Json5ValueD1Ev(&value); + } + for (const auto [text, number] : {std::pair{"1.5", 1.5}, {"-1.25e2", -125.0}, + {"4.9406564584124654e-324", std::numeric_limits::denorm_min()}, + {"2.4703282292062327e-324", 0.0}, + {"2.4703282292062328e-324", std::numeric_limits::denorm_min()}, + {"18446744073709551616", 18446744073709551616.0}, + {"1e-999", 0.0}, {"-1e-999", -0.0}}) { + Value value{}; + _ZN3sce4Json5ValueC1Ev(&value); + const int result = _ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&value, text, std::strlen(text)); + const auto parsed = *_ZNK3sce4Json5Value7getRealEv(&value); + check(result == 0 && parsed == number && std::signbit(parsed) == std::signbit(number), "Decimal-point parsing", text); + _ZN3sce4Json5ValueD1Ev(&value); + } + Value root{}; + _ZN3sce4Json5ValueC1Ev(&root); + constexpr char document[] = "{\"list\":[2.5,-0.125],\"value\":1.5}"; + check(_ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&root, document, sizeof(document) - 1) == 0, + "Nested numeric parsing", document); + const auto serialized = Serialize(root); + check(serialized == document, "Nested numeric serialization", serialized.c_str()); + check(_ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&root, "1,5", 3) == 0 && _ZNK3sce4Json5Value7getTypeEv(&root) == TypeUInteger && + *_ZNK3sce4Json5Value11getUIntegerEv(&root) == 1, "Stop at a decimal comma", "1,5"); + bool overflow = false; + try { + _ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&root, "1e309", 5); + } catch (const std::runtime_error&) { + overflow = true; + } + check(overflow, "Retain overflow rejection", "1e309"); + _ZN3sce4Json5ValueD1Ev(&root); + check(std::strcmp(std::localeconv()->decimal_point, ",") == 0, "Preserve host locale", selected); + std::setlocale(LC_NUMERIC, previous.c_str()); + return failures ? 1 : 0; +} + +int main(int argc, char** argv) { + if (argc == 2 && std::strcmp(argv[1], "--numeric-locale") == 0) return NumericLocale(); ParseAndRoundTrip(); NestingDepth(); ObjectsAndArrays(); diff --git a/core/libs/tests/GuestJxr.cpp b/core/libs/tests/GuestJxr.cpp new file mode 100644 index 0000000000..1bcda397b1 --- /dev/null +++ b/core/libs/tests/GuestJxr.cpp @@ -0,0 +1,78 @@ +#include "prx/libc/include/ApplicationHeap.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include + +extern "C" { +void* APS5_VABI malloc_nid_postfix(std::size_t size); +std::int64_t APS5_VABI PKFree_nid_postfix(void** memory); +} + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +void* lastFreed = nullptr; +int frees = 0; + +void* APS5_VABI Allocate(std::size_t size) { return std::malloc(size); } +void APS5_VABI Free(void* pointer) { + if (pointer != nullptr) { + lastFreed = pointer; + ++frees; + } + std::free(pointer); +} +void* APS5_VABI Reallocate(void* pointer, std::size_t size) { return std::realloc(pointer, size); } +void* APS5_VABI Calloc(std::size_t count, std::size_t size) { return std::calloc(count, size); } +void* APS5_VABI Align(std::size_t, std::size_t size) { return std::malloc(size); } +void* APS5_VABI Realign(void* pointer, std::size_t size, std::size_t) { return std::realloc(pointer, size); } +int APS5_VABI PosixAlign(void** pointer, std::size_t, std::size_t size) { + *pointer = std::malloc(size); + return *pointer ? 0 : 12; +} + +void InitializeHeap() { + alignas(16) static std::array process{}; + alignas(16) static std::array libc{}; + alignas(16) static std::array replacement{}; + const auto write = [](auto& data, std::size_t offset, auto value) { + std::memcpy(data.data() + offset, &value, sizeof(value)); + }; + write(process, 0, std::uint64_t{0x40}); + write(process, 8, std::uint32_t{0x4942524f}); + write(process, 0x38, libc.data()); + write(libc, 0, std::uint64_t{0x38}); + write(libc, 0x30, replacement.data()); + write(replacement, 0, std::uint64_t{0x78}); + write(replacement, 8, std::uint64_t{2}); + write(replacement, 0x20, reinterpret_cast(&Allocate)); + write(replacement, 0x28, reinterpret_cast(&Free)); + write(replacement, 0x30, reinterpret_cast(&Calloc)); + write(replacement, 0x38, reinterpret_cast(&Reallocate)); + write(replacement, 0x40, reinterpret_cast(&Align)); + write(replacement, 0x48, reinterpret_cast(&Realign)); + write(replacement, 0x50, reinterpret_cast(&PosixAlign)); + ApplicationHeapInitialize_nid_no_patch(process.data()); +} + +} + +int main() { + InitializeHeap(); + void* memory = malloc_nid_postfix(64); + Require(memory != nullptr); + void* const allocated = memory; + Require(PKFree_nid_postfix(&memory) == 0); + Require(memory == nullptr); + Require(frees == 1 && lastFreed == allocated); + Require(PKFree_nid_postfix(&memory) == 0); + Require(memory == nullptr); + Require(frees == 1); + Require(PKFree_nid_postfix(nullptr) == 0); + Require(frees == 1); + return 0; +} diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index def0645d9d..b481f508fe 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -17,6 +17,7 @@ #include #ifdef _WIN32 #include +#include #ifndef SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE #define SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE 0x2 #endif @@ -29,9 +30,14 @@ extern "C" { int APS5_VABI fstatfs_nid_postfix(int, void*); int APS5_VABI _fstatfs_nid_postfix(int, void*); +int APS5_VABI fstat_nid_postfix(int, FileStat*); +std::int64_t APS5_VABI _fstat_nid_postfix(int, FileStat*); int APS5_VABI open_nid_postfix(const char*, int, int); int APS5_VABI close_nid_postfix(int); +int APS5_VABI pipe_nid_postfix(int*); +int APS5_VABI socket_nid_postfix(int, int, int); std::int64_t APS5_VABI readlink_nid_postfix(const char*, char*, std::size_t); +int APS5_VABI lstat_nid_postfix(const char*, FileStat*); std::int64_t APS5_VABI pathconf_nid_postfix(const char*, int); int APS5_VABI fchdir_nid_postfix(int); int APS5_VABI chdir_nid_postfix(const char*); @@ -55,6 +61,7 @@ std::int64_t APS5_VABI readlinkat_nid_postfix(int, const char*, char*, std::size int APS5_VABI mkdirat_nid_postfix(int, const char*, std::uint16_t); int APS5_VABI renameat_nid_postfix(int, const char*, int, const char*); int APS5_VABI fchmodat_nid_postfix(int, const char*, int, int); +int APS5_VABI lchmod_nid_postfix(const char*, std::uint16_t); int APS5_VABI fchown_nid_postfix(int, std::uint32_t, std::uint32_t); int APS5_VABI fstatat_nid_postfix(int, const char*, FileStat*, int); std::int64_t APS5_VABI sceKernelRead(int, void*, std::size_t); @@ -281,8 +288,15 @@ int main() { Require(std::memcmp(viaPublic, viaSyscall, 4) == 0 && std::memcmp(viaPublic + 16, viaSyscall + 16, 8) == 0); Require(std::memcmp(viaPublic + 280, viaSyscall + 280, 16) == 0); Require(fstatfs_nid_postfix(descriptor, nullptr) == -1 && Error() == 14); + FileStat viaFstat{}; + FileStat viaUnderscore{}; + Require(fstat_nid_postfix(descriptor, &viaFstat) == 0 && _fstat_nid_postfix(descriptor, &viaUnderscore) == 0); + Require(std::memcmp(&viaFstat, &viaUnderscore, sizeof(FileStat)) == 0 && viaFstat.st_size == 1); + Require(fstat_nid_postfix(descriptor, nullptr) == -1 && Error() == 14); Require(close_nid_postfix(descriptor) == 0); Require(fstatfs_nid_postfix(-1, viaPublic) == -1 && Error() == 9); + Require(fstat_nid_postfix(descriptor, &viaFstat) == -1 && Error() == 9); + Require(fstat_nid_postfix(-1, &viaFstat) == -1 && Error() == 9); char buffer[64]; for (const auto& component : {std::string("missing"), std::string("file.txt")}) { const auto prefix = root.generic_string() + "/" + component + "/../"; @@ -295,6 +309,19 @@ int main() { Require(symlink_nid_postfix("file.txt", created.c_str()) == -1 && Error() == expected); Require(std::filesystem::hard_link_count(file) == 1 && !std::filesystem::exists(root / "new.txt")); } + for (const std::size_t length : {std::size_t{1024}, std::size_t{1100}}) { + const auto tooLong = std::string(length, 'a'); + const auto created = (root / "target-too-long").string(); + Require(symlink_nid_postfix(tooLong.c_str(), created.c_str()) == -1 && Error() == 63); + Require(!std::filesystem::exists(std::filesystem::symlink_status(root / "target-too-long"))); + } + { + const auto longest = std::string(1023, 'a'); + const auto created = (root / "target-longest").string(); + const int made = symlink_nid_postfix(longest.c_str(), created.c_str()); + Require(made == 0 || Error() != 63); + if (made == 0) Require(RemoveSymlink(created)); + } Require(readlink_nid_postfix(file.c_str(), buffer, sizeof(buffer)) == -1 && Error() == 22); Require(readlink_nid_postfix(root.string().c_str(), buffer, sizeof(buffer)) == -1 && Error() == 22); Require(readlink_nid_postfix((root / "missing").string().c_str(), buffer, sizeof(buffer)) == -1 && Error() == 2); @@ -310,6 +337,57 @@ int main() { Require(readlinkat_nid_postfix(-100, nullptr, buffer, sizeof(buffer)) == -1 && Error() == 14); #ifndef _WIN32 Require(readlink_nid_postfix((root / std::string(300, 'n')).string().c_str(), buffer, sizeof(buffer)) == -1 && Error() == 63); +#else + const auto setReparseTag = [](const std::filesystem::path& path, DWORD tag) { + const HANDLE handle = CreateFileW(path.c_str(), GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, + FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr); + Require(handle != INVALID_HANDLE_VALUE); + std::array reparseData{}; + REPARSE_GUID_DATA_BUFFER reparseHeader{}; + reparseHeader.ReparseTag = tag; + reparseHeader.ReparseDataLength = 4; + reparseHeader.ReparseGuid = {0x6a1e3d0b, 0x1f6c, 0x4c1e, {0x9b, 0x52, 0x3d, 0x8e, 0x41, 0x07, 0x5c, 0x2a}}; + std::memcpy(reparseData.data(), &reparseHeader, REPARSE_GUID_DATA_BUFFER_HEADER_SIZE); + DWORD returned = 0; + const bool set = DeviceIoControl(handle, FSCTL_SET_REPARSE_POINT, reparseData.data(), static_cast(reparseData.size()), + nullptr, 0, &returned, nullptr) != 0; + if (!set) Require(GetLastError() == ERROR_PRIVILEGE_NOT_HELD || GetLastError() == ERROR_ACCESS_DENIED); + Require(CloseHandle(handle) != 0); + if (!set) { + Require(std::filesystem::remove(path)); + std::fprintf(stderr, "Windows reparse point checks skipped: setting a reparse point on a file needs rights this user lacks\n"); + return false; + } + Require((GetFileAttributesW(path.c_str()) & FILE_ATTRIBUTE_REPARSE_POINT) != 0); + return true; + }; + const auto clearReparseTag = [](const std::filesystem::path& path, DWORD tag) { + const HANDLE handle = CreateFileW(path.c_str(), GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, + FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr); + Require(handle != INVALID_HANDLE_VALUE); + REPARSE_GUID_DATA_BUFFER reparseHeader{}; + reparseHeader.ReparseTag = tag; + reparseHeader.ReparseGuid = {0x6a1e3d0b, 0x1f6c, 0x4c1e, {0x9b, 0x52, 0x3d, 0x8e, 0x41, 0x07, 0x5c, 0x2a}}; + DWORD returned = 0; + Require(DeviceIoControl(handle, FSCTL_DELETE_REPARSE_POINT, &reparseHeader, REPARSE_GUID_DATA_BUFFER_HEADER_SIZE, + nullptr, 0, &returned, nullptr) != 0); + Require(CloseHandle(handle) != 0 && std::filesystem::remove(path)); + }; + const auto tagged = root / "tagged.bin"; + std::ofstream(tagged, std::ios::binary) << "abc"; + if (setReparseTag(tagged, 0x00000ABC)) { + Require(readlink_nid_postfix(tagged.string().c_str(), buffer, sizeof(buffer)) == -1 && Error() == 22); + clearReparseTag(tagged, 0x00000ABC); + } + const auto surrogate = root / "surrogate.bin"; + std::ofstream(surrogate, std::ios::binary) << "abc"; + if (setReparseTag(surrogate, 0x20000ABC)) { + Require(readlink_nid_postfix(surrogate.string().c_str(), buffer, sizeof(buffer)) == -1 && Error() == 22); + FileStat surrogateStatus{}; + Require(lstat_nid_postfix(surrogate.string().c_str(), &surrogateStatus) == 0); + Require(surrogateStatus.st_mode == 0100666 && surrogateStatus.st_nlink == 1); + clearReparseTag(surrogate, 0x20000ABC); + } #endif std::error_code error; const auto link = (root / "link").string(); @@ -364,6 +442,19 @@ int main() { Require(fileDescriptor >= 0); Require(fchdir_nid_postfix(-1) == -1 && Error() == 9); Require(fchdir_nid_postfix(fileDescriptor) == -1 && Error() == 20); + int pipeEnds[2] = {-1, -1}; + Require(pipe_nid_postfix(pipeEnds) == 0); + Require(fchdir_nid_postfix(pipeEnds[0]) == -1 && Error() == 22); + Require(fchdir_nid_postfix(pipeEnds[1]) == -1 && Error() == 22); + Require(close_nid_postfix(pipeEnds[0]) == 0 && close_nid_postfix(pipeEnds[1]) == 0); + const int characterDevice = open_nid_postfix("/dev/random", 0, 0); + Require(characterDevice >= 0); + Require(fchdir_nid_postfix(characterDevice) == -1 && Error() == 20); + Require(close_nid_postfix(characterDevice) == 0); + const int socketDescriptor = socket_nid_postfix(2, 2, 0); + Require(socketDescriptor >= 0); + Require(fchdir_nid_postfix(socketDescriptor) == -1 && Error() == 22); + Require(close_nid_postfix(socketDescriptor) == 0); Require(fchdir_nid_postfix(directory) == 0); char directoryPath[1024]; Require(getcwd_nid_postfix(directoryPath, sizeof(directoryPath)) == directoryPath); @@ -552,7 +643,8 @@ int main() { Require(unlinkat_nid_postfix(base, "file.txt", 0x800) == -1 && Error() == 20); Require(unlinkat_nid_postfix(base, ".", 0x800) == -1 && Error() == 22); Require(unlinkat_nid_postfix(base, "..", 0x800) == -1 && Error() == 22); - Require(unlinkat_nid_postfix(base, ".", 0) == -1 && Error() == 1); + Require(unlinkat_nid_postfix(base, ".", 0) == -1 && Error() == 22); + Require(unlinkat_nid_postfix(base, "..", 0) == -1 && Error() == 22); std::filesystem::create_directory(root / "usub"); Require(unlinkat_nid_postfix(base, "usub", 0) == -1 && Error() == 1); std::filesystem::create_directory(root / "nonempty"); @@ -642,6 +734,38 @@ int main() { Require(symlink_nid_postfix("loop", (root / "loop").string().c_str()) == 0); Require(fchmodat_nid_postfix(base, "loop/x", 0600, 0) == -1 && Error() == 62); Require(fchmodat_nid_postfix(base, longName.c_str(), 0600, 0) == -1 && Error() == 63); +#endif + const auto modeFile = (root / "file.txt").string(); +#ifndef _WIN32 + const auto ownerOnly = std::filesystem::perms::owner_read | std::filesystem::perms::owner_write; + const auto readable = ownerOnly | std::filesystem::perms::group_read | std::filesystem::perms::others_read; +#endif + Require(lchmod_nid_postfix(modeFile.c_str(), 0600) == 0); +#ifndef _WIN32 + Require((std::filesystem::status(modeFile).permissions() & std::filesystem::perms::all) == ownerOnly); +#endif + Require(lchmod_nid_postfix(guestFile.c_str(), 0100644) == 0); +#ifndef _WIN32 + Require((std::filesystem::status(modeFile).permissions() & std::filesystem::perms::all) == readable); +#endif + Require(lchmod_nid_postfix((root / "missing.bin").string().c_str(), 0600) == -1 && Error() == 2); + Require(lchmod_nid_postfix(nullptr, 0600) == -1 && Error() == 14); + Require(lchmod_nid_postfix("", 0600) == -1 && Error() == 2); +#ifndef _WIN32 + Require(lchmod_nid_postfix((root / "loop" / "x").string().c_str(), 0600) == -1 && Error() == 62); + Require(lchmod_nid_postfix((root / longName).string().c_str(), 0600) == -1 && Error() == 63); + const auto modeLink = (root / "mode-link.txt").string(); + Require(symlink_nid_postfix("file.txt", modeLink.c_str()) == 0); +#ifdef __APPLE__ + Require(lchmod_nid_postfix(modeLink.c_str(), 0600) == 0); + Require((std::filesystem::symlink_status(modeLink).permissions() & std::filesystem::perms::all) == ownerOnly); +#else + bool lchmodThrew = false; + try { lchmod_nid_postfix(modeLink.c_str(), 0600); } catch (const std::exception&) { lchmodThrew = true; } + Require(lchmodThrew); +#endif + Require((std::filesystem::status(modeFile).permissions() & std::filesystem::perms::all) == readable); + Require(RemoveSymlink(modeLink)); #endif Require(fchown_nid_postfix(base, 0, 0) == 0); Require(fchown_nid_postfix(base, 0xFFFFFFFFu, 0xFFFFFFFFu) == 0); @@ -691,20 +815,27 @@ int main() { Require(junctionText.find('?') == std::string::npos && std::filesystem::equivalent(junctionText, junctionTarget, junctionError) && !junctionError); Require(std::filesystem::remove(junction) && std::filesystem::remove(junctionTarget)); #endif -#ifndef _WIN32 if (created == 0) { const auto throughLink = (root / "through-link.bin").string(); Require(link_nid_postfix(symbolic.c_str(), throughLink.c_str()) == 0); +#ifdef _WIN32 + Require((GetFileAttributesW(std::filesystem::path(throughLink).c_str()) & FILE_ATTRIBUTE_REPARSE_POINT) == 0); +#else Require(!std::filesystem::is_symlink(std::filesystem::symlink_status(throughLink))); +#endif Require(std::filesystem::equivalent(throughLink, original) && std::filesystem::hard_link_count(original) == 2); Require(std::filesystem::remove(throughLink) && std::filesystem::hard_link_count(original) == 1); Require(link_nid_postfix((root / "dangling").string().c_str(), throughLink.c_str()) == -1 && Error() == 2); - const auto directoryLink = (root / "directory-link").string(); - Require(symlink_nid_postfix(".", directoryLink.c_str()) == 0); - Require(link_nid_postfix(directoryLink.c_str(), throughLink.c_str()) == -1 && Error() == 1); + const auto dotLink = (root / "dot-link").string(); + Require(symlink_nid_postfix(".", dotLink.c_str()) == 0); + Require(link_nid_postfix(dotLink.c_str(), throughLink.c_str()) == -1 && Error() == 1); +#ifdef _WIN32 + Require(symlink_nid_postfix("loop-b", (root / "loop-a").string().c_str()) == 0); + Require(symlink_nid_postfix("loop-a", (root / "loop-b").string().c_str()) == 0); + Require(link_nid_postfix((root / "loop-a").string().c_str(), throughLink.c_str()) == -1 && Error() == 62); +#endif Require(!std::filesystem::exists(std::filesystem::symlink_status(throughLink))); } -#endif RemoveTree(root); return 0; } diff --git a/core/libs/tests/GuestKernelSysThreadDefaults.cpp b/core/libs/tests/GuestKernelSysThreadDefaults.cpp new file mode 100644 index 0000000000..18a48b8f15 --- /dev/null +++ b/core/libs/tests/GuestKernelSysThreadDefaults.cpp @@ -0,0 +1,84 @@ +#include "SceTypes.hpp" +#include +#include +#include + +extern "C" { +int APS5_VABI pthread_set_defaultstacksize_np_nid_postfix(std::size_t size); +int APS5_VABI scePthreadSetDefaultstacksize(std::size_t size); +void APS5_VABI scePthreadSetName(Pthread thread, const char* name); +int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); +int APS5_VABI scePthreadJoin(Pthread thread, void** retval); +Pthread APS5_VABI scePthreadSelf(void); +int APS5_VABI scePthreadGetname(Pthread thread, char* name); +int APS5_VABI scePthreadAttrInit(PthreadAttr* attr); +int APS5_VABI scePthreadAttrDestroy(PthreadAttr* attr); +int APS5_VABI scePthreadAttrGet(Pthread thread, PthreadAttr* attr); +int APS5_VABI scePthreadAttrGetstacksize(const PthreadAttr* attr, std::size_t* stackSize); +} + +static constexpr int SCE_KERNEL_ERROR_EINVAL = static_cast(0x80020016); +static constexpr int POSIX_EINVAL = 22; +static constexpr std::size_t INITIAL_DEFAULT = 1u << 20; + +static void Require(bool value) { if (!value) std::abort(); } + +static std::size_t NewAttrStackSize() { + PthreadAttr attr = nullptr; + Require(scePthreadAttrInit(&attr) == 0); + std::size_t size = 0; + Require(scePthreadAttrGetstacksize(&attr, &size) == 0); + Require(scePthreadAttrDestroy(&attr) == 0); + return size; +} + +static void* APS5_VABI ReportStackSize(void* out) { + PthreadAttr attr = nullptr; + Require(scePthreadAttrInit(&attr) == 0); + Require(scePthreadAttrGet(scePthreadSelf(), &attr) == 0); + Require(scePthreadAttrGetstacksize(&attr, static_cast(out)) == 0); + Require(scePthreadAttrDestroy(&attr) == 0); + return nullptr; +} + +static std::size_t DefaultThreadStackSize() { + std::size_t size = 0; + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, nullptr, ReportStackSize, &size, "stack") == 0); + Require(scePthreadJoin(thread, nullptr) == 0); + return size; +} + +static void CheckDefaultStackSize() { + Require(NewAttrStackSize() == INITIAL_DEFAULT); + Require(pthread_set_defaultstacksize_np_nid_postfix(0) == POSIX_EINVAL); + Require(pthread_set_defaultstacksize_np_nid_postfix(16384) == POSIX_EINVAL); + Require(scePthreadSetDefaultstacksize(16384) == SCE_KERNEL_ERROR_EINVAL); + Require(NewAttrStackSize() == INITIAL_DEFAULT); + + Require(pthread_set_defaultstacksize_np_nid_postfix(16385) == 0); + Require(NewAttrStackSize() == 16385); + + Require(scePthreadSetDefaultstacksize(0x40000) == 0); + Require(NewAttrStackSize() == 0x40000); + Require(DefaultThreadStackSize() == 0x40000); + + Require(scePthreadSetDefaultstacksize(INITIAL_DEFAULT) == 0); + Require(DefaultThreadStackSize() == INITIAL_DEFAULT); +} + +static void CheckSetName() { + char name[32] = {}; + scePthreadSetName(scePthreadSelf(), "renamed main"); + Require(scePthreadGetname(scePthreadSelf(), name) == 0); + Require(std::strcmp(name, "renamed main") == 0); + scePthreadSetName(scePthreadSelf(), nullptr); + Require(scePthreadGetname(scePthreadSelf(), name) == 0); + Require(name[0] == '\0'); +} + +int main() { + CheckDefaultStackSize(); + CheckSetName(); + return 0; +} diff --git a/core/libs/tests/GuestLibreSsl3Digest.cpp b/core/libs/tests/GuestLibreSsl3Digest.cpp new file mode 100644 index 0000000000..19f6f8cb2b --- /dev/null +++ b/core/libs/tests/GuestLibreSsl3Digest.cpp @@ -0,0 +1,54 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +unsigned char* APS5_VABI MD5_nid_postfix(const unsigned char*, std::size_t, unsigned char*); +unsigned char* APS5_VABI SHA1_nid_postfix(const unsigned char*, std::size_t, unsigned char*); +} + +static void Require(bool condition, std::source_location location = std::source_location::current()) { + if (condition) return; + std::fprintf(stderr, "LibreSSL3 digest check failed at line %u\n", location.line()); + std::abort(); +} + +template static void RequireThrows(TAction action, std::source_location location = std::source_location::current()) { + try { action(); } catch (const std::exception&) { return; } + Require(false, location); +} + +static std::string Hex(const unsigned char* data, std::size_t size) { + static constexpr char digits[] = "0123456789abcdef"; + std::string text; + for (std::size_t i = 0; i < size; ++i) { + text += digits[data[i] >> 4]; + text += digits[data[i] & 15]; + } + return text; +} + +int main() { + const auto* abc = reinterpret_cast("abc"); + const std::string million(1000000, 'a'); + const auto* many = reinterpret_cast(million.data()); + unsigned char digest[24]; + std::memset(digest, 0xee, sizeof(digest)); + Require(MD5_nid_postfix(abc, 3, digest) == digest && Hex(digest, 16) == "900150983cd24fb0d6963f7d28e17f72" && digest[16] == 0xee); + Require(MD5_nid_postfix(nullptr, 0, digest) == digest && Hex(digest, 16) == "d41d8cd98f00b204e9800998ecf8427e"); + Require(MD5_nid_postfix(many, million.size(), digest) == digest && Hex(digest, 16) == "7707d6ae4e027c70eea2a935c2296f21"); + std::memset(digest, 0xee, sizeof(digest)); + Require(SHA1_nid_postfix(abc, 3, digest) == digest && Hex(digest, 20) == "a9993e364706816aba3e25717850c26c9cd0d89d" && digest[20] == 0xee); + Require(SHA1_nid_postfix(nullptr, 0, digest) == digest && Hex(digest, 20) == "da39a3ee5e6b4b0d3255bfef95601890afd80709"); + Require(SHA1_nid_postfix(many, million.size(), digest) == digest && Hex(digest, 20) == "34aa973cd4c4daa4f61eeb2bdbad27316534016f"); + RequireThrows([&] { MD5_nid_postfix(abc, 3, nullptr); }); + RequireThrows([&] { SHA1_nid_postfix(abc, 3, nullptr); }); + RequireThrows([&] { SHA1_nid_postfix(nullptr, 1, digest); }); + + std::puts("libSceLibreSsl3 digest tests passed"); +} diff --git a/core/libs/tests/GuestLibreSslDigest.cpp b/core/libs/tests/GuestLibreSslDigest.cpp new file mode 100644 index 0000000000..8b9ba6183b --- /dev/null +++ b/core/libs/tests/GuestLibreSslDigest.cpp @@ -0,0 +1,195 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI MD4_Init_nid_postfix(void*); +int APS5_VABI MD4_Update_nid_postfix(void*, const void*, std::size_t); +int APS5_VABI MD4_Final_nid_postfix(unsigned char*, void*); +int APS5_VABI MD5_Init_nid_postfix(void*); +int APS5_VABI MD5_Update_nid_postfix(void*, const void*, std::size_t); +int APS5_VABI MD5_Final_nid_postfix(unsigned char*, void*); +int APS5_VABI SHA1_Init_nid_postfix(void*); +int APS5_VABI SHA1_Update_nid_postfix(void*, const void*, std::size_t); +int APS5_VABI SHA1_Final_nid_postfix(unsigned char*, void*); +int APS5_VABI SHA224_Init_nid_postfix(void*); +int APS5_VABI SHA224_Update_nid_postfix(void*, const void*, std::size_t); +int APS5_VABI SHA224_Final_nid_postfix(unsigned char*, void*); +int APS5_VABI SHA256_Init_nid_postfix(void*); +int APS5_VABI SHA256_Update_nid_postfix(void*, const void*, std::size_t); +int APS5_VABI SHA256_Final_nid_postfix(unsigned char*, void*); +unsigned char* APS5_VABI SHA256_nid_postfix(const unsigned char*, std::size_t, unsigned char*); +int APS5_VABI SHA384_Init_nid_postfix(void*); +int APS5_VABI SHA384_Update_nid_postfix(void*, const void*, std::size_t); +int APS5_VABI SHA384_Final_nid_postfix(unsigned char*, void*); +int APS5_VABI SHA512_Init_nid_postfix(void*); +int APS5_VABI SHA512_Update_nid_postfix(void*, const void*, std::size_t); +int APS5_VABI SHA512_Final_nid_postfix(unsigned char*, void*); +} + +using InitFunction = int (APS5_VABI*)(void*); +using UpdateFunction = int (APS5_VABI*)(void*, const void*, std::size_t); +using FinalFunction = int (APS5_VABI*)(unsigned char*, void*); + +struct Algorithm { + const char* name; + InitFunction init; + UpdateFunction update; + FinalFunction final; + std::size_t contextSize; + std::size_t blockSize; + std::size_t digestSize; + const char* expected[6]; +}; + +static void Require(bool value, const char* what) { + if (value) return; + std::fprintf(stderr, "failed: %s\n", what); + std::abort(); +} + +template static void RequireThrows(TAction action, const char* what) { + try { action(); } catch (const std::exception&) { return; } + Require(false, what); +} + +static std::string Hex(const unsigned char* data, std::size_t size) { + static constexpr char digits[] = "0123456789abcdef"; + std::string text; + for (std::size_t i = 0; i < size; ++i) { + text += digits[data[i] >> 4]; + text += digits[data[i] & 15]; + } + return text; +} + +static std::string Digest(const Algorithm& algorithm, const std::string& message, std::size_t chunk) { + alignas(16) unsigned char context[256] {}; + unsigned char digest[64] {}; + Require(algorithm.init(context) == 1, "init returns 1"); + for (std::size_t offset = 0; offset < message.size(); offset += chunk) { + const auto length = std::min(chunk, message.size() - offset); + Require(algorithm.update(context, message.data() + offset, length) == 1, "update returns 1"); + } + Require(algorithm.final(digest, context) == 1, "final returns 1"); + return Hex(digest, algorithm.digestSize); +} + +int main() { + const std::string messages[6] { + "", + "abc", + "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq", + "abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmnhijklmnoijklmnopjklmnopqklmnopqrlmnopqrsmnopqrstnopqrstu", + "12345678901234567890123456789012345678901234567890123456789012345678901234567890", + std::string(1000000, 'a'), + }; + const Algorithm algorithms[] { + {"MD4", MD4_Init_nid_postfix, MD4_Update_nid_postfix, MD4_Final_nid_postfix, 92, 64, 16, { + "31d6cfe0d16ae931b73c59d7e0c089c0", "a448017aaf21d8525fc10ae87aa6729d", "4691a9ec81b1a6bd1ab8557240b245c5", + nullptr, "e33b4ddc9c38f2199c3e7b164fcc0536", nullptr}}, + {"MD5", MD5_Init_nid_postfix, MD5_Update_nid_postfix, MD5_Final_nid_postfix, 92, 64, 16, { + "d41d8cd98f00b204e9800998ecf8427e", "900150983cd24fb0d6963f7d28e17f72", "8215ef0796a20bcaaae116d3876c664a", + "03dd8807a93175fb062dfb55dc7d359c", "57edf4a22be3c955ac49da2e2107b67a", "7707d6ae4e027c70eea2a935c2296f21"}}, + {"SHA1", SHA1_Init_nid_postfix, SHA1_Update_nid_postfix, SHA1_Final_nid_postfix, 96, 64, 20, { + "da39a3ee5e6b4b0d3255bfef95601890afd80709", "a9993e364706816aba3e25717850c26c9cd0d89d", "84983e441c3bd26ebaae4aa1f95129e5e54670f1", + "a49b2446a02c645bf419f995b67091253a04a259", "50abf5706a150990a08b2c5ea40fa0e585554732", "34aa973cd4c4daa4f61eeb2bdbad27316534016f"}}, + {"SHA224", SHA224_Init_nid_postfix, SHA224_Update_nid_postfix, SHA224_Final_nid_postfix, 112, 64, 28, { + "d14a028c2a3a2bc9476102bb288234c415a2b01f828ea62ac5b3e42f", "23097d223405d8228642a477bda255b32aadbce4bda0b3f7e36c9da7", + "75388b16512776cc5dba5da1fd890150b0c6455cb4f58b1952522525", "c97ca9a559850ce97a04a96def6d99a9e0e0e2ab14e6b8df265fc0b3", + "b50aecbe4e9bb0b57bc5f3ae760a8e01db24f203fb3cdcd13148046e", "20794655980c91d8bbb4c1ea97618a4bf03f42581948b2ee4ee7ad67"}}, + {"SHA256", SHA256_Init_nid_postfix, SHA256_Update_nid_postfix, SHA256_Final_nid_postfix, 112, 64, 32, { + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad", + "248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1", "cf5b16a778af8380036ce59e7b0492370b249b11e8f07a51afac45037afee9d1", + "f371bc4a311f2b009eef952dd83ca80e2b60026c8e935592d0f9c308453c813e", "cdc76e5c9914fb9281a1c7e284d73e67f1809a48a497200e046d39ccc7112cd0"}}, + {"SHA384", SHA384_Init_nid_postfix, SHA384_Update_nid_postfix, SHA384_Final_nid_postfix, 216, 128, 48, { + "38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b", + "cb00753f45a35e8bb5a03d699ac65007272c32ab0eded1631a8b605a43ff5bed8086072ba1e7cc2358baeca134c825a7", + "3391fdddfc8dc7393707a65b1b4709397cf8b1d162af05abfe8f450de5f36bc6b0455a8520bc4e6f5fe95b1fe3c8452b", + "09330c33f71147e83d192fc782cd1b4753111b173b3b05d22fa08086e3b0f712fcc7c71a557e2db966c3e9fa91746039", + "b12932b0627d1c060942f5447764155655bd4da0c9afa6dd9b9ef53129af1b8fb0195996d2de9ca0df9d821ffee67026", + "9d0e1809716474cb086e834e310a4a1ced149e9c00f248527972cec5704c2a5b07b8b3dc38ecc4ebae97ddd87f3d8985"}}, + {"SHA512", SHA512_Init_nid_postfix, SHA512_Update_nid_postfix, SHA512_Final_nid_postfix, 216, 128, 64, { + "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e", + "ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f", + "204a8fc6dda82f0a0ced7beb8e08a41657c16ef468b228a8279be331a703c33596fd15c13b1b07f9aa1d3bea57789ca031ad85c7a71dd70354ec631238ca3445", + "8e959b75dae313da8cf4f72814fc143f8f7779c6eb9f7fa17299aeadb6889018501d289e4900f7e4331b99dec4b5433ac7d329eeb6dd26545e96e55b874be909", + "72ec1ef1124a45b047e8b7c75a932195135bb61de24ec0d1914042246e0aec3a2354e093d76f3048b456764346900cb130d2a4fd5dd16abb5e30bcb850dee843", + "e718483d0ce769644e2e42c7bc15b4638e1f98b13b2044285632a803afa973ebde0ff244877ea60a4cb0432ce577c31beb009c5c2c49aa2e4eadb217ad8cc09b"}}, + }; + + for (const auto& algorithm : algorithms) { + for (int i = 0; i < 6; ++i) { + if (!algorithm.expected[i]) continue; + const auto& message = messages[i]; + const std::size_t chunks[] {message.size() ? message.size() : 1, 1, algorithm.blockSize - 1, algorithm.blockSize, algorithm.blockSize + 1, 1000}; + for (auto chunk : chunks) { + if (i == 5 && chunk < 64) continue; + Require(Digest(algorithm, message, chunk) == algorithm.expected[i], algorithm.name); + } + } + + alignas(16) unsigned char context[256] {}; + alignas(16) unsigned char copy[256] {}; + unsigned char digest[64] {}; + unsigned char copyDigest[64] {}; + const bool wide = algorithm.blockSize == 128; + const auto num = [&](const unsigned char* c) { + std::uint32_t value; + std::memcpy(&value, c + (wide ? 208 : (algorithm.contextSize == 112 ? 104 : algorithm.contextSize - 4)), 4); + return value; + }; + const auto bitsLow = [&](const unsigned char* c) { + const std::size_t offset = wide ? 64 : (algorithm.contextSize == 96 ? 20 : algorithm.contextSize == 112 ? 32 : 16); + std::uint64_t value = 0; + std::memcpy(&value, c + offset, wide ? 8 : 4); + return value; + }; + Require(algorithm.init(context) == 1, "init"); + Require(algorithm.update(context, nullptr, 0) == 1, "empty update with null data"); + Require(algorithm.update(context, "ab", 2) == 1, "partial update"); + Require(num(context) == 2 && bitsLow(context) == 16, "context records the buffered bytes and the bit count"); + std::memcpy(copy, context, sizeof(copy)); + Require(algorithm.update(context, "c", 1) == 1 && algorithm.update(copy, "c", 1) == 1, "update of a copied context"); + Require(algorithm.final(digest, context) == 1 && algorithm.final(copyDigest, copy) == 1, "final of a copied context"); + Require(Hex(digest, algorithm.digestSize) == algorithm.expected[1] && !std::memcmp(digest, copyDigest, sizeof(digest)), "copied context digest"); + if (!wide) Require(num(context) == 0, "final clears the buffered count"); + RequireThrows([&] { algorithm.init(nullptr); }, "null context"); + RequireThrows([&] { algorithm.update(context, nullptr, 1); }, "null data"); + if (!wide) RequireThrows([&] { algorithm.final(nullptr, context); }, "null digest"); + } + + alignas(16) unsigned char context[256] {}; + unsigned char digest[64] {}; + Require(SHA512_Init_nid_postfix(context) == 1 && SHA512_Final_nid_postfix(nullptr, context) == 0, "SHA512_Final without a digest buffer"); + SHA256_Init_nid_postfix(context); + std::uint32_t length = 33; + std::memcpy(context + 108, &length, 4); + Require(SHA256_Final_nid_postfix(digest, context) == 0, "SHA256_Final with an oversized digest length"); + SHA256_Init_nid_postfix(context); + length = 16; + std::memcpy(context + 108, &length, 4); + std::memset(digest, 0xee, sizeof(digest)); + Require(SHA256_Final_nid_postfix(digest, context) == 1 && Hex(digest, 16) == "e3b0c44298fc1c149afbf4c8996fb924" && digest[16] == 0xee, "SHA256_Final truncates to the digest length"); + SHA512_Init_nid_postfix(context); + length = 32; + std::memcpy(context + 212, &length, 4); + RequireThrows([&] { SHA512_Final_nid_postfix(digest, context); }, "SHA-512/256 digest length"); + SHA256_Init_nid_postfix(context); + length = 64; + std::memcpy(context + 104, &length, 4); + RequireThrows([&] { SHA256_Update_nid_postfix(context, "a", 1); }, "corrupt buffered count"); + + const auto* abc = reinterpret_cast("abc"); + Require(SHA256_nid_postfix(abc, 3, digest) == digest && Hex(digest, 32) == "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad", "SHA256 one-shot"); + Require(SHA256_nid_postfix(nullptr, 0, digest) == digest && Hex(digest, 32) == "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "SHA256 one-shot of nothing"); + RequireThrows([&] { SHA256_nid_postfix(abc, 3, nullptr); }, "SHA256 one-shot without a digest buffer"); + std::puts("libSceLibreSsl digest tests passed"); +} diff --git a/core/libs/tests/GuestLocale.cpp b/core/libs/tests/GuestLocale.cpp index 71b71a1669..78aaee27be 100644 --- a/core/libs/tests/GuestLocale.cpp +++ b/core/libs/tests/GuestLocale.cpp @@ -34,6 +34,9 @@ void APS5_VABI _Locksyslock_nid_postfix(); void APS5_VABI _Unlocksyslock_nid_postfix(); std::uint32_t APS5_VABI _Towctrans_nid_postfix(std::uint32_t wc, std::uint64_t desc); int* APS5_VABI __error_nid_postfix(); +extern std::uint64_t _ZNSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix; +extern std::uint64_t _ZNSt8time_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix; +extern std::uintptr_t _ZTVSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE_nid_postfix[17]; } @@ -52,6 +55,16 @@ static void RequireException(TAction action) { Require(caught); } +static void CheckInputFacetObjects() { + Require(_ZNSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix == 0 && _ZNSt8time_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix == 0); + _ZNSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix = 7; + _ZNSt8time_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix = 9; + Require(_ZNSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix == 7 && _ZNSt8time_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix == 9); + _ZNSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix = 0; + _ZNSt8time_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix = 0; + for (const auto slot : _ZTVSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE_nid_postfix) Require(slot == 0); +} + static void CheckGuestCalls() { auto* locale = _ZSt21_sceLibcClassicLocale_nid_postfix; Require(locale != nullptr && locale == _ZNSt6locale16_GetgloballocaleEv_nid_postfix()); @@ -249,6 +262,7 @@ int main() { CheckStreamWidth(); CheckStandardStreams(); CheckWideCaseTranslation(); + CheckInputFacetObjects(); _Locksyslock_nid_postfix(); _Locksyslock_nid_postfix(); _Unlocksyslock_nid_postfix(); diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index d7a52ec814..04541e7c79 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -129,6 +129,7 @@ std::int64_t APS5_VABI lround_nid_postfix(double); std::div_t APS5_VABI div_nid_postfix(int, int); double APS5_VABI sinh_nid_postfix(double); double APS5_VABI cosh_nid_postfix(double); +double APS5_VABI _Cosh_nid_postfix(double, double); float APS5_VABI coshf_nid_postfix(float); float APS5_VABI sinhf_nid_postfix(float); float APS5_VABI _FSinh_nid_postfix(float, float); @@ -839,6 +840,14 @@ static void CheckExtendedMath() { static void CheckHyperbolic() { const double infinity = std::numeric_limits::infinity(); + Require(_Cosh_nid_postfix(0.0, 2.0) == 2.0); + Require(Near(_Cosh_nid_postfix(1.0, 2.0), 3.0861612696304874, 1e-15)); + Require(Near(_Cosh_nid_postfix(-1.0, -2.0), -3.0861612696304874, 1e-15)); + Require(std::isinf(_Cosh_nid_postfix(infinity, -1.0)) && std::signbit(_Cosh_nid_postfix(infinity, -1.0))); + Require(std::isnan(_Cosh_nid_postfix(infinity, 0.0))); + Require(Near(_Cosh_nid_postfix(711.0, 1e-300), 0x1.2190c08e2f1a4p+28, 1e-15)); + Require(_Cosh_nid_postfix(720.0, 0.0) == 0.0 && !std::signbit(_Cosh_nid_postfix(720.0, 0.0))); + Require(_Cosh_nid_postfix(720.0, -0.0) == 0.0 && std::signbit(_Cosh_nid_postfix(720.0, -0.0))); Require(sinh_nid_postfix(0.) == 0. && std::signbit(sinh_nid_postfix(-0.))); Require(cosh_nid_postfix(0.) == 1. && coshf_nid_postfix(0.f) == 1.f); Require(asinh_nid_postfix(0.) == 0. && std::signbit(asinh_nid_postfix(-0.)) && std::signbit(asinhf_nid_postfix(-0.f))); diff --git a/core/libs/tests/GuestMessageDestructorAbi.S b/core/libs/tests/GuestMessageDestructorAbi.S index 57ac11e827..7330b86885 100644 --- a/core/libs/tests/GuestMessageDestructorAbi.S +++ b/core/libs/tests/GuestMessageDestructorAbi.S @@ -141,7 +141,33 @@ ProbeDestroy: .cfi_def_cfa %rsp, 8 ret .cfi_endproc +.globl GuestCleanup +GuestCleanup: + .cfi_startproc + .cfi_personality 0x00, __gcc_personality_v0_nid_postfix + .cfi_lsda 0x00, .Lcleanup_lsda + PROLOGUE +.Lcleanup_call: + call FixtureHostThrow +.Lcleanup_call_end: + ud2 +.Lcleanup_landing: + incl FixtureGuardDestroyed(%rip) + movq %rax, %rdi + call _Unwind_Resume_nid_postfix + ud2 + .cfi_endproc + .section .rdata,"dr" +.Lcleanup_lsda: + .byte 0xff, 0xff, 0x01 + .uleb128 .Lcleanup_calls_end-.Lcleanup_calls +.Lcleanup_calls: + .uleb128 .Lcleanup_call-GuestCleanup + .uleb128 .Lcleanup_call_end-.Lcleanup_call + .uleb128 .Lcleanup_landing-GuestCleanup + .uleb128 0 +.Lcleanup_calls_end: .Linner_lsda: .byte 0xff, 0x00 .uleb128 .Linner_types_end-.Linner_types_offset diff --git a/core/libs/tests/GuestMessageDestructorAbi.cpp b/core/libs/tests/GuestMessageDestructorAbi.cpp index 68e398ef4d..8c55dbc490 100644 --- a/core/libs/tests/GuestMessageDestructorAbi.cpp +++ b/core/libs/tests/GuestMessageDestructorAbi.cpp @@ -32,6 +32,7 @@ void APS5_VABI _ZNSt11logic_errorD1Ev_nid_postfix(Object*); [[noreturn]] void APS5_VABI _ZNKSt11logic_error8_DoraiseEv_nid_postfix(const Object*); [[noreturn]] void APS5_VABI _ZSt13_Xregex_errorNSt15regex_constants10error_typeE_nid_postfix(std::regex_constants::error_type); int APS5_VABI GuestOuter(); +void APS5_VABI GuestCleanup(); void APS5_VABI GuestStdCapture(); void APS5_VABI ProbeDestroy(GuestDestroy, void*, void*); const char* APS5_VABI ProbeWhat(GuestWhat, const void*, const void*); @@ -202,9 +203,46 @@ extern "C" void APS5_VABI FixtureInspect(void* pointer) { if (!regularRelease) header->destructor = reinterpret_cast(ObserveDestroy); } +static unsigned hybridDestroyed; +struct HybridGuard { ~HybridGuard() { ++hybridDestroyed; } }; + +extern "C" [[gnu::noinline, noreturn]] void APS5_VABI FixtureHostThrow() { + HybridGuard guard; + __asm__ volatile("pxor %%xmm12, %%xmm12" ::: "xmm12"); + throw 73; +} + +[[gnu::noinline]] static void HybridMiddle() { + HybridGuard guard; + GuestCleanup(); +} + +static int TestHybridUnwind() { + const std::uint64_t expected[2] {0x123456789abcdef0, 0xfedcba9876543210}; + std::uint64_t restored[2] {}; + __asm__ volatile("movdqu %0, %%xmm12" :: "m"(expected) : "xmm12"); + unsigned caught = 0; + try { + HybridGuard guard; + try { HybridMiddle(); } + catch (int value) { + if (value != 73 || hybridDestroyed != 2 || FixtureGuardDestroyed != 1) return 63; + ++caught; + throw; + } + } catch (int value) { + if (value != 73) return 64; + ++caught; + } + __asm__ volatile("movdqu %%xmm12, %0" : "=m"(restored)); + return caught == 2 && hybridDestroyed == 3 && FixtureGuardDestroyed == 1 && + std::memcmp(restored, expected, sizeof(expected)) == 0 ? 0 : 65; +} + int main(int argc, char** argv) { SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX); if (argc != 2) return 2; + if (std::strcmp(argv[1], "hybrid") == 0) return TestHybridUnwind(); if (std::strncmp(argv[1], "vtable-", 7) == 0) return TestVtable(argv[1]); if (std::strcmp(argv[1], "control") == 0) { if (GuestOuter() != 0 || FixtureDestroyed != 1 || FixtureGuardDestroyed != 1 || diff --git a/core/libs/tests/GuestModuleInfo.cpp b/core/libs/tests/GuestModuleInfo.cpp index 6c70763d2a..d596bf0280 100644 --- a/core/libs/tests/GuestModuleInfo.cpp +++ b/core/libs/tests/GuestModuleInfo.cpp @@ -3,10 +3,12 @@ #include #include #include +#include #include #include #include #include +#include #include #include extern "C" { @@ -21,7 +23,12 @@ int APS5_VABI sceKernelGetModuleInfo2(KernelModule, ModuleInfo*); std::int32_t APS5_VABI sceKernelGetLibkernelTextLocation_nid_postfix(void**, std::size_t*); std::int32_t APS5_VABI sceKernelInternalMemoryGetModuleSegmentInfo_nid_postfix(InternalSegmentInfo*); } -static void Require(bool value) { if (!value) std::abort(); } +static void Require(bool value, const std::source_location& location = std::source_location::current()) { + if (!value) { + std::fprintf(stderr, "module info check failed at line %u\n", location.line()); + std::abort(); + } +} template static bool ThrowsInvalidArgument(TFunction function) { try { @@ -55,7 +62,11 @@ int main(int argc, char** argv) { executable = (segment.prot & 5) == 5; } Require(executable); +#if defined(_WIN32) && defined(__SEH__) + Require(info.eh_frame_addr == 0 && info.eh_frame_size == 0); +#else Require(info.eh_frame_addr != 0 && info.eh_frame_size != 0); +#endif #ifndef _WIN32 Require(info.eh_frame_hdr_addr != 0 && info.eh_frame_hdr_size != 0); #endif @@ -172,6 +183,8 @@ int main(int argc, char** argv) { Require(reloadedAdd != nullptr); const auto fresh = Query(reloadedAdd, 0); Require(fresh.eh_frame_size == info.eh_frame_size && fresh.eh_frame_hdr_size == info.eh_frame_hdr_size); - Require(fresh.eh_frame_addr - fresh.segments[0].address == info.eh_frame_addr - info.segments[0].address); + if (info.eh_frame_addr) + Require(fresh.eh_frame_addr - fresh.segments[0].address == info.eh_frame_addr - info.segments[0].address); + else Require(fresh.eh_frame_addr == 0); Require(dlclose_nid_postfix(reloaded) == 0); } diff --git a/core/libs/tests/GuestNetBufferValidation.cpp b/core/libs/tests/GuestNetBufferValidation.cpp new file mode 100644 index 0000000000..4b2c66402d --- /dev/null +++ b/core/libs/tests/GuestNetBufferValidation.cpp @@ -0,0 +1,81 @@ +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/Socket/include/SocketPoll.hpp" + +extern "C" { +int APS5_VABI sceNetInit_nid_postfix(); +int APS5_VABI sceNetSocket(const char*, int, int, int); +int APS5_VABI sceNetSocketClose(int); +int APS5_VABI sceNetSetsockopt(int, int, int, const void*, std::uint32_t); +int APS5_VABI sceNetGetsockopt(int, int, int, void*, std::uint32_t*); +int* APS5_VABI sceNetErrnoLoc(); +int APS5_VABI setsockopt_nid_postfix(int, int, int, const void*, std::uint32_t); +int* APS5_VABI __error_nid_postfix(); +} + +static void Require(bool condition, std::source_location location = std::source_location::current()) { + if (!condition) { + std::fprintf(stderr, "check failed at line %u\n", location.line()); + std::abort(); + } +} + +int main() { + Require(sceNetInit_nid_postfix() == 0); + for (const int family : {2, 28}) { + for (const int type : {1, 2}) { + const int socket = sceNetSocket("buffer-validation", family, type, type == 1 ? 6 : 17); + Require(socket >= 0); + std::shared_ptr owner; + std::uintptr_t native = 0; + Require(KernelFindSocketNative_nid_no_patch(socket, &owner, &native)); + for (const int option : {0x1001, 0x1002}) { + const auto nativeBuffer = [&] { + int value = 0; +#ifdef _WIN32 + int size = sizeof(value); +#else + socklen_t size = sizeof(value); +#endif + Require(::getsockopt(native, SOL_SOCKET, option == 0x1001 ? SO_SNDBUF : SO_RCVBUF, + reinterpret_cast(&value), &size) == 0); + return value; + }; + const int initial = 32768; + Require(sceNetSetsockopt(socket, 0xffff, option, &initial, sizeof(initial)) == 0); + const int originalNative = nativeBuffer(); + for (const int invalid : {0, -1, -4096, INT_MIN}) { + Require(sceNetSetsockopt(socket, 0xffff, option, &invalid, sizeof(invalid)) == static_cast(0x80410116u)); + Require(*sceNetErrnoLoc() == 22); + Require(setsockopt_nid_postfix(socket, 0xffff, option, &invalid, sizeof(invalid)) == -1); + Require(*__error_nid_postfix() == 22); + int value = 0; + std::uint32_t size = sizeof(value); + Require(sceNetGetsockopt(socket, 0xffff, option, &value, &size) == 0 && value == initial); + Require(nativeBuffer() == originalNative); + } + Require(sceNetSetsockopt(socket, 0xffff, option, nullptr, sizeof(initial)) == static_cast(0x80410116u)); + Require(sceNetSetsockopt(socket, 0xffff, option, &initial, 3) == static_cast(0x80410116u)); + const int valid = 8192; + Require(sceNetSetsockopt(socket, 0xffff, option, &valid, sizeof(valid)) == 0); + int value = 0; + std::uint32_t size = sizeof(value); + Require(sceNetGetsockopt(socket, 0xffff, option, &value, &size) == 0 && value == valid); + } + Require(sceNetSocketClose(socket) == 0); + } + } + const int zero = 0; + Require(sceNetSetsockopt(-1, 0xffff, 0x1001, &zero, sizeof(zero)) == static_cast(0x80410109u)); + Require(*sceNetErrnoLoc() == 9); +} diff --git a/core/libs/tests/GuestNetCtl.cpp b/core/libs/tests/GuestNetCtl.cpp index 1b7ce582b2..f45a66dc77 100644 --- a/core/libs/tests/GuestNetCtl.cpp +++ b/core/libs/tests/GuestNetCtl.cpp @@ -272,20 +272,17 @@ int main() { #ifdef __APPLE__ CheckDefaultRoutes(); #endif - constexpr int notInitialized = static_cast(0x80412101); - Require(sceNetCtlCheckCallback() == notInitialized); - Require(sceNetCtlGetState(nullptr) == notInitialized); - Require(sceNetCtlGetStateV6(nullptr) == notInitialized); - Require(sceNetCtlGetInfo(0, nullptr) == notInitialized); - Require(sceNetCtlGetResult(0, nullptr) == notInitialized); - Require(sceNetCtlGetNatInfo(nullptr) == notInitialized); - Require(sceNetCtlRegisterCallback(nullptr, nullptr, nullptr) == notInitialized); - Require(sceNetCtlRegisterCallbackV6(nullptr, nullptr, nullptr) == notInitialized); - Require(sceNetCtlUnregisterCallback(-1) == notInitialized); - Require(sceNetCtlUnregisterCallbackV6(-1) == notInitialized); - sceNetCtlTerm(); - Require(sceNetCtlCheckCallback() == notInitialized); - Require(sceNetCtlInit() == 0); + constexpr int invalidId = static_cast(0x80412105); + Require(sceNetCtlCheckCallback() == 0); + Require(sceNetCtlGetState(nullptr) == kErrInvalidAddr); + Require(sceNetCtlGetStateV6(nullptr) == kErrInvalidAddr); + Require(sceNetCtlGetInfo(0, nullptr) == kErrInvalidAddr); + Require(sceNetCtlGetResult(0, nullptr) == kErrInvalidAddr); + Require(sceNetCtlGetNatInfo(nullptr) == kErrInvalidAddr); + Require(sceNetCtlRegisterCallback(nullptr, nullptr, nullptr) == kErrInvalidAddr); + Require(sceNetCtlRegisterCallbackV6(nullptr, nullptr, nullptr) == kErrInvalidAddr); + Require(sceNetCtlUnregisterCallback(-1) == invalidId); + Require(sceNetCtlUnregisterCallbackV6(-1) == invalidId); CheckCallbacks(); Require(sceNetCtlGetStateV6(nullptr) == kErrInvalidAddr); diff --git a/core/libs/tests/GuestNpAuth.cpp b/core/libs/tests/GuestNpAuth.cpp index 95b11eddc5..695d37a67d 100644 --- a/core/libs/tests/GuestNpAuth.cpp +++ b/core/libs/tests/GuestNpAuth.cpp @@ -2,6 +2,7 @@ #include #include +#include extern "C" { int APS5_VABI sceNpAuthAbortRequest(int req_id); @@ -26,7 +27,43 @@ static void Require(bool condition) { } } +static void RequireUnstarted(int request) { + constexpr int invalidId = static_cast(0x80550307); + constexpr int sentinel = 0x13579bdf; + for (int attempt = 0; attempt != 3; ++attempt) { + int result = sentinel; + Require(sceNpAuthPollAsync(request, &result) == invalidId); + Require(result == sentinel); + Require(sceNpAuthWaitAsync(request, &result) == invalidId); + Require(result == sentinel); + } +} + +static void CheckUnstartedAborts() { + constexpr int aborted = static_cast(0x80550304); + alignas(8) char parameter[16]{}; + alignas(8) char output[16]{}; + for (const bool async : {false, true}) { + const int request = async ? sceNpAuthCreateAsyncRequest(nullptr) : sceNpAuthCreateRequest(); + Require(request > 0); + RequireUnstarted(request); + Require(sceNpAuthAbortRequest(request) == 0); + Require(sceNpAuthAbortRequest(request) == 0); + RequireUnstarted(request); + for (auto getter : {sceNpAuthGetIdToken, sceNpAuthGetIdTokenV3, sceNpAuthGetAuthorizationCode}) { + Require(getter(request, parameter, output) == aborted); + RequireUnstarted(request); + } + int issuer = 0x2468ace; + Require(sceNpAuthGetAuthorizationCodeV3(request, parameter, output, &issuer) == aborted); + Require(issuer == 0x2468ace); + RequireUnstarted(request); + Require(sceNpAuthDeleteRequest(request) == 0); + } +} + int main() { + CheckUnstartedAborts(); constexpr int invalidArgument = static_cast(0x80550301); constexpr int signedOut = static_cast(0x80550006); constexpr int aborted = static_cast(0x80550304); @@ -130,8 +167,8 @@ int main() { Require(sceNpAuthGetIdTokenV3(abortedReq, param, token) == aborted); Require(sceNpAuthGetAuthorizationCodeV3(abortedReq, param, authCode, &issuerId) == aborted); int abortedWait = 0; - Require(sceNpAuthWaitAsync(abortedReq, &abortedWait) == 0); - Require(abortedWait == aborted); + Require(sceNpAuthWaitAsync(abortedReq, &abortedWait) == invalidId); + Require(abortedWait == 0); const int syncReqA = sceNpAuthCreateRequest(); Require(sceNpAuthGetIdTokenA(syncReqA, nullptr, token) == invalidArgument); diff --git a/core/libs/tests/GuestNpCppWebApiIntrusivePtr.cpp b/core/libs/tests/GuestNpCppWebApiIntrusivePtr.cpp index 99f3f83152..f9a03f0d52 100644 --- a/core/libs/tests/GuestNpCppWebApiIntrusivePtr.cpp +++ b/core/libs/tests/GuestNpCppWebApiIntrusivePtr.cpp @@ -56,6 +56,29 @@ void APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14Sessi int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V126RequestPlayerSessionPlayerEEEE8pushBackERKS8_(IntrusivePtr*, const IntrusivePtr*); void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS2_6StringEEEEC1EPS6_PFvS8_EPNS2_10LibContextE(IntrusivePtr*, void*, Deleter, void*); void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS2_6StringEEEED1Ev(IntrusivePtr*); + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_15ProfanityFilter2V219WebApiFilterRequestEEC1Ev(IntrusivePtr*); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_18ResponseHeaderBaseEEC1Ev(IntrusivePtr*); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6StringEEC1Ev(IntrusivePtr*); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_15ProfanityFilter2V219WebApiFilterRequestEEC1ERS7_(IntrusivePtr*, const IntrusivePtr*); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_15ProfanityFilter2V219WebApiFilterRequestEED1Ev(IntrusivePtr*); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_18ResponseHeaderBaseEED1Ev(IntrusivePtr*); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6StringEED1Ev(IntrusivePtr*); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_11UserProfile2V113BasicPresenceEEEEEED1Ev(IntrusivePtr*); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V113BasicPresenceEEptEv(const IntrusivePtr*); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEptEv(const IntrusivePtr*); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEptEv(const IntrusivePtr*); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEptEv(const IntrusivePtr*); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEptEv(const IntrusivePtr*); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEptEv(const IntrusivePtr*); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_18ResponseHeaderBaseEE3getEv(const IntrusivePtr*); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_18ResponseHeaderBaseEEptEv(const IntrusivePtr*); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6StringEE3getEv(const IntrusivePtr*); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6StringEEptEv(const IntrusivePtr*); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_11UserProfile2V113BasicPresenceEEEEEEdeEv(const IntrusivePtr*); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_11UserProfile2V113BasicPresenceEEEEEEptEv(const IntrusivePtr*); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorImEEEdeEv(const IntrusivePtr*); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorImEEEptEv(const IntrusivePtr*); } static void Require(bool value) { if (!value) std::abort(); } @@ -279,6 +302,9 @@ int main() { _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V130GetBoardDefinitionResponseBodyEEC1Ev, _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V14UserEEC1Ev, _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V19RecordApi26RecordScoreResponseHeadersEEC1Ev, + _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_15ProfanityFilter2V219WebApiFilterRequestEEC1Ev, + _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_18ResponseHeaderBaseEEC1Ev, + _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6StringEEC1Ev, }; for (const auto construct : constructors) TestConstruct(construct); @@ -293,12 +319,27 @@ int main() { _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V19RecordApi26RecordScoreResponseHeadersEEptEv, _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6BinaryEEptEv, _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_12Leaderboards2V15EntryEEEEEEdeEv, + _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V113BasicPresenceEEptEv, + _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEptEv, + _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEptEv, + _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEptEv, + _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEptEv, + _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEptEv, + _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_18ResponseHeaderBaseEE3getEv, + _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_18ResponseHeaderBaseEEptEv, + _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6StringEE3getEv, + _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6StringEEptEv, + _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_11UserProfile2V113BasicPresenceEEEEEEdeEv, + _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_11UserProfile2V113BasicPresenceEEEEEEptEv, + _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorImEEEdeEv, + _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorImEEEptEv, }; for (const auto access : accessors) TestAccess(access); const Copy copies[] = { _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V121GetRankingRequestBodyEEC1ERS7_, _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEEC1ERS7_, + _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_15ProfanityFilter2V219WebApiFilterRequestEEC1ERS7_, }; for (const auto copy : copies) TestCopy(copy); @@ -311,6 +352,10 @@ int main() { {_ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V14UserEED1Ev, 0}, {_ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V19RecordApi26RecordScoreResponseHeadersEED1Ev, 0}, {_ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_12Leaderboards2V15EntryEEEEEED1Ev, VectorReferenceCountOffset}, + {_ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_15ProfanityFilter2V219WebApiFilterRequestEED1Ev, 0}, + {_ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_18ResponseHeaderBaseEED1Ev, 0}, + {_ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6StringEED1Ev, 0}, + {_ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_11UserProfile2V113BasicPresenceEEEEEED1Ev, VectorReferenceCountOffset}, }; for (const auto& testCase : destructors) TestDestroy(testCase); diff --git a/core/libs/tests/GuestNpCppWebApiSignedOut.cpp b/core/libs/tests/GuestNpCppWebApiSignedOut.cpp new file mode 100644 index 0000000000..455bc254b0 --- /dev/null +++ b/core/libs/tests/GuestNpCppWebApiSignedOut.cpp @@ -0,0 +1,189 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include + +extern "C" { +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(void*); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(void*); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEE21setOnFinishedCallbackEPFvliS8_PvEPFvliSC_ESC_(); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEE28setResponseInformationOptionEP37SceNpWebApi2ResponseInformationOption(); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE(); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv(); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEE18getResponseHeadersERSA_(); +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V110FriendsApi21ParameterToGetFriendsC1Ev(void*); +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V110FriendsApi21ParameterToGetFriendsD1Ev(void*); +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V110FriendsApi21ParameterToGetFriends10initializeEPNS1_6Common10LibContextEPKc(); +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V110FriendsApi21ParameterToGetFriends9terminateEv(); +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V110FriendsApi10getFriendsEiRKNS4_21ParameterToGetFriendsERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_18GetFriendsResponseEEENSA_INS8_18ResponseHeaderBaseEEEEE(); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(void*); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(void*); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEE21setOnFinishedCallbackEPFvliS8_PvEPFvliSC_ESC_(); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEE28setResponseInformationOptionEP37SceNpWebApi2ResponseInformationOption(); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE(); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv(); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEE18getResponseHeadersERSA_(); +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V19BlocksApi27ParameterToGetBlockingUsersC1Ev(void*); +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V19BlocksApi27ParameterToGetBlockingUsersD1Ev(void*); +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V19BlocksApi27ParameterToGetBlockingUsers10initializeEPNS1_6Common10LibContextE(); +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V19BlocksApi27ParameterToGetBlockingUsers9terminateEv(); +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V19BlocksApi16getBlockingUsersEiRKNS4_27ParameterToGetBlockingUsersERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_24GetBlockingUsersResponseEEENSA_INS8_18ResponseHeaderBaseEEEEE(); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(void*); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(void*); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE21setOnFinishedCallbackEPFvliS8_PvEPFvliSC_ESC_(); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE28setResponseInformationOptionEP37SceNpWebApi2ResponseInformationOption(); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE(); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv(); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE18getResponseHeadersERSA_(); +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V111PresenceApi28ParameterToGetBasicPresencesC1Ev(void*); +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V111PresenceApi28ParameterToGetBasicPresencesD1Ev(void*); +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V111PresenceApi28ParameterToGetBasicPresences10initializeEPNS1_6Common10LibContextEPKc(); +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V111PresenceApi28ParameterToGetBasicPresences9terminateEv(); +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V111PresenceApi17getBasicPresencesEiRKNS4_28ParameterToGetBasicPresencesERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_25GetBasicPresencesResponseEEENSA_INS8_18ResponseHeaderBaseEEEEE(); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(void*); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(void*); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEENS4_INS2_18ResponseHeaderBaseEEEE21setOnFinishedCallbackEPFvliS8_PvEPFvliSC_ESC_(); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEENS4_INS2_18ResponseHeaderBaseEEEE28setResponseInformationOptionEP37SceNpWebApi2ResponseInformationOption(); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE(); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv(); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEENS4_INS2_18ResponseHeaderBaseEEEE18getResponseHeadersERSA_(); +int APS5_VABI _ZN3sce2Np9CppWebApi15ProfanityFilter2V212ProfanityApi26ParameterToFilterProfanityC1Ev(void*); +int APS5_VABI _ZN3sce2Np9CppWebApi15ProfanityFilter2V212ProfanityApi26ParameterToFilterProfanityD1Ev(void*); +int APS5_VABI _ZN3sce2Np9CppWebApi15ProfanityFilter2V212ProfanityApi26ParameterToFilterProfanity10initializeEPNS1_6Common10LibContextEPKcSA_NS6_12IntrusivePtrINS3_19WebApiFilterRequestEEE(); +int APS5_VABI _ZN3sce2Np9CppWebApi15ProfanityFilter2V212ProfanityApi26ParameterToFilterProfanity9terminateEv(); +int APS5_VABI _ZN3sce2Np9CppWebApi15ProfanityFilter2V212ProfanityApi15filterProfanityEiRKNS4_26ParameterToFilterProfanityERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_23FilterProfanityResponseEEENSA_INS8_18ResponseHeaderBaseEEEEE(); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(void*); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(void*); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEE21setOnFinishedCallbackEPFvliS8_PvEPFvliSC_ESC_(); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEE28setResponseInformationOptionEP37SceNpWebApi2ResponseInformationOption(); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE(); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv(); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEE18getResponseHeadersERSA_(); +int APS5_VABI _ZN3sce2Np9CppWebApi30CommunicationRestrictionStatus2V333CommunicationRestrictionStatusApi44ParameterToGetCommunicationRestrictionStatusC1Ev(void*); +int APS5_VABI _ZN3sce2Np9CppWebApi30CommunicationRestrictionStatus2V333CommunicationRestrictionStatusApi44ParameterToGetCommunicationRestrictionStatusD1Ev(void*); +int APS5_VABI _ZN3sce2Np9CppWebApi30CommunicationRestrictionStatus2V333CommunicationRestrictionStatusApi44ParameterToGetCommunicationRestrictionStatus10initializeEPNS1_6Common10LibContextEm(); +int APS5_VABI _ZN3sce2Np9CppWebApi30CommunicationRestrictionStatus2V333CommunicationRestrictionStatusApi44ParameterToGetCommunicationRestrictionStatus9terminateEv(); +int APS5_VABI _ZN3sce2Np9CppWebApi30CommunicationRestrictionStatus2V333CommunicationRestrictionStatusApi33getCommunicationRestrictionStatusEiRKNS4_44ParameterToGetCommunicationRestrictionStatusERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_38CommunicationRestrictionStatusResponseEEENSA_INS8_18ResponseHeaderBaseEEEEE(); +int APS5_VABI _ZN3sce2Np9CppWebApi15ProfanityFilter2V226WebApiFilterRequestFactory6createEPNS1_6Common10LibContextEPKcPNS5_12IntrusivePtrINS3_19WebApiFilterRequestEEE(); +} + +static void Require(bool value) { if (!value) std::abort(); } + +static constexpr int NotSignedIn = static_cast(0x80552907); + +using Object = int (APS5_VABI*)(void*); +using Call = int (APS5_VABI*)(); + +struct RequestFlow { + Object transactionConstruct; + Object transactionDestroy; + Call setOnFinishedCallback; + Call setResponseInformationOption; + Call start; + Call finish; + Call getResponseHeaders; + Object parameterConstruct; + Object parameterDestroy; + Call initialize; + Call terminate; + Call call; +}; + +static const RequestFlow flows[] = { + { + _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev, + _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev, + _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEE21setOnFinishedCallbackEPFvliS8_PvEPFvliSC_ESC_, + _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEE28setResponseInformationOptionEP37SceNpWebApi2ResponseInformationOption, + _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE, + _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv, + _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEE18getResponseHeadersERSA_, + _ZN3sce2Np9CppWebApi11UserProfile2V110FriendsApi21ParameterToGetFriendsC1Ev, + _ZN3sce2Np9CppWebApi11UserProfile2V110FriendsApi21ParameterToGetFriendsD1Ev, + _ZN3sce2Np9CppWebApi11UserProfile2V110FriendsApi21ParameterToGetFriends10initializeEPNS1_6Common10LibContextEPKc, + _ZN3sce2Np9CppWebApi11UserProfile2V110FriendsApi21ParameterToGetFriends9terminateEv, + _ZN3sce2Np9CppWebApi11UserProfile2V110FriendsApi10getFriendsEiRKNS4_21ParameterToGetFriendsERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_18GetFriendsResponseEEENSA_INS8_18ResponseHeaderBaseEEEEE, + }, + { + _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev, + _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev, + _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEE21setOnFinishedCallbackEPFvliS8_PvEPFvliSC_ESC_, + _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEE28setResponseInformationOptionEP37SceNpWebApi2ResponseInformationOption, + _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE, + _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv, + _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEE18getResponseHeadersERSA_, + _ZN3sce2Np9CppWebApi11UserProfile2V19BlocksApi27ParameterToGetBlockingUsersC1Ev, + _ZN3sce2Np9CppWebApi11UserProfile2V19BlocksApi27ParameterToGetBlockingUsersD1Ev, + _ZN3sce2Np9CppWebApi11UserProfile2V19BlocksApi27ParameterToGetBlockingUsers10initializeEPNS1_6Common10LibContextE, + _ZN3sce2Np9CppWebApi11UserProfile2V19BlocksApi27ParameterToGetBlockingUsers9terminateEv, + _ZN3sce2Np9CppWebApi11UserProfile2V19BlocksApi16getBlockingUsersEiRKNS4_27ParameterToGetBlockingUsersERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_24GetBlockingUsersResponseEEENSA_INS8_18ResponseHeaderBaseEEEEE, + }, + { + _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev, + _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev, + _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE21setOnFinishedCallbackEPFvliS8_PvEPFvliSC_ESC_, + _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE28setResponseInformationOptionEP37SceNpWebApi2ResponseInformationOption, + _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE, + _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv, + _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE18getResponseHeadersERSA_, + _ZN3sce2Np9CppWebApi11UserProfile2V111PresenceApi28ParameterToGetBasicPresencesC1Ev, + _ZN3sce2Np9CppWebApi11UserProfile2V111PresenceApi28ParameterToGetBasicPresencesD1Ev, + _ZN3sce2Np9CppWebApi11UserProfile2V111PresenceApi28ParameterToGetBasicPresences10initializeEPNS1_6Common10LibContextEPKc, + _ZN3sce2Np9CppWebApi11UserProfile2V111PresenceApi28ParameterToGetBasicPresences9terminateEv, + _ZN3sce2Np9CppWebApi11UserProfile2V111PresenceApi17getBasicPresencesEiRKNS4_28ParameterToGetBasicPresencesERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_25GetBasicPresencesResponseEEENSA_INS8_18ResponseHeaderBaseEEEEE, + }, + { + _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev, + _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev, + _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEENS4_INS2_18ResponseHeaderBaseEEEE21setOnFinishedCallbackEPFvliS8_PvEPFvliSC_ESC_, + _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEENS4_INS2_18ResponseHeaderBaseEEEE28setResponseInformationOptionEP37SceNpWebApi2ResponseInformationOption, + _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE, + _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv, + _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEENS4_INS2_18ResponseHeaderBaseEEEE18getResponseHeadersERSA_, + _ZN3sce2Np9CppWebApi15ProfanityFilter2V212ProfanityApi26ParameterToFilterProfanityC1Ev, + _ZN3sce2Np9CppWebApi15ProfanityFilter2V212ProfanityApi26ParameterToFilterProfanityD1Ev, + _ZN3sce2Np9CppWebApi15ProfanityFilter2V212ProfanityApi26ParameterToFilterProfanity10initializeEPNS1_6Common10LibContextEPKcSA_NS6_12IntrusivePtrINS3_19WebApiFilterRequestEEE, + _ZN3sce2Np9CppWebApi15ProfanityFilter2V212ProfanityApi26ParameterToFilterProfanity9terminateEv, + _ZN3sce2Np9CppWebApi15ProfanityFilter2V212ProfanityApi15filterProfanityEiRKNS4_26ParameterToFilterProfanityERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_23FilterProfanityResponseEEENSA_INS8_18ResponseHeaderBaseEEEEE, + }, + { + _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev, + _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev, + _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEE21setOnFinishedCallbackEPFvliS8_PvEPFvliSC_ESC_, + _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEE28setResponseInformationOptionEP37SceNpWebApi2ResponseInformationOption, + _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE, + _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv, + _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEE18getResponseHeadersERSA_, + _ZN3sce2Np9CppWebApi30CommunicationRestrictionStatus2V333CommunicationRestrictionStatusApi44ParameterToGetCommunicationRestrictionStatusC1Ev, + _ZN3sce2Np9CppWebApi30CommunicationRestrictionStatus2V333CommunicationRestrictionStatusApi44ParameterToGetCommunicationRestrictionStatusD1Ev, + _ZN3sce2Np9CppWebApi30CommunicationRestrictionStatus2V333CommunicationRestrictionStatusApi44ParameterToGetCommunicationRestrictionStatus10initializeEPNS1_6Common10LibContextEm, + _ZN3sce2Np9CppWebApi30CommunicationRestrictionStatus2V333CommunicationRestrictionStatusApi44ParameterToGetCommunicationRestrictionStatus9terminateEv, + _ZN3sce2Np9CppWebApi30CommunicationRestrictionStatus2V333CommunicationRestrictionStatusApi33getCommunicationRestrictionStatusEiRKNS4_44ParameterToGetCommunicationRestrictionStatusERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_38CommunicationRestrictionStatusResponseEEENSA_INS8_18ResponseHeaderBaseEEEEE, + }, +}; + +static void TestRequestFlow(const RequestFlow& flow) { + alignas(16) unsigned char transaction[0x100]; + alignas(16) unsigned char parameter[0x100]; + std::memset(transaction, 0x5a, sizeof(transaction)); + std::memset(parameter, 0x5a, sizeof(parameter)); + + Require(flow.transactionConstruct(transaction) == 0); + Require(flow.setOnFinishedCallback() == 0); + Require(flow.setResponseInformationOption() == 0); + Require(flow.parameterConstruct(parameter) == 0); + Require(flow.initialize() == 0); + Require(flow.call() == NotSignedIn); + Require(flow.start() == NotSignedIn); + Require(flow.getResponseHeaders() == NotSignedIn); + Require(flow.finish() == 0); + Require(flow.terminate() == 0); + Require(flow.parameterDestroy(parameter) == 0); + Require(flow.transactionDestroy(transaction) == 0); +} + +int main() { + for (const auto& flow : flows) TestRequestFlow(flow); + Require(_ZN3sce2Np9CppWebApi15ProfanityFilter2V226WebApiFilterRequestFactory6createEPNS1_6Common10LibContextEPKcPNS5_12IntrusivePtrINS3_19WebApiFilterRequestEEE() == NotSignedIn); + return 0; +} diff --git a/core/libs/tests/GuestNpManager.cpp b/core/libs/tests/GuestNpManager.cpp index 75393b1321..67e63ff3b0 100644 --- a/core/libs/tests/GuestNpManager.cpp +++ b/core/libs/tests/GuestNpManager.cpp @@ -25,6 +25,8 @@ int APS5_VABI sceNpGetAccountId(const NpOnlineId* online_id, std::uint64_t* acco int APS5_VABI sceNpSetGamePresenceOnline(int req_id, const NpOnlineId* online_id, std::uint32_t presence); int APS5_VABI sceNpRegisterPlusEventCallback(void* callback, void* userdata); int APS5_VABI sceNpUnregisterPlusEventCallback(void); +int APS5_VABI sceNpIsPlusMember(int user_id, bool* is_member); +int APS5_VABI sceNpSetContentRestriction(const NpContentRestriction* restriction); } extern "C" { @@ -37,6 +39,7 @@ int APS5_VABI sceNpSetTimeout(int reqId, std::int32_t resolveRetry, std::uint32_ extern "C" { int APS5_VABI sceNpCheckCallback(void); +int APS5_VABI sceNpCheckCallbackForLib(void); int APS5_VABI sceNpRegisterStateCallback(void* callback, void* userdata); int APS5_VABI sceNpUnregisterStateCallback(void); int APS5_VABI sceNpRegisterStateCallbackA(void* callback, void* userdata); @@ -46,10 +49,20 @@ void APS5_VABI sceNpRegisterGamePresenceCallback(void* callback, void* userdata) int APS5_VABI sceNpUnregisterNpReachabilityStateCallback(void); } +extern "C" { +int APS5_VABI sceNpCheckNpAvailabilityA(int reqId, int userId); +int APS5_VABI sceNpGetAccountDateOfBirthA(int userId, void* dateOfBirth); +int APS5_VABI sceNpGetAccountLanguageA(int reqId, int userId, void* language); +int APS5_VABI sceNpGetGamePresenceStatus(const NpOnlineId* onlineId, std::uint32_t* status); +int APS5_VABI sceNpGetGamePresenceStatusA(int userId, std::uint32_t* status); +int APS5_VABI sceNpGetParentalControlInfoA(int reqId, int userId, std::int8_t* age, void* info); +} + namespace { constexpr int InvalidArgument = static_cast(0x80550003u); constexpr int SignedOut = static_cast(0x80550006u); +constexpr int InvalidSize = static_cast(0x80550011u); constexpr int UserNotFound = static_cast(0x80550007u); constexpr int AlreadyRegistered = static_cast(0x80550008u); constexpr int NotRegistered = static_cast(0x80550009u); @@ -157,10 +170,14 @@ void CheckStateCallbacksA() { "unregistering one id removed another callback"); } Require(sceNpRegisterStateCallbackA(callbacks[3], &userdata[3]) == ids[3], "registering again takes the freed slot"); - Require(sceNpCheckCallback() == 0, "sceNpCheckCallback failed after registering again"); + Require(sceNpCheckCallbackForLib() == 0, "sceNpCheckCallbackForLib failed after registering again"); for (int index = 0; index < 8; ++index) { Require(stateCallsA[index].calls == (index == 3 ? 2 : 1), "only the registration made again gets SIGNED_OUT"); } + Require(sceNpCheckCallbackForLib() == 0 && sceNpCheckCallback() == 0, "checking again failed"); + for (int index = 0; index < 8; ++index) { + Require(stateCallsA[index].calls == (index == 3 ? 2 : 1), "sceNpCheckCallbackForLib left the state pending"); + } for (int index = 0; index < 8; ++index) { Require(sceNpUnregisterStateCallbackA(ids[index]) == 0, "unregistering a state callback"); } @@ -273,13 +290,111 @@ void CheckOnlineIdQueries() { } } +void CheckUserIdQueries() { + constexpr int InvalidUserId = -1; + constexpr int OtherUserId = 0x10000; + constexpr int NeverCreated = 0x7fffffff; + unsigned char output[64]; + std::memset(output, 0x5a, sizeof(output)); + unsigned char untouched[64]; + std::memset(untouched, 0x5a, sizeof(untouched)); + std::int8_t age = 0x5a; + const int request = sceNpCreateRequest(); + Require(request > 0, "sceNpCreateRequest must return a positive request id"); + for (const int userId : {InitialUserId, OtherUserId}) { + Require(sceNpCheckNpAvailabilityA(request, userId) == SignedOut, "sceNpCheckNpAvailabilityA must report the user as signed out"); + Require(sceNpGetAccountLanguageA(request, userId, output) == SignedOut, "sceNpGetAccountLanguageA must report the user as signed out"); + Require(sceNpGetParentalControlInfoA(request, userId, &age, output) == SignedOut, "sceNpGetParentalControlInfoA must report the user as signed out"); + Require(sceNpGetAccountDateOfBirthA(userId, output) == SignedOut, "sceNpGetAccountDateOfBirthA must report the user as signed out"); + } + Require(sceNpCheckNpAvailabilityA(request, InvalidUserId) == InvalidArgument, "sceNpCheckNpAvailabilityA must reject the invalid user id"); + Require(sceNpCheckNpAvailabilityA(0, InitialUserId) == InvalidArgument, "sceNpCheckNpAvailabilityA must reject request id 0"); + Require(sceNpCheckNpAvailabilityA(NeverCreated, InitialUserId) == RequestNotFound, "sceNpCheckNpAvailabilityA must reject a request that was never created"); + Require(sceNpCheckNpAvailabilityA(NeverCreated, InvalidUserId) == InvalidArgument, "sceNpCheckNpAvailabilityA must check the user before the request"); + Require(sceNpGetAccountLanguageA(request, InitialUserId, nullptr) == InvalidArgument, "sceNpGetAccountLanguageA must reject a null output"); + Require(sceNpGetAccountLanguageA(request, InvalidUserId, output) == InvalidArgument, "sceNpGetAccountLanguageA must reject the invalid user id"); + Require(sceNpGetAccountLanguageA(0, InitialUserId, output) == InvalidArgument, "sceNpGetAccountLanguageA must reject request id 0"); + Require(sceNpGetAccountLanguageA(NeverCreated, InitialUserId, output) == RequestNotFound, "sceNpGetAccountLanguageA must reject a request that was never created"); + Require(sceNpGetAccountLanguageA(NeverCreated, InitialUserId, nullptr) == InvalidArgument, "sceNpGetAccountLanguageA must check the output before the request"); + Require(sceNpGetParentalControlInfoA(request, InitialUserId, nullptr, output) == InvalidArgument, "sceNpGetParentalControlInfoA must reject a null age"); + Require(sceNpGetParentalControlInfoA(request, InitialUserId, &age, nullptr) == InvalidArgument, "sceNpGetParentalControlInfoA must reject a null info"); + Require(sceNpGetParentalControlInfoA(request, InvalidUserId, &age, output) == InvalidArgument, "sceNpGetParentalControlInfoA must reject the invalid user id"); + Require(sceNpGetParentalControlInfoA(0, InitialUserId, &age, output) == InvalidArgument, "sceNpGetParentalControlInfoA must reject request id 0"); + Require(sceNpGetParentalControlInfoA(NeverCreated, InitialUserId, &age, output) == RequestNotFound, "sceNpGetParentalControlInfoA must reject a request that was never created"); + Require(sceNpGetParentalControlInfoA(NeverCreated, InvalidUserId, &age, output) == InvalidArgument, "sceNpGetParentalControlInfoA must check the user before the request"); + Require(sceNpGetAccountDateOfBirthA(InitialUserId, nullptr) == InvalidArgument, "sceNpGetAccountDateOfBirthA must reject a null output"); + Require(sceNpGetAccountDateOfBirthA(InvalidUserId, output) == InvalidArgument, "sceNpGetAccountDateOfBirthA must reject the invalid user id"); + Require(sceNpDeleteRequest(request) == 0, "sceNpDeleteRequest failed"); + Require(sceNpCheckNpAvailabilityA(request, InitialUserId) == RequestNotFound, "sceNpCheckNpAvailabilityA must reject a deleted request"); + Require(sceNpGetAccountLanguageA(request, InitialUserId, output) == RequestNotFound, "sceNpGetAccountLanguageA must reject a deleted request"); + Require(sceNpGetParentalControlInfoA(request, InitialUserId, &age, output) == RequestNotFound, "sceNpGetParentalControlInfoA must reject a deleted request"); + Require(std::memcmp(output, untouched, sizeof(output)) == 0 && age == 0x5a, "a signed-out account query wrote its output"); + constexpr std::uint32_t Sentinel = 0x5a5a5a5au; + constexpr std::uint32_t Offline = 0; + NpOnlineId onlineId{}; + std::memcpy(onlineId.data, "player", 6); + for (const int userId : {InitialUserId, OtherUserId}) { + std::uint32_t status = Sentinel; + Require(sceNpGetGamePresenceStatusA(userId, &status) == 0 && status == Offline, "sceNpGetGamePresenceStatusA must report the user as offline"); + } + std::uint32_t status = Sentinel; + Require(sceNpGetGamePresenceStatusA(InvalidUserId, &status) == InvalidArgument, "sceNpGetGamePresenceStatusA must reject the invalid user id"); + Require(sceNpGetGamePresenceStatusA(InitialUserId, nullptr) == InvalidArgument, "sceNpGetGamePresenceStatusA must reject a null status"); + Require(sceNpGetGamePresenceStatus(nullptr, &status) == InvalidArgument, "sceNpGetGamePresenceStatus must reject a null online id"); + Require(sceNpGetGamePresenceStatus(&onlineId, nullptr) == InvalidArgument, "sceNpGetGamePresenceStatus must reject a null status"); + Require(status == Sentinel, "a rejected presence query wrote its status"); + Require(sceNpGetGamePresenceStatus(&onlineId, &status) == 0 && status == Offline, "sceNpGetGamePresenceStatus must report the online id as offline"); +} + +} + +struct ContentRestrictionFields { + std::uint64_t size; + std::int8_t defaultAgeRestriction; + std::uint8_t padding[3]; + std::int32_t ageRestrictionCount; + const void* ageRestrictions; +}; + +int SetContentRestriction(const ContentRestrictionFields& fields) { + return sceNpSetContentRestriction(reinterpret_cast(&fields)); +} + +void CheckPlusAndContentRestriction() { + bool isMember = true; + Require(sceNpIsPlusMember(0x10000, &isMember) == 0 && !isMember, "sceNpIsPlusMember must report no Plus membership"); + Require(sceNpIsPlusMember(0x10000, nullptr) == InvalidArgument, "sceNpIsPlusMember must reject a null output"); + + const std::uint8_t ages[8]{}; + ContentRestrictionFields fields{sizeof(ContentRestrictionFields), 12, {}, 2, ages}; + Require(SetContentRestriction(fields) == 0, "sceNpSetContentRestriction must accept a valid restriction"); + fields.ageRestrictionCount = 0; + fields.ageRestrictions = nullptr; + Require(SetContentRestriction(fields) == 0, "sceNpSetContentRestriction must accept no age restrictions"); + Require(sceNpSetContentRestriction(nullptr) == InvalidArgument, "sceNpSetContentRestriction must reject null"); + auto bad = fields; + bad.size = sizeof(ContentRestrictionFields) + 8; + Require(SetContentRestriction(bad) == InvalidSize, "sceNpSetContentRestriction must check the size"); + bad = fields; + bad.defaultAgeRestriction = -1; + Require(SetContentRestriction(bad) == InvalidArgument, "sceNpSetContentRestriction must reject a negative default age"); + bad = fields; + bad.ageRestrictionCount = 0x101; + bad.ageRestrictions = ages; + Require(SetContentRestriction(bad) == InvalidArgument, "sceNpSetContentRestriction must reject more than 256 restrictions"); + bad.ageRestrictionCount = 0x100; + Require(SetContentRestriction(bad) == 0, "sceNpSetContentRestriction must accept 256 restrictions"); + bad.ageRestrictions = nullptr; + Require(SetContentRestriction(bad) == InvalidArgument, "sceNpSetContentRestriction must reject restrictions without an array"); } int main() { CheckOnlineIdQueries(); + CheckUserIdQueries(); CheckStateCallbacksA(); CheckLegacyStateCallback(); CheckRequestTimeouts(); + CheckPlusAndContentRestriction(); NpId npId{}; std::memset(&npId, 0x5a, sizeof(npId)); NpId untouched{}; diff --git a/core/libs/tests/GuestNpTrophy.cpp b/core/libs/tests/GuestNpTrophy.cpp new file mode 100644 index 0000000000..e8a9b8749f --- /dev/null +++ b/core/libs/tests/GuestNpTrophy.cpp @@ -0,0 +1,119 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceNpTrophyCreateContext(int* context, int userId, std::uint32_t serviceLabel, std::uint64_t options); +int APS5_VABI sceNpTrophyDestroyContext(int context); +int APS5_VABI sceNpTrophyCreateHandle(int* handle); +int APS5_VABI sceNpTrophyDestroyHandle(int handle); +int APS5_VABI sceNpTrophyRegisterContext(int context, int handle, std::uint64_t options); +int APS5_VABI sceNpTrophyGetTrophyUnlockState(int context, int handle, std::uint32_t* flags, std::uint32_t* count); +int APS5_VABI sceNpTrophyUnlockTrophy(int context, int handle, int trophyId, int* platinumId); +int APS5_VABI sceNpTrophyGetGameIcon(int context, int handle, void* buffer, std::uint64_t* size); +int APS5_VABI sceNpTrophyGetTrophyIcon(int context, int handle, int trophyId, void* buffer, std::uint64_t* size); +} + +namespace { + +constexpr int InvalidArgument = static_cast(0x80551604u); +constexpr int InsufficientBuffer = static_cast(0x80551605u); +constexpr int InvalidContext = static_cast(0x80551609u); +constexpr int InvalidTrophyId = static_cast(0x8055160au); +constexpr int AlreadyUnlocked = static_cast(0x8055160cu); +constexpr int NotRegistered = static_cast(0x8055160fu); +constexpr int AlreadyRegistered = static_cast(0x80551610u); +constexpr int ContextAlreadyExists = static_cast(0x80551613u); +constexpr int TitleConfNotInstalled = static_cast(0x80551626u); +constexpr int TrophyNotUnlocked = static_cast(0x8055161au); + +void Require(bool condition, const char* message) { + if (!condition) { + std::fprintf(stderr, "NpTrophy: %s\n", message); + std::abort(); + } +} + +void PutBigEndian(std::vector& bytes, std::size_t at, std::uint64_t value, int width) { + for (int i = 0; i < width; ++i) bytes[at + i] = static_cast(value >> (8 * (width - 1 - i))); +} + +void WritePackage(const std::filesystem::path& path, const std::vector>& files) { + const std::size_t header = 0x60, entry = 0x40; + std::vector bytes(header + entry * files.size()); + PutBigEndian(bytes, 0, 0xdca24d00u, 4); + PutBigEndian(bytes, 0x10, files.size(), 4); + PutBigEndian(bytes, 0x14, entry, 4); + for (std::size_t i = 0; i < files.size(); ++i) { + const auto at = header + entry * i; + std::memcpy(bytes.data() + at, files[i].first.data(), files[i].first.size()); + PutBigEndian(bytes, at + 32, bytes.size(), 8); + PutBigEndian(bytes, at + 40, files[i].second.size(), 8); + bytes.insert(bytes.end(), files[i].second.begin(), files[i].second.end()); + } + std::filesystem::create_directories(path.parent_path()); + std::ofstream(path, std::ios::binary).write(reinterpret_cast(bytes.data()), static_cast(bytes.size())); +} + +} + +int main() { + const auto root = std::filesystem::temp_directory_path() / ("anyps5-np-trophy-" + std::to_string(std::random_device{}())); + std::filesystem::remove_all(root); + WritePackage(root / "app0/sce_sys/trophy/trophy00.trp", {{"TROPCONF.ESFM", "conf"}, {"ICON0.PNG", "game-icon"}, {"TROP000.PNG", "first"}, {"TROP001.PNG", "second"}}); + std::filesystem::current_path(root); + + int context = 0, handle = 0, missing = 0, platinum = 0; + Require(sceNpTrophyCreateContext(nullptr, 1, 0, 0) == InvalidArgument, "null context"); + Require(sceNpTrophyCreateContext(&context, 1, 0, 1) == InvalidArgument, "context options"); + Require(sceNpTrophyCreateContext(&context, 1, 0, 0) == 0, "create the context"); + Require(sceNpTrophyCreateContext(&missing, 1, 0, 0) == ContextAlreadyExists, "second context for the same user and label"); + Require(sceNpTrophyCreateHandle(&handle) == 0, "create the handle"); + + std::uint32_t flags[4] = {}, count = 0; + Require(sceNpTrophyGetTrophyUnlockState(context, handle, flags, &count) == NotRegistered, "state before registration"); + Require(sceNpTrophyRegisterContext(context, handle, 0) == 0, "register the context"); + Require(sceNpTrophyRegisterContext(context, handle, 0) == AlreadyRegistered, "register twice"); + Require(sceNpTrophyGetTrophyUnlockState(context, handle, flags, &count) == 0 && count == 2 && flags[0] == 0, "two trophies, none unlocked"); + + std::uint64_t size = 0; + Require(sceNpTrophyGetGameIcon(context, handle, nullptr, &size) == 0 && size == 9, "game icon size"); + char icon[16] = {}; + size = 4; + Require(sceNpTrophyGetGameIcon(context, handle, icon, &size) == InsufficientBuffer, "game icon into a short buffer"); + size = sizeof(icon); + Require(sceNpTrophyGetGameIcon(context, handle, icon, &size) == 0 && size == 9 && std::memcmp(icon, "game-icon", 9) == 0, "game icon bytes"); + + size = sizeof(icon); + Require(sceNpTrophyGetTrophyIcon(context, handle, 1, icon, &size) == TrophyNotUnlocked, "icon of a locked trophy"); + Require(sceNpTrophyUnlockTrophy(context, handle, 2, &platinum) == InvalidTrophyId, "unlock a trophy past the count"); + Require(sceNpTrophyUnlockTrophy(context, handle, 1, &platinum) == 0 && platinum == -1, "unlock trophy 1"); + Require(sceNpTrophyUnlockTrophy(context, handle, 1, &platinum) == AlreadyUnlocked, "unlock trophy 1 twice"); + Require(sceNpTrophyGetTrophyUnlockState(context, handle, flags, &count) == 0 && flags[0] == 2u, "trophy 1 unlocked"); + size = sizeof(icon); + Require(sceNpTrophyGetTrophyIcon(context, handle, 1, icon, &size) == 0 && size == 6 && std::memcmp(icon, "second", 6) == 0, "icon of the unlocked trophy"); + int saved = -1; + { + std::ifstream state(root / "_trophy/trophy00.1"); + state >> saved; + } + Require(saved == 1, "unlock state saved for the user"); + + Require(sceNpTrophyCreateContext(&missing, 1, 1, 0) == 0, "context for a label without a package"); + Require(sceNpTrophyRegisterContext(missing, handle, 0) == TitleConfNotInstalled, "register without a trophy package"); + Require(sceNpTrophyDestroyContext(missing) == 0, "destroy the second context"); + Require(sceNpTrophyDestroyHandle(handle) == 0 && sceNpTrophyDestroyContext(context) == 0, "destroy the handle and context"); + Require(sceNpTrophyDestroyContext(context) == InvalidContext, "destroy the context twice"); + + std::filesystem::current_path(root.parent_path()); + std::error_code error; + std::filesystem::remove_all(root, error); + return 0; +} diff --git a/core/libs/tests/GuestNpTrophyData.cpp b/core/libs/tests/GuestNpTrophyData.cpp index 5c4eaf025a..8bf4e7ad00 100644 --- a/core/libs/tests/GuestNpTrophyData.cpp +++ b/core/libs/tests/GuestNpTrophyData.cpp @@ -2,6 +2,7 @@ #include "prx/libSceNpTrophy2/include/NpTrophy2Types.hpp" #include "prx/libSceNpTrophy2/include/TrophyStore.hpp" +#include #include #include #include @@ -36,6 +37,7 @@ int APS5_VABI sceNpTrophy2GetTrophyInfoArray(int, int, uint32_t, uint32_t, NpTro } static constexpr int InitialUser = 0x10000000; +static constexpr int InvalidTrophyId = static_cast(0x8055390Au); static const char TropConf[] = R"({"defaultLanguage":"en-US","trophies":[ {"id":"0","grade":"P"}, @@ -168,8 +170,18 @@ static void ReportTrophyData(int context) { Require(details.trophy_grade == 1 && std::strcmp(details.name, "All of them") == 0, "the platinum is not reported"); const auto steps = Trophy(context, 5, &details); Require(details.target.type == 1 && details.target.value == 5 && !steps.unlocked && steps.progress.type == 1 && steps.progress.value == 0, "the progressive trophy does not report its target"); - NpTrophy2Data unknown{}; - Require(ThrowsInvalidArgument([context, &unknown] { sceNpTrophy2GetTrophyInfo(context, 1, 99, nullptr, &unknown); }), "a trophy that is not in the package was reported"); + const auto untouched = [](const void* bytes, std::size_t size) { + const auto* first = static_cast(bytes); + return std::all_of(first, first + size, [](unsigned char byte) { return byte == 0x5a; }); + }; + for (const int unknownId : {8, 99, -1}) { + NpTrophy2Details unknownDetails{}; + NpTrophy2Data unknown{}; + std::memset(&unknownDetails, 0x5a, sizeof(unknownDetails)); + std::memset(&unknown, 0x5a, sizeof(unknown)); + Require(sceNpTrophy2GetTrophyInfo(context, 1, unknownId, &unknownDetails, &unknown) == InvalidTrophyId, "a trophy that is not in the package did not return the invalid trophy id error"); + Require(untouched(&unknownDetails, sizeof(unknownDetails)) && untouched(&unknown, sizeof(unknown)), "a trophy that is not in the package wrote its details or data"); + } std::vector list(10); std::vector listed(10); diff --git a/core/libs/tests/GuestNpTus.cpp b/core/libs/tests/GuestNpTus.cpp index 11d9cf5cf5..d252efcfaa 100644 --- a/core/libs/tests/GuestNpTus.cpp +++ b/core/libs/tests/GuestNpTus.cpp @@ -8,6 +8,8 @@ extern "C" { int APS5_VABI sceNpTusCreateNpTitleCtxA(std::uint32_t serviceLabel, int userId); int APS5_VABI sceNpTusDeleteNpTitleCtx(int titleCtxId); +int APS5_VABI sceNpTssCreateNpTitleCtx(std::uint32_t serviceLabel, const NpId* npId); +int APS5_VABI sceNpTssCreateNpTitleCtxA(std::uint32_t serviceLabel, int userId); int APS5_VABI sceNpTusCreateRequest(int titleCtxId); int APS5_VABI sceNpTusDeleteRequest(int reqId); int APS5_VABI sceNpTusAbortRequest(int reqId); @@ -51,6 +53,34 @@ static void RequireFinished(int reqId, int expected) { Require(sceNpTusPollAsync(reqId, &result) == 0 && result == expected); } +static void CheckTssTitleContexts() { + const int shared = sceNpTssCreateNpTitleCtxA(0, 1); + Require(shared > 0); + const int request = sceNpTusCreateRequest(shared); + Require(request > 0); + Require(sceNpTusDeleteNpTitleCtx(shared) == 0); + Require(sceNpTusDeleteNpTitleCtx(shared) == INVALID_ID); + Require(sceNpTusDeleteRequest(request) == INVALID_ID); + + const NpId npId{}; + Require(sceNpTssCreateNpTitleCtx(0, nullptr) == INSUFFICIENT_ARGUMENT); + Require(sceNpTssCreateNpTitleCtx(0xFFFFFFFFu, nullptr) == INSUFFICIENT_ARGUMENT); + Require(sceNpTssCreateNpTitleCtx(0xFFFFFFFFu, &npId) == INVALID_ARGUMENT); + + std::array contexts{}; + for (std::size_t index = 0; index < contexts.size(); ++index) { + const std::size_t form = index % 3; + contexts[index] = form == 0 ? sceNpTusCreateNpTitleCtxA(0, 1) : form == 1 ? sceNpTssCreateNpTitleCtxA(0, 1) : sceNpTssCreateNpTitleCtx(0, &npId); + Require(contexts[index] > 0); + } + Require(sceNpTssCreateNpTitleCtxA(0, 1) == TOO_MANY_OBJECTS); + Require(sceNpTssCreateNpTitleCtx(0, &npId) == TOO_MANY_OBJECTS); + Require(sceNpTusCreateNpTitleCtxA(0, 1) == TOO_MANY_OBJECTS); + Require(sceNpTssCreateNpTitleCtx(0, nullptr) == INSUFFICIENT_ARGUMENT); + Require(sceNpTssCreateNpTitleCtx(0xFFFFFFFFu, &npId) == INVALID_ARGUMENT); + for (const int context : contexts) Require(sceNpTusDeleteNpTitleCtx(context) == 0); +} + static void CheckMultiSlotVariables() { const int context = sceNpTusCreateNpTitleCtxA(0, 1); Require(context > 0); @@ -271,4 +301,5 @@ int main() { Require(sceNpTusCreateRequest(contexts[1]) > 0); for (const int child : children) Require(sceNpTusPollAsync(child, &result) == INVALID_ID); for (std::size_t index = 1; index < contexts.size(); ++index) Require(sceNpTusDeleteNpTitleCtx(contexts[index]) == 0); + CheckTssTitleContexts(); } diff --git a/core/libs/tests/GuestNpTusOffline.cpp b/core/libs/tests/GuestNpTusOffline.cpp new file mode 100644 index 0000000000..af24d244c8 --- /dev/null +++ b/core/libs/tests/GuestNpTusOffline.cpp @@ -0,0 +1,268 @@ +#include "SceTypes.hpp" +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceNpTusCreateNpTitleCtxA(std::uint32_t, int); +int APS5_VABI sceNpTusCreateRequest(int); +int APS5_VABI sceNpTusDeleteRequest(int); +int APS5_VABI sceNpTusWaitAsync(int, int*); +int APS5_VABI sceNpTssGetSmallStorage(int reqId, void* data, std::uint64_t maxSize, std::uint64_t* contentLength, void* option); +int APS5_VABI sceNpTssGetSmallStorageAsync(int reqId, void* data, std::uint64_t maxSize, std::uint64_t* contentLength, void* option); +int APS5_VABI sceNpTusAddAndGetVariable(int reqId, const void* targetNpId, std::int32_t slotId, std::int64_t inVariable, const void* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option); +int APS5_VABI sceNpTusAddAndGetVariableA(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, std::int64_t inVariable, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option); +int APS5_VABI sceNpTusAddAndGetVariableAVUser(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int64_t inVariable, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option); +int APS5_VABI sceNpTusAddAndGetVariableAVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int64_t inVariable, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option); +int APS5_VABI sceNpTusAddAndGetVariableAsync(int reqId, const void* targetNpId, std::int32_t slotId, std::int64_t inVariable, const void* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option); +int APS5_VABI sceNpTusAddAndGetVariableForCrossSave(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, std::int64_t inVariable, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option); +int APS5_VABI sceNpTusAddAndGetVariableForCrossSaveAsync(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, std::int64_t inVariable, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option); +int APS5_VABI sceNpTusAddAndGetVariableForCrossSaveVUser(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int64_t inVariable, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option); +int APS5_VABI sceNpTusAddAndGetVariableForCrossSaveVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int64_t inVariable, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option); +int APS5_VABI sceNpTusAddAndGetVariableVUser(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int64_t inVariable, const void* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option); +int APS5_VABI sceNpTusAddAndGetVariableVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int64_t inVariable, const void* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option); +int APS5_VABI sceNpTusDeleteMultiSlotData(int reqId, const void* targetNpId, const std::int32_t* slotIds, int arrayLen, void* option); +int APS5_VABI sceNpTusDeleteMultiSlotDataA(int reqId, std::uint64_t targetAccountId, const std::int32_t* slotIds, int arrayLen, void* option); +int APS5_VABI sceNpTusDeleteMultiSlotDataAAsync(int reqId, std::uint64_t targetAccountId, const std::int32_t* slotIds, int arrayLen, void* option); +int APS5_VABI sceNpTusDeleteMultiSlotDataAsync(int reqId, const void* targetNpId, const std::int32_t* slotIds, int arrayLen, void* option); +int APS5_VABI sceNpTusGetDataA(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, void* dataStatus, std::uint64_t dataStatusSize, void* data, std::uint64_t recvSize, void* option); +int APS5_VABI sceNpTusGetDataAAsync(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, void* dataStatus, std::uint64_t dataStatusSize, void* data, std::uint64_t recvSize, void* option); +int APS5_VABI sceNpTusGetDataAVUser(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, void* dataStatus, std::uint64_t dataStatusSize, void* data, std::uint64_t recvSize, void* option); +int APS5_VABI sceNpTusGetDataAVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, void* dataStatus, std::uint64_t dataStatusSize, void* data, std::uint64_t recvSize, void* option); +int APS5_VABI sceNpTusGetDataAsync(int reqId, const void* targetNpId, std::int32_t slotId, void* dataStatus, std::uint64_t dataStatusSize, void* data, std::uint64_t recvSize, void* option); +int APS5_VABI sceNpTusGetDataForCrossSave(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, void* dataStatus, std::uint64_t dataStatusSize, void* data, std::uint64_t recvSize, void* option); +int APS5_VABI sceNpTusGetDataForCrossSaveAsync(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, void* dataStatus, std::uint64_t dataStatusSize, void* data, std::uint64_t recvSize, void* option); +int APS5_VABI sceNpTusGetDataVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, void* dataStatus, std::uint64_t dataStatusSize, void* data, std::uint64_t recvSize, void* option); +int APS5_VABI sceNpTusGetFriendsDataStatus(int reqId, std::int32_t slotId, std::int32_t includeSelf, std::int32_t sortType, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetFriendsDataStatusAsync(int reqId, std::int32_t slotId, std::int32_t includeSelf, std::int32_t sortType, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetFriendsVariable(int reqId, std::int32_t slotId, std::int32_t includeSelf, std::int32_t sortType, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetFriendsVariableAsync(int reqId, std::int32_t slotId, std::int32_t includeSelf, std::int32_t sortType, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiSlotDataStatus(int reqId, const void* targetNpId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiSlotDataStatusA(int reqId, std::uint64_t targetAccountId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiSlotDataStatusAAsync(int reqId, std::uint64_t targetAccountId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiSlotDataStatusAVUser(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiSlotDataStatusAVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiSlotDataStatusAsync(int reqId, const void* targetNpId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiSlotDataStatusForCrossSave(int reqId, std::uint64_t targetAccountId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiSlotDataStatusForCrossSaveAsync(int reqId, std::uint64_t targetAccountId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiSlotDataStatusForCrossSaveVUser(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiSlotDataStatusForCrossSaveVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiSlotDataStatusVUser(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiSlotDataStatusVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiUserDataStatus(int reqId, const void* targetNpIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiUserDataStatusA(int reqId, const std::uint64_t* targetAccountIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiUserDataStatusAAsync(int reqId, const std::uint64_t* targetAccountIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiUserDataStatusAVUser(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiUserDataStatusAVUserAsync(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiUserDataStatusAsync(int reqId, const void* targetNpIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiUserDataStatusForCrossSave(int reqId, const std::uint64_t* targetAccountIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiUserDataStatusForCrossSaveAsync(int reqId, const std::uint64_t* targetAccountIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiUserDataStatusForCrossSaveVUser(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiUserDataStatusForCrossSaveVUserAsync(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiUserDataStatusVUser(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiUserDataStatusVUserAsync(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiUserVariable(int reqId, const void* targetNpIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiUserVariableA(int reqId, const std::uint64_t* targetAccountIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiUserVariableAAsync(int reqId, const std::uint64_t* targetAccountIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiUserVariableAVUser(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiUserVariableAVUserAsync(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiUserVariableAsync(int reqId, const void* targetNpIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiUserVariableForCrossSaveAsync(int reqId, const std::uint64_t* targetAccountIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiUserVariableForCrossSaveVUser(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiUserVariableForCrossSaveVUserAsync(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiUserVariableVUser(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiUserVariableVUserAsync(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusSetDataA(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, std::uint64_t totalSize, std::uint64_t sendSize, const void* data, const void* info, std::uint64_t infoStructSize, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* option); +int APS5_VABI sceNpTusSetDataAAsync(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, std::uint64_t totalSize, std::uint64_t sendSize, const void* data, const void* info, std::uint64_t infoStructSize, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* option); +int APS5_VABI sceNpTusSetDataAVUser(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::uint64_t totalSize, std::uint64_t sendSize, const void* data, const void* info, std::uint64_t infoStructSize, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* option); +int APS5_VABI sceNpTusSetDataAVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::uint64_t totalSize, std::uint64_t sendSize, const void* data, const void* info, std::uint64_t infoStructSize, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* option); +int APS5_VABI sceNpTusSetDataAsync(int reqId, const void* targetNpId, std::int32_t slotId, std::uint64_t totalSize, std::uint64_t sendSize, const void* data, const void* info, std::uint64_t infoStructSize, const void* isLastChangedAuthor, const void* isLastChangedDate, void* option); +int APS5_VABI sceNpTusSetDataVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::uint64_t totalSize, std::uint64_t sendSize, const void* data, const void* info, std::uint64_t infoStructSize, const void* isLastChangedAuthor, const void* isLastChangedDate, void* option); +int APS5_VABI sceNpTusTryAndSetVariable(int reqId, const void* targetNpId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const void* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option); +int APS5_VABI sceNpTusTryAndSetVariableA(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option); +int APS5_VABI sceNpTusTryAndSetVariableAAsync(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option); +int APS5_VABI sceNpTusTryAndSetVariableAVUser(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option); +int APS5_VABI sceNpTusTryAndSetVariableAVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option); +int APS5_VABI sceNpTusTryAndSetVariableAsync(int reqId, const void* targetNpId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const void* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option); +int APS5_VABI sceNpTusTryAndSetVariableForCrossSave(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option); +int APS5_VABI sceNpTusTryAndSetVariableForCrossSaveAsync(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option); +int APS5_VABI sceNpTusTryAndSetVariableForCrossSaveVUser(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option); +int APS5_VABI sceNpTusTryAndSetVariableForCrossSaveVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option); +int APS5_VABI sceNpTusTryAndSetVariableVUser(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const void* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option); +int APS5_VABI sceNpTusTryAndSetVariableVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const void* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option); +} + +namespace { + +constexpr int SignedOut = static_cast(0x80550006u); +constexpr int InvalidArgument = static_cast(0x80550704u); +constexpr int InvalidId = static_cast(0x8055070Eu); +constexpr int InvalidType = static_cast(0x80550711u); +constexpr int TooManySlotId = static_cast(0x80550718u); +constexpr int TooManyNpId = static_cast(0x80550719u); + +int context = 0; +alignas(8) unsigned char buffer[1024]; +unsigned char npId[64]; +NpOnlineId virtualUser{}; +NpOnlineId virtualUsers[2]{}; +NpOnlineId emptyUser{}; +unsigned char npIds[2][64]; +std::uint64_t accounts[2] = {1, 2}; +std::int32_t slots[2] = {1, 2}; +std::int32_t manySlots[65] = {}; +std::uint64_t length = 0; +int failures = 0; + +void Require(bool value, const char* name, const char* what) { + if (value) return; + std::fprintf(stderr, "%s: %s\n", name, what); + ++failures; +} + +int NewRequest() { + const int req = sceNpTusCreateRequest(context); + if (req <= 0) { + std::fprintf(stderr, "sceNpTusCreateRequest failed\n"); + std::abort(); + } + return req; +} + +void CheckAsync(const char* name, const std::function& valid, const std::function& unknownRequest, const std::function& withOption) { + int req = NewRequest(); + Require(valid(req) == 0, name, "valid arguments did not start the request"); + int result = 0; + Require(sceNpTusWaitAsync(req, &result) == 0 && result == SignedOut, name, "the request did not finish signed out"); + sceNpTusDeleteRequest(req); + Require(unknownRequest() == InvalidId, name, "an unknown request was not refused with INVALID_ID"); + if (withOption) { + req = NewRequest(); + Require(withOption(req) == InvalidArgument, name, "a non-null option was not refused with INVALID_ARGUMENT"); + sceNpTusDeleteRequest(req); + } +} + +void CheckSync(const char* name, const std::function& call) { + const int req = NewRequest(); + Require(call(req) == SignedOut, name, "the synchronous call did not return SIGNED_OUT"); + sceNpTusDeleteRequest(req); +} + +} + +int main() { + std::memset(npId, 'n', sizeof(npId)); + std::memset(npIds, 'n', sizeof(npIds)); + std::strcpy(virtualUser.data, "virtual"); + std::strcpy(virtualUsers[0].data, "first"); + std::strcpy(virtualUsers[1].data, "second"); + context = sceNpTusCreateNpTitleCtxA(1, 1); + if (context <= 0) return 1; + CheckAsync("sceNpTssGetSmallStorageAsync", [&](int req) { return sceNpTssGetSmallStorageAsync(req, buffer, 16, &length, nullptr); }, [&]() { int req = 9999; return sceNpTssGetSmallStorageAsync(9999, buffer, 16, &length, nullptr); }, [&](int req) { return sceNpTssGetSmallStorageAsync(req, buffer, 16, &length, &buffer); }); + CheckAsync("sceNpTusAddAndGetVariableAVUserAsync", [&](int req) { return sceNpTusAddAndGetVariableAVUserAsync(req, &virtualUser, 1, 5, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }, [&]() { int req = 9999; return sceNpTusAddAndGetVariableAVUserAsync(9999, &virtualUser, 1, 5, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }, [&](int req) { return sceNpTusAddAndGetVariableAVUserAsync(req, &virtualUser, 1, 5, nullptr, nullptr, buffer, sizeof(buffer), &buffer); }); + CheckAsync("sceNpTusAddAndGetVariableAsync", [&](int req) { return sceNpTusAddAndGetVariableAsync(req, &npId, 1, 5, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }, [&]() { int req = 9999; return sceNpTusAddAndGetVariableAsync(9999, &npId, 1, 5, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }, [&](int req) { return sceNpTusAddAndGetVariableAsync(req, &npId, 1, 5, nullptr, nullptr, buffer, sizeof(buffer), &buffer); }); + CheckAsync("sceNpTusAddAndGetVariableForCrossSaveAsync", [&](int req) { return sceNpTusAddAndGetVariableForCrossSaveAsync(req, 1, 1, 5, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }, [&]() { int req = 9999; return sceNpTusAddAndGetVariableForCrossSaveAsync(9999, 1, 1, 5, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }, [&](int req) { return sceNpTusAddAndGetVariableForCrossSaveAsync(req, 1, 1, 5, nullptr, nullptr, buffer, sizeof(buffer), &buffer); }); + CheckAsync("sceNpTusAddAndGetVariableForCrossSaveVUserAsync", [&](int req) { return sceNpTusAddAndGetVariableForCrossSaveVUserAsync(req, &virtualUser, 1, 5, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }, [&]() { int req = 9999; return sceNpTusAddAndGetVariableForCrossSaveVUserAsync(9999, &virtualUser, 1, 5, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }, [&](int req) { return sceNpTusAddAndGetVariableForCrossSaveVUserAsync(req, &virtualUser, 1, 5, nullptr, nullptr, buffer, sizeof(buffer), &buffer); }); + CheckAsync("sceNpTusAddAndGetVariableVUserAsync", [&](int req) { return sceNpTusAddAndGetVariableVUserAsync(req, &virtualUser, 1, 5, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }, [&]() { int req = 9999; return sceNpTusAddAndGetVariableVUserAsync(9999, &virtualUser, 1, 5, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }, [&](int req) { return sceNpTusAddAndGetVariableVUserAsync(req, &virtualUser, 1, 5, nullptr, nullptr, buffer, sizeof(buffer), &buffer); }); + CheckAsync("sceNpTusDeleteMultiSlotDataAAsync", [&](int req) { return sceNpTusDeleteMultiSlotDataAAsync(req, 1, slots, 2, nullptr); }, [&]() { int req = 9999; return sceNpTusDeleteMultiSlotDataAAsync(9999, 1, slots, 2, nullptr); }, [&](int req) { return sceNpTusDeleteMultiSlotDataAAsync(req, 1, slots, 2, &buffer); }); + CheckAsync("sceNpTusDeleteMultiSlotDataAsync", [&](int req) { return sceNpTusDeleteMultiSlotDataAsync(req, &npId, slots, 2, nullptr); }, [&]() { int req = 9999; return sceNpTusDeleteMultiSlotDataAsync(9999, &npId, slots, 2, nullptr); }, [&](int req) { return sceNpTusDeleteMultiSlotDataAsync(req, &npId, slots, 2, &buffer); }); + CheckAsync("sceNpTusGetDataAAsync", [&](int req) { return sceNpTusGetDataAAsync(req, 1, 1, buffer, sizeof(buffer), buffer, 16, nullptr); }, [&]() { int req = 9999; return sceNpTusGetDataAAsync(9999, 1, 1, buffer, sizeof(buffer), buffer, 16, nullptr); }, [&](int req) { return sceNpTusGetDataAAsync(req, 1, 1, buffer, sizeof(buffer), buffer, 16, &buffer); }); + CheckAsync("sceNpTusGetDataAVUserAsync", [&](int req) { return sceNpTusGetDataAVUserAsync(req, &virtualUser, 1, buffer, sizeof(buffer), buffer, 16, nullptr); }, [&]() { int req = 9999; return sceNpTusGetDataAVUserAsync(9999, &virtualUser, 1, buffer, sizeof(buffer), buffer, 16, nullptr); }, [&](int req) { return sceNpTusGetDataAVUserAsync(req, &virtualUser, 1, buffer, sizeof(buffer), buffer, 16, &buffer); }); + CheckAsync("sceNpTusGetDataAsync", [&](int req) { return sceNpTusGetDataAsync(req, &npId, 1, buffer, sizeof(buffer), buffer, 16, nullptr); }, [&]() { int req = 9999; return sceNpTusGetDataAsync(9999, &npId, 1, buffer, sizeof(buffer), buffer, 16, nullptr); }, [&](int req) { return sceNpTusGetDataAsync(req, &npId, 1, buffer, sizeof(buffer), buffer, 16, &buffer); }); + CheckAsync("sceNpTusGetDataForCrossSaveAsync", [&](int req) { return sceNpTusGetDataForCrossSaveAsync(req, 1, 1, buffer, sizeof(buffer), buffer, 16, nullptr); }, [&]() { int req = 9999; return sceNpTusGetDataForCrossSaveAsync(9999, 1, 1, buffer, sizeof(buffer), buffer, 16, nullptr); }, [&](int req) { return sceNpTusGetDataForCrossSaveAsync(req, 1, 1, buffer, sizeof(buffer), buffer, 16, &buffer); }); + CheckAsync("sceNpTusGetDataVUserAsync", [&](int req) { return sceNpTusGetDataVUserAsync(req, &virtualUser, 1, buffer, sizeof(buffer), buffer, 16, nullptr); }, [&]() { int req = 9999; return sceNpTusGetDataVUserAsync(9999, &virtualUser, 1, buffer, sizeof(buffer), buffer, 16, nullptr); }, [&](int req) { return sceNpTusGetDataVUserAsync(req, &virtualUser, 1, buffer, sizeof(buffer), buffer, 16, &buffer); }); + CheckAsync("sceNpTusGetFriendsDataStatusAsync", [&](int req) { return sceNpTusGetFriendsDataStatusAsync(req, 1, 0, 1, buffer, sizeof(buffer), 2, nullptr); }, [&]() { int req = 9999; return sceNpTusGetFriendsDataStatusAsync(9999, 1, 0, 1, buffer, sizeof(buffer), 2, nullptr); }, [&](int req) { return sceNpTusGetFriendsDataStatusAsync(req, 1, 0, 1, buffer, sizeof(buffer), 2, &buffer); }); + CheckAsync("sceNpTusGetFriendsVariableAsync", [&](int req) { return sceNpTusGetFriendsVariableAsync(req, 1, 0, 1, buffer, sizeof(buffer), 2, nullptr); }, [&]() { int req = 9999; return sceNpTusGetFriendsVariableAsync(9999, 1, 0, 1, buffer, sizeof(buffer), 2, nullptr); }, [&](int req) { return sceNpTusGetFriendsVariableAsync(req, 1, 0, 1, buffer, sizeof(buffer), 2, &buffer); }); + CheckAsync("sceNpTusGetMultiSlotDataStatusAAsync", [&](int req) { return sceNpTusGetMultiSlotDataStatusAAsync(req, 1, slots, buffer, sizeof(buffer), 2, nullptr); }, [&]() { int req = 9999; return sceNpTusGetMultiSlotDataStatusAAsync(9999, 1, slots, buffer, sizeof(buffer), 2, nullptr); }, [&](int req) { return sceNpTusGetMultiSlotDataStatusAAsync(req, 1, slots, buffer, sizeof(buffer), 2, &buffer); }); + CheckAsync("sceNpTusGetMultiSlotDataStatusAVUserAsync", [&](int req) { return sceNpTusGetMultiSlotDataStatusAVUserAsync(req, &virtualUser, slots, buffer, sizeof(buffer), 2, nullptr); }, [&]() { int req = 9999; return sceNpTusGetMultiSlotDataStatusAVUserAsync(9999, &virtualUser, slots, buffer, sizeof(buffer), 2, nullptr); }, [&](int req) { return sceNpTusGetMultiSlotDataStatusAVUserAsync(req, &virtualUser, slots, buffer, sizeof(buffer), 2, &buffer); }); + CheckAsync("sceNpTusGetMultiSlotDataStatusAsync", [&](int req) { return sceNpTusGetMultiSlotDataStatusAsync(req, &npId, slots, buffer, sizeof(buffer), 2, nullptr); }, [&]() { int req = 9999; return sceNpTusGetMultiSlotDataStatusAsync(9999, &npId, slots, buffer, sizeof(buffer), 2, nullptr); }, [&](int req) { return sceNpTusGetMultiSlotDataStatusAsync(req, &npId, slots, buffer, sizeof(buffer), 2, &buffer); }); + CheckAsync("sceNpTusGetMultiSlotDataStatusForCrossSaveAsync", [&](int req) { return sceNpTusGetMultiSlotDataStatusForCrossSaveAsync(req, 1, slots, buffer, sizeof(buffer), 2, nullptr); }, [&]() { int req = 9999; return sceNpTusGetMultiSlotDataStatusForCrossSaveAsync(9999, 1, slots, buffer, sizeof(buffer), 2, nullptr); }, [&](int req) { return sceNpTusGetMultiSlotDataStatusForCrossSaveAsync(req, 1, slots, buffer, sizeof(buffer), 2, &buffer); }); + CheckAsync("sceNpTusGetMultiSlotDataStatusForCrossSaveVUserAsync", [&](int req) { return sceNpTusGetMultiSlotDataStatusForCrossSaveVUserAsync(req, &virtualUser, slots, buffer, sizeof(buffer), 2, nullptr); }, [&]() { int req = 9999; return sceNpTusGetMultiSlotDataStatusForCrossSaveVUserAsync(9999, &virtualUser, slots, buffer, sizeof(buffer), 2, nullptr); }, [&](int req) { return sceNpTusGetMultiSlotDataStatusForCrossSaveVUserAsync(req, &virtualUser, slots, buffer, sizeof(buffer), 2, &buffer); }); + CheckAsync("sceNpTusGetMultiSlotDataStatusVUserAsync", [&](int req) { return sceNpTusGetMultiSlotDataStatusVUserAsync(req, &virtualUser, slots, buffer, sizeof(buffer), 2, nullptr); }, [&]() { int req = 9999; return sceNpTusGetMultiSlotDataStatusVUserAsync(9999, &virtualUser, slots, buffer, sizeof(buffer), 2, nullptr); }, [&](int req) { return sceNpTusGetMultiSlotDataStatusVUserAsync(req, &virtualUser, slots, buffer, sizeof(buffer), 2, &buffer); }); + CheckAsync("sceNpTusGetMultiUserDataStatusAAsync", [&](int req) { return sceNpTusGetMultiUserDataStatusAAsync(req, accounts, 1, buffer, sizeof(buffer), 2, nullptr); }, [&]() { int req = 9999; return sceNpTusGetMultiUserDataStatusAAsync(9999, accounts, 1, buffer, sizeof(buffer), 2, nullptr); }, [&](int req) { return sceNpTusGetMultiUserDataStatusAAsync(req, accounts, 1, buffer, sizeof(buffer), 2, &buffer); }); + CheckAsync("sceNpTusGetMultiUserDataStatusAVUserAsync", [&](int req) { return sceNpTusGetMultiUserDataStatusAVUserAsync(req, virtualUsers, 1, buffer, sizeof(buffer), 2, nullptr); }, [&]() { int req = 9999; return sceNpTusGetMultiUserDataStatusAVUserAsync(9999, virtualUsers, 1, buffer, sizeof(buffer), 2, nullptr); }, [&](int req) { return sceNpTusGetMultiUserDataStatusAVUserAsync(req, virtualUsers, 1, buffer, sizeof(buffer), 2, &buffer); }); + CheckAsync("sceNpTusGetMultiUserDataStatusAsync", [&](int req) { return sceNpTusGetMultiUserDataStatusAsync(req, npIds, 1, buffer, sizeof(buffer), 2, nullptr); }, [&]() { int req = 9999; return sceNpTusGetMultiUserDataStatusAsync(9999, npIds, 1, buffer, sizeof(buffer), 2, nullptr); }, [&](int req) { return sceNpTusGetMultiUserDataStatusAsync(req, npIds, 1, buffer, sizeof(buffer), 2, &buffer); }); + CheckAsync("sceNpTusGetMultiUserDataStatusForCrossSaveAsync", [&](int req) { return sceNpTusGetMultiUserDataStatusForCrossSaveAsync(req, accounts, 1, buffer, sizeof(buffer), 2, nullptr); }, [&]() { int req = 9999; return sceNpTusGetMultiUserDataStatusForCrossSaveAsync(9999, accounts, 1, buffer, sizeof(buffer), 2, nullptr); }, [&](int req) { return sceNpTusGetMultiUserDataStatusForCrossSaveAsync(req, accounts, 1, buffer, sizeof(buffer), 2, &buffer); }); + CheckAsync("sceNpTusGetMultiUserDataStatusForCrossSaveVUserAsync", [&](int req) { return sceNpTusGetMultiUserDataStatusForCrossSaveVUserAsync(req, virtualUsers, 1, buffer, sizeof(buffer), 2, nullptr); }, [&]() { int req = 9999; return sceNpTusGetMultiUserDataStatusForCrossSaveVUserAsync(9999, virtualUsers, 1, buffer, sizeof(buffer), 2, nullptr); }, [&](int req) { return sceNpTusGetMultiUserDataStatusForCrossSaveVUserAsync(req, virtualUsers, 1, buffer, sizeof(buffer), 2, &buffer); }); + CheckAsync("sceNpTusGetMultiUserDataStatusVUserAsync", [&](int req) { return sceNpTusGetMultiUserDataStatusVUserAsync(req, virtualUsers, 1, buffer, sizeof(buffer), 2, nullptr); }, [&]() { int req = 9999; return sceNpTusGetMultiUserDataStatusVUserAsync(9999, virtualUsers, 1, buffer, sizeof(buffer), 2, nullptr); }, [&](int req) { return sceNpTusGetMultiUserDataStatusVUserAsync(req, virtualUsers, 1, buffer, sizeof(buffer), 2, &buffer); }); + CheckAsync("sceNpTusGetMultiUserVariableAAsync", [&](int req) { return sceNpTusGetMultiUserVariableAAsync(req, accounts, 1, buffer, sizeof(buffer), 2, nullptr); }, [&]() { int req = 9999; return sceNpTusGetMultiUserVariableAAsync(9999, accounts, 1, buffer, sizeof(buffer), 2, nullptr); }, [&](int req) { return sceNpTusGetMultiUserVariableAAsync(req, accounts, 1, buffer, sizeof(buffer), 2, &buffer); }); + CheckAsync("sceNpTusGetMultiUserVariableAVUserAsync", [&](int req) { return sceNpTusGetMultiUserVariableAVUserAsync(req, virtualUsers, 1, buffer, sizeof(buffer), 2, nullptr); }, [&]() { int req = 9999; return sceNpTusGetMultiUserVariableAVUserAsync(9999, virtualUsers, 1, buffer, sizeof(buffer), 2, nullptr); }, [&](int req) { return sceNpTusGetMultiUserVariableAVUserAsync(req, virtualUsers, 1, buffer, sizeof(buffer), 2, &buffer); }); + CheckAsync("sceNpTusGetMultiUserVariableAsync", [&](int req) { return sceNpTusGetMultiUserVariableAsync(req, npIds, 1, buffer, sizeof(buffer), 2, nullptr); }, [&]() { int req = 9999; return sceNpTusGetMultiUserVariableAsync(9999, npIds, 1, buffer, sizeof(buffer), 2, nullptr); }, [&](int req) { return sceNpTusGetMultiUserVariableAsync(req, npIds, 1, buffer, sizeof(buffer), 2, &buffer); }); + CheckAsync("sceNpTusGetMultiUserVariableForCrossSaveAsync", [&](int req) { return sceNpTusGetMultiUserVariableForCrossSaveAsync(req, accounts, 1, buffer, sizeof(buffer), 2, nullptr); }, [&]() { int req = 9999; return sceNpTusGetMultiUserVariableForCrossSaveAsync(9999, accounts, 1, buffer, sizeof(buffer), 2, nullptr); }, [&](int req) { return sceNpTusGetMultiUserVariableForCrossSaveAsync(req, accounts, 1, buffer, sizeof(buffer), 2, &buffer); }); + CheckAsync("sceNpTusGetMultiUserVariableForCrossSaveVUserAsync", [&](int req) { return sceNpTusGetMultiUserVariableForCrossSaveVUserAsync(req, virtualUsers, 1, buffer, sizeof(buffer), 2, nullptr); }, [&]() { int req = 9999; return sceNpTusGetMultiUserVariableForCrossSaveVUserAsync(9999, virtualUsers, 1, buffer, sizeof(buffer), 2, nullptr); }, [&](int req) { return sceNpTusGetMultiUserVariableForCrossSaveVUserAsync(req, virtualUsers, 1, buffer, sizeof(buffer), 2, &buffer); }); + CheckAsync("sceNpTusGetMultiUserVariableVUserAsync", [&](int req) { return sceNpTusGetMultiUserVariableVUserAsync(req, virtualUsers, 1, buffer, sizeof(buffer), 2, nullptr); }, [&]() { int req = 9999; return sceNpTusGetMultiUserVariableVUserAsync(9999, virtualUsers, 1, buffer, sizeof(buffer), 2, nullptr); }, [&](int req) { return sceNpTusGetMultiUserVariableVUserAsync(req, virtualUsers, 1, buffer, sizeof(buffer), 2, &buffer); }); + CheckAsync("sceNpTusSetDataAAsync", [&](int req) { return sceNpTusSetDataAAsync(req, 1, 1, 16, 8, buffer, buffer, sizeof(buffer), nullptr, nullptr, nullptr); }, [&]() { int req = 9999; return sceNpTusSetDataAAsync(9999, 1, 1, 16, 8, buffer, buffer, sizeof(buffer), nullptr, nullptr, nullptr); }, [&](int req) { return sceNpTusSetDataAAsync(req, 1, 1, 16, 8, buffer, buffer, sizeof(buffer), nullptr, nullptr, &buffer); }); + CheckAsync("sceNpTusSetDataAVUserAsync", [&](int req) { return sceNpTusSetDataAVUserAsync(req, &virtualUser, 1, 16, 8, buffer, buffer, sizeof(buffer), nullptr, nullptr, nullptr); }, [&]() { int req = 9999; return sceNpTusSetDataAVUserAsync(9999, &virtualUser, 1, 16, 8, buffer, buffer, sizeof(buffer), nullptr, nullptr, nullptr); }, [&](int req) { return sceNpTusSetDataAVUserAsync(req, &virtualUser, 1, 16, 8, buffer, buffer, sizeof(buffer), nullptr, nullptr, &buffer); }); + CheckAsync("sceNpTusSetDataAsync", [&](int req) { return sceNpTusSetDataAsync(req, &npId, 1, 16, 8, buffer, buffer, sizeof(buffer), nullptr, nullptr, nullptr); }, [&]() { int req = 9999; return sceNpTusSetDataAsync(9999, &npId, 1, 16, 8, buffer, buffer, sizeof(buffer), nullptr, nullptr, nullptr); }, [&](int req) { return sceNpTusSetDataAsync(req, &npId, 1, 16, 8, buffer, buffer, sizeof(buffer), nullptr, nullptr, &buffer); }); + CheckAsync("sceNpTusSetDataVUserAsync", [&](int req) { return sceNpTusSetDataVUserAsync(req, &virtualUser, 1, 16, 8, buffer, buffer, sizeof(buffer), nullptr, nullptr, nullptr); }, [&]() { int req = 9999; return sceNpTusSetDataVUserAsync(9999, &virtualUser, 1, 16, 8, buffer, buffer, sizeof(buffer), nullptr, nullptr, nullptr); }, [&](int req) { return sceNpTusSetDataVUserAsync(req, &virtualUser, 1, 16, 8, buffer, buffer, sizeof(buffer), nullptr, nullptr, &buffer); }); + CheckAsync("sceNpTusTryAndSetVariableAAsync", [&](int req) { return sceNpTusTryAndSetVariableAAsync(req, 1, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }, [&]() { int req = 9999; return sceNpTusTryAndSetVariableAAsync(9999, 1, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }, [&](int req) { return sceNpTusTryAndSetVariableAAsync(req, 1, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), &buffer); }); + CheckAsync("sceNpTusTryAndSetVariableAVUserAsync", [&](int req) { return sceNpTusTryAndSetVariableAVUserAsync(req, &virtualUser, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }, [&]() { int req = 9999; return sceNpTusTryAndSetVariableAVUserAsync(9999, &virtualUser, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }, [&](int req) { return sceNpTusTryAndSetVariableAVUserAsync(req, &virtualUser, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), &buffer); }); + CheckAsync("sceNpTusTryAndSetVariableAsync", [&](int req) { return sceNpTusTryAndSetVariableAsync(req, &npId, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }, [&]() { int req = 9999; return sceNpTusTryAndSetVariableAsync(9999, &npId, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }, [&](int req) { return sceNpTusTryAndSetVariableAsync(req, &npId, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), &buffer); }); + CheckAsync("sceNpTusTryAndSetVariableForCrossSaveAsync", [&](int req) { return sceNpTusTryAndSetVariableForCrossSaveAsync(req, 1, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }, [&]() { int req = 9999; return sceNpTusTryAndSetVariableForCrossSaveAsync(9999, 1, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }, [&](int req) { return sceNpTusTryAndSetVariableForCrossSaveAsync(req, 1, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), &buffer); }); + CheckAsync("sceNpTusTryAndSetVariableForCrossSaveVUserAsync", [&](int req) { return sceNpTusTryAndSetVariableForCrossSaveVUserAsync(req, &virtualUser, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }, [&]() { int req = 9999; return sceNpTusTryAndSetVariableForCrossSaveVUserAsync(9999, &virtualUser, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }, [&](int req) { return sceNpTusTryAndSetVariableForCrossSaveVUserAsync(req, &virtualUser, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), &buffer); }); + CheckAsync("sceNpTusTryAndSetVariableVUserAsync", [&](int req) { return sceNpTusTryAndSetVariableVUserAsync(req, &virtualUser, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }, [&]() { int req = 9999; return sceNpTusTryAndSetVariableVUserAsync(9999, &virtualUser, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }, [&](int req) { return sceNpTusTryAndSetVariableVUserAsync(req, &virtualUser, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), &buffer); }); + CheckSync("sceNpTssGetSmallStorage", [&](int req) { return sceNpTssGetSmallStorage(req, buffer, 16, &length, nullptr); }); + CheckSync("sceNpTusAddAndGetVariable", [&](int req) { return sceNpTusAddAndGetVariable(req, &npId, 1, 5, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }); + CheckSync("sceNpTusAddAndGetVariableA", [&](int req) { return sceNpTusAddAndGetVariableA(req, 1, 1, 5, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }); + CheckSync("sceNpTusAddAndGetVariableAVUser", [&](int req) { return sceNpTusAddAndGetVariableAVUser(req, &virtualUser, 1, 5, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }); + CheckSync("sceNpTusAddAndGetVariableForCrossSave", [&](int req) { return sceNpTusAddAndGetVariableForCrossSave(req, 1, 1, 5, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }); + CheckSync("sceNpTusAddAndGetVariableForCrossSaveVUser", [&](int req) { return sceNpTusAddAndGetVariableForCrossSaveVUser(req, &virtualUser, 1, 5, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }); + CheckSync("sceNpTusAddAndGetVariableVUser", [&](int req) { return sceNpTusAddAndGetVariableVUser(req, &virtualUser, 1, 5, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }); + CheckSync("sceNpTusDeleteMultiSlotData", [&](int req) { return sceNpTusDeleteMultiSlotData(req, &npId, slots, 2, nullptr); }); + CheckSync("sceNpTusDeleteMultiSlotDataA", [&](int req) { return sceNpTusDeleteMultiSlotDataA(req, 1, slots, 2, nullptr); }); + CheckSync("sceNpTusGetDataA", [&](int req) { return sceNpTusGetDataA(req, 1, 1, buffer, sizeof(buffer), buffer, 16, nullptr); }); + CheckSync("sceNpTusGetDataAVUser", [&](int req) { return sceNpTusGetDataAVUser(req, &virtualUser, 1, buffer, sizeof(buffer), buffer, 16, nullptr); }); + CheckSync("sceNpTusGetDataForCrossSave", [&](int req) { return sceNpTusGetDataForCrossSave(req, 1, 1, buffer, sizeof(buffer), buffer, 16, nullptr); }); + CheckSync("sceNpTusGetFriendsDataStatus", [&](int req) { return sceNpTusGetFriendsDataStatus(req, 1, 0, 1, buffer, sizeof(buffer), 2, nullptr); }); + CheckSync("sceNpTusGetFriendsVariable", [&](int req) { return sceNpTusGetFriendsVariable(req, 1, 0, 1, buffer, sizeof(buffer), 2, nullptr); }); + CheckSync("sceNpTusGetMultiSlotDataStatus", [&](int req) { return sceNpTusGetMultiSlotDataStatus(req, &npId, slots, buffer, sizeof(buffer), 2, nullptr); }); + CheckSync("sceNpTusGetMultiSlotDataStatusA", [&](int req) { return sceNpTusGetMultiSlotDataStatusA(req, 1, slots, buffer, sizeof(buffer), 2, nullptr); }); + CheckSync("sceNpTusGetMultiSlotDataStatusAVUser", [&](int req) { return sceNpTusGetMultiSlotDataStatusAVUser(req, &virtualUser, slots, buffer, sizeof(buffer), 2, nullptr); }); + CheckSync("sceNpTusGetMultiSlotDataStatusForCrossSave", [&](int req) { return sceNpTusGetMultiSlotDataStatusForCrossSave(req, 1, slots, buffer, sizeof(buffer), 2, nullptr); }); + CheckSync("sceNpTusGetMultiSlotDataStatusForCrossSaveVUser", [&](int req) { return sceNpTusGetMultiSlotDataStatusForCrossSaveVUser(req, &virtualUser, slots, buffer, sizeof(buffer), 2, nullptr); }); + CheckSync("sceNpTusGetMultiSlotDataStatusVUser", [&](int req) { return sceNpTusGetMultiSlotDataStatusVUser(req, &virtualUser, slots, buffer, sizeof(buffer), 2, nullptr); }); + CheckSync("sceNpTusGetMultiUserDataStatus", [&](int req) { return sceNpTusGetMultiUserDataStatus(req, npIds, 1, buffer, sizeof(buffer), 2, nullptr); }); + CheckSync("sceNpTusGetMultiUserDataStatusA", [&](int req) { return sceNpTusGetMultiUserDataStatusA(req, accounts, 1, buffer, sizeof(buffer), 2, nullptr); }); + CheckSync("sceNpTusGetMultiUserDataStatusAVUser", [&](int req) { return sceNpTusGetMultiUserDataStatusAVUser(req, virtualUsers, 1, buffer, sizeof(buffer), 2, nullptr); }); + CheckSync("sceNpTusGetMultiUserDataStatusForCrossSave", [&](int req) { return sceNpTusGetMultiUserDataStatusForCrossSave(req, accounts, 1, buffer, sizeof(buffer), 2, nullptr); }); + CheckSync("sceNpTusGetMultiUserDataStatusForCrossSaveVUser", [&](int req) { return sceNpTusGetMultiUserDataStatusForCrossSaveVUser(req, virtualUsers, 1, buffer, sizeof(buffer), 2, nullptr); }); + CheckSync("sceNpTusGetMultiUserDataStatusVUser", [&](int req) { return sceNpTusGetMultiUserDataStatusVUser(req, virtualUsers, 1, buffer, sizeof(buffer), 2, nullptr); }); + CheckSync("sceNpTusGetMultiUserVariable", [&](int req) { return sceNpTusGetMultiUserVariable(req, npIds, 1, buffer, sizeof(buffer), 2, nullptr); }); + CheckSync("sceNpTusGetMultiUserVariableA", [&](int req) { return sceNpTusGetMultiUserVariableA(req, accounts, 1, buffer, sizeof(buffer), 2, nullptr); }); + CheckSync("sceNpTusGetMultiUserVariableAVUser", [&](int req) { return sceNpTusGetMultiUserVariableAVUser(req, virtualUsers, 1, buffer, sizeof(buffer), 2, nullptr); }); + CheckSync("sceNpTusGetMultiUserVariableForCrossSaveVUser", [&](int req) { return sceNpTusGetMultiUserVariableForCrossSaveVUser(req, virtualUsers, 1, buffer, sizeof(buffer), 2, nullptr); }); + CheckSync("sceNpTusGetMultiUserVariableVUser", [&](int req) { return sceNpTusGetMultiUserVariableVUser(req, virtualUsers, 1, buffer, sizeof(buffer), 2, nullptr); }); + CheckSync("sceNpTusSetDataA", [&](int req) { return sceNpTusSetDataA(req, 1, 1, 16, 8, buffer, buffer, sizeof(buffer), nullptr, nullptr, nullptr); }); + CheckSync("sceNpTusSetDataAVUser", [&](int req) { return sceNpTusSetDataAVUser(req, &virtualUser, 1, 16, 8, buffer, buffer, sizeof(buffer), nullptr, nullptr, nullptr); }); + CheckSync("sceNpTusTryAndSetVariable", [&](int req) { return sceNpTusTryAndSetVariable(req, &npId, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }); + CheckSync("sceNpTusTryAndSetVariableA", [&](int req) { return sceNpTusTryAndSetVariableA(req, 1, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }); + CheckSync("sceNpTusTryAndSetVariableAVUser", [&](int req) { return sceNpTusTryAndSetVariableAVUser(req, &virtualUser, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }); + CheckSync("sceNpTusTryAndSetVariableForCrossSave", [&](int req) { return sceNpTusTryAndSetVariableForCrossSave(req, 1, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }); + CheckSync("sceNpTusTryAndSetVariableForCrossSaveVUser", [&](int req) { return sceNpTusTryAndSetVariableForCrossSaveVUser(req, &virtualUser, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }); + CheckSync("sceNpTusTryAndSetVariableVUser", [&](int req) { return sceNpTusTryAndSetVariableVUser(req, &virtualUser, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }); + { + int req = NewRequest(); + Require(sceNpTusAddAndGetVariableVUserAsync(req, &emptyUser, 1, 5, nullptr, nullptr, buffer, sizeof(buffer), nullptr) == InvalidArgument, "sceNpTusAddAndGetVariableVUserAsync", "an empty virtual user was taken"); + NpOnlineId partlyEmpty[2]{}; + std::strcpy(partlyEmpty[0].data, "first"); + Require(sceNpTusGetMultiUserVariableVUserAsync(req, partlyEmpty, 1, buffer, sizeof(buffer), 2, nullptr) == InvalidArgument, "sceNpTusGetMultiUserVariableVUserAsync", "an empty virtual user in the array was taken"); + Require(sceNpTusTryAndSetVariableAAsync(req, 1, 1, 0, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), nullptr) == InvalidType, "sceNpTusTryAndSetVariableAAsync", "operation type 0 was taken"); + Require(sceNpTusTryAndSetVariableAAsync(req, 1, 1, 7, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), nullptr) == InvalidType, "sceNpTusTryAndSetVariableAAsync", "operation type 7 was taken"); + Require(sceNpTusTryAndSetVariableAsync(req, npId, 1, 7, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), nullptr) == InvalidArgument, "sceNpTusTryAndSetVariableAsync", "operation type 7 was not refused with INVALID_ARGUMENT"); + Require(sceNpTssGetSmallStorageAsync(req, buffer, 64 * 1024 + 1, &length, nullptr) == InvalidArgument, "sceNpTssGetSmallStorageAsync", "a size above 64 KiB was taken"); + Require(sceNpTusDeleteMultiSlotDataAAsync(req, 1, manySlots, 65, nullptr) == TooManySlotId, "sceNpTusDeleteMultiSlotDataAAsync", "65 slots were taken"); + Require(sceNpTusGetMultiSlotDataStatusAsync(req, npId, manySlots, buffer, sizeof(buffer), 65, nullptr) == TooManyNpId, "sceNpTusGetMultiSlotDataStatusAsync", "65 slots did not return TOO_MANY_NPID"); + const std::int32_t negative[2] = {1, -1}; + Require(sceNpTusDeleteMultiSlotDataAAsync(req, 1, negative, 2, nullptr) == InvalidArgument, "sceNpTusDeleteMultiSlotDataAAsync", "a negative slot id was taken"); + Require(sceNpTusGetMultiUserVariableAAsync(req, accounts, 1, buffer, sizeof(buffer), 102, nullptr) == TooManyNpId, "sceNpTusGetMultiUserVariableAAsync", "102 users were taken"); + Require(sceNpTusGetFriendsVariableAsync(req, 1, 0, 5, buffer, sizeof(buffer), 2, nullptr) == InvalidArgument, "sceNpTusGetFriendsVariableAsync", "sort type 5 was taken"); + Require(sceNpTusGetFriendsVariableAsync(req, 1, 0, 1, buffer, sizeof(buffer), 101, nullptr) == TooManyNpId, "sceNpTusGetFriendsVariableAsync", "101 friends were taken"); + sceNpTusDeleteRequest(req); + } + if (failures != 0) return 1; + std::puts("NpTus offline request tests passed"); + return 0; +} diff --git a/core/libs/tests/GuestNpWebApi.cpp b/core/libs/tests/GuestNpWebApi.cpp new file mode 100644 index 0000000000..9281e58632 --- /dev/null +++ b/core/libs/tests/GuestNpWebApi.cpp @@ -0,0 +1,67 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include + +extern "C" { +int APS5_VABI sceNpWebApiInitialize(int libHttpCtxId, std::size_t poolSize); +int APS5_VABI sceNpWebApiTerminate(int libCtxId); +int APS5_VABI sceNpWebApiCreateHandle(int libCtxId); +int APS5_VABI sceNpWebApiDeleteHandle(int libCtxId, int handleId); +int APS5_VABI sceNpWebApiCreatePushEventFilter(int libCtxId, const void* filterParam, std::size_t filterParamNum); +int APS5_VABI sceNpWebApiDeletePushEventFilter(int libCtxId, int filterId); +int APS5_VABI sceNpWebApiCreateServicePushEventFilter(int libCtxId, int handleId, const char* npServiceName, unsigned npServiceLabel, const void* filterParam, std::size_t filterParamNum); +int APS5_VABI sceNpWebApiDeleteServicePushEventFilter(int libCtxId, int filterId); +} + +namespace { + +constexpr int InvalidArgument = static_cast(0x80552902u); +constexpr int LibContextNotFound = static_cast(0x80552904u); +constexpr int PushEventFilterNotFound = static_cast(0x8055290Bu); +constexpr int HandleNotFound = static_cast(0x8055290Du); +constexpr int ServicePushEventFilterNotFound = static_cast(0x8055290Eu); +constexpr int SignedInUserNotFound = static_cast(0x80552910u); + +void Require(bool condition, const char* message) { + if (!condition) { + std::fprintf(stderr, "NpWebApi: %s\n", message); + std::abort(); + } +} + +} + +int main() { + const int context = sceNpWebApiInitialize(1, 0x10000); + Require(context > 0 && context <= 0x7fff, "library context id out of range"); + const int other = sceNpWebApiInitialize(1, 0x10000); + Require(other > 0 && other != context, "a second library context must get its own id"); + Require(sceNpWebApiCreateHandle(0x7fff + 1) == LibContextNotFound, "handle of an unknown context"); + + const int handle = sceNpWebApiCreateHandle(context); + Require(handle > 0 && static_cast(handle) < 0xf0000000u, "handle id out of range"); + const char filterParam[16] = {}; + Require(sceNpWebApiCreatePushEventFilter(context, nullptr, 1) == InvalidArgument, "push event filter without its parameter"); + Require(sceNpWebApiCreatePushEventFilter(context, filterParam, 0) == InvalidArgument, "push event filter with no parameters"); + const int filter = sceNpWebApiCreatePushEventFilter(context, filterParam, 1); + Require(filter > 0 && filter != handle, "push event filter id"); + + Require(sceNpWebApiCreateServicePushEventFilter(context, handle, nullptr, 0, filterParam, 1) == InvalidArgument, "service filter without a service name"); + Require(sceNpWebApiCreateServicePushEventFilter(0x7fff + 1, handle, "service", 0, filterParam, 1) == LibContextNotFound, "service filter of an unknown context"); + Require(sceNpWebApiCreateServicePushEventFilter(other, handle, "service", 0, filterParam, 1) == HandleNotFound, "service filter with another context's handle"); + Require(sceNpWebApiCreateServicePushEventFilter(context, handle, "service", 0, filterParam, 1) == SignedInUserNotFound, "service filter without a signed-in user"); + Require(sceNpWebApiDeleteServicePushEventFilter(context, 1) == ServicePushEventFilterNotFound, "no service filter exists"); + + Require(sceNpWebApiDeletePushEventFilter(other, filter) == PushEventFilterNotFound, "filter deleted through another context"); + Require(sceNpWebApiDeletePushEventFilter(context, filter) == 0, "delete the push event filter"); + Require(sceNpWebApiDeletePushEventFilter(context, filter) == PushEventFilterNotFound, "push event filter deleted twice"); + Require(sceNpWebApiDeleteHandle(context, handle) == 0, "delete the handle"); + Require(sceNpWebApiDeleteHandle(context, handle) == HandleNotFound, "handle deleted twice"); + + Require(sceNpWebApiTerminate(context) == 0, "terminate the library context"); + Require(sceNpWebApiTerminate(context) == LibContextNotFound, "library context terminated twice"); + Require(sceNpWebApiCreateHandle(context) == LibContextNotFound, "handle of a terminated context"); + Require(sceNpWebApiTerminate(other) == 0, "terminate the second library context"); + return 0; +} diff --git a/core/libs/tests/GuestPad.cpp b/core/libs/tests/GuestPad.cpp index 335c38cea7..6dfa8c9a9c 100644 --- a/core/libs/tests/GuestPad.cpp +++ b/core/libs/tests/GuestPad.cpp @@ -30,6 +30,9 @@ int APS5_VABI scePadSetAngularVelocityDeadbandState(int, bool); int APS5_VABI scePadSetAngularVelocityBiasCorrectionState(int, bool); int APS5_VABI scePadIsRemoteController(int, bool*); int APS5_VABI scePadGetAccessibilityControllerState(int, std::uint32_t*); +bool APS5_VABI scePadIsDS4Connected(std::int32_t); +bool APS5_VABI scePadIsBlasterConnected(std::int32_t); +bool APS5_VABI scePadIsMoveConnected(std::int32_t); int APS5_VABI scePadGetControllerInformation(int, PadControllerInformation*); int APS5_VABI scePadGetExtControllerInformation(int, void*); int APS5_VABI scePadResetLightBar(int); @@ -149,6 +152,17 @@ static void CheckAngularVelocityDeadband(int handle) { PadPublishInput_nid_postfix(PadInputState{}); } +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() { PadInputState touch; touch.buttons = static_cast(Pad::PadButton::TouchPad); @@ -305,6 +319,14 @@ static void CheckRemoteController(int handle) { Require(!remote); } +static void CheckTrackerDevices(int handle) { + for (const int candidate : {handle, handle + 1}) { + Require(!scePadIsDS4Connected(candidate)); + Require(!scePadIsBlasterConnected(candidate)); + Require(!scePadIsMoveConnected(candidate)); + } +} + static void CheckAccessibilityControllerState(int handle) { std::uint32_t state = 7; Require(scePadGetAccessibilityControllerState(handle + 1, nullptr) == PAD_ERROR_INVALID_ARG); @@ -442,12 +464,14 @@ int main() { CheckTouchContact(); CheckVirtualTouchContact(); CheckTouchPadInformation(handle); + CheckStickDeadZones(handle); CheckPartialTrigger(); CheckReadQueue(handle); CheckTapBetweenReads(); CheckLatchedPressLifetime(handle); CheckReadStateHandle(handle); CheckRemoteController(handle); + CheckTrackerDevices(handle); CheckAccessibilityControllerState(handle); CheckInjectedState(commandPath); PadLightBarParam lightBar{255, 128, 64}; diff --git a/core/libs/tests/GuestPoll.cpp b/core/libs/tests/GuestPoll.cpp index 131e75eb9d..d81f0760f6 100644 --- a/core/libs/tests/GuestPoll.cpp +++ b/core/libs/tests/GuestPoll.cpp @@ -31,6 +31,7 @@ int APS5_VABI getsockname_nid_postfix(int, void*, std::uint32_t*); std::int64_t APS5_VABI send_nid_postfix(int, const void*, std::uint64_t, int); std::int64_t APS5_VABI recv_nid_postfix(int, void*, std::uint64_t, int); int APS5_VABI close_nid_postfix(int); +int APS5_VABI shutdown_nid_postfix(int, int); int APS5_VABI open_nid_postfix(const char*, int, int); int APS5_VABI mkdir_nid_postfix(const char*, unsigned short); int APS5_VABI rmdir_nid_postfix(const char*); @@ -212,8 +213,41 @@ static void PollsUrgentData() { Require(close_nid_postfix(client) == 0); Require(close_nid_postfix(listener) == 0); } +static void PollsAStreamWhosePeerClosed() { + const int listener = socket_nid_postfix(2, 1, 0); + Require(listener >= 0); + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(bind_nid_postfix(listener, address.data(), address.size()) == 0); + std::uint32_t size = address.size(); + Require(getsockname_nid_postfix(listener, address.data(), &size) == 0 && listen_nid_postfix(listener, 4) == 0); + const int client = socket_nid_postfix(2, 1, 0); + Require(client >= 0 && connect_nid_postfix(client, address.data(), address.size()) == 0); + const int server = accept_nid_postfix(listener, nullptr, nullptr); + Require(server >= 0); + + Require(shutdown_nid_postfix(client, 1) == 0); + PollDescriptor wait{server, In, 0}; + Require(poll_nid_postfix(&wait, 1, 1000) == 1 && wait.revents == In); + wait = {server, static_cast(In | Out), 0}; + Require(poll_nid_postfix(&wait, 1, 1000) == 1 && wait.revents == (In | Out)); + wait = {server, ReadNormal, 0}; + Require(poll_nid_postfix(&wait, 1, 1000) == 1 && wait.revents == ReadNormal); + wait = {server, Out, 0}; + Require(poll_nid_postfix(&wait, 1, 1000) == 1 && wait.revents == Out); + char byte; + Require(recv_nid_postfix(server, &byte, 1, 0) == 0); + + Require(shutdown_nid_postfix(server, 1) == 0); + wait = {server, In, 0}; + Require(poll_nid_postfix(&wait, 1, 1000) == 1 && wait.revents == (In | Hup)); + wait = {client, In, 0}; + Require(poll_nid_postfix(&wait, 1, 1000) == 1 && wait.revents == (In | Hup)); + for (const int descriptor : {client, server, listener}) Require(close_nid_postfix(descriptor) == 0); +} + int main() { PollsUrgentData(); + PollsAStreamWhosePeerClosed(); const int listener = socket_nid_postfix(2, 1, 0); Require(listener >= 0 && listener < 1024); std::array address{16, 2, 0, 0, 127, 0, 0, 1}; diff --git a/core/libs/tests/GuestPosixFilesystem.cpp b/core/libs/tests/GuestPosixFilesystem.cpp index 0e3326d3d0..fbe8e7566e 100644 --- a/core/libs/tests/GuestPosixFilesystem.cpp +++ b/core/libs/tests/GuestPosixFilesystem.cpp @@ -1,4 +1,5 @@ #include "prx/libc/include/General.hpp" +#include "SceTypes.hpp" #include #include #include @@ -27,6 +28,20 @@ int APS5_VABI socket_nid_postfix(int, int, int); std::int64_t APS5_VABI read_nid_postfix(int, void*, std::size_t); std::int64_t APS5_VABI write_nid_postfix(int, const void*, std::size_t); std::int64_t APS5_VABI lseek_nid_postfix(int, std::int64_t, int); +int APS5_VABI access_nid_postfix(const char*, int); +int APS5_VABI __wrap_access(const char*, int); +int APS5_VABI clock_gettime_nid_postfix(int, KernelTimespec*); +int APS5_VABI __wrap_clock_gettime(int, KernelTimespec*); +char* APS5_VABI getcwd_nid_postfix(char*, std::size_t); +char* APS5_VABI __wrap_getcwd(char*, std::size_t); +char* APS5_VABI getenv_nid_postfix(const char*); +char* APS5_VABI __wrap_getenv(const char*); +int APS5_VABI setenv_nid_postfix(const char*, const char*, int); +int APS5_VABI lstat_nid_postfix(const char*, FileStat*); +int APS5_VABI __wrap_lstat(const char*, FileStat*); +void* APS5_VABI mmap_nid_postfix(void*, std::size_t, int, int, int, std::int64_t) noexcept; +void* APS5_VABI __wrap_mmap(void*, std::size_t, int, int, int, std::int64_t); +int APS5_VABI munmap_nid_postfix(void*, std::size_t) noexcept; } static void Require(bool condition, std::source_location location = std::source_location::current()) { @@ -134,6 +149,52 @@ int main() { Require(isatty_nid_postfix(slave) == 1 && *__error_nid_postfix() == 123); Require(::close(slave) == 0 && ::close(terminal) == 0); #endif + Require(setenv_nid_postfix("WEBKIT_WRAP_SET", "set", 1) == 0); + char* const variable = getenv_nid_postfix("WEBKIT_WRAP_SET"); + Require(variable != nullptr && std::strcmp(variable, "set") == 0 && __wrap_getenv("WEBKIT_WRAP_SET") == variable); + Require(getenv_nid_postfix("WEBKIT_WRAP_UNSET") == nullptr && __wrap_getenv("WEBKIT_WRAP_UNSET") == nullptr); + Require(access_nid_postfix(pattern.c_str(), 0) == 0 && __wrap_access(pattern.c_str(), 0) == 0); + Require(access_nid_postfix("/anyps5-posix-test/absent", 0) == -1); + const int accessError = *__error_nid_postfix(); + *__error_nid_postfix() = 0; + Require(__wrap_access("/anyps5-posix-test/absent", 0) == -1 && *__error_nid_postfix() == accessError && accessError == 2); + std::array expectedDirectory{}; + std::array wrappedDirectory{}; + Require(getcwd_nid_postfix(expectedDirectory.data(), expectedDirectory.size()) == expectedDirectory.data()); + Require(__wrap_getcwd(wrappedDirectory.data(), wrappedDirectory.size()) == wrappedDirectory.data()); + Require(std::strcmp(expectedDirectory.data(), wrappedDirectory.data()) == 0); + Require(getcwd_nid_postfix(wrappedDirectory.data(), 1) == nullptr); + const int directoryError = *__error_nid_postfix(); + *__error_nid_postfix() = 0; + Require(__wrap_getcwd(wrappedDirectory.data(), 1) == nullptr && *__error_nid_postfix() == directoryError && directoryError == 34); + FileStat expectedStatus{}; + FileStat wrappedStatus{}; + Require(lstat_nid_postfix(pattern.c_str(), &expectedStatus) == 0 && __wrap_lstat(pattern.c_str(), &wrappedStatus) == 0); + Require((wrappedStatus.st_mode & 0170000) == 0100000 && wrappedStatus.st_size == static_cast(sizeof(payload))); + Require(std::memcmp(&expectedStatus, &wrappedStatus, sizeof(FileStat)) == 0); + const auto notAfter = [](const KernelTimespec& earlier, const KernelTimespec& later) { + return earlier.tv_sec < later.tv_sec || (earlier.tv_sec == later.tv_sec && earlier.tv_nsec <= later.tv_nsec); + }; + KernelTimespec before{}; + KernelTimespec during{}; + KernelTimespec after{}; + Require(clock_gettime_nid_postfix(4, &before) == 0 && __wrap_clock_gettime(4, &during) == 0 && clock_gettime_nid_postfix(4, &after) == 0); + Require(notAfter(before, during) && notAfter(during, after)); + Require(clock_gettime_nid_postfix(4, nullptr) == -1 && *__error_nid_postfix() == 14); + *__error_nid_postfix() = 0; + Require(__wrap_clock_gettime(4, nullptr) == -1 && *__error_nid_postfix() == 14); + constexpr std::size_t page = 0x4000; + void* const failed = reinterpret_cast(static_cast(-1)); + void* const mapped = __wrap_mmap(nullptr, page, 3, 0x1002, -1, 0); + Require(mapped != failed); + auto* const bytes = static_cast(mapped); + bytes[0] = 0x5a; + bytes[page - 1] = 0xa5; + Require(bytes[0] == 0x5a && bytes[page - 1] == 0xa5); + Require(munmap_nid_postfix(mapped, page) == 0); + Require(mmap_nid_postfix(nullptr, page, 8, 0x1002, -1, 0) == failed && *__error_nid_postfix() == 22); + *__error_nid_postfix() = 0; + Require(__wrap_mmap(nullptr, page, 8, 0x1002, -1, 0) == failed && *__error_nid_postfix() == 22); RemovePathAlias_nid_no_patch("/anyps5-posix-test"); Require(std::filesystem::remove_all(root) > 0); } diff --git a/core/libs/tests/GuestRaiseInHostCode.cpp b/core/libs/tests/GuestRaiseInHostCode.cpp index 955d4605b6..0a41f83ca8 100644 --- a/core/libs/tests/GuestRaiseInHostCode.cpp +++ b/core/libs/tests/GuestRaiseInHostCode.cpp @@ -1,4 +1,5 @@ #include "SceTypes.hpp" +#include #include #include #include @@ -54,8 +55,11 @@ int main() { Pthread writer = nullptr; Require(scePthreadCreate(&writer, nullptr, Writer, nullptr, "host writer") == 0); while (!started.load()) std::this_thread::yield(); + auto longest = std::chrono::steady_clock::duration::zero(); for (int raise = 1; raise <= Raises; ++raise) { + const auto raisedAt = std::chrono::steady_clock::now(); Require(sceKernelRaiseException(writer, SigUsr1) == 0); + longest = std::max(longest, std::chrono::steady_clock::now() - raisedAt); while (deliveries.load() < raise) std::this_thread::yield(); Require(std::fputc('y', shared) == 'y'); released.store(raise); @@ -64,6 +68,7 @@ int main() { Require(scePthreadJoin(writer, nullptr) == 0); Require(sceKernelRemoveExceptionHandler(SigUsr1) == 0); std::fclose(shared); + std::printf("longest raise: %.2f ms\n", std::chrono::duration(longest).count()); done.store(true); watchdog.join(); } diff --git a/core/libs/tests/GuestRtc.cpp b/core/libs/tests/GuestRtc.cpp index 53e55fd327..13d1678125 100644 --- a/core/libs/tests/GuestRtc.cpp +++ b/core/libs/tests/GuestRtc.cpp @@ -67,6 +67,8 @@ int APS5_VABI sceRtcSetCurrentTick(const RtcTick*); int APS5_VABI sceRtcSetCurrentNetworkTick(const RtcTick*); int APS5_VABI sceRtcSetCurrentAdNetworkTick(const RtcTick*); int APS5_VABI sceRtcSetCurrentDebugNetworkTick(const RtcTick*); +int APS5_VABI sceRtcInit(void); +int APS5_VABI sceRtcEnd(void); } static void Require(bool value) { if (!value) std::abort(); } @@ -907,4 +909,6 @@ int main() { Require(sceRtcGetCurrentDebugNetworkTick(¤t) == 0 && current.tick >= debugClock.tick && current.tick < debugClock.tick + 1000000); Require(sceRtcGetCurrentRawNetworkTick(¤t) == 0 && current.tick > unixEpochTick); } + Require(sceRtcInit() == 0); + Require(sceRtcEnd() == 0); } diff --git a/core/libs/tests/GuestSaveDataPs4.cpp b/core/libs/tests/GuestSaveDataPs4.cpp index 39e7b14d1e..6ee6868bd4 100644 --- a/core/libs/tests/GuestSaveDataPs4.cpp +++ b/core/libs/tests/GuestSaveDataPs4.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -10,10 +11,16 @@ extern "C" int APS5_VABI sceSaveDataTransferringMountPs4(const SaveDataTransferringMount*, SaveDataMountResult*); extern "C" int APS5_VABI sceSaveDataDirNameSearchPs4(const SaveDataDirNameSearchCond*, SaveDataDirNameSearchResult*); extern "C" int APS5_VABI sceSaveDataDirNameSearch(const SaveDataDirNameSearchCond*, SaveDataDirNameSearchResult*); +extern "C" int APS5_VABI sceSaveDataInitialize(const void*); +extern "C" int APS5_VABI sceSaveDataTerminate(); +extern "C" int APS5_VABI sceSaveDataSetupSaveDataMemory(std::int32_t, std::size_t, const SaveDataParam*); +extern "C" int APS5_VABI sceSaveDataSetSaveDataMemory(std::int32_t, void*, std::size_t, std::int64_t); +extern "C" int APS5_VABI sceSaveDataGetSaveDataMemory(std::int32_t, void*, std::size_t, std::int64_t); namespace { constexpr int SaveDataErrorNotFound = -2137063416; +constexpr int SaveDataErrorNotInitialized = -2137063423; int failures = 0; @@ -73,6 +80,22 @@ void TestDirNameSearchPs4() { Check(ThrowsRuntimeError([&] { sceSaveDataDirNameSearchPs4(&cond, nullptr); }), "null result throws"); } +void TestSaveDataMemoryV1() { + Check(sceSaveDataSetupSaveDataMemory(7, 0x1000, nullptr) == SaveDataErrorNotInitialized, "v1 SetupSaveDataMemory before v1 Initialize"); + Check(sceSaveDataInitialize(nullptr) == 0, "v1 Initialize"); + Check(sceSaveDataSetupSaveDataMemory(7, 0x1000, nullptr) == 0, "v1 SetupSaveDataMemory"); + char written[16] = "settings-v1"; + Check(sceSaveDataSetSaveDataMemory(7, written, sizeof(written), 0x100) == 0, "v1 SetSaveDataMemory at an offset"); + char read[16] = {}; + Check(sceSaveDataGetSaveDataMemory(7, read, sizeof(read), 0x100) == 0, "v1 GetSaveDataMemory"); + Check(std::memcmp(read, written, sizeof(read)) == 0, "v1 GetSaveDataMemory returns the bytes v1 set"); + std::ifstream slot("_sd_mem/u7/slot0.bin", std::ios::binary); + slot.seekg(0x100); + char stored[16] = {}; + Check(static_cast(slot.read(stored, sizeof(stored))) && std::memcmp(stored, written, sizeof(stored)) == 0, "v1 SetSaveDataMemory stores into slot 0 of the user"); + Check(sceSaveDataTerminate() == 0, "Terminate after v1 Initialize"); +} + } int main() { @@ -83,6 +106,7 @@ int main() { TestTransferringMountPs4(); TestDirNameSearchPs4(); + TestSaveDataMemoryV1(); std::filesystem::current_path(previous); std::error_code error; diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index fd59160c0c..60298ce61c 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -13,6 +13,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libkernel/Socket/include/SocketSelectDeadline.hpp" #include "prx/libkernel/Time/include/Time.hpp" +#include "prx/libSceNet/src/ResolverTestHooks.hpp" #include "SceTypes.hpp" #include #include @@ -55,6 +56,8 @@ int APS5_VABI sceNetShutdown(int, int); int APS5_VABI sceNetSetsockopt(int, int, int, const void*, std::uint32_t); int APS5_VABI sceNetGetsockopt(int, int, int, void*, std::uint32_t*); int* APS5_VABI sceNetErrnoLoc(void); +int APS5_VABI sceNetEtherNtostr(const NetEtherAddr*, char*, std::size_t); +int APS5_VABI sceNetEtherStrton(const char*, NetEtherAddr*); int APS5_VABI sceNetEpollCreate(const char*, int); int APS5_VABI sceNetEpollControl(int, int, int, const NetEpollEvent*); int APS5_VABI sceNetEpollWait(int, NetEpollEvent*, int, int); @@ -144,6 +147,20 @@ static void Require(bool condition) { if (!condition) std::abort(); } +static bool FailResolverLookup(const char*, int, std::vector>&) { + return false; +} + +class ScopedResolverLookupOverride { +public: + explicit ScopedResolverLookupOverride(GuestSceNetTest::ResolverLookup lookup) { + GuestSceNetTest::SetResolverLookupForTests_nid_no_patch(lookup); + } + ~ScopedResolverLookupOverride() { + GuestSceNetTest::SetResolverLookupForTests_nid_no_patch(nullptr); + } +}; + static void APS5_VABI IgnoreNetCtlEvent(int, void*) {} static bool Failed(std::int64_t result, int error) { @@ -305,14 +322,20 @@ static void CheckRandom() { Require(Failed(sceNetGetRandom(nullptr), 22)); std::array values{}; for (auto& value : values) Require(sceNetGetRandom(&value) == 0); + std::array guarded{0x5a5a5a5au, 0x5a5a5a5au}; + Require(sceNetGetRandom(guarded.data()) == 0 && guarded[1] == 0x5a5a5a5au && *sceNetErrnoLoc() == 22); Require(std::any_of(values.begin() + 1, values.end(), [&](std::uint32_t value) { return value != values[0]; })); } static void CheckNetMemory() { - Require(sceNetMemoryAllocate(0, 0) == nullptr); - Require(sceNetMemoryAllocate(-1, 2) == nullptr); - Require(sceNetMemoryAllocate(0, 4) == nullptr); - Require(sceNetMemoryAllocate(INT32_MIN, -1) == nullptr); + *sceNetErrnoLoc() = 33; + for (const std::int32_t size : {0, -1, INT32_MIN}) { + Require(sceNetMemoryAllocate(size, 0) == nullptr); + Require(sceNetMemoryAllocate(size, 2) == nullptr); + Require(sceNetMemoryAllocate(size, 4) == nullptr); + Require(sceNetMemoryAllocate(size, -1) == nullptr); + } + Require(*sceNetErrnoLoc() == 33); auto* zeroed = static_cast(sceNetMemoryAllocate(4096, 2)); Require(zeroed != nullptr); Require(std::all_of(zeroed, zeroed + 4096, [](std::uint8_t byte) { return byte == 0; })); @@ -323,20 +346,20 @@ static void CheckNetMemory() { Require(zeroed[4095] == 0xa5); sceNetMemoryFree(plain); sceNetMemoryFree(zeroed); + for (const int flags : {1, 3, 4, 6, 8, 0x10, 0x100, 0x40000001, -5, INT32_MIN}) { + auto* block = static_cast(sceNetMemoryAllocate(16, flags)); + Require(block != nullptr && reinterpret_cast(block) % 64 == 0); + if ((flags & 2) != 0) Require(std::all_of(block, block + 16, [](std::uint8_t byte) { return byte == 0; })); + std::memset(block, 0x5a, 16); + sceNetMemoryFree(block); + } + for (const std::int32_t size : {1, 7, 256, 65536}) { + void* block = sceNetMemoryAllocate(size, 0); + Require(block != nullptr && reinterpret_cast(block) % 64 == 0); + sceNetMemoryFree(block); + } sceNetMemoryFree(nullptr); - for (const int flags : {1, 0x40000001, -5}) { - auto* allocation = static_cast(sceNetMemoryAllocate(16, flags)); - Require(allocation != nullptr); - if ((flags & 2) != 0) Require(std::all_of(allocation, allocation + 16, [](std::uint8_t byte) { return byte == 0; })); - sceNetMemoryFree(allocation); - } - bool threw = false; - try { - sceNetMemoryAllocate(16, 4); - } catch (const std::runtime_error&) { - threw = true; - } - Require(threw); + Require(*sceNetErrnoLoc() == 33); } static void CheckUnboundSocketName() { @@ -596,6 +619,36 @@ static void CheckReceiveTimeout(int receiver, int sender, const std::array= 0); + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(sceNetBind_nid_postfix(listener, address.data(), address.size()) == 0 && sceNetListen(listener, 1) == 0); + std::uint32_t size = address.size(); + Require(sceNetGetsockname(listener, address.data(), &size) == 0); + const int client = sceNetSocket("stream-timeout-client", 2, 1, 6); + Require(client >= 0 && sceNetConnect(client, address.data(), address.size()) == 0); + const int accepted = sceNetAccept(listener, nullptr, nullptr); + Require(accepted >= 0); + const KernelTimeval timeout{0, 50000}; + Require(sceNetSetsockopt(client, 0xffff, 0x1006, &timeout, sizeof(timeout)) == 0); + char buffer[8]{}; + NetIovec in[] = {{buffer, sizeof(buffer)}}; + NetMsghdr receive{nullptr, 0, in, 1, nullptr, 0, 0}; + const auto start = std::chrono::steady_clock::now(); + Require(Failed(sceNetRecv(client, buffer, sizeof(buffer), 0), 35)); + Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(40)); + Require(Failed(sceNetRecvfrom(client, buffer, sizeof(buffer), 0, nullptr, nullptr), 35)); + Require(Failed(sceNetRecvmsg(client, &receive, 0), 35)); + const char payload[] = "late"; + Require(sceNetSend(accepted, payload, sizeof(payload), 0) == sizeof(payload)); + Require(sceNetRecv(client, buffer, sizeof(buffer), 0) == sizeof(payload) && std::strcmp(buffer, payload) == 0); + Require(sceNetSend(client, payload, sizeof(payload), 0) == sizeof(payload)); + std::memset(buffer, 0, sizeof(buffer)); + Require(sceNetRecv(accepted, buffer, sizeof(buffer), 0) == sizeof(payload) && std::strcmp(buffer, payload) == 0); + for (const int descriptor : {client, accepted, listener}) Require(sceNetSocketClose(descriptor) == 0); +} + static void CheckMicrosecondTimeouts(int receiver) { const int receiveTimeout = 50000; const int sendTimeout = 70000; @@ -884,6 +937,96 @@ static void CheckInetPtonEx() { } } +static void CheckEtherAddress() { + struct GuardedAddress { + std::uint8_t before; + NetEtherAddr address; + std::uint8_t after; + }; + static_assert(sizeof(GuardedAddress) == 8); + const GuardedAddress original{0x12, {{0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5}}, 0x34}; + for (const char* text : {"00:01:02:0A:0b:0F", "00-01-02-0A-0b-0F", "00.01 02+0A/0b_0F", "00:01:02:0A:0b:0F trailing"}) { + auto output = original; + const std::array expected{0, 1, 2, 10, 11, 15}; + *sceNetErrnoLoc() = 33; + Require(sceNetEtherStrton(text, &output.address) == 0); + Require(std::memcmp(output.address.data, expected.data(), expected.size()) == 0); + Require(output.before == original.before && output.after == original.after && *sceNetErrnoLoc() == 33); + } + for (unsigned value = 0; value < 256; ++value) { + NetEtherAddr input{}; + for (unsigned index = 0; index < 6; ++index) input.data[index] = static_cast(value + index * 43); + char text[18]{}; + Require(sceNetEtherNtostr(&input, text, sizeof(text)) == 0); + auto output = original; + Require(sceNetEtherStrton(text, &output.address) == 0); + Require(std::memcmp(input.data, output.address.data, sizeof(input.data)) == 0); + Require(output.before == original.before && output.after == original.after); + } + const char digits[] = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ:-. +"; + for (std::size_t index = 0; index + 1 < sizeof(digits); ++index) { + const unsigned digit = index < 36 ? index : index < 62 ? index - 26 : 0x7f; + for (std::size_t position = 0; position < 2; ++position) { + std::string text = "01:23:45:67:89:ab"; + text[position] = digits[index]; + const std::array expected{static_cast(position == 0 ? digit * 16 + 1 : digit), 0x23, 0x45, 0x67, 0x89, 0xab}; + auto output = original; + *sceNetErrnoLoc() = 33; + Require(sceNetEtherStrton(text.c_str(), &output.address) == 0); + Require(std::memcmp(output.address.data, expected.data(), expected.size()) == 0); + Require(output.before == original.before && output.after == original.after && *sceNetErrnoLoc() == 33); + } + } + for (const char* text : {"", "0", "0:1:2:a:B:f", "0-1-2-a-B-f", "0:1-2:a-B:f", "0:1:2:3:4", "0:1:2:3:4:", + "0:1:2:3:4:5:6", "000:1:2:3:4:5", "0:1:2:3:4:555", ":0:1:2:3:4:5", "0::1:2:3:4:5", "0.1.2.3.4.5", + "0x0:1:2:3:4:5", "0:1:2:3:4:g", "+0:1:2:3:4:5", " 0:1:2:3:4:5", "0:1:2:3:4:5 ", "0:1:2:3:4:5\n", "001a2bcdeff0"}) { + auto output = original; + Require(Failed(sceNetEtherStrton(text, &output.address), 22)); + Require(std::memcmp(&output, &original, sizeof(output)) == 0); + } + const std::string valid = "00:11:22:33:44:55"; + std::vector fixed(valid.begin(), valid.end()); + auto fixedOutput = original; + Require(sceNetEtherStrton(fixed.data(), &fixedOutput.address) == 0); + const std::array fixedExpected{0, 0x11, 0x22, 0x33, 0x44, 0x55}; + Require(std::memcmp(fixedOutput.address.data, fixedExpected.data(), fixedExpected.size()) == 0); + Require(fixedOutput.before == original.before && fixedOutput.after == original.after); + for (std::size_t length = 0; length < valid.size(); ++length) { + std::vector text(length + 1, '\0'); + std::memcpy(text.data(), valid.data(), length); + auto output = original; + Require(Failed(sceNetEtherStrton(text.data(), &output.address), 22)); + Require(std::memcmp(&output, &original, sizeof(output)) == 0); + } + for (std::size_t position = 0; position < valid.size(); ++position) { + auto text = valid; + text[position] = '\0'; + auto output = original; + Require(Failed(sceNetEtherStrton(text.c_str(), &output.address), 22)); + Require(std::memcmp(&output, &original, sizeof(output)) == 0); + } + auto output = original; + Require(Failed(sceNetEtherStrton(nullptr, &output.address), 22)); + Require(std::memcmp(&output, &original, sizeof(output)) == 0); + Require(Failed(sceNetEtherStrton(valid.c_str(), nullptr), 22)); + Require(Failed(sceNetEtherStrton(nullptr, nullptr), 22)); + for (std::size_t position = 0; position < valid.size(); ++position) { + auto text = valid; + text[position] = static_cast(0xff); + output = original; + bool threw = false; + try { + Require(sceNetEtherStrton(text.c_str(), &output.address) == 0); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw == (position % 3 != 2)); + if (threw) Require(std::memcmp(&output, &original, sizeof(output)) == 0); + else Require(std::memcmp(output.address.data, fixedExpected.data(), fixedExpected.size()) == 0); + Require(output.before == original.before && output.after == original.after); + } +} + static void CheckSystemTime() { *sceNetErrnoLoc() = 22; sceNetGetSystemTime(nullptr); @@ -912,6 +1055,8 @@ static void CheckUsleep() { Require(std::chrono::steady_clock::now() - start >= std::chrono::microseconds(2000)); *sceNetErrnoLoc() = 0; Require(Failed(sceNetUsleep(-1), 22)); + *sceNetErrnoLoc() = 0; + Require(Failed(sceNetUsleep(INT32_MIN), 22)); } static void CheckEpollIdent() { @@ -1444,6 +1589,7 @@ static void CheckDatagramDisconnect() { } int main() { + CheckEtherAddress(); const auto epoch = std::chrono::steady_clock::time_point{}; Require(KernelSocketSelect::DeadlineAfter(epoch, 2, 250000) == epoch + std::chrono::milliseconds(2250)); Require(KernelSocketSelect::DeadlineAfter(epoch, INT64_MAX, 0) == std::chrono::steady_clock::time_point::max()); @@ -1676,6 +1822,7 @@ int main() { stdio_readable[0] = 1; Require(select_nid_postfix(1, stdio_readable, nullptr, nullptr, poll_now) == 0 && stdio_readable[0] == 0); CheckReceiveTimeout(udp_receiver, udp_sender, address); + CheckStreamReceiveTimeout(); CheckMicrosecondTimeouts(udp_receiver); CheckMessages(udp_receiver, udp_sender, address); Require(sceNetSocketClose(udp_sender) == 0); @@ -1686,8 +1833,13 @@ int main() { int resolver_error = -1; Require(sceNetResolverGetError(resolver, &resolver_error) == 0 && resolver_error == 0); std::array ipv4{}; - Require(sceNetResolverStartNtoa(resolver, "guest-sce-net.invalid.", ipv4.data(), 5000000, 1, 0) == - static_cast(0x804101E1)); + ipv4.fill(0xA5); + { + ScopedResolverLookupOverride lookupOverride(FailResolverLookup); + Require(sceNetResolverStartNtoa(resolver, "guest-sce-net.invalid.", ipv4.data(), 5000000, 1, 0) == + static_cast(0x804101E1)); + } + Require(std::all_of(ipv4.begin(), ipv4.end(), [](std::uint8_t byte) { return byte == 0xA5; })); Require(sceNetResolverGetError(resolver, &resolver_error) == 0 && resolver_error == static_cast(0x804101E1)); Require(sceNetResolverStartNtoa(resolver, nullptr, ipv4.data(), 5000000, 1, 0) == static_cast(0x80410116)); @@ -1701,7 +1853,12 @@ int main() { Require(std::all_of(resolved6.begin(), resolved6.end() - 1, [](std::uint8_t byte) { return byte == 0; }) && resolved6[15] == 1); Require(sceNetResolverGetError(resolver, &resolver_error) == 0 && resolver_error == 0); Require(sceNetResolverStartNtoa6(resolver, nullptr, resolved6.data(), 5000000, 1, 0) == static_cast(0x80410116)); - Require(sceNetResolverStartNtoa6(resolver, "127.0.0.1", resolved6.data(), 5000000, 1, 0) == static_cast(0x804101E1)); + const auto resolved6BeforeFailure = resolved6; + { + ScopedResolverLookupOverride lookupOverride(FailResolverLookup); + Require(sceNetResolverStartNtoa6(resolver, "127.0.0.1", resolved6.data(), 5000000, 1, 0) == static_cast(0x804101E1)); + } + Require(resolved6 == resolved6BeforeFailure); Require(sceNetResolverGetError(resolver, &resolver_error) == 0 && resolver_error == static_cast(0x804101E1)); Require(sceNetResolverStartNtoa(resolver, "localhost", ipv4.data(), 5000000, 1, 0) == 0); Require(sceNetResolverGetError(resolver, &resolver_error) == 0 && resolver_error == 0); @@ -1718,6 +1875,7 @@ int main() { Require(std::all_of(records.begin() + 32 * record_count, records.begin() + 320, [](std::uint8_t byte) { return byte == 0; })); Require(std::all_of(records.begin() + 328, records.begin() + 384, [](std::uint8_t byte) { return byte == 0; })); Require(std::all_of(records.begin() + 384, records.end(), [](std::uint8_t byte) { return byte == 0xA5; })); + const auto resolvedRecords = records; for (int first = 0; first < record_count; ++first) { for (int second = first + 1; second < record_count; ++second) { Require(std::memcmp(records.data() + 32 * first, records.data() + 32 * second, 4) != 0); @@ -1728,8 +1886,12 @@ int main() { static_cast(0x80410116) && *sceNetErrnoLoc() == 22 && records == resolved); Require(sceNetResolverStartNtoaMultipleRecordsEx(resolver, "localhost", nullptr, 5000000, 1, 0) == static_cast(0x80410116) && *sceNetErrnoLoc() == 22); - Require(sceNetResolverStartNtoaMultipleRecordsEx(resolver, "guest-sce-net.invalid.", records.data(), 5000000, 1, 0) == - static_cast(0x804101E1) && records == resolved); + { + ScopedResolverLookupOverride lookupOverride(FailResolverLookup); + Require(sceNetResolverStartNtoaMultipleRecordsEx(resolver, "guest-sce-net.invalid.", records.data(), 5000000, 1, 0) == + static_cast(0x804101E1)); + } + Require(records == resolvedRecords); std::array plain{}; plain.fill(0xA5); @@ -1740,8 +1902,13 @@ int main() { static_cast(0x80410116) && *sceNetErrnoLoc() == 22 && plain == resolved); Require(sceNetResolverStartNtoaMultipleRecords(resolver, "localhost", nullptr, 5000000, 1, 0) == static_cast(0x80410116) && *sceNetErrnoLoc() == 22); - Require(sceNetResolverStartNtoaMultipleRecords(resolver, "guest-sce-net.invalid", plain.data(), 5000000, 1, 0) == - static_cast(0x804101E1) && plain == resolved); + const auto resolvedPlain = plain; + { + ScopedResolverLookupOverride lookupOverride(FailResolverLookup); + Require(sceNetResolverStartNtoaMultipleRecords(resolver, "guest-sce-net.invalid", plain.data(), 5000000, 1, 0) == + static_cast(0x804101E1)); + } + Require(plain == resolvedPlain); Require(sceNetResolverGetError(resolver, &resolver_error) == 0 && resolver_error == static_cast(0x804101E1)); for (const int flags : {0x1, 0x10000}) { bool threw = false; diff --git a/core/libs/tests/GuestScreenShot.cpp b/core/libs/tests/GuestScreenShot.cpp new file mode 100644 index 0000000000..836b70da94 --- /dev/null +++ b/core/libs/tests/GuestScreenShot.cpp @@ -0,0 +1,123 @@ +#include "ProcessParametersFixture.hpp" +#include "SceTypes.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceScreenShotDisable(); +int APS5_VABI sceScreenShotEnable(); +int APS5_VABI sceScreenShotIsDisabled(int id, int* isDisabled); +int APS5_VABI sceScreenShotDisableNotification(); +int APS5_VABI sceScreenShotEnableNotification(); +int APS5_VABI sceScreenShotSetOverlayImage(const char* path, int offsetX, int offsetY); +int APS5_VABI sceScreenShotSetOverlayImageWithOrigin(const char* path, int offsetX, int offsetY, int origin); +} + +namespace { + +constexpr int ErrorArgument = static_cast(0x80BE0001); +constexpr int ErrorOverlayFile = static_cast(0x80BE0003); +constexpr int ErrorOverlaySize = static_cast(0x80BE000E); + +alignas(16) std::array process{}; +std::string pathPrefix; + +void Require(bool condition, const char* message) { + if (condition) return; + std::fprintf(stderr, "ScreenShot: %s\n", message); + std::abort(); +} + +void InstallProcessParameters(std::uint32_t sdkVersion) { + const std::uint64_t size = process.size(); + const std::uint32_t magic = 0x4942524f; + std::memcpy(process.data(), &size, sizeof(size)); + std::memcpy(process.data() + 8, &magic, sizeof(magic)); + std::memcpy(process.data() + 0x10, &sdkVersion, sizeof(sdkVersion)); + Require(ProcessParametersFixture::Install(process.data(), process.size()), "process parameters were not installed"); +} + +class TempPath { +public: + explicit TempPath(const char* tag) : name(pathPrefix + tag) {} + ~TempPath() { + std::error_code error; + std::filesystem::remove_all(name, error); + } + const char* c_str() const { return name.c_str(); } + void Fill(std::size_t bytes) const { + std::ofstream file(name, std::ios::binary | std::ios::trunc); + const std::string chunk(4096, 'x'); + for (std::size_t written = 0; written < bytes; written += chunk.size()) { + file.write(chunk.data(), static_cast(std::min(chunk.size(), bytes - written))); + } + } + +private: + std::string name; +}; + +void TestDisableState() { + int value = -1; + Require(sceScreenShotIsDisabled(0, &value) == 0 && value == 0, "capture starts enabled"); + Require(sceScreenShotDisable() == 0, "disable"); + Require(sceScreenShotIsDisabled(0, &value) == 0 && value == 1, "disable is reported"); + Require(sceScreenShotEnable() == 0, "enable"); + Require(sceScreenShotIsDisabled(0, &value) == 0 && value == 0, "enable is reported"); + value = 7; + Require(sceScreenShotIsDisabled(-1, &value) == ErrorArgument && value == 7, "id -1 is rejected"); + Require(sceScreenShotIsDisabled(0, nullptr) == ErrorArgument, "null result is rejected"); + Require(sceScreenShotDisableNotification() == 0 && sceScreenShotEnableNotification() == 0, "notification toggles"); +} + +void TestOverlay(bool large) { + const int maxX = large ? 3839 : 1919; + const int maxY = large ? 2159 : 1079; + const std::size_t maxSize = large ? 8u << 20 : 2u << 20; + TempPath image("image"); + image.Fill(4096); + Require(sceScreenShotSetOverlayImage(image.c_str(), 0, 0) == 0, "overlay at the origin"); + Require(sceScreenShotSetOverlayImage(image.c_str(), maxX, maxY) == 0, "overlay at the last offsets"); + Require(sceScreenShotSetOverlayImage(image.c_str(), maxX + 1, 0) == ErrorArgument, "x past the screen"); + Require(sceScreenShotSetOverlayImage(image.c_str(), 0, maxY + 1) == ErrorArgument, "y past the screen"); + Require(sceScreenShotSetOverlayImage(image.c_str(), -1, 0) == ErrorArgument, "negative x"); + Require(sceScreenShotSetOverlayImage(nullptr, 0, 0) == ErrorArgument, "null path"); + const std::string longPath(1024, 'a'); + Require(sceScreenShotSetOverlayImage(longPath.c_str(), 0, 0) == ErrorArgument, "path of 1024 characters"); + TempPath missing("missing"); + Require(sceScreenShotSetOverlayImage(missing.c_str(), 0, 0) == ErrorOverlayFile, "missing file"); + TempPath directory("directory"); + std::filesystem::create_directory(directory.c_str()); + Require(sceScreenShotSetOverlayImage(directory.c_str(), 0, 0) == ErrorOverlayFile, "directory"); + TempPath empty("empty"); + empty.Fill(0); + Require(sceScreenShotSetOverlayImage(empty.c_str(), 0, 0) == ErrorOverlaySize, "empty file"); + TempPath full("full"); + full.Fill(maxSize); + Require(sceScreenShotSetOverlayImage(full.c_str(), 0, 0) == 0, "file at the size limit"); + full.Fill(maxSize + 1); + Require(sceScreenShotSetOverlayImage(full.c_str(), 0, 0) == ErrorOverlaySize, "file past the size limit"); + for (int origin = 1; origin <= 9; ++origin) Require(sceScreenShotSetOverlayImageWithOrigin(image.c_str(), 0, 0, origin) == 0, "origins 1 to 9"); + Require(sceScreenShotSetOverlayImageWithOrigin(image.c_str(), 0, 0, 0) == ErrorArgument, "origin 0"); + Require(sceScreenShotSetOverlayImageWithOrigin(image.c_str(), 0, 0, 10) == ErrorArgument, "origin 10"); + Require(sceScreenShotSetOverlayImageWithOrigin(nullptr, 0, 0, 0) == ErrorArgument, "origin checked first"); +} + +} + +int main(int argc, char** argv) { + const bool large = argc > 1 && std::strcmp(argv[1], "large") == 0; + pathPrefix = std::string("anyps5-screenshot-") + (large ? "large-" : "small-") + std::to_string(std::random_device{}()) + "-"; + InstallProcessParameters(large ? 0x3500000u : 0x34fffffu); + TestDisableState(); + TestOverlay(large); + return 0; +} diff --git a/core/libs/tests/GuestSignals.cpp b/core/libs/tests/GuestSignals.cpp index 7f4740ac07..a5ca92484b 100644 --- a/core/libs/tests/GuestSignals.cpp +++ b/core/libs/tests/GuestSignals.cpp @@ -36,6 +36,7 @@ struct GuestSigaction { GuestSignalSet mask; }; static_assert(sizeof(GuestSigaction) == 32); +extern GuestSignalSet _sigintr_nid_postfix; static bool Same(const GuestSigaction& left, const GuestSigaction& right) { return left.handler == right.handler && left.flags == right.flags && std::memcmp(&left.mask, &right.mask, sizeof(left.mask)) == 0; } @@ -398,4 +399,24 @@ int main() { Require(sigpending_nid_postfix(&pendingIgnored) == 0 && pendingIgnored.bits[0] == 0); Require(sigprocmask_nid_postfix(3, &noneBlocked, nullptr) == 0); Require(sigaction_nid_postfix(30, &restored, nullptr) == 0); + Require(_sigintr_nid_postfix.bits[0] == 0 && _sigintr_nid_postfix.bits[1] == 0 && _sigintr_nid_postfix.bits[2] == 0 && + _sigintr_nid_postfix.bits[3] == 0); + GuestSigaction interruptible{}; + Require(signal_nid_postfix(30, Callback) == nullptr); + Require(sigaction_nid_postfix(30, nullptr, &interruptible) == 0 && interruptible.flags == 0x2); + _sigintr_nid_postfix.bits[0] = 1u << 29; + Require(signal_nid_postfix(30, Callback) == Callback); + Require(sigaction_nid_postfix(30, nullptr, &interruptible) == 0 && + interruptible.handler == reinterpret_cast(Callback) && interruptible.flags == 0); + signal_nid_postfix(31, Callback); + Require(sigaction_nid_postfix(31, nullptr, &interruptible) == 0 && interruptible.flags == 0x2); + _sigintr_nid_postfix.bits[3] = 1u << 31; + Require(signal_nid_postfix(128, Callback) == nullptr); + Require(sigaction_nid_postfix(128, nullptr, &interruptible) == 0 && interruptible.flags == 0); + _sigintr_nid_postfix.bits[0] = 0; + _sigintr_nid_postfix.bits[3] = 0; + Require(signal_nid_postfix(30, Callback) == Callback); + Require(sigaction_nid_postfix(30, nullptr, &interruptible) == 0 && interruptible.flags == 0x2); + Require(signal_nid_postfix(30, nullptr) == Callback && signal_nid_postfix(31, nullptr) == Callback && + signal_nid_postfix(128, nullptr) == Callback); } diff --git a/core/libs/tests/GuestSocketPair.cpp b/core/libs/tests/GuestSocketPair.cpp index ec71ffc24c..0b827364c4 100644 --- a/core/libs/tests/GuestSocketPair.cpp +++ b/core/libs/tests/GuestSocketPair.cpp @@ -213,9 +213,9 @@ int main(int argc, char** argv) { Require(Exchange(pair[1], pair[0], "still open")); Require(close_nid_postfix(pair[0]) == 0); Require(Readable(pair[1], In | Hup) && recv_nid_postfix(pair[1], received, sizeof(received), 0) == 0); -#ifndef _WIN32 - Require(send_nid_postfix(pair[1], ping, sizeof(ping), NoSignal) == -1 && *__error_nid_postfix() == 32); -#endif + std::int64_t sent = 0; + for (int i = 0; i < 100 && sent >= 0; ++i) sent = send_nid_postfix(pair[1], ping, sizeof(ping), NoSignal); + Require(sent == -1 && *__error_nid_postfix() == 32); Require(close_nid_postfix(pair[1]) == 0); Require(close_nid_postfix(pair[1]) == -1 && *__error_nid_postfix() == 9); diff --git a/core/libs/tests/GuestSocketReadWrite.cpp b/core/libs/tests/GuestSocketReadWrite.cpp index 5a8d48781b..4af5913b09 100644 --- a/core/libs/tests/GuestSocketReadWrite.cpp +++ b/core/libs/tests/GuestSocketReadWrite.cpp @@ -148,11 +148,9 @@ int main() { Require(close_nid_postfix(client) == 0); Require(Readable(server, In | Hup) && read_nid_postfix(server, received, sizeof(received)) == 0); Require(sceKernelRead(server, received, sizeof(received)) == 0); -#ifndef _WIN32 std::int64_t written = 0; for (int i = 0; i < 100 && written >= 0; ++i) written = write_nid_postfix(server, request, sizeof(request)); Require(written == -1 && *__error_nid_postfix() == 32); -#endif Require(close_nid_postfix(server) == 0); *__error_nid_postfix() = 0; Require(read_nid_postfix(server, received, sizeof(received)) == -1 && *__error_nid_postfix() == 9); diff --git a/core/libs/tests/GuestSslTransport.cpp b/core/libs/tests/GuestSslTransport.cpp index 927dc47414..4d956c68be 100644 --- a/core/libs/tests/GuestSslTransport.cpp +++ b/core/libs/tests/GuestSslTransport.cpp @@ -39,6 +39,8 @@ int APS5_VABI sceSslSetVerifyCallback(int, int (APS5_VABI *)(int, unsigned int, int APS5_VABI sceSslConnect(int); int APS5_VABI sceSslRead(int, void*, std::size_t, int); int APS5_VABI sceSslWrite(int, void*, std::size_t, int); +int APS5_VABI sceSslRecv(int, void*, std::size_t); +int APS5_VABI sceSslSend(int, void*, std::size_t); int APS5_VABI sceSslClose(int); int APS5_VABI sceSslEnableVerifyOption(int, std::uint32_t); int APS5_VABI sceSslDisableVerifyOption(int, std::uint32_t); @@ -458,13 +460,10 @@ int main(int argc, char** argv) { Require(selected && std::strcmp(selected, "http/1.1") == 0, "the negotiated ALPN protocol is not reported"); Require(sceSslWrite(connection, buffer.data(), 1, 1) == ErrorInvalidValue, "unknown write flags were accepted"); char request[] = "ping"; - Require(sceSslWrite(connection, request, 4, 0) == 4, "write the request"); - Require(sceSslRead(connection, buffer.data(), buffer.size(), 4) == ErrorInvalidValue, "unknown read flags were accepted"); - Require(sceSslRead(0, nullptr, 1, -1) == ErrorInvalidValue, "read flags did not precede other validation"); - Require(sceSslRead(connection, buffer.data(), 2, 3) == 2 && std::memcmp(buffer.data(), "po", 2) == 0, "short peek failed"); - Require(sceSslRead(connection, buffer.data(), 4, 3) == 4 && std::memcmp(buffer.data(), "pong", 4) == 0, "repeated larger peek consumed bytes"); + Require(sceSslSend(connection, request, 4) == 4, "send the request"); Require(sceSslRead(connection, buffer.data(), 2, 1) == 2 && std::memcmp(buffer.data(), "po", 2) == 0, "a partial read did not return the available bytes"); - Require(sceSslRead(connection, buffer.data(), buffer.size(), 0) == 2 && std::memcmp(buffer.data(), "ng", 2) == 0, "a full read did not stop at the end of the stream"); + Require(sceSslRecv(connection, buffer.data(), 2) == 2 && std::memcmp(buffer.data(), "ng", 2) == 0, "receive did not fill the remaining reply"); + Require(sceSslRecv(connection, buffer.data(), buffer.size()) == 0, "receive did not stop at the end of the stream"); Require(sceSslRead(connection, buffer.data(), buffer.size(), 1) == 0, "close notify did not end the stream"); Require(sceSslClose(connection) == 0, "close the connection"); Require(sceSslRead(connection, buffer.data(), buffer.size(), 0) == 0, "a read after the close did not return 0"); diff --git a/core/libs/tests/GuestStrings.cpp b/core/libs/tests/GuestStrings.cpp index ef2f2f0a69..a30f57eb0a 100644 --- a/core/libs/tests/GuestStrings.cpp +++ b/core/libs/tests/GuestStrings.cpp @@ -131,6 +131,13 @@ static void CheckBoundsCheckedFunctions() { Require(memcpy_s_nid_postfix(bytes, 2, source, 4) == 34 && bytes[0] == 0 && bytes[1] == 0 && bytes[2] == 3); char overlap[6] = "abcde"; Require(memmove_s_nid_postfix(overlap + 1, 5, overlap, 3) == 0 && std::strcmp(overlap, "aabce") == 0); + char strOverlap[16] = "abcdef"; + Require(strncpy_s_nid_postfix(strOverlap + 1, 10, strOverlap, 3) == 22 && strOverlap[1] == '\0'); + char catOverlap[16] = "abcdef"; + Require(strncat_s_nid_postfix(catOverlap + 3, 10, catOverlap, 3) == 22 && catOverlap[3] == '\0'); + char nonOverlap[16] = {}; + std::strcpy(nonOverlap + 8, "cd"); + Require(strcpy_s_nid_postfix(nonOverlap, SIZE_MAX >> 1, nonOverlap + 8) == 0 && std::strcmp(nonOverlap, "cd") == 0); Require(memset_s_nid_postfix(bytes, sizeof(bytes), 9, 8) == 22 && bytes[0] == 9 && bytes[3] == 9); char sentinel[4] = {1, 2, 3, 4}; Require(memset_s_nid_postfix(sentinel, sizeof(sentinel), 7, 2) == 0 && sentinel[1] == 7 && sentinel[2] == 3); diff --git a/core/libs/tests/GuestSystemFont.cpp b/core/libs/tests/GuestSystemFont.cpp index f7f07156cf..392d3ad044 100644 --- a/core/libs/tests/GuestSystemFont.cpp +++ b/core/libs/tests/GuestSystemFont.cpp @@ -127,9 +127,9 @@ static void WriteFile(const std::filesystem::path& path, const std::vector= 1 && paramId <= 5) || paramId == 7 || paramId == 400 || paramId == 1000) continue; + if ((paramId >= 1 && paramId <= 5) || paramId == SYSTEM_SERVICE_PARAM_ID_100 || paramId == 400) continue; int value = -12345; - if ((paramId >= 100 && paramId <= 111) || (paramId >= 200 && paramId <= 213) || paramId == 300) { + if ((paramId >= 101 && paramId <= 111) || (paramId >= 200 && paramId <= 213) || paramId == 300) { bool threw = false; try { static_cast(sceSystemServiceParamGetInt(paramId, &value)); @@ -85,7 +85,14 @@ int main() { SetLanguage("arabic"); Require(sceSystemServiceParamGetInt(SYSTEM_SERVICE_PARAM_ID_LANG, nullptr) == SYSTEM_SERVICE_ERROR_PARAMETER); Require(sceSystemServiceParamGetInt(SYSTEM_SERVICE_PARAM_ID_400, nullptr) == SYSTEM_SERVICE_ERROR_PARAMETER); - int enterButton = -1; - Require(sceSystemServiceParamGetInt(SYSTEM_SERVICE_PARAM_ID_ENTER_BUTTON_ASSIGN, &enterButton) == SYSTEM_SERVICE_OK); - Require(enterButton == SYSTEM_SERVICE_PARAM_ENTER_BUTTON_CROSS); + for (int paramId : {0, SYSTEM_SERVICE_PARAM_ID_SYSTEM_NAME, SYSTEM_SERVICE_PARAM_ID_GAME_PARENTAL_LEVEL, 8, 20, 401, 999, + SYSTEM_SERVICE_PARAM_ID_ENTER_BUTTON_ASSIGN, 1001, 1008}) { + int unknown = -1; + Require(sceSystemServiceParamGetInt(paramId, &unknown) == SYSTEM_SERVICE_ERROR_PARAMETER); + Require(unknown == -1); + Require(sceSystemServiceParamGetInt(paramId, nullptr) == SYSTEM_SERVICE_ERROR_PARAMETER); + } + int param100 = -1; + Require(sceSystemServiceParamGetInt(SYSTEM_SERVICE_PARAM_ID_100, ¶m100) == SYSTEM_SERVICE_OK); + Require(param100 == 0); } diff --git a/core/libs/tests/GuestTcp.cpp b/core/libs/tests/GuestTcp.cpp index 332b9ebede..11fe4a9366 100644 --- a/core/libs/tests/GuestTcp.cpp +++ b/core/libs/tests/GuestTcp.cpp @@ -277,11 +277,9 @@ int main() { Require(send_nid_postfix(client, request, sizeof(request), 0x20000) == -1 && *__error_nid_postfix() == 32); Require(close_nid_postfix(accepted) == 0); -#ifndef _WIN32 std::int64_t sent = 0; for (int i = 0; i < 100 && sent >= 0; ++i) sent = send_nid_postfix(client, request, sizeof(request), 0x20000); Require(sent == -1 && *__error_nid_postfix() == 32); -#endif Require(close_nid_postfix(client) == 0); Require(close_nid_postfix(listener) == 0); Require(close_nid_postfix(listener) == -1 && *__error_nid_postfix() == 9); diff --git a/core/libs/tests/GuestUdp.cpp b/core/libs/tests/GuestUdp.cpp index ae2ba5fe9a..7efa4731f4 100644 --- a/core/libs/tests/GuestUdp.cpp +++ b/core/libs/tests/GuestUdp.cpp @@ -99,6 +99,31 @@ static void CheckAddressLengths(int family) { Require(close_nid_postfix(sender) == 0 && close_nid_postfix(receiver) == 0); } +static void CheckSendToPortZero() { + const int receiver = socket_nid_postfix(2, 2, 0); + const int sender = socket_nid_postfix(2, 2, 0); + Require(receiver >= 0 && sender >= 0); + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(bind_nid_postfix(receiver, address.data(), address.size()) == 0); + std::uint32_t size = address.size(); + Require(getsockname_nid_postfix(receiver, address.data(), &size) == 0); + std::array portZero{16, 2, 0, 0, 127, 0, 0, 1}; + Require(sendto_nid_postfix(sender, "x", 1, 0, portZero.data(), portZero.size()) == -1 && *__error_nid_postfix() == 49); + char buffer[4]; + Require(recv_nid_postfix(receiver, buffer, sizeof(buffer), 0x80) == -1 && *__error_nid_postfix() == 35); + Require(sendto_nid_postfix(sender, "x", 1, 0, address.data(), address.size()) == 1); + Require(recv_nid_postfix(receiver, buffer, sizeof(buffer), 0) == 1); + const int bound = socket_nid_postfix(2, 2, 0); + Require(bound >= 0); + std::array bindAddress{16, 2, 0, 0, 127, 0, 0, 1}; + Require(bind_nid_postfix(bound, bindAddress.data(), bindAddress.size()) == 0); + Require(sendto_nid_postfix(bound, "x", 1, 0, portZero.data(), portZero.size()) == 1); + Require(recv_nid_postfix(receiver, buffer, sizeof(buffer), 0x80) == -1 && *__error_nid_postfix() == 35); + std::array anyDestination{16, 2, 0, 0, 0, 0, 0, 0}; + Require(sendto_nid_postfix(bound, "x", 1, 0, anyDestination.data(), anyDestination.size()) == -1 && *__error_nid_postfix() == 49); + Require(close_nid_postfix(bound) == 0 && close_nid_postfix(receiver) == 0 && close_nid_postfix(sender) == 0); +} + static void CheckDontWait() { const int receiver = socket_nid_postfix(2, 2, 0); const int sender = socket_nid_postfix(2, 2, 0); @@ -136,6 +161,7 @@ int main() { CheckAddressLengths(2); CheckAddressLengths(28); CheckDontWait(); + CheckSendToPortZero(); const int receiver = socket_nid_postfix(2, 2, 0); const int sender = socket_nid_postfix(2, 2, 17); Require(receiver >= 0 && sender >= 0 && receiver != sender); diff --git a/core/libs/tests/GuestUltCondition.cpp b/core/libs/tests/GuestUltCondition.cpp index 5bec09da1b..3922c753ef 100644 --- a/core/libs/tests/GuestUltCondition.cpp +++ b/core/libs/tests/GuestUltCondition.cpp @@ -15,6 +15,7 @@ int APS5_VABI sceUltMutexCreate(void*, const char*, void*, const UltMutexOptPara int APS5_VABI sceUltMutexLock(void*); int APS5_VABI sceUltMutexUnlock(void*); int APS5_VABI sceUltMutexDestroy(void*); +int APS5_VABI sceUltConditionVariableCreate(void*, const char*, void*, const void*, std::uint32_t); int APS5_VABI _sceUltConditionVariableCreate(void*, const char*, void*, const void*, std::uint32_t); int APS5_VABI sceUltConditionVariableWait(void*); int APS5_VABI sceUltConditionVariableSignal(void*); @@ -65,6 +66,9 @@ static void Validation() { Object unused; CHECK(sceUltMutexDestroy(nullptr) == Null); CHECK(sceUltMutexDestroy(&unused) == State); + CHECK(sceUltConditionVariableCreate(nullptr, nullptr, nullptr, nullptr, 0) == Null); + CHECK(sceUltConditionVariableCreate(&unused, nullptr, &unused, nullptr, 0) == Invalid); + CHECK(sceUltConditionVariableCreate(&f.condition, nullptr, &f.mutex, nullptr, 0) == State); CHECK(_sceUltConditionVariableCreate(nullptr, nullptr, nullptr, nullptr, 0) == Null); CHECK(_sceUltConditionVariableCreate(&unused, nullptr, &unused, nullptr, 0) == Invalid); CHECK(_sceUltConditionVariableCreate(&f.condition, nullptr, &f.mutex, nullptr, 0) == State); diff --git a/core/libs/tests/GuestUltSync.cpp b/core/libs/tests/GuestUltSync.cpp index 2a368aa57b..7b2e1e51ed 100644 --- a/core/libs/tests/GuestUltSync.cpp +++ b/core/libs/tests/GuestUltSync.cpp @@ -25,6 +25,8 @@ int APS5_VABI sceUltUlthreadOptParamInitialize(void*, std::uint32_t); int APS5_VABI _sceUltWaitingQueueResourcePoolCreate(void*, const char*, std::uint32_t, std::uint32_t, void*, const void*); int APS5_VABI sceUltWaitingQueueResourcePoolDestroy(void*); int APS5_VABI sceUltReaderWriterLockOptParamInitialize(void*, std::uint32_t); +int APS5_VABI sceUltConditionVariableOptParamInitialize(void*, std::uint32_t); +int APS5_VABI _sceUltConditionVariableOptParamInitialize(void*, std::uint32_t); int APS5_VABI sceUltReaderWriterLockCreate(void*, const char*, void*, const void*, std::uint32_t); int APS5_VABI sceUltReaderWriterLockDestroy(void*); int APS5_VABI sceUltReaderWriterLockLockRead(void*); @@ -98,7 +100,8 @@ void MutexTryLock() { void OptParams() { using Initializer = int (APS5_VABI *)(void*, std::uint32_t); for (const Initializer initialize : {sceUltSemaphoreOptParamInitialize, sceUltQueueOptParamInitialize, sceUltQueueDataResourcePoolOptParamInitialize, - sceUltWaitingQueueResourcePoolOptParamInitialize, sceUltUlthreadOptParamInitialize, sceUltReaderWriterLockOptParamInitialize}) { + sceUltWaitingQueueResourcePoolOptParamInitialize, sceUltUlthreadOptParamInitialize, sceUltReaderWriterLockOptParamInitialize, + sceUltConditionVariableOptParamInitialize, _sceUltConditionVariableOptParamInitialize}) { std::uint8_t bytes[136]; std::memset(bytes, 0xa5, sizeof(bytes)); CHECK(initialize(nullptr, 0) == Null); diff --git a/core/libs/tests/GuestUsbd.cpp b/core/libs/tests/GuestUsbd.cpp index 59e59542b0..358e369eea 100644 --- a/core/libs/tests/GuestUsbd.cpp +++ b/core/libs/tests/GuestUsbd.cpp @@ -50,6 +50,7 @@ void APS5_VABI sceUsbdFillInterruptTransfer(void* transfer, void* deviceHandle, std::int32_t APS5_VABI sceUsbdCancelTransfer(void* transfer); std::int32_t APS5_VABI sceUsbdEventHandlingOk(); void APS5_VABI sceUsbdFreeConfigDescriptor(void* config); +void APS5_VABI sceUsbdFillControlSetup(std::uint8_t* buffer, std::uint8_t requestType, std::uint8_t request, std::uint16_t value, std::uint16_t index, std::uint16_t length); } namespace { @@ -293,6 +294,13 @@ int main(int argc, char** argv) { sceUsbdFreeTransfer(nullptr); Require(sceUsbdEventHandlingOk() == 1, "event handling not allowed"); sceUsbdFreeConfigDescriptor(nullptr); + Require(Throws([] { sceUsbdFillControlSetup(nullptr, 0x80, 6, 0x0100, 0, 18); }), "null control setup buffer accepted"); + std::uint8_t setupBuffer[8] = {}; + sceUsbdFillControlSetup(setupBuffer, 0x80, 6, 0x0100, 0x0409, 18); + Require(setupBuffer[0] == 0x80 && setupBuffer[1] == 6, "request type or request not filled"); + Require(setupBuffer[2] == 0x00 && setupBuffer[3] == 0x01, "value not filled little-endian"); + Require(setupBuffer[4] == 0x09 && setupBuffer[5] == 0x04, "index not filled little-endian"); + Require(setupBuffer[6] == 18 && setupBuffer[7] == 0, "length not filled little-endian"); if (argc == 2 && std::strcmp(argv[1], "device") == 0) Device(); sceUsbdExit(); std::puts("USBD tests passed"); diff --git a/core/libs/tests/GuestUserService.cpp b/core/libs/tests/GuestUserService.cpp index 4e6e1809a4..3e6d7559da 100644 --- a/core/libs/tests/GuestUserService.cpp +++ b/core/libs/tests/GuestUserService.cpp @@ -5,6 +5,8 @@ #include extern "C" { +int APS5_VABI sceUserServiceGetUserNumber(int user_id, int32_t* number); +int APS5_VABI sceUserServiceGetPlatformPrivacyWs1(int32_t user_id, int32_t* value); int APS5_VABI sceUserServiceGetForegroundUser(int* user_id); int APS5_VABI sceUserServiceGetRegisteredUserIdList(UserServiceRegisteredUserIdList* user_id_list); int APS5_VABI sceUserServiceGetUserColor(int user_id, int* color); @@ -28,6 +30,17 @@ void Require(bool value) { if (!value) std::abort(); } int main() { Require(sceUserServiceGetForegroundUser(nullptr) == USER_SERVICE_ERROR_INVALID_ARGUMENT); + int32_t userNumber = -1; + Require(sceUserServiceGetUserNumber(USER_SERVICE_INITIAL_USER_ID, nullptr) == USER_SERVICE_ERROR_INVALID_ARGUMENT); + Require(sceUserServiceGetUserNumber(123, &userNumber) == USER_SERVICE_ERROR_INVALID_ARGUMENT); + Require(sceUserServiceGetUserNumber(USER_SERVICE_INITIAL_USER_ID, &userNumber) == USER_SERVICE_OK); + Require(userNumber == 1); + + int32_t privacy = -1; + Require(sceUserServiceGetPlatformPrivacyWs1(USER_SERVICE_INITIAL_USER_ID, nullptr) == USER_SERVICE_ERROR_INVALID_ARGUMENT); + Require(sceUserServiceGetPlatformPrivacyWs1(USER_SERVICE_INITIAL_USER_ID, &privacy) == USER_SERVICE_OK); + Require(privacy == 0); + int foreground = -1; Require(sceUserServiceGetForegroundUser(&foreground) == USER_SERVICE_OK); Require(foreground == USER_SERVICE_INITIAL_USER_ID); diff --git a/core/libs/tests/GuestVdecsw.cpp b/core/libs/tests/GuestVdecsw.cpp index 14fdca44a9..6bb929a1e0 100644 --- a/core/libs/tests/GuestVdecsw.cpp +++ b/core/libs/tests/GuestVdecsw.cpp @@ -9,6 +9,7 @@ #include "prx/libc/include/General.hpp" #include "Vp9Fixture.hpp" #include "H264Fixture.hpp" +#include "HevcFixture.hpp" struct ComputeMemoryInfo { std::uint64_t thisSize; @@ -133,6 +134,51 @@ struct AvcPictureInfo { std::uint8_t vui[44]; }; +struct HevcPictureInfo { + std::uint64_t thisSize; + bool isValid; + std::uint64_t ptsData; + std::uint64_t dtsData; + std::uint64_t attachedData; + std::uint32_t picWidthInLumaSamples; + std::uint32_t picHeightInLumaSamples; + std::uint8_t bitDepthLumaMinus8; + std::uint8_t bitDepthChromaMinus8; + std::uint8_t timingInfoPresentFlag; + std::uint32_t numUnitsInTick; + std::uint32_t timeScale; + std::uint32_t aspectRatioInfoPresentFlag; + std::uint8_t aspectRatioIdc; + std::uint16_t sarWidth; + std::uint16_t sarHeight; + std::uint8_t videoSignal[8]; + std::uint32_t picStruct; + std::uint32_t sourceScanType; + std::uint32_t duplicateFlag; + std::uint32_t conformanceWindowFlag; + std::uint32_t confWinLeftOffset; + std::uint32_t confWinRightOffset; + std::uint32_t confWinTopOffset; + std::uint32_t confWinBottomOffset; + std::uint32_t defaultDisplayWindow[5]; + std::uint8_t chromaLocation[4]; + std::uint8_t presentFlags[11]; + std::uint8_t idrPictureFlag; + std::uint8_t irapPictureFlag; + std::uint8_t generalProfileSpace; + std::uint8_t generalTierFlag; + std::uint8_t generalProfileIdc; + std::uint8_t generalSourceFlags[3]; + std::uint8_t generalLevelIdc; + std::uint8_t subLayer[9]; + std::uint8_t frameCroppingFlag; + std::uint32_t frameCropLeftOffset; + std::uint32_t frameCropRightOffset; + std::uint32_t frameCropTopOffset; + std::uint32_t frameCropBottomOffset; +}; + +static_assert(sizeof(HevcPictureInfo) == 0xb8 && offsetof(HevcPictureInfo, idrPictureFlag) == 0x93 && offsetof(HevcPictureInfo, frameCroppingFlag) == 0xa5); static_assert(sizeof(DecoderConfigInfo) == 0x50 && sizeof(OutputInfo) == 0x38 && offsetof(OutputInfo, isLastFrame) == 9 && offsetof(OutputInfo, pictureCount) == 0xb && sizeof(AvcPictureInfo) == 0x78); extern "C" { @@ -150,7 +196,7 @@ int APS5_VABI sceVdecswTrySyncDecodeOutput(std::uint64_t, OutputInfo*); int APS5_VABI sceVdecswSyncDecodeOutput(std::uint64_t, OutputInfo*); int APS5_VABI sceVdecswGetVp9PictureInfo(const OutputInfo*, Vp9PictureInfo*); int APS5_VABI sceVdecswGetAvcPictureInfo(const OutputInfo*, AvcPictureInfo*, AvcPictureInfo*); -int APS5_VABI sceVdecswGetHevcPictureInfo(const OutputInfo*, void*); +int APS5_VABI sceVdecswGetHevcPictureInfo(const OutputInfo*, HevcPictureInfo*); int APS5_VABI sceVdecswFinalizeDecodeSequence(std::uint64_t); int APS5_VABI sceVdecswResetDecoder(std::uint64_t); } @@ -158,11 +204,15 @@ int APS5_VABI sceVdecswResetDecoder(std::uint64_t); namespace { using namespace H264Fixture; +using namespace HevcFixture; using namespace Vp9Fixture; constexpr int OutputPending = static_cast(0x81510115u); constexpr int InputQueueEmpty = static_cast(0x81510116u); +constexpr std::uint32_t CodecAvc = 1; +constexpr std::uint32_t CodecHevc = 974921; constexpr std::uint32_t CodecVp9 = 2382845; +constexpr std::array HevcDisplayOrderUnits{0, 3, 2, 1, 5, 4}; void check(bool condition, const std::string& message) { if (!condition) throw std::runtime_error(message); @@ -173,12 +223,12 @@ struct Session { std::uint64_t decoder = 0; DecoderMemoryInfo memory{sizeof(DecoderMemoryInfo)}; - Session() { + explicit Session(std::uint32_t codec = CodecAvc, std::uint32_t profile = 100, std::uint32_t level = 42) { ComputeMemoryInfo compute{sizeof(ComputeMemoryInfo)}; check(sceVdecswQueryComputeMemoryInfo(&compute) == 0 && compute.cpuGpuMemorySize != 0, "compute memory query failed"); const ComputeConfigInfo computeConfig{sizeof(ComputeConfigInfo), 3, 3, true}; check(sceVdecswAllocateComputeQueue(&computeConfig, &compute, &queue) == 0, "compute queue allocation failed"); - DecoderConfigInfo config{sizeof(DecoderConfigInfo), 1, 1, 100, 42, static_cast(Width), static_cast(Height), 4, 3, queue, 0x3f, 0, true, {}}; + DecoderConfigInfo config{sizeof(DecoderConfigInfo), 1, codec, profile, level, static_cast(Width), static_cast(Height), 4, 3, queue, 0x3f, 0, true, {}}; check(sceVdecswQueryDecoderMemoryInfo(&config, &memory) == 0 && memory.maxFrameBufferSize != 0, "decoder memory query failed"); check(sceVdecswCreateDecoder(&config, &memory, &decoder) == 0, "decoder creation failed"); } @@ -245,6 +295,76 @@ void testDecode() { check(sceVdecswTrySyncDecodeInput(session.decoder, &result) == InputQueueEmpty, "a finalized decoder did not report an empty input queue"); } +void testHevc() { + Session session(CodecHevc, 1, 30); + const auto units = hevcUnits(HevcStream.data(), HevcStream.size()); + check(units.size() == HevcHashes.size(), "unexpected HEVC access unit count"); + std::vector buffer(session.memory.maxFrameBufferSize); + std::size_t pictures = 0; + bool ended = false; + const auto drain = [&] { + for (;;) { + const FrameBuffer frame{sizeof(FrameBuffer), buffer.data(), buffer.size()}; + check(sceVdecswSetDecodeOutput(session.decoder, &frame) == 0, "HEVC output buffer was rejected"); + OutputInfo output{sizeof(OutputInfo)}; + const auto status = sceVdecswTrySyncDecodeOutput(session.decoder, &output); + if (status == OutputPending) return; + check(status == 0, "HEVC output sync failed"); + if (!output.isValid) { + ended = true; + return; + } + check(pictures < HevcHashes.size() && output.codecType == CodecHevc && output.pictureCount == 1 && output.frameWidth == Width && output.frameHeight == Height, "unexpected HEVC picture geometry"); + HevcPictureInfo info{sizeof(HevcPictureInfo)}; + check(sceVdecswGetHevcPictureInfo(&output, &info) == 0 && info.isValid, "HEVC picture info missing"); + const auto unit = HevcDisplayOrderUnits[pictures]; + check(info.ptsData == 1000 + unit && info.dtsData == unit && info.attachedData == 0xa0 + unit, "HEVC pictures out of display order"); + check(info.idrPictureFlag == (pictures == 0 ? 1 : 0) && info.irapPictureFlag == (pictures == 0 ? 1 : 0), "HEVC random access flags differ"); + check(info.generalProfileIdc == 1 && info.generalLevelIdc == 30 && info.bitDepthLumaMinus8 == 0 && info.bitDepthChromaMinus8 == 0, "HEVC profile, level or bit depth differs"); + check(info.picWidthInLumaSamples == Width && info.picHeightInLumaSamples == Height && info.conformanceWindowFlag == 0 && info.frameCroppingFlag == 0, "HEVC picture geometry in the picture info is wrong"); + AvcPictureInfo avc{sizeof(AvcPictureInfo)}; + bool avcThrows = false; + try { + sceVdecswGetAvcPictureInfo(&output, &avc, nullptr); + } catch (const std::exception&) { + avcThrows = true; + } + check(avcThrows, "AVC picture info was returned for an HEVC picture"); + check(hashNv12(buffer.data(), output.framePitch, output.frameHeight) == HevcHashes[pictures], "HEVC picture " + std::to_string(pictures) + " differs from the reference"); + ++pictures; + } + }; + for (std::size_t unit = 0; unit < units.size(); ++unit) { + const InputData input{sizeof(InputData), units[unit].data(), units[unit].size(), 1000 + unit, unit, 0xa0 + unit}; + check(sceVdecswSetDecodeInput(session.decoder, &input) == 0, "HEVC decode input was rejected"); + InputResult consumed{sizeof(InputResult)}; + check(sceVdecswTrySyncDecodeInput(session.decoder, &consumed) == 0 && consumed.decodedAu == units[unit].data(), "an HEVC access unit was not consumed"); + drain(); + } + check(sceVdecswFinalizeDecodeSequence(session.decoder) == 0, "HEVC finalize failed"); + while (!ended) drain(); + check(pictures == HevcHashes.size(), "missing HEVC pictures after finalizing the sequence"); +} + +void testHevcCrop() { + Session session(CodecHevc, 1, 30); + std::vector buffer(session.memory.maxFrameBufferSize); + const FrameBuffer frame{sizeof(FrameBuffer), buffer.data(), buffer.size()}; + check(sceVdecswSetDecodeOutput(session.decoder, &frame) == 0, "cropped HEVC output buffer was rejected"); + const InputData input{sizeof(InputData), HevcCroppedStream.data(), HevcCroppedStream.size(), 1000, 0, 0xa0}; + check(sceVdecswSetDecodeInput(session.decoder, &input) == 0, "cropped HEVC decode input was rejected"); + InputResult consumed{sizeof(InputResult)}; + check(sceVdecswSyncDecodeInput(session.decoder, &consumed) == 0, "the cropped HEVC access unit was not consumed"); + check(sceVdecswFinalizeDecodeSequence(session.decoder) == 0, "cropped HEVC finalize failed"); + OutputInfo output{sizeof(OutputInfo)}; + check(sceVdecswSyncDecodeOutput(session.decoder, &output) == 0 && output.isValid && output.frameWidth == 64 && output.frameHeight == 40, "unexpected cropped HEVC picture geometry"); + HevcPictureInfo info{sizeof(HevcPictureInfo)}; + check(sceVdecswGetHevcPictureInfo(&output, &info) == 0 && info.isValid, "cropped HEVC picture info missing"); + check(info.picWidthInLumaSamples == 64 && info.picHeightInLumaSamples == 40, "cropped HEVC coded size differs"); + check(info.conformanceWindowFlag == 1 && info.confWinLeftOffset == 0 && info.confWinRightOffset == 0 && info.confWinTopOffset == 0 && info.confWinBottomOffset == 2, "HEVC conformance window differs"); + check(info.frameCroppingFlag == 1 && info.frameCropLeftOffset == 0 && info.frameCropRightOffset == 0 && info.frameCropTopOffset == 0 && info.frameCropBottomOffset == 2, "HEVC frame cropping differs"); +} + void testWithoutOutput() { Session session; const auto units = accessUnits(false); @@ -278,13 +398,14 @@ void testSyncWrappers() { check(sceVdecswFinalizeDecodeSequence(session.decoder) == 0, "finalize failed"); OutputInfo output{sizeof(OutputInfo)}; check(sceVdecswSyncDecodeOutput(session.decoder, &output) == 0 && output.isValid && output.isLastFrame, "sync output did not return the finalized picture"); + HevcPictureInfo hevc{sizeof(HevcPictureInfo)}; bool hevcThrows = false; try { - sceVdecswGetHevcPictureInfo(&output, &result); + sceVdecswGetHevcPictureInfo(&output, &hevc); } catch (const std::exception&) { hevcThrows = true; } - check(hevcThrows, "GetHevcPictureInfo must still throw"); + check(hevcThrows, "HEVC picture info was returned for an H.264 picture"); } struct Vp9Session { @@ -457,9 +578,29 @@ void testVp9PictureInfoOfAvcPicture() { } +void testHevcMain10() { + Session session(CodecHevc, 2, 30); + std::vector buffer(session.memory.maxFrameBufferSize); + const FrameBuffer frame{sizeof(FrameBuffer), buffer.data(), buffer.size()}; + check(sceVdecswSetDecodeOutput(session.decoder, &frame) == 0, "Main 10 HEVC output buffer was rejected"); + const InputData input{sizeof(InputData), HevcMain10Stream.data(), HevcMain10Stream.size(), 1000, 0, 0xa0}; + check(sceVdecswSetDecodeInput(session.decoder, &input) == 0, "Main 10 HEVC decode input was rejected"); + InputResult consumed{sizeof(InputResult)}; + check(sceVdecswSyncDecodeInput(session.decoder, &consumed) == 0, "the Main 10 HEVC access unit was not consumed"); + check(sceVdecswFinalizeDecodeSequence(session.decoder) == 0, "Main 10 HEVC finalize failed"); + OutputInfo output{sizeof(OutputInfo)}; + check(sceVdecswSyncDecodeOutput(session.decoder, &output) == 0 && output.isValid, "the Main 10 HEVC picture was not output"); + HevcPictureInfo info{sizeof(HevcPictureInfo)}; + check(sceVdecswGetHevcPictureInfo(&output, &info) == 0 && info.isValid, "Main 10 HEVC picture info missing"); + check(info.generalProfileIdc == 2 && info.bitDepthLumaMinus8 == 2 && info.bitDepthChromaMinus8 == 2, "Main 10 HEVC profile or bit depth differs"); +} + int main() { try { testDecode(); + testHevc(); + testHevcCrop(); + testHevcMain10(); testWithoutOutput(); testSyncWrappers(); testVp9PictureInfo(); diff --git a/core/libs/tests/GuestVideodec.cpp b/core/libs/tests/GuestVideodec.cpp index f4ad8e987e..894d6f96ee 100644 --- a/core/libs/tests/GuestVideodec.cpp +++ b/core/libs/tests/GuestVideodec.cpp @@ -83,8 +83,33 @@ struct InputData { std::uint64_t attachedData; }; +struct Decoder2ConfigInfo { + std::uint64_t thisSize; + std::uint32_t resourceType; + std::uint32_t codecType; + std::uint32_t profile; + std::uint32_t maxLevel; + std::int32_t maxFrameWidth; + std::int32_t maxFrameHeight; + std::int32_t maxDpbFrameCount; + std::uint32_t decodePipelineDepth; +}; + +struct Decoder2MemoryInfo { + std::uint64_t thisSize; + std::uint64_t cpuMemorySize; + void* cpuMemory; + std::uint64_t gpuMemorySize; + void* gpuMemory; + std::uint64_t cpuGpuMemorySize; + void* cpuGpuMemory; + std::uint64_t maxFrameBufferSize; + std::uint32_t frameBufferAlignment; +}; + static_assert(sizeof(PictureInfo) == 112 && sizeof(AvcInfo) == 36); static_assert(sizeof(ConfigInfo) == 40 && sizeof(ResourceInfo) == 56 && sizeof(Ctrl) == 24); +static_assert(sizeof(Decoder2ConfigInfo) == 40 && sizeof(Decoder2MemoryInfo) == 72); extern "C" { int APS5_VABI sceVideodecCreateDecoder(const ConfigInfo*, const ResourceInfo*, Ctrl*); @@ -92,6 +117,8 @@ int APS5_VABI sceVideodecDecode(Ctrl*, const InputData*, FrameBuffer*, PictureIn int APS5_VABI sceVideodecDeleteDecoder(Ctrl*); int APS5_VABI sceVideodecFlush(Ctrl*, FrameBuffer*, PictureInfo*); int APS5_VABI sceVideodecReset(Ctrl*); +int APS5_VABI sceVideodecQueryResourceInfo(const ConfigInfo*, ResourceInfo*); +int APS5_VABI sceVideodec2QueryDecoderMemoryInfo_nid_postfix(const Decoder2ConfigInfo*, Decoder2MemoryInfo*); } namespace { @@ -105,6 +132,7 @@ constexpr int ErrorAuSize = static_cast(0x80C10009); constexpr int ErrorAuPointer = static_cast(0x80C1000A); constexpr int ErrorFrameBufferSize = static_cast(0x80C1000B); constexpr int ErrorFrameBufferPointer = static_cast(0x80C1000C); +constexpr int ErrorConfigInfo = static_cast(0x80C1000E); constexpr int ErrorArgumentPointer = static_cast(0x80C1000F); constexpr std::uint32_t Pitch = 64; @@ -230,6 +258,50 @@ void testReset() { check(!picture.isValid, "a reset decoder produced a picture"); } +void testQueryResourceInfo(std::int32_t width, std::int32_t height, std::uint32_t profile, std::uint32_t level) { + const ConfigInfo config{sizeof(ConfigInfo), 1, profile, level, width, height, 4, 0}; + ResourceInfo resource{sizeof(ResourceInfo)}; + check(sceVideodecQueryResourceInfo(&config, &resource) == 0, "the resource query failed"); + const Decoder2ConfigInfo mapped{sizeof(Decoder2ConfigInfo), 1, 1, profile, level, width, height, 4, 1}; + Decoder2MemoryInfo memory{sizeof(Decoder2MemoryInfo)}; + check(sceVideodec2QueryDecoderMemoryInfo_nid_postfix(&mapped, &memory) == 0, "the Videodec2 memory query failed"); + check(resource.thisSize == sizeof(ResourceInfo), "the resource information size was changed"); + check(resource.cpuMemorySize == memory.cpuMemorySize, "the CPU memory size differs from libSceVideodec2"); + check(resource.cpuGpuMemorySize == memory.cpuGpuMemorySize, "the CPU-GPU memory size differs from libSceVideodec2"); + check(resource.maxFrameBufferSize == memory.maxFrameBufferSize, "the frame buffer size differs from libSceVideodec2"); + check(resource.frameBufferAlignment == memory.frameBufferAlignment, "the frame buffer alignment differs from libSceVideodec2"); +} + +void testQueryResourceInfo() { + testQueryResourceInfo(static_cast(Width), static_cast(CodedHeight), 100, 42); + testQueryResourceInfo(1920, 1080, 100, 42); + testQueryResourceInfo(3840, 2160, 100, 51); + const ConfigInfo config{sizeof(ConfigInfo), 1, 100, 42, static_cast(Width), + static_cast(CodedHeight), 4, 0}; + ResourceInfo resource{sizeof(ResourceInfo)}; + check(sceVideodecQueryResourceInfo(&config, &resource) == 0, "the resource query failed"); + check(resource.maxFrameBufferSize >= FrameBytes, "the frame buffer size does not hold a decoded picture"); + Ctrl ctrl{sizeof(Ctrl)}; + check(sceVideodecCreateDecoder(&config, &resource, &ctrl) == 0, "the queried resources were refused"); + check(sceVideodecDeleteDecoder(&ctrl) == 0, "the decoder was not deleted"); + check(sceVideodecQueryResourceInfo(nullptr, &resource) == ErrorArgumentPointer, "a null configuration was accepted"); + check(sceVideodecQueryResourceInfo(&config, nullptr) == ErrorArgumentPointer, "null resource information was accepted"); + const ResourceInfo sentinel{sizeof(ResourceInfo), 7, nullptr, 7, nullptr, 7, 7}; + auto refused = sentinel; + auto shortConfig = config; + shortConfig.thisSize = sizeof(ConfigInfo) - 8; + check(sceVideodecQueryResourceInfo(&shortConfig, &refused) == ErrorStructSize, "a short configuration was accepted"); + auto shortResource = sentinel; + shortResource.thisSize = sizeof(ResourceInfo) - 8; + check(sceVideodecQueryResourceInfo(&config, &shortResource) == ErrorStructSize, "short resource information was accepted"); + auto unsized = config; + unsized.maxFrameWidth = 0; + check(sceVideodecQueryResourceInfo(&unsized, &refused) == ErrorConfigInfo, "a configuration without a frame size was accepted"); + check(refused.cpuMemorySize == 7 && refused.cpuGpuMemorySize == 7 && refused.maxFrameBufferSize == 7 && refused.frameBufferAlignment == 7, + "a refused query wrote the resource information"); + check(shortResource.cpuMemorySize == 7 && shortResource.maxFrameBufferSize == 7, "a refused query wrote the resource information"); +} + } int main() { @@ -238,6 +310,7 @@ int main() { testDecodeCropped(); testErrors(); testReset(); + testQueryResourceInfo(); std::puts("Videodec tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/tests/GuestVideodec2.cpp b/core/libs/tests/GuestVideodec2.cpp index 2cbaf79f8b..565f4454b4 100644 --- a/core/libs/tests/GuestVideodec2.cpp +++ b/core/libs/tests/GuestVideodec2.cpp @@ -11,6 +11,7 @@ #include #include "prx/libc/include/General.hpp" #include "H264Fixture.hpp" +#include "HevcFixture.hpp" #include "Vp9Fixture.hpp" struct DecoderConfigInfo { @@ -119,95 +120,54 @@ int APS5_VABI sceVideodec2GetAvcPictureInfo_nid_postfix(const OutputInfo*, AvcPi namespace { using namespace H264Fixture; - +using namespace HevcFixture; constexpr std::uint32_t CodecHevc = 974921; constexpr std::uint32_t CodecVp9 = 2382845; -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, - 0x95, 0x90, 0x09, 0x00, 0x00, 0x00, 0x01, 0x42, 0x01, 0x01, 0x01, 0x60, 0x00, 0x00, 0x03, 0x00, - 0x90, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x1e, 0xa0, 0x20, 0x82, 0x96, 0x59, 0x59, 0x2e, - 0x4c, 0xaf, 0x01, 0x68, 0x08, 0x00, 0x00, 0x03, 0x00, 0x08, 0x00, 0x00, 0x03, 0x00, 0xf0, 0x40, - 0x00, 0x00, 0x00, 0x01, 0x44, 0x01, 0xc1, 0x73, 0xd0, 0x89, 0x00, 0x00, 0x01, 0x28, 0x01, 0xaf, - 0x08, 0xe0, 0xbb, 0xcb, 0xc1, 0x71, 0x26, 0x0f, 0xbf, 0x6c, 0xcb, 0x4d, 0x2b, 0x2c, 0xf4, 0x08, - 0xe0, 0x17, 0xeb, 0x16, 0xcc, 0x07, 0xd8, 0xdd, 0x11, 0xf8, 0x5e, 0x73, 0x79, 0x57, 0xcb, 0x69, - 0xa3, 0x1c, 0x9c, 0x43, 0x14, 0xdb, 0x61, 0x0e, 0xc5, 0xc6, 0x9c, 0x9c, 0xf8, 0xf2, 0x07, 0x35, - 0xf1, 0x98, 0x6c, 0x1a, 0x86, 0x53, 0x5a, 0xcd, 0x4d, 0xda, 0xff, 0xfb, 0x01, 0xda, 0xc3, 0x61, - 0xb7, 0x8c, 0xae, 0xc9, 0x76, 0xe4, 0x05, 0xd6, 0xfd, 0xe4, 0xcd, 0x93, 0x2c, 0xb1, 0x41, 0x29, - 0xce, 0x22, 0xf7, 0x77, 0x64, 0x0f, 0x1f, 0xd2, 0xc3, 0x68, 0x32, 0x2f, 0x83, 0xd7, 0x53, 0xbe, - 0x23, 0xff, 0x8c, 0x46, 0x18, 0x33, 0x90, 0x28, 0xa3, 0xef, 0xd7, 0xb1, 0x46, 0x84, 0xa0, 0xc2, - 0xd4, 0xd9, 0xce, 0xdf, 0x0a, 0x05, 0xec, 0xad, 0x63, 0xa4, 0x78, 0x27, 0x80, 0x4e, 0x3b, 0x4c, - 0xa6, 0x42, 0xbc, 0xe3, 0x39, 0x4d, 0xc4, 0xd3, 0x44, 0xf1, 0x22, 0xd0, 0x64, 0x9f, 0xd0, 0x88, - 0x7f, 0x39, 0x6d, 0x06, 0xc7, 0x79, 0x75, 0x2a, 0x44, 0x3d, 0x21, 0x41, 0x53, 0x60, 0x8d, 0x08, - 0x9f, 0xd0, 0x34, 0xa6, 0x72, 0x70, 0x3b, 0xa9, 0xfa, 0x62, 0xb1, 0x19, 0x9b, 0x0b, 0xa2, 0xfc, - 0xb4, 0xb3, 0x2c, 0x58, 0xad, 0xc6, 0xf3, 0xea, 0xc4, 0xd0, 0xf9, 0xea, 0x9e, 0xa7, 0xa4, 0x87, - 0xb8, 0x19, 0x68, 0xf1, 0xc1, 0x6b, 0x87, 0xa5, 0x99, 0xa0, 0x69, 0x24, 0x28, 0x80, 0xba, 0xd2, - 0x57, 0xc6, 0xc0, 0x1e, 0x69, 0x1b, 0xce, 0x96, 0xcd, 0x85, 0x9d, 0x59, 0x32, 0x92, 0x71, 0xe7, - 0x15, 0xf2, 0x9b, 0x71, 0x4b, 0xd4, 0xb6, 0x99, 0xb0, 0x71, 0x9b, 0x9b, 0xdb, 0x33, 0x28, 0x86, - 0x5e, 0x44, 0xf3, 0x5d, 0x5d, 0x7b, 0x87, 0x6c, 0x99, 0x8a, 0x6d, 0xaf, 0xf8, 0xdd, 0x39, 0x14, - 0xc0, 0x10, 0x85, 0xaf, 0x3a, 0x77, 0x60, 0x8b, 0xb7, 0x5f, 0x67, 0x45, 0xb4, 0xa7, 0xe7, 0x44, - 0x77, 0x0a, 0xb9, 0xe1, 0xb2, 0xbe, 0xde, 0x1b, 0x5a, 0xb8, 0xc8, 0x9b, 0x1f, 0x15, 0x37, 0xdc, - 0xeb, 0x55, 0x12, 0x37, 0xa1, 0xab, 0x9c, 0xcd, 0xdd, 0x42, 0x5d, 0x90, 0x89, 0xc5, 0x99, 0x54, - 0x6e, 0x9a, 0xd2, 0x71, 0x07, 0xe3, 0x81, 0x20, 0xad, 0xd4, 0x7c, 0xad, 0x69, 0xaf, 0xc7, 0x2d, - 0x7c, 0x84, 0xf1, 0x5d, 0x43, 0x4a, 0x3c, 0xf6, 0xf8, 0x7e, 0x6a, 0xb2, 0xdd, 0x00, 0xda, 0x30, - 0x00, 0x00, 0x00, 0x01, 0x46, 0x01, 0x30, 0x00, 0x00, 0x01, 0x02, 0x01, 0xd0, 0x19, 0x5f, 0x84, - 0x31, 0x84, 0x50, 0x62, 0xcd, 0x44, 0xa6, 0x4e, 0xbb, 0x2e, 0x87, 0xda, 0x21, 0x25, 0x1c, 0x13, - 0xfe, 0xc2, 0x97, 0xa3, 0xd5, 0xfd, 0xe2, 0x62, 0xd6, 0xe5, 0x50, 0x7a, 0xda, 0x98, 0x8d, 0xcf, - 0xbe, 0x40, 0x5d, 0x69, 0xb6, 0x5f, 0xdc, 0x32, 0x68, 0x05, 0x67, 0x70, 0xfd, 0xa0, 0xd3, 0xcd, - 0x88, 0xeb, 0x12, 0x2e, 0xac, 0xf5, 0x57, 0xa2, 0xc2, 0x81, 0x22, 0x6b, 0x8d, 0x69, 0x6d, 0xac, - 0x41, 0x05, 0x5b, 0x0b, 0x79, 0xe0, 0x86, 0x05, 0x86, 0x8b, 0x02, 0x51, 0x7d, 0x6e, 0x13, 0x63, - 0x36, 0xe6, 0x89, 0x22, 0x75, 0xd9, 0xb0, 0x39, 0x27, 0xcd, 0xac, 0x78, 0xe0, 0x3a, 0x6a, 0x17, - 0x15, 0x05, 0x83, 0x3a, 0xe9, 0x38, 0x7c, 0x75, 0x38, 0xf3, 0xd8, 0x42, 0x57, 0xa1, 0xef, 0x68, - 0xb8, 0xb5, 0x44, 0xfe, 0x4d, 0x66, 0xa1, 0x09, 0xc5, 0x62, 0xff, 0x87, 0x7e, 0xdb, 0x41, 0x6f, - 0x4d, 0x5f, 0x16, 0x49, 0xef, 0xca, 0xda, 0x67, 0x01, 0x29, 0x0b, 0xd6, 0xd8, 0xfc, 0x0b, 0x22, - 0xc8, 0x28, 0xff, 0xb4, 0xb3, 0xa9, 0xf8, 0x9b, 0x78, 0x7d, 0x33, 0xbf, 0xa6, 0x9b, 0x39, 0x64, - 0x57, 0x41, 0x1f, 0x27, 0x05, 0xfc, 0xcd, 0x44, 0xcd, 0xb2, 0xea, 0x79, 0xe0, 0x00, 0x00, 0x00, - 0x01, 0x46, 0x01, 0x50, 0x00, 0x00, 0x01, 0x02, 0x01, 0xe0, 0x44, 0x97, 0xe1, 0x8f, 0x40, 0x3d, - 0x73, 0xde, 0xdf, 0xf5, 0x1c, 0xff, 0xf6, 0xe2, 0x12, 0x16, 0x3d, 0xb6, 0x75, 0xad, 0x9e, 0xb5, - 0x2b, 0xe7, 0x4a, 0x40, 0xe9, 0x91, 0xd6, 0x86, 0x86, 0x2d, 0xcb, 0xe4, 0xb6, 0x58, 0x4a, 0x2d, - 0x7b, 0x1b, 0xdb, 0xc5, 0x48, 0x2d, 0x09, 0x4d, 0x32, 0x30, 0xde, 0xaa, 0x61, 0xc2, 0x27, 0x64, - 0x54, 0x71, 0x89, 0x32, 0x05, 0xc0, 0x00, 0x00, 0x00, 0x01, 0x46, 0x01, 0x50, 0x00, 0x00, 0x01, - 0x00, 0x01, 0xe0, 0x24, 0xff, 0xfa, 0x2c, 0x6e, 0xdc, 0x78, 0x3d, 0x08, 0x6d, 0x3b, 0xcf, 0xcc, - 0xb6, 0x16, 0xeb, 0x9d, 0x5f, 0x46, 0x0b, 0x24, 0x0e, 0x97, 0x99, 0x06, 0x00, 0x00, 0x00, 0x01, - 0x46, 0x01, 0x30, 0x00, 0x00, 0x01, 0x02, 0x01, 0xd0, 0x28, 0x95, 0xd7, 0xdc, 0x41, 0xc0, 0x61, - 0x14, 0xd6, 0xee, 0xd4, 0xe8, 0x24, 0x3e, 0x7f, 0x82, 0x22, 0xb0, 0x20, 0xb9, 0xf1, 0x69, 0x9b, - 0xfe, 0xcc, 0xb7, 0x2e, 0xbd, 0xdb, 0x20, 0xfa, 0xe5, 0x97, 0xd9, 0x95, 0x1d, 0x7e, 0x49, 0x35, - 0x69, 0xdb, 0x40, 0xa3, 0x5c, 0x0f, 0x74, 0x6f, 0x00, 0x3d, 0x36, 0x05, 0xc0, 0xc4, 0x37, 0x15, - 0xf9, 0x01, 0x2d, 0xdf, 0xcb, 0x70, 0x3d, 0xfa, 0x47, 0xec, 0x80, 0x07, 0x48, 0x08, 0xc8, 0x54, - 0xd3, 0xe3, 0x00, 0xa3, 0xe4, 0xce, 0x40, 0x05, 0x0b, 0x51, 0x73, 0x56, 0x33, 0x82, 0xa8, 0x0a, - 0xc0, 0xd3, 0xc5, 0xf1, 0xeb, 0x32, 0x8e, 0x4e, 0xa7, 0x6f, 0xb2, 0x64, 0x43, 0x54, 0x71, 0xd4, - 0xaf, 0xc0, 0x00, 0x00, 0x00, 0x01, 0x46, 0x01, 0x50, 0x00, 0x00, 0x01, 0x00, 0x01, 0xe0, 0x82, - 0x2f, 0x5f, 0xdc, 0x63, 0x70, 0x3f, 0xaf, 0x66, 0x75, 0x52, 0xe7, 0x7f, 0x62, 0xfb, 0x84, 0x9c, - 0xef, 0x60, 0x8f, 0xfa, 0x7e, 0x5f, 0x8b, 0x40, 0x15, 0x34, 0x69, 0xe2, 0xc4, 0xfc, 0xff, 0x53, - 0xb8, 0x1e, 0xc7, 0x20, 0x55, 0xdb, 0x5e, 0x05, 0xbd, 0x7a, 0x85, 0xb4, 0xb7, 0xc1, 0x8d, 0xdb, - 0x0f, 0xa6, 0xab, 0xb9, 0x00, 0x2a, 0xdb, 0x83, 0xa0, 0xb0, 0xc5, 0x76, 0x6e, 0xe1, 0xb8, 0x72, - 0xfd, 0x09, 0x61, 0x19, 0xe0, -}; - -constexpr std::array HevcHashes{ - 0x7291c31ee57c5065ull, 0x37b15adc47810098ull, 0xc84737b65a65b247ull, - 0xe39bc891f8286267ull, 0x734caf1ba961b3c2ull, 0x7deb44bd1ed30531ull, -}; - -constexpr std::array HevcMain10Stream{ - 0x00, 0x00, 0x00, 0x01, 0x46, 0x01, 0x10, 0x00, 0x00, 0x00, 0x01, 0x40, 0x01, 0x0c, 0x01, 0xff, - 0xff, 0x02, 0x20, 0x00, 0x00, 0x03, 0x00, 0x90, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x1e, - 0x95, 0x98, 0x09, 0x00, 0x00, 0x00, 0x01, 0x42, 0x01, 0x01, 0x02, 0x20, 0x00, 0x00, 0x03, 0x00, - 0x90, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x1e, 0xa0, 0x20, 0x82, 0x93, 0x65, 0x95, 0x9a, - 0xe4, 0xca, 0xf0, 0x16, 0x80, 0x80, 0x00, 0x00, 0x03, 0x00, 0x80, 0x00, 0x00, 0x0f, 0x04, 0x00, - 0x00, 0x00, 0x01, 0x44, 0x01, 0xc1, 0x73, 0xd0, 0x89, 0x00, 0x00, 0x01, 0x28, 0x01, 0xaf, 0x08, - 0x60, 0xf2, 0xfb, 0x04, 0xef, 0x67, 0x67, 0xdc, 0xc7, 0x8e, 0xc7, 0x42, 0x63, 0xeb, 0x7d, 0x28, - 0x73, 0x95, 0x14, 0x0c, 0x71, 0x10, 0x8b, 0x65, 0x04, 0xbd, 0x03, 0xe6, 0x04, 0x35, 0xb9, 0x9b, - 0xec, 0x60, 0x2e, 0xea, 0xcf, 0xd0, 0xdd, 0x2d, 0x3a, 0x62, 0x33, 0x9d, 0x7a, 0x74, 0xb0, 0xeb, - 0x0c, 0x06, 0xe6, 0xa5, 0x30, 0xa7, 0xea, 0xed, 0xc2, 0xa5, 0x8e, 0x7c, 0xda, 0x1b, 0x9d, 0xe4, - 0xac, 0x2b, 0x38, 0xdb, 0x9c, 0xe8, 0x51, 0x34, 0x46, 0xd2, 0x04, 0x1a, 0x77, 0x87, 0x7a, 0x19, - 0x70, 0xf9, 0x4f, 0xcb, 0x03, 0x3c, 0x74, 0x25, 0xb9, 0x92, 0x3b, 0x95, 0xe9, 0xd8, 0xaa, 0x9b, - 0x6b, 0x9b, 0x1f, 0x70, 0x25, 0x2a, 0xe7, 0x6c, 0xc3, 0x7e, 0x55, 0x39, 0xf5, 0x06, 0x5a, 0x23, - 0xb1, 0x2e, 0xf6, 0x01, 0xac, 0x71, 0xb9, 0x20, 0xb4, 0xef, 0x1d, 0x42, 0x7e, 0x0e, 0x01, 0x1d, - 0xaf, 0x0a, 0x13, 0x04, 0x8e, 0x03, 0x4d, 0x1e, 0xc4, 0xe0, -}; +constexpr std::array AvcIpppStream{ + 0x00, 0x00, 0x00, 0x01, 0x09, 0x10, 0x00, 0x00, 0x00, 0x01, 0x67, 0x42, 0xc0, 0x0a, 0xda, 0x11, + 0xec, 0x04, 0x40, 0x00, 0x00, 0x03, 0x00, 0x40, 0x00, 0x00, 0x0f, 0x03, 0xc4, 0x89, 0xa8, 0x00, + 0x00, 0x00, 0x01, 0x68, 0xce, 0x0f, 0x2c, 0x80, 0x00, 0x00, 0x01, 0x65, 0x88, 0x84, 0x09, 0x7f, + 0xe4, 0x5e, 0xd2, 0xf1, 0x40, 0x00, 0x40, 0x07, 0xc5, 0x00, 0x02, 0x50, 0x06, 0x46, 0x28, 0xcd, + 0x0f, 0x80, 0x38, 0x07, 0xff, 0xc0, 0x07, 0xd8, 0x76, 0x33, 0x23, 0x81, 0xce, 0x7d, 0x73, 0x08, + 0x2f, 0xc0, 0xff, 0x1f, 0xc1, 0x61, 0x01, 0x2f, 0xc1, 0x00, 0x01, 0xf0, 0x19, 0x8d, 0x80, 0x38, + 0xd1, 0x0d, 0xcc, 0x7a, 0x99, 0xdf, 0xff, 0xda, 0x00, 0x3d, 0x82, 0x8e, 0xf2, 0x8f, 0x22, 0x18, + 0x94, 0x07, 0x24, 0x94, 0x45, 0x22, 0x44, 0x5c, 0xb8, 0x7f, 0xe1, 0xa2, 0x99, 0xa3, 0xef, 0xe2, + 0x80, 0x01, 0x9f, 0x03, 0x81, 0x0e, 0x01, 0x80, 0x70, 0x00, 0x10, 0x05, 0x00, 0x04, 0x00, 0x02, + 0x00, 0x6e, 0xbf, 0x6f, 0x66, 0xdb, 0x0c, 0x3f, 0x7c, 0x5b, 0xcc, 0xf6, 0x6c, 0xcc, 0x30, 0xe1, + 0x8c, 0x1c, 0x00, 0x04, 0x04, 0x80, 0x00, 0x42, 0x73, 0xfd, 0x1f, 0xfe, 0x20, 0x00, 0x13, 0x00, + 0x01, 0x08, 0xf8, 0x80, 0x00, 0x20, 0x2c, 0x1f, 0x04, 0xbb, 0xf3, 0xc0, 0x23, 0xdd, 0x63, 0x6f, + 0xf9, 0xe1, 0x04, 0x82, 0x53, 0x90, 0x01, 0x89, 0xe5, 0x89, 0x44, 0x5a, 0xf8, 0x39, 0xe0, 0x03, + 0x20, 0x10, 0x50, 0x00, 0x25, 0xf9, 0x8d, 0xe0, 0x04, 0xf3, 0x62, 0xfd, 0x35, 0x7d, 0x77, 0x7e, + 0x0b, 0xe1, 0x4d, 0xfe, 0x85, 0x2b, 0xef, 0xe8, 0xf0, 0x80, 0xa2, 0x80, 0x03, 0x04, 0x00, 0x06, + 0xc0, 0x72, 0x80, 0x02, 0x8c, 0x14, 0xe0, 0x9d, 0xcd, 0xe8, 0x3f, 0xe9, 0xb5, 0x9b, 0x06, 0x4d, + 0x5e, 0x80, 0xa2, 0x1f, 0x06, 0xda, 0xdf, 0x36, 0xfd, 0xa1, 0x0f, 0xfe, 0x08, 0xb0, 0x26, 0x48, + 0xaf, 0x17, 0xe9, 0xc5, 0x9f, 0xbf, 0xba, 0x2e, 0x69, 0xa9, 0xe6, 0x7c, 0xbf, 0x4f, 0xfe, 0xc1, + 0x64, 0x0a, 0xaa, 0x69, 0xda, 0x43, 0xff, 0xf9, 0xfd, 0xfc, 0xff, 0x77, 0xe9, 0x1e, 0x20, 0x18, + 0x30, 0x1f, 0x80, 0x12, 0xe4, 0x51, 0x4d, 0x9b, 0xff, 0x9b, 0x83, 0xa0, 0x01, 0x72, 0xd1, 0x7f, + 0x83, 0xa2, 0xe9, 0x82, 0x73, 0x13, 0xd3, 0x8d, 0x59, 0x95, 0x1f, 0xa6, 0x70, 0x4a, 0xeb, 0xb5, + 0xe0, 0x42, 0x10, 0xc0, 0x02, 0x00, 0x1c, 0x20, 0x00, 0x28, 0x14, 0x68, 0xe0, 0x1a, 0x82, 0xd4, + 0x87, 0x10, 0x00, 0x46, 0x54, 0x41, 0xc1, 0x81, 0x9a, 0x0c, 0x5d, 0xd0, 0x03, 0xd8, 0x0f, 0x52, + 0x07, 0x2c, 0x00, 0x7a, 0x92, 0x12, 0x02, 0xdc, 0x01, 0x67, 0x3a, 0xc1, 0xa4, 0x5a, 0x83, 0x95, + 0xc0, 0x30, 0x02, 0xf8, 0x49, 0xe2, 0x1e, 0x2b, 0xd5, 0x4a, 0x29, 0xa9, 0x6d, 0xf2, 0xc0, 0x7b, + 0x27, 0x2c, 0xb3, 0xde, 0x18, 0x00, 0x11, 0x71, 0xe0, 0x70, 0x02, 0x0c, 0x34, 0x07, 0x06, 0x01, + 0xa2, 0x90, 0x71, 0x40, 0x05, 0x12, 0x98, 0x01, 0x01, 0x63, 0xe2, 0xa8, 0xc0, 0xb8, 0xe0, 0xe0, + 0xf0, 0x06, 0x8a, 0x41, 0xd0, 0x01, 0x59, 0x12, 0x08, 0x30, 0x7d, 0x39, 0x02, 0x1d, 0x5e, 0x01, + 0x80, 0x26, 0x39, 0x0f, 0xc1, 0xef, 0x83, 0xc3, 0xf8, 0x3c, 0x7c, 0x4f, 0x86, 0xdd, 0x50, 0x6d, + 0x69, 0xd0, 0xef, 0xe9, 0x70, 0x35, 0xc4, 0xb3, 0x84, 0x40, 0x00, 0x40, 0x48, 0x00, 0x54, 0x84, + 0x00, 0x0d, 0x51, 0x81, 0xc3, 0x80, 0x3d, 0xd2, 0x1c, 0x20, 0x1c, 0xca, 0xa0, 0x10, 0x1a, 0x46, + 0x38, 0x04, 0x06, 0xd0, 0x75, 0x00, 0x2f, 0x48, 0x1e, 0x80, 0x3f, 0x49, 0x14, 0xc0, 0x3f, 0xc4, + 0x52, 0x01, 0x7e, 0x78, 0x36, 0x19, 0x08, 0x43, 0xf6, 0xc5, 0xc0, 0x01, 0xf8, 0x9f, 0x6e, 0x65, + 0xa4, 0xff, 0xff, 0x86, 0x0e, 0xf8, 0x00, 0x8b, 0xfb, 0xe4, 0xf4, 0x00, 0x57, 0x5c, 0x07, 0x4b, + 0x80, 0x66, 0xe3, 0xd2, 0xcd, 0xa7, 0x03, 0xa0, 0x41, 0x81, 0x20, 0x00, 0x20, 0x24, 0x80, 0x73, + 0x80, 0x0e, 0x52, 0x0e, 0xe5, 0xc0, 0x27, 0xd9, 0x08, 0x9c, 0x6c, 0xa6, 0x58, 0x86, 0xc5, 0xe9, + 0x0e, 0x40, 0x0c, 0xc8, 0x17, 0x5c, 0x50, 0x1a, 0xc1, 0x74, 0x47, 0x9f, 0xf0, 0x81, 0x0d, 0xb0, + 0x00, 0x00, 0x00, 0x01, 0x09, 0x30, 0x00, 0x00, 0x01, 0x41, 0x9a, 0x20, 0x25, 0xde, 0x49, 0xba, + 0xbd, 0x70, 0x47, 0xbd, 0x5a, 0xa0, 0x00, 0x00, 0x00, 0x01, 0x09, 0x30, 0x00, 0x00, 0x01, 0x41, + 0x9a, 0x40, 0x2d, 0xc4, 0xf0, 0x47, 0x55, 0xf7, 0xfa, 0xdf, 0xe0, 0x97, 0xbb, 0xee, 0xad, 0xf0, + 0x43, 0xd5, 0x7b, 0x00, 0x00, 0x00, 0x01, 0x09, 0x30, 0x00, 0x00, 0x01, 0x41, 0x9a, 0x60, 0x35, + 0xc6, 0xc0}; void check(bool condition, const std::string& message) { if (!condition) throw std::runtime_error(message); @@ -263,17 +223,6 @@ void testFailures() { check(sceVideodec2DeleteDecoder_nid_postfix(handle) == 0, "delete failed"); } -std::vector> hevcUnits(const std::uint8_t* stream, std::size_t size) { - std::vector starts; - for (std::size_t index = 0; index + 4 < size; ++index) { - if (stream[index] == 0 && stream[index + 1] == 0 && stream[index + 2] == 0 && stream[index + 3] == 1 && ((stream[index + 4] >> 1u) & 0x3fu) == 35) starts.push_back(index); - } - starts.push_back(size); - std::vector> units; - for (std::size_t unit = 0; unit + 1 < starts.size(); ++unit) units.emplace_back(stream + starts[unit], stream + starts[unit + 1]); - return units; -} - using Vp9Fixture::Vp9Hashes; using Vp9Fixture::ivfFrames; @@ -678,6 +627,93 @@ std::uint64_t frameBufferBytes() { return memory.maxFrameBufferSize; } +void testEmptyAccessUnit() { + constexpr int AccessUnitSizeError = static_cast(0x811D010Du); + constexpr int AccessUnitPointerError = static_cast(0x811D010Eu); + std::vector buffer(frameBufferBytes()); + const auto handle = createDecoder(CodecHevc, 1); + const auto units = hevcUnits(HevcStream.data(), HevcStream.size()); + 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"); + if (!output.isValid) return false; + check(pictures < HevcHashes.size(), "more pictures than access units"); + check(hashNv12(buffer.data(), output.framePitch, output.frameHeight) == HevcHashes[pictures], "picture " + std::to_string(pictures) + " differs from the reference after a refused access unit"); + ++pictures; + return true; + }; + const auto refuse = [&](const std::uint8_t* data, std::uint64_t size, int expected, const char* message) { + const InputData input{sizeof(InputData), data, size, 2000, 0, 0}; + FrameBuffer frame{sizeof(FrameBuffer), buffer.data(), buffer.size(), true}; + OutputInfo output{sizeof(OutputInfo)}; + output.isValid = true; + output.pictureCount = 1; + check(sceVideodec2Decode_nid_postfix(handle, &input, &frame, &output) == expected, message); + check(!output.isValid && output.pictureCount == 0 && !frame.isAccepted, "a refused access unit handed out a picture"); + }; + for (std::size_t unit = 0; unit < units.size(); ++unit) { + if (unit == 2) { + refuse(units[unit].data(), 0, AccessUnitSizeError, "an empty access unit did not return ACCESS_UNIT_SIZE"); + refuse(nullptr, units[unit].size(), AccessUnitPointerError, "a null access unit did not return ACCESS_UNIT_POINTER"); + } + const InputData input{sizeof(InputData), units[unit].data(), units[unit].size(), 1000 + unit, unit, 0}; + FrameBuffer frame{sizeof(FrameBuffer), buffer.data(), buffer.size(), false}; + OutputInfo output{sizeof(OutputInfo)}; + check(sceVideodec2Decode_nid_postfix(handle, &input, &frame, &output) == 0, "decode failed"); + take(output, frame); + } + for (;;) { + FrameBuffer frame{sizeof(FrameBuffer), buffer.data(), buffer.size(), false}; + OutputInfo output{sizeof(OutputInfo)}; + check(sceVideodec2Flush_nid_postfix(handle, &frame, &output) == 0, "flush failed"); + if (!take(output, frame)) break; + } + check(pictures == HevcHashes.size(), "refused access units cost pictures"); + check(sceVideodec2DeleteDecoder_nid_postfix(handle) == 0, "delete failed"); +} + +void testFlushAfterEveryUnit() { + std::vector> units; + for (std::size_t index = 0; index + 4 < AvcIpppStream.size(); ++index) { + if (AvcIpppStream[index] == 0 && AvcIpppStream[index + 1] == 0 && AvcIpppStream[index + 2] == 0 && AvcIpppStream[index + 3] == 1 && (AvcIpppStream[index + 4] & 0x1fu) == 9) units.emplace_back(); + check(!units.empty(), "the IPPP stream does not start with an access unit delimiter"); + units.back().push_back(AvcIpppStream[index]); + } + for (std::size_t index = AvcIpppStream.size() - 4; index < AvcIpppStream.size(); ++index) units.back().push_back(AvcIpppStream[index]); + check(units.size() == 4, "unexpected IPPP access unit count"); + std::vector buffer(frameBufferBytes()); + const auto decode = [&](bool flushEachUnit) { + const auto handle = createDecoder(1, 66); + std::vector hashes; + const auto take = [&](const OutputInfo& output, const FrameBuffer& frame) { + if (!output.isValid) return false; + check(frame.isAccepted && output.frameWidth == Width && output.frameHeight == CodedHeight, "unexpected IPPP picture"); + hashes.push_back(hashNv12(buffer.data(), output.framePitch, output.frameHeight)); + return true; + }; + const auto flush = [&] { + FrameBuffer frame{sizeof(FrameBuffer), buffer.data(), buffer.size(), false}; + OutputInfo output{sizeof(OutputInfo)}; + check(sceVideodec2Flush_nid_postfix(handle, &frame, &output) == 0, "flush failed"); + return take(output, frame); + }; + for (std::size_t unit = 0; unit < units.size(); ++unit) { + const InputData input{sizeof(InputData), units[unit].data(), units[unit].size(), 3000 + unit, unit, 0}; + FrameBuffer frame{sizeof(FrameBuffer), buffer.data(), buffer.size(), false}; + OutputInfo output{sizeof(OutputInfo)}; + check(sceVideodec2Decode_nid_postfix(handle, &input, &frame, &output) == 0, "IPPP decode failed"); + take(output, frame); + if (flushEachUnit) flush(); + } + while (flush()) {} + check(sceVideodec2DeleteDecoder_nid_postfix(handle) == 0, "delete failed"); + return hashes; + }; + const auto reference = decode(false); + check(reference.size() == units.size(), "an IPPP picture is missing without flushes"); + check(decode(true) == reference, "a flush after each access unit lost or changed the pictures that follow it"); +} + void testDecodeAfterFlush() { std::vector buffer(frameBufferBytes()); const auto handle = createDecoder(1, 100); @@ -845,6 +881,8 @@ int main() { testVp9PictureMetadata(); testTenBit(); testDecodeAfterFlush(); + testEmptyAccessUnit(); + testFlushAfterEveryUnit(); testPictureOwnership(); testConcurrentOwnership(); std::puts("Videodec2 tests passed"); diff --git a/core/libs/tests/GuestVoice.cpp b/core/libs/tests/GuestVoice.cpp index cb12940acb..471a1b9218 100644 --- a/core/libs/tests/GuestVoice.cpp +++ b/core/libs/tests/GuestVoice.cpp @@ -34,6 +34,9 @@ int APS5_VABI sceVoiceResumePortAll(void); int APS5_VABI sceVoiceGetResourceInfo(VoiceResourceInfo*); int APS5_VABI sceVoiceEnableChat(void); int APS5_VABI sceVoiceDisableChat(void); +int APS5_VABI sceVoiceSetMuteFlagAll(bool); +int APS5_VABI sceVoiceSetBitRate(std::uint32_t, std::uint32_t); +int APS5_VABI sceVoiceUpdatePort(std::uint32_t, const VoicePortParam*); } namespace { @@ -70,6 +73,9 @@ void TestNotInitialized() { Require(std::memcmp(&info, &sentinel, sizeof(info)) == 0); Require(sceVoicePausePortAll() == NotInitialized); Require(sceVoiceResumePortAll() == NotInitialized); + Require(sceVoiceSetMuteFlagAll(true) == NotInitialized); + Require(sceVoiceSetBitRate(0, 16000) == NotInitialized); + Require(sceVoiceUpdatePort(0, nullptr) == NotInitialized); } std::uint32_t CreatePort(std::int32_t type, float volume) { @@ -139,6 +145,24 @@ int main() { Require(sceVoiceSetThreadsParams(&threads) == invalidArgument); threads.mode = 1; RequireThrows([&] { sceVoiceSetThreadsParams(&threads); }); + VoiceThreadsParams scheduled{1, 700, 0x7f, 700, 0, 0x7f, 700, 0, 0x7f}; + Require(sceVoiceSetThreadsParams(&scheduled) == 0); + scheduled = {1, 256, 0x1fff, 767, 0, 1, 512, 0, 0x1000}; + Require(sceVoiceSetThreadsParams(&scheduled) == 0); + for (const auto priority : {255, 768, 0, -1}) { + for (int slot = 0; slot < 3; ++slot) { + VoiceThreadsParams outside{1, 700, 0x7f, 700, 0, 0x7f, 700, 0, 0x7f}; + (slot == 0 ? outside.firstPriority : slot == 1 ? outside.secondPriority : outside.thirdPriority) = priority; + RequireThrows([&] { sceVoiceSetThreadsParams(&outside); }); + } + } + for (const std::uint64_t affinity : {std::uint64_t{0}, std::uint64_t{0x2000}, std::uint64_t{0x3fff}, UINT64_MAX}) { + for (int slot = 0; slot < 3; ++slot) { + VoiceThreadsParams outside{1, 700, 0x7f, 700, 0, 0x7f, 700, 0, 0x7f}; + (slot == 0 ? outside.firstAffinity : slot == 1 ? outside.secondAffinity : outside.thirdAffinity) = affinity; + RequireThrows([&] { sceVoiceSetThreadsParams(&outside); }); + } + } threads.mode = 0; Require(sceVoiceStart(nullptr) == invalidArgument); Require(sceVoiceStart(&start) == invalidArgument); @@ -155,6 +179,8 @@ int main() { start.mem_size = 0x40000; Require(sceVoiceSetThreadsParams(nullptr) == invalidArgument); Require(sceVoiceSetThreadsParams(&threads) == static_cast(0x804e080bu)); + scheduled = {1, 700, 0x7f, 700, 0, 0x7f, 700, 0, 0x7f}; + Require(sceVoiceSetThreadsParams(&scheduled) == static_cast(0x804e080bu)); Require(sceVoiceGetResourceInfo(nullptr) == invalidArgument); struct ResourceOutput { VoiceResourceInfo info; @@ -220,6 +246,56 @@ int main() { Require(sceVoiceReadFromOPort(output, data, &size) == 0 && size == 0); Require(sceVoiceDisconnectIPortFromOPort(input, output) == 0); + Require(sceVoiceGetMuteFlag(input, &muted) == 0 && !muted); + Require(sceVoiceSetMuteFlagAll(true) == 0); + Require(sceVoiceGetMuteFlag(input, &muted) == 0 && muted); + Require(sceVoiceSetMuteFlagAll(false) == 0); + Require(sceVoiceGetMuteFlag(input, &muted) == 0 && !muted); + Require(sceVoiceGetMuteFlag(output, &muted) == 0 && muted); + + std::uint32_t portBitrate = 0; + Require(sceVoiceSetBitRate(input, 7300) == 0); + Require(sceVoiceGetBitRate(input, &portBitrate) == 0 && portBitrate == 7300); + VoicePortInfo rateInfo{}; + Require(sceVoiceGetPortInfo(input, &rateInfo) == 0 && rateInfo.frame_size == 19); + Require(sceVoiceSetBitRate(output + 1, 7300) == portNotFound); + Require(sceVoiceSetBitRate(128, 7300) == invalidPort); + RequireThrows([&] { sceVoiceSetBitRate(output, 7300); }); + RequireThrows([&] { sceVoiceSetBitRate(input, 7301); }); + RequireThrows([&] { sceVoiceSetBitRate(input, 0x80000000u); }); + Require(sceVoiceGetBitRate(input, &portBitrate) == 0 && portBitrate == 7300); + + VoicePortParam update{}; + update.port_type = 2; + update.mute = 1; + update.volume = 0.75f; + update.voice.bitrate = 14400; + Require(sceVoiceUpdatePort(input, &update) == 0); + Require(sceVoiceGetMuteFlag(input, &muted) == 0 && muted); + Require(sceVoiceGetVolume(input, &volume) == 0 && volume == 0.75f); + Require(sceVoiceGetBitRate(input, &portBitrate) == 0 && portBitrate == 14400); + Require(sceVoiceGetPortInfo(input, &rateInfo) == 0 && rateInfo.frame_size == 27); + update.voice.bitrate = 1; + RequireThrows([&] { sceVoiceUpdatePort(input, &update); }); + Require(sceVoiceGetBitRate(input, &portBitrate) == 0 && portBitrate == 14400); + update.voice.bitrate = 14400; + update.port_type = 3; + RequireThrows([&] { sceVoiceUpdatePort(input, &update); }); + RequireThrows([&] { sceVoiceUpdatePort(input, nullptr); }); + Require(sceVoiceUpdatePort(output + 1, &update) == portNotFound); + Require(sceVoiceUpdatePort(128, &update) == invalidPort); + VoicePortParam pcm{}; + pcm.port_type = 3; + pcm.volume = std::numeric_limits::quiet_NaN(); + Require(sceVoiceUpdatePort(output, &pcm) == 0); + Require(sceVoiceGetMuteFlag(output, &muted) == 0 && !muted); + Require(sceVoiceGetVolume(output, &volume) == 0 && volume == 0.0f); + pcm.volume = 0.25f; + pcm.mute = 1; + Require(sceVoiceUpdatePort(output, &pcm) == 0); + + Require(sceVoiceSetMuteFlagAll(true) == 0); + Require(sceVoiceDeletePort(input) == 0); Require(sceVoiceResetPort(input) == portNotFound); Require(sceVoicePausePort(input) == portNotFound); diff --git a/core/libs/tests/GuestVorbisDec.cpp b/core/libs/tests/GuestVorbisDec.cpp index 80bce8c123..d34eb5d9ad 100644 --- a/core/libs/tests/GuestVorbisDec.cpp +++ b/core/libs/tests/GuestVorbisDec.cpp @@ -1,99 +1,649 @@ -#include "SceTypes.hpp" +#include "prx/libSceVorbisDec/include/GuestVorbisFile.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include +#include + +#include +#include +#include +#include #include #include #include +#include #include +#include + +using namespace VorbisDec; extern "C" { -int APS5_VABI sceVorbisDecOpenCallbacks(void); -int APS5_VABI sceVorbisDecClear(void); -int APS5_VABI sceVorbisDecBitrate(void); -int APS5_VABI sceVorbisDecBitrateInstant(void); -int APS5_VABI sceVorbisDecStreams(void); -int APS5_VABI sceVorbisDecSeekable(void); -int APS5_VABI sceVorbisDecSerialNumber(void); -int APS5_VABI sceVorbisDecRawTotal(void); -int APS5_VABI sceVorbisDecPcmTotal(void); -int APS5_VABI sceVorbisDecTimeTotal(void); -int APS5_VABI sceVorbisDecRawSeek(void); -int APS5_VABI sceVorbisDecPcmSeek(void); -int APS5_VABI sceVorbisDecPcmSeekPage(void); -int APS5_VABI sceVorbisDecTimeSeek(void); -int APS5_VABI sceVorbisDecTimeSeekPage(void); -int APS5_VABI sceVorbisDecRawSeekLap(void); -int APS5_VABI sceVorbisDecPcmSeekLap(void); -int APS5_VABI sceVorbisDecPcmSeekPageLap(void); -int APS5_VABI sceVorbisDecTimeSeekLap(void); -int APS5_VABI sceVorbisDecTimeSeekPageLap(void); -int APS5_VABI sceVorbisDecRawTell(void); -int APS5_VABI sceVorbisDecPcmTell(void); -int APS5_VABI sceVorbisDecTimeTell(void); -int APS5_VABI sceVorbisDecInfo(void); -int APS5_VABI sceVorbisDecComment(void); -int APS5_VABI sceVorbisDecRead(void); -int APS5_VABI sceVorbisDecReadFloat(void); -int APS5_VABI sceVorbisDecReadFilter(void); -int APS5_VABI sceVorbisDecCrossLap(void); -int APS5_VABI sceVorbisDecHalfrate(void); -int APS5_VABI sceVorbisDecHalfrateP(void); +int APS5_VABI sceVorbisDecOpenCallbacks(void* datasource, void* vf, const char* initial, std::int64_t ibytes, GuestCallbacks callbacks); +int APS5_VABI sceVorbisDecClear(void* vf); +std::int64_t APS5_VABI sceVorbisDecBitrate(void* vf, int link); +std::int64_t APS5_VABI sceVorbisDecBitrateInstant(void* vf); +std::int64_t APS5_VABI sceVorbisDecStreams(void* vf); +std::int64_t APS5_VABI sceVorbisDecSeekable(void* vf); +std::int64_t APS5_VABI sceVorbisDecSerialNumber(void* vf, int link); +std::int64_t APS5_VABI sceVorbisDecRawTotal(void* vf, int link); +std::int64_t APS5_VABI sceVorbisDecPcmTotal(void* vf, int link); +double APS5_VABI sceVorbisDecTimeTotal(void* vf, int link); +int APS5_VABI sceVorbisDecRawSeek(void* vf, std::int64_t position); +int APS5_VABI sceVorbisDecPcmSeek(void* vf, std::int64_t position); +int APS5_VABI sceVorbisDecPcmSeekPage(void* vf, std::int64_t position); +int APS5_VABI sceVorbisDecTimeSeek(void* vf, double position); +int APS5_VABI sceVorbisDecTimeSeekPage(void* vf, double position); +int APS5_VABI sceVorbisDecRawSeekLap(void* vf, std::int64_t position); +int APS5_VABI sceVorbisDecPcmSeekLap(void* vf, std::int64_t position); +int APS5_VABI sceVorbisDecPcmSeekPageLap(void* vf, std::int64_t position); +int APS5_VABI sceVorbisDecTimeSeekLap(void* vf, double position); +int APS5_VABI sceVorbisDecTimeSeekPageLap(void* vf, double position); +std::int64_t APS5_VABI sceVorbisDecRawTell(void* vf); +std::int64_t APS5_VABI sceVorbisDecPcmTell(void* vf); +double APS5_VABI sceVorbisDecTimeTell(void* vf); +GuestInfo* APS5_VABI sceVorbisDecInfo(void* vf, int link); +GuestComment* APS5_VABI sceVorbisDecComment(void* vf, int link); +std::int64_t APS5_VABI sceVorbisDecRead(void* vf, char* buffer, int length, int bigEndian, int word, int isSigned, int* bitstream); +std::int64_t APS5_VABI sceVorbisDecReadFloat(void* vf, float*** pcmChannels, int samples, int* bitstream); +std::int64_t APS5_VABI sceVorbisDecReadFilter(void* vf, char* buffer, int length, int bigEndian, int word, int isSigned, int* bitstream, GuestFilter filter, void* filterParam); +int APS5_VABI sceVorbisDecCrossLap(void* first, void* second); +int APS5_VABI sceVorbisDecHalfrate(void* vf, int flag); +int APS5_VABI sceVorbisDecHalfrateP(void* vf); int APS5_VABI sceVorbisDecGetDecodePriority(void); int APS5_VABI sceVorbisDecSetDecodePriority(void); int APS5_VABI sceVorbisDecGetLastDecodeError(void); } -template -static bool Throws(TCall call) { +namespace { + +constexpr int FirstSerial = 0x1234abcd; +constexpr int SecondSerial = -0x61c88647; +constexpr int FirstChannels = 3; +constexpr int SecondChannels = 2; +constexpr long FirstRate = 44100; +constexpr long SecondRate = 22050; +constexpr long FirstSamples = 44100; +constexpr long SecondSamples = 11025; +constexpr std::int64_t DecodedBytes = (FirstSamples * FirstChannels + SecondSamples * SecondChannels) * 2; +constexpr unsigned char Pattern = 0xA5; + +void Check(bool value, int line) { + if (value) return; + std::fprintf(stderr, "VorbisDec check failed at line %d\n", line); + std::abort(); +} +#define Require(value) Check((value), __LINE__) + +template +bool Throws(TCall call) { try { call(); - } catch (const std::runtime_error&) { + } catch (const TException&) { return true; } return false; } -static void Require(bool value, const char* what) { - if (value) return; - std::printf("VorbisDec test failed: %s\n", what); - std::abort(); +struct Source { + const std::vector* bytes = nullptr; + std::size_t position = 0; + int closes = 0; + void* probe = nullptr; + std::int64_t probed = -1; + bool throwOnRead = false; + bool throwOnClose = false; +}; + +std::size_t ReadBytes(Source& source, void* buffer, std::size_t size, std::size_t count) { + if (source.throwOnRead) throw std::runtime_error("read callback"); + if (source.probe) source.probed = sceVorbisDecStreams(source.probe); + if (size == 0) return 0; + const std::size_t items = std::min(count, (source.bytes->size() - source.position) / size); + if (items != 0) std::memcpy(buffer, source.bytes->data() + source.position, items * size); + source.position += items * size; + return items; +} + +int SeekBytes(Source& source, std::int64_t offset, int whence) { + const auto size = static_cast(source.bytes->size()); + const std::int64_t base = whence == SEEK_SET ? 0 : whence == SEEK_CUR ? static_cast(source.position) : size; + if (offset < -base || offset > size - base) return -1; + source.position = static_cast(base + offset); + return 0; +} + +std::size_t APS5_VABI GuestRead(void* buffer, std::size_t size, std::size_t count, void* source) { + return ReadBytes(*static_cast(source), buffer, size, count); +} + +int APS5_VABI GuestSeek(void* source, std::int64_t offset, int whence) { + return SeekBytes(*static_cast(source), offset, whence); +} + +int APS5_VABI GuestClose(void* source) { + auto& closing = *static_cast(source); + ++closing.closes; + if (closing.throwOnClose) throw std::runtime_error("close callback"); + return 0; +} + +std::int64_t APS5_VABI GuestTell(void* source) { + return static_cast(static_cast(source)->position); +} + +std::size_t HostRead(void* buffer, std::size_t size, std::size_t count, void* source) { + return ReadBytes(*static_cast(source), buffer, size, count); +} + +int HostSeek(void* source, ogg_int64_t offset, int whence) { + return SeekBytes(*static_cast(source), offset, whence); +} + +int HostClose(void* source) { + ++static_cast(source)->closes; + return 0; +} + +long HostTell(void* source) { + return static_cast(static_cast(source)->position); +} + +constexpr GuestCallbacks SeekableCallbacks{GuestRead, GuestSeek, GuestClose, GuestTell}; +constexpr GuestCallbacks StreamingCallbacks{GuestRead, nullptr, GuestClose, nullptr}; +const ov_callbacks HostSeekableCallbacks{HostRead, HostSeek, HostClose, HostTell}; +const ov_callbacks HostStreamingCallbacks{HostRead, nullptr, HostClose, nullptr}; + +struct GuestFile { + alignas(16) std::array bytes; + + GuestFile() { bytes.fill(Pattern); } + + void* Get() { return bytes.data(); } + + bool Untouched() const { + return std::all_of(bytes.begin(), bytes.end(), [](unsigned char value) { return value == Pattern; }); + } +}; + +struct FilterLog { + int calls = 0; + std::int64_t firstChannels = 0; + std::int64_t lastChannels = 0; + std::int64_t samples = 0; +}; + +void Halve(float** pcm, std::int64_t channels, std::int64_t samples) { + for (std::int64_t channel = 0; channel < channels; ++channel) + for (std::int64_t index = 0; index < samples; ++index) pcm[channel][index] *= 0.5f; +} + +void APS5_VABI GuestHalve(float** pcm, std::int64_t channels, std::int64_t samples, void* param) { + auto* log = static_cast(param); + if (log->calls++ == 0) log->firstChannels = channels; + log->lastChannels = channels; + log->samples += samples; + Halve(pcm, channels, samples); +} + +void HostHalve(float** pcm, long channels, long samples, void*) { + Halve(pcm, channels, samples); +} + +void AppendPage(std::vector& out, const ogg_page& page) { + out.insert(out.end(), page.header, page.header + page.header_len); + out.insert(out.end(), page.body, page.body + page.body_len); +} + +void EncodeLink(std::vector& out, int serial, int channels, long rate, long samples, const char* title) { + vorbis_info info; + vorbis_info_init(&info); + Require(vorbis_encode_init_vbr(&info, channels, rate, 0.1f) == 0); + vorbis_comment comment; + vorbis_comment_init(&comment); + vorbis_comment_add_tag(&comment, "TITLE", title); + vorbis_comment_add_tag(&comment, "ARTIST", "AnyPS5"); + vorbis_dsp_state dsp; + Require(vorbis_analysis_init(&dsp, &info) == 0); + vorbis_block block; + vorbis_block_init(&dsp, &block); + ogg_stream_state stream; + ogg_stream_init(&stream, serial); + ogg_packet identification; + ogg_packet comments; + ogg_packet codebooks; + vorbis_analysis_headerout(&dsp, &comment, &identification, &comments, &codebooks); + ogg_stream_packetin(&stream, &identification); + ogg_stream_packetin(&stream, &comments); + ogg_stream_packetin(&stream, &codebooks); + ogg_page page; + while (ogg_stream_flush(&stream, &page) != 0) AppendPage(out, page); + for (long done = 0;;) { + const long count = std::min(1024L, samples - done); + if (count > 0) { + float** buffer = vorbis_analysis_buffer(&dsp, static_cast(count)); + for (int channel = 0; channel < channels; ++channel) { + const double frequency = 220.0 * (channel + 1) + 37.0 * serial / 1e9; + for (long index = 0; index < count; ++index) { + buffer[channel][index] = static_cast(0.3 * std::sin(6.283185307179586 * frequency * static_cast(done + index) / static_cast(rate))); + } + } + } + vorbis_analysis_wrote(&dsp, static_cast(count)); + done += count; + while (vorbis_analysis_blockout(&dsp, &block) == 1) { + vorbis_analysis(&block, nullptr); + vorbis_bitrate_addblock(&block); + ogg_packet packet; + while (vorbis_bitrate_flushpacket(&dsp, &packet) == 1) { + ogg_stream_packetin(&stream, &packet); + while (ogg_stream_pageout_fill(&stream, &page, 1024) != 0) AppendPage(out, page); + } + } + if (count == 0) break; + } + while (ogg_stream_flush(&stream, &page) != 0) AppendPage(out, page); + ogg_stream_clear(&stream); + vorbis_block_clear(&block); + vorbis_dsp_clear(&dsp); + vorbis_comment_clear(&comment); + vorbis_info_clear(&info); +} + +std::int64_t RequireSameRead(void* vf, OggVorbis_File& reference, int bigEndian = 0, int word = 2, int isSigned = 1) { + std::array guest{}; + std::array host{}; + int guestStream = -1; + int hostStream = -1; + const std::int64_t got = sceVorbisDecRead(vf, guest.data(), static_cast(guest.size()), bigEndian, word, isSigned, &guestStream); + const long expected = ov_read(&reference, host.data(), static_cast(host.size()), bigEndian, word, isSigned, &hostStream); + Require(got == expected); + Require(got <= 0 || (guestStream == hostStream && std::memcmp(guest.data(), host.data(), static_cast(got)) == 0)); + return got; +} + +std::int64_t DecodeTogether(void* vf, OggVorbis_File& reference) { + std::int64_t total = 0; + for (;;) { + const std::int64_t got = RequireSameRead(vf, reference); + Require(got >= 0 || got == OV_HOLE); + if (got == 0) return total; + if (got > 0) total += got; + } +} + +void RequireSameTells(void* vf, OggVorbis_File& reference) { + Require(sceVorbisDecPcmTell(vf) == ov_pcm_tell(&reference)); + Require(sceVorbisDecRawTell(vf) == ov_raw_tell(&reference)); + Require(sceVorbisDecTimeTell(vf) == ov_time_tell(&reference)); +} + +void TestSeekable(const std::vector& stream) { + GuestFile guest; + GuestFile probe; + void* vf = guest.Get(); + Source source{&stream}; + source.probe = probe.Get(); + Require(sceVorbisDecOpenCallbacks(&source, vf, nullptr, 0, SeekableCallbacks) == 0); + Require(source.probed == 0 && source.closes == 0); + Source referenceSource{&stream}; + OggVorbis_File reference{}; + Require(ov_open_callbacks(&referenceSource, &reference, nullptr, 0, HostSeekableCallbacks) == 0); + + Require(sceVorbisDecStreams(vf) == 2 && sceVorbisDecSeekable(vf) == 1); + Require(sceVorbisDecSerialNumber(vf, 0) == FirstSerial && sceVorbisDecSerialNumber(vf, 1) == SecondSerial); + Require(sceVorbisDecSerialNumber(vf, -1) == FirstSerial && sceVorbisDecSerialNumber(vf, 7) == SecondSerial); + Require(sceVorbisDecPcmTotal(vf, 0) == FirstSamples && sceVorbisDecPcmTotal(vf, 1) == SecondSamples); + Require(sceVorbisDecPcmTotal(vf, -1) == FirstSamples + SecondSamples && sceVorbisDecPcmTotal(vf, 2) == OV_EINVAL); + Require(sceVorbisDecRawTotal(vf, -1) == ov_raw_total(&reference, -1) && sceVorbisDecRawTotal(vf, -1) > 0); + Require(sceVorbisDecRawTotal(vf, 0) == ov_raw_total(&reference, 0) && sceVorbisDecRawTotal(vf, 1) == ov_raw_total(&reference, 1)); + Require(sceVorbisDecTimeTotal(vf, 0) == 1.0 && sceVorbisDecTimeTotal(vf, 1) == 0.5 && sceVorbisDecTimeTotal(vf, -1) == 1.5); + Require(sceVorbisDecBitrate(vf, -1) == ov_bitrate(&reference, -1) && sceVorbisDecBitrate(vf, 1) == ov_bitrate(&reference, 1)); + Require(sceVorbisDecBitrate(vf, -1) > 0 && sceVorbisDecBitrate(vf, 2) == OV_EINVAL); + Require(sceVorbisDecBitrateInstant(vf) == OV_FALSE); + + GuestInfo* first = sceVorbisDecInfo(vf, 0); + GuestInfo* second = sceVorbisDecInfo(vf, 1); + Require(first && second && first != second && sceVorbisDecInfo(vf, 0) == first); + Require(sceVorbisDecInfo(vf, -1) == first && sceVorbisDecInfo(vf, 2) == nullptr); + Require(first->version == 0 && first->channels == FirstChannels && first->rate == FirstRate); + Require(second->version == 0 && second->channels == SecondChannels && second->rate == SecondRate); + for (int link = 0; link < 2; ++link) { + const GuestInfo* info = sceVorbisDecInfo(vf, link); + const vorbis_info* host = ov_info(&reference, link); + Require(info->bitrate_upper == host->bitrate_upper && info->bitrate_nominal == host->bitrate_nominal); + Require(info->bitrate_lower == host->bitrate_lower && info->bitrate_window == host->bitrate_window && info->codec_setup != nullptr); + } + const GuestComment* comment = sceVorbisDecComment(vf, 1); + Require(comment && comment->comments == 2 && comment->comment_lengths[0] == 12 && comment->comment_lengths[1] == 13); + Require(std::strcmp(comment->user_comments[0], "TITLE=second") == 0 && std::strcmp(comment->user_comments[1], "ARTIST=AnyPS5") == 0); + Require(std::strcmp(comment->vendor, ov_comment(&reference, 1)->vendor) == 0); + Require(sceVorbisDecComment(vf, -1) == sceVorbisDecComment(vf, 0) && sceVorbisDecComment(vf, 2) == nullptr); + Require(std::strcmp(sceVorbisDecComment(vf, 0)->user_comments[0], "TITLE=first") == 0); + + const std::int64_t firstRead = RequireSameRead(vf, reference); + Require(firstRead > 0 && sceVorbisDecBitrateInstant(vf) == ov_bitrate_instant(&reference)); + Require(sceVorbisDecInfo(vf, -1) == first); + Require(firstRead + DecodeTogether(vf, reference) == DecodedBytes); + Require(sceVorbisDecPcmTell(vf) == FirstSamples + SecondSamples && sceVorbisDecTimeTell(vf) == 1.5); + RequireSameTells(vf, reference); + Require(sceVorbisDecInfo(vf, -1) == second && sceVorbisDecSerialNumber(vf, -1) == SecondSerial); + + Require(sceVorbisDecPcmSeek(vf, 0) == 0 && ov_pcm_seek(&reference, 0) == 0); + Require(DecodeTogether(vf, reference) == DecodedBytes); + + Require(sceVorbisDecPcmSeek(vf, 1000) == 0 && ov_pcm_seek(&reference, 1000) == 0); + Require(sceVorbisDecPcmTell(vf) == 1000); + RequireSameTells(vf, reference); + Require(RequireSameRead(vf, reference) > 0 && RequireSameRead(vf, reference, 1, 2, 0) > 0 && RequireSameRead(vf, reference, 0, 1, 0) > 0); + Require(sceVorbisDecPcmSeekPage(vf, FirstSamples + 3000) == ov_pcm_seek_page(&reference, FirstSamples + 3000)); + RequireSameTells(vf, reference); + Require(RequireSameRead(vf, reference) > 0 && sceVorbisDecInfo(vf, -1) == second); + Require(sceVorbisDecTimeSeek(vf, 0.25) == 0 && ov_time_seek(&reference, 0.25) == 0); + Require(sceVorbisDecPcmTell(vf) == FirstRate / 4); + RequireSameTells(vf, reference); + Require(RequireSameRead(vf, reference) > 0 && sceVorbisDecInfo(vf, -1) == first); + Require(sceVorbisDecTimeSeekPage(vf, 1.2) == ov_time_seek_page(&reference, 1.2)); + RequireSameTells(vf, reference); + Require(RequireSameRead(vf, reference) > 0); + const auto middle = static_cast(stream.size() / 2); + Require(sceVorbisDecRawSeek(vf, middle) == 0 && ov_raw_seek(&reference, middle) == 0); + RequireSameTells(vf, reference); + Require(RequireSameRead(vf, reference) > 0); + + Require(sceVorbisDecPcmSeekLap(vf, 5000) == 0 && ov_pcm_seek_lap(&reference, 5000) == 0); + RequireSameTells(vf, reference); + Require(RequireSameRead(vf, reference) > 0 && RequireSameRead(vf, reference) > 0); + Require(sceVorbisDecPcmSeekPageLap(vf, 30000) == ov_pcm_seek_page_lap(&reference, 30000)); + RequireSameTells(vf, reference); + Require(RequireSameRead(vf, reference) > 0); + Require(sceVorbisDecRawSeekLap(vf, middle / 2) == ov_raw_seek_lap(&reference, middle / 2)); + RequireSameTells(vf, reference); + Require(RequireSameRead(vf, reference) > 0); + Require(sceVorbisDecTimeSeekLap(vf, 1.1) == 0 && ov_time_seek_lap(&reference, 1.1) == 0); + RequireSameTells(vf, reference); + Require(RequireSameRead(vf, reference) > 0 && sceVorbisDecInfo(vf, -1) == second); + Require(sceVorbisDecTimeSeekPageLap(vf, 0.75) == ov_time_seek_page_lap(&reference, 0.75)); + RequireSameTells(vf, reference); + Require(RequireSameRead(vf, reference) > 0 && sceVorbisDecInfo(vf, -1) == first); + + Require(sceVorbisDecPcmSeek(vf, -1) == OV_EINVAL && sceVorbisDecPcmSeek(vf, FirstSamples + SecondSamples + 1) == OV_EINVAL); + Require(sceVorbisDecTimeSeek(vf, -0.5) == OV_EINVAL && sceVorbisDecTimeSeek(vf, 2.0) == OV_EINVAL); + Require(sceVorbisDecRawSeek(vf, -1) == OV_EINVAL && sceVorbisDecRawSeek(vf, middle * 3) == OV_EINVAL); + + Require(source.closes == 0 && sceVorbisDecClear(vf) == 0 && source.closes == 1); + Require(sceVorbisDecStreams(vf) == 0 && sceVorbisDecInfo(vf, 0) == nullptr && sceVorbisDecPcmTell(vf) == OV_EINVAL); + Require(sceVorbisDecClear(vf) == 0 && source.closes == 1); + Require(ov_clear(&reference) == 0 && referenceSource.closes == 1); + Require(source.probed == 0 && guest.Untouched() && probe.Untouched()); +} + +void TestStreaming(const std::vector& stream) { + GuestFile guest; + void* vf = guest.Get(); + const auto* initial = reinterpret_cast(stream.data()); + Source source{&stream, 64}; + Require(sceVorbisDecOpenCallbacks(&source, vf, initial, 64, StreamingCallbacks) == 0); + Source referenceSource{&stream, 64}; + OggVorbis_File reference{}; + Require(ov_open_callbacks(&referenceSource, &reference, initial, 64, HostStreamingCallbacks) == 0); + + Require(sceVorbisDecSeekable(vf) == 0 && sceVorbisDecStreams(vf) == 1); + Require(sceVorbisDecPcmTotal(vf, -1) == OV_EINVAL && sceVorbisDecRawTotal(vf, 0) == OV_EINVAL && sceVorbisDecTimeTotal(vf, -1) == OV_EINVAL); + Require(sceVorbisDecRawSeek(vf, 0) == OV_ENOSEEK && sceVorbisDecPcmSeek(vf, 0) == OV_ENOSEEK && sceVorbisDecTimeSeekPage(vf, 0.0) == OV_ENOSEEK); + Require(sceVorbisDecBitrate(vf, 0) == ov_bitrate(&reference, 0) && sceVorbisDecBitrate(vf, 1) == ov_bitrate(&reference, 1)); + Require(sceVorbisDecSerialNumber(vf, 3) == FirstSerial); + GuestInfo* info = sceVorbisDecInfo(vf, 0); + Require(info && info == sceVorbisDecInfo(vf, 5) && info == sceVorbisDecInfo(vf, -1)); + Require(info->channels == FirstChannels && info->rate == FirstRate); + const GuestComment* comment = sceVorbisDecComment(vf, 4); + Require(comment && std::strcmp(comment->user_comments[0], "TITLE=first") == 0); + + Require(DecodeTogether(vf, reference) == DecodedBytes); + Require(info->channels == SecondChannels && info->rate == SecondRate && sceVorbisDecInfo(vf, 0) == info); + Require(std::strcmp(comment->user_comments[0], "TITLE=second") == 0); + Require(sceVorbisDecSerialNumber(vf, 0) == SecondSerial); + RequireSameTells(vf, reference); + Require(sceVorbisDecClear(vf) == 0 && source.closes == 1); + Require(ov_clear(&reference) == 0 && referenceSource.closes == 1); + Require(guest.Untouched()); +} + +void TestFloatAndFilter(const std::vector& stream) { + GuestFile guest; + void* vf = guest.Get(); + Source source{&stream}; + Require(sceVorbisDecOpenCallbacks(&source, vf, nullptr, 0, SeekableCallbacks) == 0); + Source referenceSource{&stream}; + OggVorbis_File reference{}; + Require(ov_open_callbacks(&referenceSource, &reference, nullptr, 0, HostSeekableCallbacks) == 0); + + for (int round = 0; round < 6; ++round) { + float** pcm = nullptr; + float** hostPcm = nullptr; + int guestStream = -1; + int hostStream = -1; + const std::int64_t got = sceVorbisDecReadFloat(vf, &pcm, 300, &guestStream); + const long expected = ov_read_float(&reference, &hostPcm, 300, &hostStream); + Require(got == expected && got > 0 && got <= 300 && guestStream == hostStream && guestStream == 0); + for (int channel = 0; channel < FirstChannels; ++channel) { + Require(std::memcmp(pcm[channel], hostPcm[channel], static_cast(got) * sizeof(float)) == 0); + } + } + Require(sceVorbisDecReadFloat(vf, nullptr, 50, nullptr) == ov_read_float(&reference, nullptr, 50, nullptr)); + RequireSameTells(vf, reference); + + FilterLog log; + std::int64_t total = 0; + for (;;) { + std::array guestBytes{}; + std::array hostBytes{}; + int guestStream = -1; + int hostStream = -1; + const std::int64_t got = sceVorbisDecReadFilter(vf, guestBytes.data(), static_cast(guestBytes.size()), 0, 2, 1, &guestStream, GuestHalve, &log); + const long expected = ov_read_filter(&reference, hostBytes.data(), static_cast(hostBytes.size()), 0, 2, 1, &hostStream, HostHalve, nullptr); + Require(got == expected && got >= 0); + if (got == 0) break; + Require(guestStream == hostStream && std::memcmp(guestBytes.data(), hostBytes.data(), static_cast(got)) == 0); + total += got; + } + Require(total > 0 && log.calls > 0 && log.samples > 0 && log.firstChannels == FirstChannels && log.lastChannels == SecondChannels); + + Require(sceVorbisDecPcmSeek(vf, 0) == 0 && ov_pcm_seek(&reference, 0) == 0); + std::array filtered{}; + std::array plain{}; + int bitstream = -1; + const int calls = log.calls; + const std::int64_t got = sceVorbisDecReadFilter(vf, filtered.data(), static_cast(filtered.size()), 0, 2, 1, &bitstream, nullptr, &log); + Require(got > 0 && got == ov_read(&reference, plain.data(), static_cast(plain.size()), 0, 2, 1, &bitstream)); + Require(std::memcmp(filtered.data(), plain.data(), static_cast(got)) == 0 && log.calls == calls); + Require(sceVorbisDecClear(vf) == 0 && source.closes == 1 && ov_clear(&reference) == 0); + Require(guest.Untouched()); +} + +void TestHalfrate(const std::vector& stream) { + GuestFile guest; + void* vf = guest.Get(); + Source source{&stream}; + Require(sceVorbisDecOpenCallbacks(&source, vf, nullptr, 0, SeekableCallbacks) == 0); + Source referenceSource{&stream}; + OggVorbis_File reference{}; + Require(ov_open_callbacks(&referenceSource, &reference, nullptr, 0, HostSeekableCallbacks) == 0); + + Require(sceVorbisDecHalfrateP(vf) == 0); + Require(RequireSameRead(vf, reference) > 0 && RequireSameRead(vf, reference) > 0); + Require(sceVorbisDecHalfrate(vf, 1) == 0 && ov_halfrate(&reference, 1) == 0); + Require(sceVorbisDecHalfrateP(vf) == 1 && ov_halfrate_p(&reference) == 1); + RequireSameTells(vf, reference); + const std::int64_t half = DecodeTogether(vf, reference); + Require(half > 0 && half < DecodedBytes / 2); + RequireSameTells(vf, reference); + Require(sceVorbisDecPcmTell(vf) > FirstSamples); + Require(sceVorbisDecHalfrate(vf, 0) == 0 && sceVorbisDecHalfrateP(vf) == 0); + Require(sceVorbisDecClear(vf) == 0 && source.closes == 1 && ov_clear(&reference) == 0); + Require(guest.Untouched()); +} + +void TestCrossLap(const std::vector& stream) { + GuestFile first; + GuestFile second; + GuestFile closed; + GuestFile otherClosed; + Source firstSource{&stream}; + Source secondSource{&stream}; + Require(sceVorbisDecOpenCallbacks(&secondSource, second.Get(), nullptr, 0, SeekableCallbacks) == 0); + firstSource.probe = second.Get(); + Require(sceVorbisDecOpenCallbacks(&firstSource, first.Get(), nullptr, 0, SeekableCallbacks) == 0); + Require(firstSource.probed == 2); + Source firstReferenceSource{&stream}; + Source secondReferenceSource{&stream}; + OggVorbis_File firstReference{}; + OggVorbis_File secondReference{}; + Require(ov_open_callbacks(&firstReferenceSource, &firstReference, nullptr, 0, HostSeekableCallbacks) == 0); + Require(ov_open_callbacks(&secondReferenceSource, &secondReference, nullptr, 0, HostSeekableCallbacks) == 0); + + Require(RequireSameRead(first.Get(), firstReference) > 0 && RequireSameRead(first.Get(), firstReference) > 0); + Require(sceVorbisDecPcmSeek(second.Get(), FirstSamples + 2000) == 0 && ov_pcm_seek(&secondReference, FirstSamples + 2000) == 0); + Require(sceVorbisDecCrossLap(first.Get(), second.Get()) == 0 && ov_crosslap(&firstReference, &secondReference) == 0); + for (int round = 0; round < 3; ++round) { + Require(RequireSameRead(second.Get(), secondReference) > 0); + Require(RequireSameRead(first.Get(), firstReference) > 0); + } + RequireSameTells(first.Get(), firstReference); + RequireSameTells(second.Get(), secondReference); + Require(sceVorbisDecCrossLap(first.Get(), first.Get()) == 0); + Require(sceVorbisDecCrossLap(first.Get(), closed.Get()) == OV_EINVAL && sceVorbisDecCrossLap(closed.Get(), second.Get()) == OV_EINVAL); + Require(sceVorbisDecCrossLap(closed.Get(), otherClosed.Get()) == OV_EINVAL && sceVorbisDecCrossLap(closed.Get(), closed.Get()) == 0); + Require(sceVorbisDecCrossLap(nullptr, nullptr) == 0); + OggVorbis_File zeroed{}; + Require(sceVorbisDecCrossLap(closed.Get(), nullptr) == OV_EINVAL && ov_crosslap(&zeroed, nullptr) == OV_EINVAL); + Require(Throws([&] { sceVorbisDecCrossLap(first.Get(), nullptr); })); + Require(Throws([&] { sceVorbisDecCrossLap(nullptr, closed.Get()); })); + Require(sceVorbisDecClear(first.Get()) == 0 && sceVorbisDecClear(second.Get()) == 0); + Require(firstSource.closes == 1 && secondSource.closes == 1); + Require(ov_clear(&firstReference) == 0 && ov_clear(&secondReference) == 0); + Require(first.Untouched() && second.Untouched() && closed.Untouched() && otherClosed.Untouched()); +} + +void TestNeverOpened() { + GuestFile guest; + void* vf = guest.Get(); + Require(sceVorbisDecStreams(vf) == 0 && sceVorbisDecSeekable(vf) == 0); + Require(sceVorbisDecSerialNumber(vf, -1) == 0 && sceVorbisDecSerialNumber(vf, 0) == 0 && sceVorbisDecSerialNumber(vf, 3) == 0); + Require(sceVorbisDecBitrate(vf, -1) == OV_EINVAL && sceVorbisDecBitrate(vf, 0) == OV_EINVAL && sceVorbisDecBitrateInstant(vf) == OV_EINVAL); + Require(sceVorbisDecRawTotal(vf, -1) == OV_EINVAL && sceVorbisDecPcmTotal(vf, 0) == OV_EINVAL && sceVorbisDecTimeTotal(vf, -1) == OV_EINVAL); + Require(sceVorbisDecRawSeek(vf, 0) == OV_EINVAL && sceVorbisDecPcmSeek(vf, 0) == OV_EINVAL && sceVorbisDecPcmSeekPage(vf, 0) == OV_EINVAL); + Require(sceVorbisDecTimeSeek(vf, 0.0) == OV_EINVAL && sceVorbisDecTimeSeekPage(vf, 0.0) == OV_EINVAL); + Require(sceVorbisDecRawSeekLap(vf, 0) == OV_EINVAL && sceVorbisDecPcmSeekLap(vf, 0) == OV_EINVAL && sceVorbisDecPcmSeekPageLap(vf, 0) == OV_EINVAL); + Require(sceVorbisDecTimeSeekLap(vf, 0.0) == OV_EINVAL && sceVorbisDecTimeSeekPageLap(vf, 0.0) == OV_EINVAL); + Require(sceVorbisDecRawTell(vf) == OV_EINVAL && sceVorbisDecPcmTell(vf) == OV_EINVAL && sceVorbisDecTimeTell(vf) == OV_EINVAL); + Require(sceVorbisDecInfo(vf, -1) == nullptr && sceVorbisDecInfo(vf, 0) == nullptr); + Require(sceVorbisDecComment(vf, -1) == nullptr && sceVorbisDecComment(vf, 0) == nullptr); + std::array buffer{}; + int bitstream = 7; + Require(sceVorbisDecRead(vf, buffer.data(), static_cast(buffer.size()), 0, 2, 1, &bitstream) == OV_EINVAL && bitstream == 7); + float** pcm = nullptr; + Require(sceVorbisDecReadFloat(vf, &pcm, 64, &bitstream) == OV_EINVAL && pcm == nullptr && bitstream == 7); + FilterLog log; + Require(sceVorbisDecReadFilter(vf, buffer.data(), static_cast(buffer.size()), 0, 2, 1, &bitstream, GuestHalve, &log) == OV_EINVAL); + Require(log.calls == 0 && bitstream == 7); + Require(sceVorbisDecHalfrate(vf, 1) == OV_EINVAL && sceVorbisDecHalfrateP(vf) == OV_EINVAL); + Require(sceVorbisDecClear(vf) == 0 && sceVorbisDecClear(nullptr) == 0); + Require(guest.Untouched()); + + Source source; + Require(Throws([&] { sceVorbisDecOpenCallbacks(&source, nullptr, nullptr, 0, SeekableCallbacks); })); + Require(Throws([] { sceVorbisDecStreams(nullptr); })); + Require(Throws([] { sceVorbisDecInfo(nullptr, 0); })); + Require(Throws([] { sceVorbisDecPcmSeek(nullptr, 0); })); +} + +void TestFailedOpens(const std::vector& stream) { + GuestFile guest; + void* vf = guest.Get(); + std::vector noise(8192); + std::uint32_t state = 0x2545f491; + for (auto& value : noise) { + state = state * 1664525u + 1013904223u; + value = static_cast(state >> 24); + } + Source noiseSource{&noise}; + Require(sceVorbisDecOpenCallbacks(&noiseSource, vf, nullptr, 0, SeekableCallbacks) == OV_ENOTVORBIS); + Require(noiseSource.closes == 0 && sceVorbisDecStreams(vf) == 0 && sceVorbisDecInfo(vf, 0) == nullptr); + Require(sceVorbisDecClear(vf) == 0 && noiseSource.closes == 0); + + Source untold{&stream}; + constexpr GuestCallbacks NoTell{GuestRead, GuestSeek, GuestClose, nullptr}; + Require(sceVorbisDecOpenCallbacks(&untold, vf, nullptr, 0, NoTell) == OV_EINVAL); + Require(untold.closes == 0 && sceVorbisDecStreams(vf) == 0 && sceVorbisDecClear(vf) == 0 && untold.closes == 0); + + constexpr GuestCallbacks NoRead{nullptr, GuestSeek, GuestClose, GuestTell}; + Source unread{&stream}; + OggVorbis_File reference{}; + const ov_callbacks hostNoRead{nullptr, HostSeek, HostClose, HostTell}; + Require(sceVorbisDecOpenCallbacks(&unread, vf, nullptr, 0, NoRead) == ov_open_callbacks(&unread, &reference, nullptr, 0, hostNoRead)); + Require(unread.closes == 0 && sceVorbisDecStreams(vf) == 0); + + Require(sceVorbisDecOpenCallbacks(nullptr, vf, nullptr, 0, SeekableCallbacks) == OV_ENOTVORBIS); + + Source previous{&stream}; + Source next{&stream}; + Require(sceVorbisDecOpenCallbacks(&previous, vf, nullptr, 0, SeekableCallbacks) == 0); + Require(sceVorbisDecOpenCallbacks(&next, vf, nullptr, 0, StreamingCallbacks) == 0); + Require(previous.closes == 0 && sceVorbisDecSeekable(vf) == 0 && sceVorbisDecStreams(vf) == 1); + Require(sceVorbisDecClear(vf) == 0 && previous.closes == 0 && next.closes == 1); + + Source unsized{&stream}; + Require(sceVorbisDecOpenCallbacks(&unsized, vf, nullptr, std::int64_t{1} << 32, SeekableCallbacks) == 0); + Require(sceVorbisDecStreams(vf) == 2 && sceVorbisDecClear(vf) == 0 && unsized.closes == 1); + + const auto* initial = reinterpret_cast(stream.data()); + const auto size = static_cast(stream.size()); + Require(sceVorbisDecOpenCallbacks(nullptr, vf, initial, size, SeekableCallbacks) == 0); + Require(ov_open_callbacks(nullptr, &reference, initial, static_cast(size), HostSeekableCallbacks) == 0); + Require(sceVorbisDecSeekable(vf) == 0 && DecodeTogether(vf, reference) == DecodedBytes); + Require(sceVorbisDecClear(vf) == 0 && ov_clear(&reference) == 0); + Require(guest.Untouched()); +} + +void TestThrowingCallbacks(const std::vector& stream) { + GuestFile guest; + Source source{&stream}; + Require(sceVorbisDecOpenCallbacks(&source, guest.Get(), nullptr, 0, SeekableCallbacks) == 0); + std::array buffer{}; + int bitstream = -1; + source.throwOnRead = true; + bool threw = false; + for (int call = 0; call < 1024 && !threw; ++call) { + threw = Throws([&] { sceVorbisDecRead(guest.Get(), buffer.data(), static_cast(buffer.size()), 0, 2, 1, &bitstream); }); + } + Require(threw); + source.throwOnRead = false; + source.throwOnClose = true; + Require(Throws([&] { sceVorbisDecClear(guest.Get()); })); + Require(source.closes == 1 && sceVorbisDecStreams(guest.Get()) == 0); + Require(sceVorbisDecClear(guest.Get()) == 0 && source.closes == 1); + Require(guest.Untouched()); +} + } int main() { - Require(Throws([] { sceVorbisDecOpenCallbacks(); }), "sceVorbisDecOpenCallbacks must throw"); - Require(Throws([] { sceVorbisDecClear(); }), "sceVorbisDecClear must throw"); - Require(Throws([] { sceVorbisDecBitrate(); }), "sceVorbisDecBitrate must throw"); - Require(Throws([] { sceVorbisDecBitrateInstant(); }), "sceVorbisDecBitrateInstant must throw"); - Require(Throws([] { sceVorbisDecStreams(); }), "sceVorbisDecStreams must throw"); - Require(Throws([] { sceVorbisDecSeekable(); }), "sceVorbisDecSeekable must throw"); - Require(Throws([] { sceVorbisDecSerialNumber(); }), "sceVorbisDecSerialNumber must throw"); - Require(Throws([] { sceVorbisDecRawTotal(); }), "sceVorbisDecRawTotal must throw"); - Require(Throws([] { sceVorbisDecPcmTotal(); }), "sceVorbisDecPcmTotal must throw"); - Require(Throws([] { sceVorbisDecTimeTotal(); }), "sceVorbisDecTimeTotal must throw"); - Require(Throws([] { sceVorbisDecRawSeek(); }), "sceVorbisDecRawSeek must throw"); - Require(Throws([] { sceVorbisDecPcmSeek(); }), "sceVorbisDecPcmSeek must throw"); - Require(Throws([] { sceVorbisDecPcmSeekPage(); }), "sceVorbisDecPcmSeekPage must throw"); - Require(Throws([] { sceVorbisDecTimeSeek(); }), "sceVorbisDecTimeSeek must throw"); - Require(Throws([] { sceVorbisDecTimeSeekPage(); }), "sceVorbisDecTimeSeekPage must throw"); - Require(Throws([] { sceVorbisDecRawSeekLap(); }), "sceVorbisDecRawSeekLap must throw"); - Require(Throws([] { sceVorbisDecPcmSeekLap(); }), "sceVorbisDecPcmSeekLap must throw"); - Require(Throws([] { sceVorbisDecPcmSeekPageLap(); }), "sceVorbisDecPcmSeekPageLap must throw"); - Require(Throws([] { sceVorbisDecTimeSeekLap(); }), "sceVorbisDecTimeSeekLap must throw"); - Require(Throws([] { sceVorbisDecTimeSeekPageLap(); }), "sceVorbisDecTimeSeekPageLap must throw"); - Require(Throws([] { sceVorbisDecRawTell(); }), "sceVorbisDecRawTell must throw"); - Require(Throws([] { sceVorbisDecPcmTell(); }), "sceVorbisDecPcmTell must throw"); - Require(Throws([] { sceVorbisDecTimeTell(); }), "sceVorbisDecTimeTell must throw"); - Require(Throws([] { sceVorbisDecInfo(); }), "sceVorbisDecInfo must throw"); - Require(Throws([] { sceVorbisDecComment(); }), "sceVorbisDecComment must throw"); - Require(Throws([] { sceVorbisDecRead(); }), "sceVorbisDecRead must throw"); - Require(Throws([] { sceVorbisDecReadFloat(); }), "sceVorbisDecReadFloat must throw"); - Require(Throws([] { sceVorbisDecReadFilter(); }), "sceVorbisDecReadFilter must throw"); - Require(Throws([] { sceVorbisDecCrossLap(); }), "sceVorbisDecCrossLap must throw"); - Require(Throws([] { sceVorbisDecHalfrate(); }), "sceVorbisDecHalfrate must throw"); - Require(Throws([] { sceVorbisDecHalfrateP(); }), "sceVorbisDecHalfrateP must throw"); - Require(Throws([] { sceVorbisDecGetDecodePriority(); }), "sceVorbisDecGetDecodePriority must throw"); - Require(Throws([] { sceVorbisDecSetDecodePriority(); }), "sceVorbisDecSetDecodePriority must throw"); - Require(Throws([] { sceVorbisDecGetLastDecodeError(); }), "sceVorbisDecGetLastDecodeError must throw"); + std::vector stream; + EncodeLink(stream, FirstSerial, FirstChannels, FirstRate, FirstSamples, "first"); + EncodeLink(stream, SecondSerial, SecondChannels, SecondRate, SecondSamples, "second"); + + TestSeekable(stream); + TestStreaming(stream); + TestFloatAndFilter(stream); + TestHalfrate(stream); + TestCrossLap(stream); + TestNeverOpened(); + TestFailedOpens(stream); + TestThrowingCallbacks(stream); + + Require(Throws([] { sceVorbisDecGetDecodePriority(); })); + Require(Throws([] { sceVorbisDecSetDecodePriority(); })); + Require(Throws([] { sceVorbisDecGetLastDecodeError(); })); std::puts("All VorbisDec guest tests passed"); return 0; diff --git a/core/libs/tests/GuestWebBrowserDialogCookies.cpp b/core/libs/tests/GuestWebBrowserDialogCookies.cpp new file mode 100644 index 0000000000..723bada25d --- /dev/null +++ b/core/libs/tests/GuestWebBrowserDialogCookies.cpp @@ -0,0 +1,146 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include + +struct ResetCookieParam { + std::uint64_t size; + std::uint8_t reserved[256]; +}; + +struct SetCookieParam { + std::uint64_t size; + const char* url; + const char* cookie; + std::uint8_t reserved[256]; +}; + +extern "C" { +int APS5_VABI sceWebBrowserDialogInitialize(void); +int APS5_VABI sceWebBrowserDialogTerminate(void); +int APS5_VABI sceWebBrowserDialogOpen(const void* param); +int APS5_VABI sceWebBrowserDialogClose(void); +int APS5_VABI sceWebBrowserDialogUpdateStatus(void); +int APS5_VABI sceWebBrowserDialogSetCookie(const SetCookieParam* param); +int APS5_VABI sceWebBrowserDialogResetCookie(const ResetCookieParam* param); +} + +namespace { + +constexpr int NOT_INITIALIZED = static_cast(0x80B80003u); +constexpr int INVALID_STATE = static_cast(0x80B80006u); +constexpr int NOT_RUNNING = static_cast(0x80B8000Bu); +constexpr int PARAM_INVALID = static_cast(0x80B8000Au); +constexpr int ARG_NULL = static_cast(0x80B8000Du); + +void Require(bool value, const char* what) { + if (value) return; + std::fprintf(stderr, "FAIL: %s\n", what); + std::abort(); +} + +SetCookieParam Cookie(const char* url, const char* cookie) { + SetCookieParam param {}; + param.size = sizeof(param); + param.url = url; + param.cookie = cookie; + return param; +} + +ResetCookieParam Reset() { + ResetCookieParam param {}; + param.size = sizeof(param); + return param; +} + +void CheckSetCookieParameters() { + auto valid = Cookie("https://example.com", "a=b"); + Require(sceWebBrowserDialogSetCookie(&valid) == 0, "valid SetCookie"); + Require(sceWebBrowserDialogSetCookie(nullptr) == ARG_NULL, "SetCookie null"); + + auto wrongSize = valid; + wrongSize.size = sizeof(ResetCookieParam); + Require(sceWebBrowserDialogSetCookie(&wrongSize) == PARAM_INVALID, "SetCookie size"); + auto noUrl = Cookie(nullptr, "a=b"); + Require(sceWebBrowserDialogSetCookie(&noUrl) == PARAM_INVALID, "SetCookie null url"); + auto emptyUrl = Cookie("", "a=b"); + Require(sceWebBrowserDialogSetCookie(&emptyUrl) == PARAM_INVALID, "SetCookie empty url"); + auto noCookie = Cookie("https://example.com", nullptr); + Require(sceWebBrowserDialogSetCookie(&noCookie) == PARAM_INVALID, "SetCookie null cookie"); + auto emptyCookie = Cookie("https://example.com", ""); + Require(sceWebBrowserDialogSetCookie(&emptyCookie) == PARAM_INVALID, "SetCookie empty cookie"); + + const std::string longestUrl(0x7ff, 'u'); + const std::string longUrl(0x800, 'u'); + auto longest = Cookie(longestUrl.c_str(), "a=b"); + Require(sceWebBrowserDialogSetCookie(&longest) == 0, "SetCookie 2047-byte url"); + auto tooLong = Cookie(longUrl.c_str(), "a=b"); + Require(sceWebBrowserDialogSetCookie(&tooLong) == PARAM_INVALID, "SetCookie 2048-byte url"); + + const std::string longestCookie(0xfff, 'c'); + const std::string longCookie(0x1000, 'c'); + auto longestValue = Cookie("https://example.com", longestCookie.c_str()); + Require(sceWebBrowserDialogSetCookie(&longestValue) == 0, "SetCookie 4095-byte cookie"); + auto tooLongValue = Cookie("https://example.com", longCookie.c_str()); + Require(sceWebBrowserDialogSetCookie(&tooLongValue) == PARAM_INVALID, "SetCookie 4096-byte cookie"); + + auto reserved = valid; + reserved.reserved[255] = 1; + Require(sceWebBrowserDialogSetCookie(&reserved) == PARAM_INVALID, "SetCookie reserved"); +} + +void CheckResetCookieParameters() { + auto valid = Reset(); + Require(sceWebBrowserDialogResetCookie(&valid) == 0, "valid ResetCookie"); + Require(sceWebBrowserDialogResetCookie(nullptr) == ARG_NULL, "ResetCookie null"); + auto wrongSize = valid; + wrongSize.size = sizeof(SetCookieParam); + Require(sceWebBrowserDialogResetCookie(&wrongSize) == PARAM_INVALID, "ResetCookie size"); + auto reserved = valid; + reserved.reserved[0] = 1; + Require(sceWebBrowserDialogResetCookie(&reserved) == PARAM_INVALID, "ResetCookie reserved"); +} + +void CheckOpenDialog() { + std::uint8_t open[64] = {}; + auto cookie = Cookie("https://example.com", "a=b"); + auto reset = Reset(); + Require(sceWebBrowserDialogOpen(open) == 0, "Open"); + Require(sceWebBrowserDialogSetCookie(&cookie) == INVALID_STATE, "SetCookie while open"); + Require(sceWebBrowserDialogSetCookie(nullptr) == INVALID_STATE, "SetCookie checks the open dialog before null"); + Require(sceWebBrowserDialogResetCookie(nullptr) == ARG_NULL, "ResetCookie checks null before the open dialog"); + Require(sceWebBrowserDialogResetCookie(&reset) == INVALID_STATE, "ResetCookie while open"); + Require(sceWebBrowserDialogUpdateStatus() == 3, "UpdateStatus finished"); + Require(sceWebBrowserDialogSetCookie(&cookie) == 0, "SetCookie after UpdateStatus"); + Require(sceWebBrowserDialogResetCookie(&reset) == 0, "ResetCookie after UpdateStatus"); + + Require(sceWebBrowserDialogOpen(open) == 0, "second Open"); + Require(sceWebBrowserDialogClose() == NOT_RUNNING, "Close of a finished dialog"); + Require(sceWebBrowserDialogSetCookie(&cookie) == 0, "SetCookie after Close"); + + Require(sceWebBrowserDialogOpen(open) == 0, "third Open"); + Require(sceWebBrowserDialogTerminate() == 0, "Terminate"); + Require(sceWebBrowserDialogInitialize() == 0, "Initialize again"); + Require(sceWebBrowserDialogResetCookie(&reset) == 0, "ResetCookie after Terminate"); +} + +} + +int main() { + auto cookie = Cookie("https://example.com", "a=b"); + auto reset = Reset(); + Require(sceWebBrowserDialogSetCookie(&cookie) == NOT_INITIALIZED, "SetCookie before Initialize"); + Require(sceWebBrowserDialogSetCookie(nullptr) == NOT_INITIALIZED, "SetCookie null before Initialize"); + Require(sceWebBrowserDialogResetCookie(&reset) == NOT_INITIALIZED, "ResetCookie before Initialize"); + Require(sceWebBrowserDialogResetCookie(nullptr) == NOT_INITIALIZED, "ResetCookie null before Initialize"); + + Require(sceWebBrowserDialogInitialize() == 0, "Initialize"); + CheckSetCookieParameters(); + CheckResetCookieParameters(); + CheckOpenDialog(); + Require(sceWebBrowserDialogTerminate() == 0, "final Terminate"); + std::puts("guest web browser dialog cookies: PASS"); + return 0; +} diff --git a/core/libs/tests/GuestWideConversion.cpp b/core/libs/tests/GuestWideConversion.cpp index 07b28e68ca..1ade68c23f 100644 --- a/core/libs/tests/GuestWideConversion.cpp +++ b/core/libs/tests/GuestWideConversion.cpp @@ -139,13 +139,15 @@ static void CheckScan() { double real = 0; Require(swscanf_s_nid_postfix(u"1.5e3", u"%lf", &real) == 1 && real == 1500.0, "%lf"); - Require(swscanf_s_nid_postfix(u"1e+x", u"%lf", &real) == 0, "%lf incomplete exponent is a matching failure"); + Require(swscanf_s_nid_postfix(u"1e+", u"%lf", &real) == 1 && real == 1.0, "%lf converts the mantissa of an incomplete exponent"); long double extended = 0; Require(swscanf_s_nid_postfix(u"2.25", u"%Lf", &extended) == 1 && extended == 2.25L, "%Lf"); float single = 0; Require(swscanf_s_nid_postfix(u"-inf", u"%f", &single) == 1 && single == -std::numeric_limits::infinity(), "%f infinity"); Require(swscanf_s_nid_postfix(u"a", u"b%d", &first) == 0, "literal mismatch is a matching failure"); + Require(swscanf_s_nid_postfix(u"", u"x") == 0, "literal at end of input returns zero"); + Require(swscanf_s_nid_postfix(u"", u"%%") == -1, "percent literal at end of input is an input failure"); Require(swscanf_s_nid_postfix(u"", u"%d", &first) == -1, "empty input is an input failure"); Require(swscanf_s_nid_postfix(u" ", u"%ls", text, std::size_t{6}) == -1, "white space only is an input failure"); Require(swscanf_s_nid_postfix(u"5", u"%d%d", &first, &second) == 1, "input failure after a conversion"); @@ -203,13 +205,15 @@ static void CheckUnsizedScan() { double real = 0; Require(swscanf_nid_postfix(u"1.5e3", u"%lf", &real) == 1 && real == 1500.0, "swscanf %lf"); - Require(swscanf_nid_postfix(u"1e+x", u"%lf", &real) == 0, "swscanf %lf incomplete exponent is a matching failure"); + Require(swscanf_nid_postfix(u"1e+", u"%lf", &real) == 1 && real == 1.0, "swscanf %lf converts the mantissa of an incomplete exponent"); long double extended = 0; Require(swscanf_nid_postfix(u"2.25", u"%Lf", &extended) == 1 && extended == 2.25L, "swscanf %Lf"); float single = 0; Require(swscanf_nid_postfix(u"-inf", u"%f", &single) == 1 && single == -std::numeric_limits::infinity(), "swscanf %f infinity"); Require(swscanf_nid_postfix(u"a", u"b%d", &first) == 0, "swscanf literal mismatch is a matching failure"); + Require(swscanf_nid_postfix(u"", u"x") == 0, "swscanf literal at end of input returns zero"); + Require(swscanf_nid_postfix(u"", u"%%") == -1, "swscanf percent literal at end of input is an input failure"); Require(swscanf_nid_postfix(u"3 x", u"%d%d", &first, &second) == 1 && first == 3, "swscanf partial match"); Require(swscanf_nid_postfix(u"", u"%d", &first) == -1, "swscanf empty input is an input failure"); Require(swscanf_nid_postfix(u"", u"%ls", text) == -1, "swscanf empty input before %ls"); diff --git a/core/libs/tests/GuestWideStreamScan.cpp b/core/libs/tests/GuestWideStreamScan.cpp index 0da21861b0..632a989759 100644 --- a/core/libs/tests/GuestWideStreamScan.cpp +++ b/core/libs/tests/GuestWideStreamScan.cpp @@ -101,6 +101,14 @@ static void CheckConsumption() { Require(fgetwc_nid_postfix(stream) == u'b', "the mismatching character is pushed back"); Close(stream); + stream = Open(""); + Require(ScanStream(stream, u"x") == 0, "a literal at end of input returns zero"); + Close(stream); + + stream = Open(""); + Require(ScanStream(stream, u"%%") == -1, "a percent literal at end of input is an input failure"); + Close(stream); + stream = Open("x"); Require(ScanStream(stream, u"%d", &value) == 0, "a letter is not an integer"); Require(fgetwc_nid_postfix(stream) == u'x', "the rejected letter is pushed back"); @@ -120,6 +128,10 @@ static void CheckConsumption() { Require(ScanStream(stream, u"%lf", &real) == 1 && real == 2.5, "a float"); Require(fgetwc_nid_postfix(stream) == u'x', "the character after the float"); Close(stream); + + stream = Open("1e+"); + Require(ScanStream(stream, u"%lf", &real) == 1 && real == 1.0, "an incomplete exponent converts its mantissa"); + Close(stream); } static void CheckWideText() { diff --git a/core/libs/tests/GuestWideStrings.cpp b/core/libs/tests/GuestWideStrings.cpp index 30f48c4efb..c55aac8ebb 100644 --- a/core/libs/tests/GuestWideStrings.cpp +++ b/core/libs/tests/GuestWideStrings.cpp @@ -32,6 +32,9 @@ char16_t* APS5_VABI wcsncpy_nid_postfix(char16_t* dest, const char16_t* src, std int APS5_VABI wmemcpy_s_nid_postfix(char16_t* dest, std::size_t destsz, const char16_t* src, std::size_t count); int APS5_VABI wmemmove_s_nid_postfix(char16_t* dest, std::size_t destsz, const char16_t* src, std::size_t count); int APS5_VABI wcsncpy_s_nid_postfix(char16_t* dest, std::size_t destsz, const char16_t* src, std::size_t count); +int APS5_VABI wcscpy_s_nid_postfix(char16_t* dest, std::size_t destsz, const char16_t* src); +void* APS5_VABI set_constraint_handler_s_nid_postfix(void* handler); +void APS5_VABI ignore_handler_s_nid_postfix(const char* message, void* pointer, int error); char16_t* APS5_VABI wcsncat_nid_postfix(char16_t* dest, const char16_t* src, std::size_t n); int APS5_VABI wcsncat_s_nid_postfix(char16_t* dest, std::size_t destsz, const char16_t* src, std::size_t count); int APS5_VABI wcscat_s_nid_postfix(char16_t* dest, std::size_t destsz, const char16_t* src); @@ -205,6 +208,19 @@ int main() { wcsncpy_s_nid_postfix(strBuf, 16, u"abcdef", 6); require(wcsncpy_s_nid_postfix(strBuf + 1, 10, strBuf, 3) == 22 && strBuf[1] == 0); + char16_t cpyBuf[16]; + require(wcscpy_s_nid_postfix(cpyBuf, 16, u"wide") == 0 && same(cpyBuf, u"wide", 5)); + void* previousHandler = set_constraint_handler_s_nid_postfix(reinterpret_cast(&ignore_handler_s_nid_postfix)); + require(wcscpy_s_nid_postfix(nullptr, 16, u"wide") == 22); + require(wcscpy_s_nid_postfix(cpyBuf, 0, u"wide") == 22); + require(wcscpy_s_nid_postfix(cpyBuf, 16, nullptr) == 22 && cpyBuf[0] == 0); + require(wcscpy_s_nid_postfix(cpyBuf, 4, u"wide") == 34 && cpyBuf[0] == 0); + cpyBuf[0] = 0x1234; + require(wcscpy_s_nid_postfix(cpyBuf, ((SIZE_MAX >> 1) / sizeof(char16_t)) + 1, u"a") == 34 && cpyBuf[0] == 0x1234); + wcscpy_s_nid_postfix(cpyBuf, 16, u"abcdef"); + require(wcscpy_s_nid_postfix(cpyBuf + 1, 10, cpyBuf) == 22 && cpyBuf[1] == 0); + set_constraint_handler_s_nid_postfix(previousHandler); + char16_t ncatBuf[16]; wmemset_nid_postfix(ncatBuf, 0xaaaa, 16); ncatBuf[0] = 0; diff --git a/core/libs/tests/HevcFixture.hpp b/core/libs/tests/HevcFixture.hpp new file mode 100644 index 0000000000..7bacee3a02 --- /dev/null +++ b/core/libs/tests/HevcFixture.hpp @@ -0,0 +1,133 @@ +#pragma once + +#include +#include +#include +#include + +namespace HevcFixture { + +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, + 0x95, 0x90, 0x09, 0x00, 0x00, 0x00, 0x01, 0x42, 0x01, 0x01, 0x01, 0x60, 0x00, 0x00, 0x03, 0x00, + 0x90, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x1e, 0xa0, 0x20, 0x82, 0x96, 0x59, 0x59, 0x2e, + 0x4c, 0xaf, 0x01, 0x68, 0x08, 0x00, 0x00, 0x03, 0x00, 0x08, 0x00, 0x00, 0x03, 0x00, 0xf0, 0x40, + 0x00, 0x00, 0x00, 0x01, 0x44, 0x01, 0xc1, 0x73, 0xd0, 0x89, 0x00, 0x00, 0x01, 0x28, 0x01, 0xaf, + 0x08, 0xe0, 0xbb, 0xcb, 0xc1, 0x71, 0x26, 0x0f, 0xbf, 0x6c, 0xcb, 0x4d, 0x2b, 0x2c, 0xf4, 0x08, + 0xe0, 0x17, 0xeb, 0x16, 0xcc, 0x07, 0xd8, 0xdd, 0x11, 0xf8, 0x5e, 0x73, 0x79, 0x57, 0xcb, 0x69, + 0xa3, 0x1c, 0x9c, 0x43, 0x14, 0xdb, 0x61, 0x0e, 0xc5, 0xc6, 0x9c, 0x9c, 0xf8, 0xf2, 0x07, 0x35, + 0xf1, 0x98, 0x6c, 0x1a, 0x86, 0x53, 0x5a, 0xcd, 0x4d, 0xda, 0xff, 0xfb, 0x01, 0xda, 0xc3, 0x61, + 0xb7, 0x8c, 0xae, 0xc9, 0x76, 0xe4, 0x05, 0xd6, 0xfd, 0xe4, 0xcd, 0x93, 0x2c, 0xb1, 0x41, 0x29, + 0xce, 0x22, 0xf7, 0x77, 0x64, 0x0f, 0x1f, 0xd2, 0xc3, 0x68, 0x32, 0x2f, 0x83, 0xd7, 0x53, 0xbe, + 0x23, 0xff, 0x8c, 0x46, 0x18, 0x33, 0x90, 0x28, 0xa3, 0xef, 0xd7, 0xb1, 0x46, 0x84, 0xa0, 0xc2, + 0xd4, 0xd9, 0xce, 0xdf, 0x0a, 0x05, 0xec, 0xad, 0x63, 0xa4, 0x78, 0x27, 0x80, 0x4e, 0x3b, 0x4c, + 0xa6, 0x42, 0xbc, 0xe3, 0x39, 0x4d, 0xc4, 0xd3, 0x44, 0xf1, 0x22, 0xd0, 0x64, 0x9f, 0xd0, 0x88, + 0x7f, 0x39, 0x6d, 0x06, 0xc7, 0x79, 0x75, 0x2a, 0x44, 0x3d, 0x21, 0x41, 0x53, 0x60, 0x8d, 0x08, + 0x9f, 0xd0, 0x34, 0xa6, 0x72, 0x70, 0x3b, 0xa9, 0xfa, 0x62, 0xb1, 0x19, 0x9b, 0x0b, 0xa2, 0xfc, + 0xb4, 0xb3, 0x2c, 0x58, 0xad, 0xc6, 0xf3, 0xea, 0xc4, 0xd0, 0xf9, 0xea, 0x9e, 0xa7, 0xa4, 0x87, + 0xb8, 0x19, 0x68, 0xf1, 0xc1, 0x6b, 0x87, 0xa5, 0x99, 0xa0, 0x69, 0x24, 0x28, 0x80, 0xba, 0xd2, + 0x57, 0xc6, 0xc0, 0x1e, 0x69, 0x1b, 0xce, 0x96, 0xcd, 0x85, 0x9d, 0x59, 0x32, 0x92, 0x71, 0xe7, + 0x15, 0xf2, 0x9b, 0x71, 0x4b, 0xd4, 0xb6, 0x99, 0xb0, 0x71, 0x9b, 0x9b, 0xdb, 0x33, 0x28, 0x86, + 0x5e, 0x44, 0xf3, 0x5d, 0x5d, 0x7b, 0x87, 0x6c, 0x99, 0x8a, 0x6d, 0xaf, 0xf8, 0xdd, 0x39, 0x14, + 0xc0, 0x10, 0x85, 0xaf, 0x3a, 0x77, 0x60, 0x8b, 0xb7, 0x5f, 0x67, 0x45, 0xb4, 0xa7, 0xe7, 0x44, + 0x77, 0x0a, 0xb9, 0xe1, 0xb2, 0xbe, 0xde, 0x1b, 0x5a, 0xb8, 0xc8, 0x9b, 0x1f, 0x15, 0x37, 0xdc, + 0xeb, 0x55, 0x12, 0x37, 0xa1, 0xab, 0x9c, 0xcd, 0xdd, 0x42, 0x5d, 0x90, 0x89, 0xc5, 0x99, 0x54, + 0x6e, 0x9a, 0xd2, 0x71, 0x07, 0xe3, 0x81, 0x20, 0xad, 0xd4, 0x7c, 0xad, 0x69, 0xaf, 0xc7, 0x2d, + 0x7c, 0x84, 0xf1, 0x5d, 0x43, 0x4a, 0x3c, 0xf6, 0xf8, 0x7e, 0x6a, 0xb2, 0xdd, 0x00, 0xda, 0x30, + 0x00, 0x00, 0x00, 0x01, 0x46, 0x01, 0x30, 0x00, 0x00, 0x01, 0x02, 0x01, 0xd0, 0x19, 0x5f, 0x84, + 0x31, 0x84, 0x50, 0x62, 0xcd, 0x44, 0xa6, 0x4e, 0xbb, 0x2e, 0x87, 0xda, 0x21, 0x25, 0x1c, 0x13, + 0xfe, 0xc2, 0x97, 0xa3, 0xd5, 0xfd, 0xe2, 0x62, 0xd6, 0xe5, 0x50, 0x7a, 0xda, 0x98, 0x8d, 0xcf, + 0xbe, 0x40, 0x5d, 0x69, 0xb6, 0x5f, 0xdc, 0x32, 0x68, 0x05, 0x67, 0x70, 0xfd, 0xa0, 0xd3, 0xcd, + 0x88, 0xeb, 0x12, 0x2e, 0xac, 0xf5, 0x57, 0xa2, 0xc2, 0x81, 0x22, 0x6b, 0x8d, 0x69, 0x6d, 0xac, + 0x41, 0x05, 0x5b, 0x0b, 0x79, 0xe0, 0x86, 0x05, 0x86, 0x8b, 0x02, 0x51, 0x7d, 0x6e, 0x13, 0x63, + 0x36, 0xe6, 0x89, 0x22, 0x75, 0xd9, 0xb0, 0x39, 0x27, 0xcd, 0xac, 0x78, 0xe0, 0x3a, 0x6a, 0x17, + 0x15, 0x05, 0x83, 0x3a, 0xe9, 0x38, 0x7c, 0x75, 0x38, 0xf3, 0xd8, 0x42, 0x57, 0xa1, 0xef, 0x68, + 0xb8, 0xb5, 0x44, 0xfe, 0x4d, 0x66, 0xa1, 0x09, 0xc5, 0x62, 0xff, 0x87, 0x7e, 0xdb, 0x41, 0x6f, + 0x4d, 0x5f, 0x16, 0x49, 0xef, 0xca, 0xda, 0x67, 0x01, 0x29, 0x0b, 0xd6, 0xd8, 0xfc, 0x0b, 0x22, + 0xc8, 0x28, 0xff, 0xb4, 0xb3, 0xa9, 0xf8, 0x9b, 0x78, 0x7d, 0x33, 0xbf, 0xa6, 0x9b, 0x39, 0x64, + 0x57, 0x41, 0x1f, 0x27, 0x05, 0xfc, 0xcd, 0x44, 0xcd, 0xb2, 0xea, 0x79, 0xe0, 0x00, 0x00, 0x00, + 0x01, 0x46, 0x01, 0x50, 0x00, 0x00, 0x01, 0x02, 0x01, 0xe0, 0x44, 0x97, 0xe1, 0x8f, 0x40, 0x3d, + 0x73, 0xde, 0xdf, 0xf5, 0x1c, 0xff, 0xf6, 0xe2, 0x12, 0x16, 0x3d, 0xb6, 0x75, 0xad, 0x9e, 0xb5, + 0x2b, 0xe7, 0x4a, 0x40, 0xe9, 0x91, 0xd6, 0x86, 0x86, 0x2d, 0xcb, 0xe4, 0xb6, 0x58, 0x4a, 0x2d, + 0x7b, 0x1b, 0xdb, 0xc5, 0x48, 0x2d, 0x09, 0x4d, 0x32, 0x30, 0xde, 0xaa, 0x61, 0xc2, 0x27, 0x64, + 0x54, 0x71, 0x89, 0x32, 0x05, 0xc0, 0x00, 0x00, 0x00, 0x01, 0x46, 0x01, 0x50, 0x00, 0x00, 0x01, + 0x00, 0x01, 0xe0, 0x24, 0xff, 0xfa, 0x2c, 0x6e, 0xdc, 0x78, 0x3d, 0x08, 0x6d, 0x3b, 0xcf, 0xcc, + 0xb6, 0x16, 0xeb, 0x9d, 0x5f, 0x46, 0x0b, 0x24, 0x0e, 0x97, 0x99, 0x06, 0x00, 0x00, 0x00, 0x01, + 0x46, 0x01, 0x30, 0x00, 0x00, 0x01, 0x02, 0x01, 0xd0, 0x28, 0x95, 0xd7, 0xdc, 0x41, 0xc0, 0x61, + 0x14, 0xd6, 0xee, 0xd4, 0xe8, 0x24, 0x3e, 0x7f, 0x82, 0x22, 0xb0, 0x20, 0xb9, 0xf1, 0x69, 0x9b, + 0xfe, 0xcc, 0xb7, 0x2e, 0xbd, 0xdb, 0x20, 0xfa, 0xe5, 0x97, 0xd9, 0x95, 0x1d, 0x7e, 0x49, 0x35, + 0x69, 0xdb, 0x40, 0xa3, 0x5c, 0x0f, 0x74, 0x6f, 0x00, 0x3d, 0x36, 0x05, 0xc0, 0xc4, 0x37, 0x15, + 0xf9, 0x01, 0x2d, 0xdf, 0xcb, 0x70, 0x3d, 0xfa, 0x47, 0xec, 0x80, 0x07, 0x48, 0x08, 0xc8, 0x54, + 0xd3, 0xe3, 0x00, 0xa3, 0xe4, 0xce, 0x40, 0x05, 0x0b, 0x51, 0x73, 0x56, 0x33, 0x82, 0xa8, 0x0a, + 0xc0, 0xd3, 0xc5, 0xf1, 0xeb, 0x32, 0x8e, 0x4e, 0xa7, 0x6f, 0xb2, 0x64, 0x43, 0x54, 0x71, 0xd4, + 0xaf, 0xc0, 0x00, 0x00, 0x00, 0x01, 0x46, 0x01, 0x50, 0x00, 0x00, 0x01, 0x00, 0x01, 0xe0, 0x82, + 0x2f, 0x5f, 0xdc, 0x63, 0x70, 0x3f, 0xaf, 0x66, 0x75, 0x52, 0xe7, 0x7f, 0x62, 0xfb, 0x84, 0x9c, + 0xef, 0x60, 0x8f, 0xfa, 0x7e, 0x5f, 0x8b, 0x40, 0x15, 0x34, 0x69, 0xe2, 0xc4, 0xfc, 0xff, 0x53, + 0xb8, 0x1e, 0xc7, 0x20, 0x55, 0xdb, 0x5e, 0x05, 0xbd, 0x7a, 0x85, 0xb4, 0xb7, 0xc1, 0x8d, 0xdb, + 0x0f, 0xa6, 0xab, 0xb9, 0x00, 0x2a, 0xdb, 0x83, 0xa0, 0xb0, 0xc5, 0x76, 0x6e, 0xe1, 0xb8, 0x72, + 0xfd, 0x09, 0x61, 0x19, 0xe0, +}; + +inline constexpr std::array HevcMain10Stream{ + 0x00, 0x00, 0x00, 0x01, 0x46, 0x01, 0x10, 0x00, 0x00, 0x00, 0x01, 0x40, 0x01, 0x0c, 0x01, 0xff, + 0xff, 0x02, 0x20, 0x00, 0x00, 0x03, 0x00, 0x90, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x1e, + 0x95, 0x98, 0x09, 0x00, 0x00, 0x00, 0x01, 0x42, 0x01, 0x01, 0x02, 0x20, 0x00, 0x00, 0x03, 0x00, + 0x90, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x1e, 0xa0, 0x20, 0x82, 0x93, 0x65, 0x95, 0x9a, + 0xe4, 0xca, 0xf0, 0x16, 0x80, 0x80, 0x00, 0x00, 0x03, 0x00, 0x80, 0x00, 0x00, 0x0f, 0x04, 0x00, + 0x00, 0x00, 0x01, 0x44, 0x01, 0xc1, 0x73, 0xd0, 0x89, 0x00, 0x00, 0x01, 0x28, 0x01, 0xaf, 0x08, + 0x60, 0xf2, 0xfb, 0x04, 0xef, 0x67, 0x67, 0xdc, 0xc7, 0x8e, 0xc7, 0x42, 0x63, 0xeb, 0x7d, 0x28, + 0x73, 0x95, 0x14, 0x0c, 0x71, 0x10, 0x8b, 0x65, 0x04, 0xbd, 0x03, 0xe6, 0x04, 0x35, 0xb9, 0x9b, + 0xec, 0x60, 0x2e, 0xea, 0xcf, 0xd0, 0xdd, 0x2d, 0x3a, 0x62, 0x33, 0x9d, 0x7a, 0x74, 0xb0, 0xeb, + 0x0c, 0x06, 0xe6, 0xa5, 0x30, 0xa7, 0xea, 0xed, 0xc2, 0xa5, 0x8e, 0x7c, 0xda, 0x1b, 0x9d, 0xe4, + 0xac, 0x2b, 0x38, 0xdb, 0x9c, 0xe8, 0x51, 0x34, 0x46, 0xd2, 0x04, 0x1a, 0x77, 0x87, 0x7a, 0x19, + 0x70, 0xf9, 0x4f, 0xcb, 0x03, 0x3c, 0x74, 0x25, 0xb9, 0x92, 0x3b, 0x95, 0xe9, 0xd8, 0xaa, 0x9b, + 0x6b, 0x9b, 0x1f, 0x70, 0x25, 0x2a, 0xe7, 0x6c, 0xc3, 0x7e, 0x55, 0x39, 0xf5, 0x06, 0x5a, 0x23, + 0xb1, 0x2e, 0xf6, 0x01, 0xac, 0x71, 0xb9, 0x20, 0xb4, 0xef, 0x1d, 0x42, 0x7e, 0x0e, 0x01, 0x1d, + 0xaf, 0x0a, 0x13, 0x04, 0x8e, 0x03, 0x4d, 0x1e, 0xc4, 0xe0, +}; + +inline constexpr std::array HevcCroppedStream{ + 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, 0x95, 0x98, 0x09, 0x00, 0x00, 0x00, 0x01, + 0x42, 0x01, 0x01, 0x01, 0x60, 0x00, 0x00, 0x03, 0x00, 0x90, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, + 0x00, 0x1e, 0xa0, 0x20, 0x82, 0x9f, 0x79, 0x65, 0x66, 0xab, 0xca, 0xf0, 0x16, 0x80, 0x80, 0x00, + 0x00, 0x03, 0x00, 0x80, 0x00, 0x00, 0x0f, 0x04, 0x00, 0x00, 0x00, 0x01, 0x44, 0x01, 0xc1, 0x73, + 0xd1, 0x89, 0x00, 0x00, 0x01, 0x28, 0x01, 0xaf, 0x08, 0x59, 0xf2, 0xec, 0x80, 0xf9, 0x31, 0xa3, + 0xfa, 0x90, 0xca, 0x97, 0x42, 0xb5, 0x6f, 0xcb, 0xc8, 0xd2, 0x51, 0xa2, 0x3f, 0xe0, 0x0d, 0x82, + 0x6f, 0x1f, 0x37, 0xd4, 0x65, 0x18, 0x78, 0x60, 0x51, 0x3b, 0x98, 0xd0, 0x44, 0xfa, 0xff, 0x0b, + 0xd5, 0xdc, 0x09, 0xea, 0xf7, 0x79, 0xb4, 0xd2, 0xbc, 0x97, 0x53, 0xf2, 0x7d, 0x10, 0x5c, 0xc8, + 0x3a, 0xe0, 0xa7, 0xd8, 0xe2, 0xfc, 0x1d, 0x80, 0x09, 0xcd, 0x31, 0x09, 0xa4, 0xa9, 0x05, 0x5d, + 0x08, 0x74, 0x50, 0xf5, 0xbe, 0xf9, 0xc8, 0xa6, 0xef, 0xe4, 0xff, 0xd6, 0x86, 0x02, 0xb6, 0xa2, + 0x3f, 0x73, 0x80, 0xdb, 0x4e, 0x76, 0xa8, 0xc7, 0xac, 0x4b, 0x5b, 0xbf, 0xe1, 0x88, 0x18, 0xce, + 0x14, 0x5e, 0xf4, 0xb3, 0x57, 0xe6, 0x83, 0xe1, 0x94, 0x8f, 0x55, 0x77, 0xbb, 0x1c, 0x77, 0xd3, + 0x9e, 0xdc, 0xd5, 0x8a, 0x61, 0xb2, 0xf4, 0x77, 0xb8, 0xd1, 0x27, 0x5c, 0x41, 0x75, 0x0a, 0x82, + 0xf5, 0xe8, 0x32, 0x45, 0xae, 0x94, 0x07, 0xcb, 0xde, 0x35, 0x53, 0x3c, 0x0d, 0xf6, 0x5d, 0x1f, + 0x97, 0x0b, 0x04, 0x90, 0x4b, 0xd3, 0xf6, 0x04, 0x58, 0x26, 0xe2, 0x08, 0xe5, 0xb3, 0xa3, 0x05, + 0x5a, 0x8f, 0x6b, 0x03, 0x4b, 0x0a, 0xd4, 0x63, 0x9d, 0x2e, 0xe2, 0x19, 0xa3, 0xde, 0xb5, 0x53, + 0xf2, 0x07, 0xdc, 0xc8, 0xda, 0x2c, 0xfa, 0x2e, 0x62, 0x39, 0xf3, 0xfa, 0x2a, 0x77, 0xc2, 0xb9, + 0x76, 0x63, 0x58, 0xd3, 0xf8, 0xa6, 0xcb, 0x2b, 0xa4, 0xa8, 0xc8, 0x4a, 0xed, 0xbe, 0x8e, 0xf0, + 0xc9, 0x12, 0xf4, 0xb2, 0x91, 0xad, 0xd2, 0x18, 0xcc, 0x0c, 0x4d, 0x56, 0xd9, 0xa3, 0x09, 0x6c, + 0x01, 0x3a, 0xa4, 0xa5, 0x10, 0x98, 0x4d, 0x85, 0x92, 0xa2, 0xfa, 0x50, 0xfe, 0x2d, 0x42, 0xab, + 0xf3, 0x98, 0xe7, 0x16, 0xf1, 0x0b, 0xac, 0x74, 0x5d, 0xcd, 0xfa, 0xe3, 0x75, 0xf3, 0x66, 0x1e, + 0xf0, 0xa3, 0xad, 0x82, 0x8e, 0x25, 0x2c, 0x8f, 0xe1, 0xac, 0xcb, 0xca, 0x0b, 0x19, 0x32, 0x83, + 0x4a, 0xfd, 0x30, 0xc4, 0xe5, 0x97, 0x73, 0xfd, 0x43, 0xd0, +}; + +inline constexpr std::array HevcHashes{ + 0x7291c31ee57c5065ull, 0x37b15adc47810098ull, 0xc84737b65a65b247ull, + 0xe39bc891f8286267ull, 0x734caf1ba961b3c2ull, 0x7deb44bd1ed30531ull, +}; + +inline std::vector> hevcUnits(const std::uint8_t* stream, std::size_t size) { + std::vector starts; + for (std::size_t index = 0; index + 4 < size; ++index) { + if (stream[index] == 0 && stream[index + 1] == 0 && stream[index + 2] == 0 && stream[index + 3] == 1 && ((stream[index + 4] >> 1u) & 0x3fu) == 35) starts.push_back(index); + } + starts.push_back(size); + std::vector> units; + for (std::size_t unit = 0; unit + 1 < starts.size(); ++unit) units.emplace_back(stream + starts[unit], stream + starts[unit + 1]); + return units; +} + +} diff --git a/core/libs/tests/NetEpollLifetime.cpp b/core/libs/tests/NetEpollLifetime.cpp new file mode 100644 index 0000000000..47d551e7e1 --- /dev/null +++ b/core/libs/tests/NetEpollLifetime.cpp @@ -0,0 +1,212 @@ +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +static int InterceptPoll(WSAPOLLFD* descriptors, ULONG count, INT timeout); +#define WSAPoll InterceptPoll +#else +#include +static int InterceptPoll(pollfd* descriptors, nfds_t count, int timeout); +#define poll InterceptPoll +#endif +#include "../prx/libSceNet/Export.cpp" +#ifdef _WIN32 +#undef WSAPoll +#else +#undef poll +#endif +#include +#include + +static void Require(bool value, const std::source_location location = std::source_location::current()) { + if (value) return; + std::fprintf(stderr, "requirement failed at %s:%u\n", location.file_name(), location.line()); + std::abort(); +} + +struct PollGate { + std::promise arrived; + std::future released; + int result = 0; +}; + +static std::atomic pollGate{nullptr}; + +#ifdef _WIN32 +static int InterceptPoll(WSAPOLLFD* descriptors, ULONG count, INT timeout) { + const int result = ::WSAPoll(descriptors, count, timeout); +#else +static int InterceptPoll(pollfd* descriptors, nfds_t count, int timeout) { + const int result = ::poll(descriptors, count, timeout); +#endif + if (auto* gate = pollGate.exchange(nullptr)) { + gate->result = result; + gate->arrived.set_value(); + gate->released.wait(); + } + return result; +} + +class ReuseSlot { +public: +#ifndef _WIN32 + ReuseSlot() : descriptor(::open("/dev/null", O_RDONLY | O_CLOEXEC)) { Require(descriptor >= 0); } + ~ReuseSlot() { if (descriptor >= 0) ::close(descriptor); } + void Release() { Require(::close(descriptor) == 0); descriptor = -1; } +private: + int descriptor; +#else + void Release() {} +#endif +}; + +static constexpr std::uint64_t OldData = 0x123456789abcdef0; +static constexpr std::uint64_t NewData = 0xfedcba9876543210; + +static int Socket() { + const int descriptor = sceNetSocket("epoll-lifetime", 2, 2, 17); + Require(descriptor >= 0); + return descriptor; +} + +static std::array Bind(int descriptor) { + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(sceNetBind_nid_postfix(descriptor, address.data(), address.size()) == 0); + std::uint32_t size = address.size(); + Require(sceNetGetsockname(descriptor, address.data(), &size) == 0 && size == address.size()); + return address; +} + +static void Send(int sender, const std::array& address) { + const char payload[] = "epoll-lifetime"; + Require(sceNetSendto(sender, payload, sizeof(payload), 0, address.data(), address.size()) == sizeof(payload)); +} + +static void Register(int epoll, int descriptor, int operation, std::uint64_t data, std::uint32_t events = 1) { + NetEpollEvent event{}; + event.events = events; + event.ident = 0xaaaaaaaaaaaaaaaa; + event.reserved = 0x55555555; + event.data.u64 = data; + Require(sceNetEpollControl(epoll, operation, descriptor, &event) == 0); +} + +static void CheckEvent(const NetEpollEvent& event, int descriptor, std::uint64_t data, std::uint32_t events = 1) { + Require(event.ident == static_cast(descriptor)); + Require(event.data.u64 == data && event.events == events && event.reserved == 0); +} + +static void CheckReuse() { + ReuseSlot slot; + const int original = Socket(); + const auto retained = g_socks.at(original).native; + const int sender = Socket(); + std::array epolls{sceNetEpollCreate("reuse-a", 0), sceNetEpollCreate("reuse-b", 0)}; + for (const int epoll : epolls) { + Require(epoll >= 0); + Register(epoll, original, 1, OldData); + } + Require(sceNetSocketClose(original) == 0); + slot.Release(); + const int replacement = Socket(); + Require(replacement == original); + Send(sender, Bind(replacement)); + for (const int epoll : epolls) { + NetEpollEvent event{}; + Require(sceNetEpollWait(epoll, &event, 1, 0) == 0); + Register(epoll, replacement, 1, NewData); + Require(sceNetEpollWait(epoll, &event, 1, 1000000) == 1); + CheckEvent(event, replacement, NewData); + Require(sceNetEpollDestroy(epoll) == 0); + } + Require(sceNetSocketClose(replacement) == 0 && sceNetSocketClose(sender) == 0); +} + +static void CheckControl() { + const int descriptor = Socket(); + const int epoll = sceNetEpollCreate("control", 0); + Require(epoll >= 0); + Require(sceNetEpollControl(epoll, 3, descriptor, nullptr) == static_cast(0x80410102)); + Require(*sceNetErrnoLoc() == 2); + Register(epoll, descriptor, 1, OldData); + Require(sceNetSocketClose(descriptor) == 0); + NetEpollEvent event{}; + event.events = 1; + for (const int closed : {descriptor, -1}) { + for (const int operation : {3, 1, 2}) { + Require(sceNetEpollControl(epoll, operation, closed, operation == 3 ? nullptr : &event) == static_cast(0x80410109)); + Require(*sceNetErrnoLoc() == 9); + } + } + Require(sceNetEpollDestroy(epoll) == 0); +} + +struct WaitResult { + int count = -1; + int error = 0; + NetEpollEvent event{}; +}; + +static void CheckWait(const std::string& operation) { + ReuseSlot slot; + int receiver = Socket(); + const int sender = Socket(); + Send(sender, Bind(receiver)); + const int epoll = sceNetEpollCreate("inflight", 0); + Require(epoll >= 0); + Register(epoll, receiver, 1, OldData); + std::promise release; + PollGate gate{{}, release.get_future()}; + auto arrived = gate.arrived.get_future(); + pollGate.store(&gate); + auto waiter = std::async(std::launch::async, [epoll] { + WaitResult result; + result.count = sceNetEpollWait(epoll, &result.event, 1, 200000); + result.error = *sceNetErrnoLoc(); + return result; + }); + Require(arrived.wait_for(std::chrono::seconds(2)) == std::future_status::ready); + Require(gate.result == 1); + if (operation == "close") { + const int original = receiver; + Require(sceNetSocketClose(original) == 0); + slot.Release(); + receiver = Socket(); + Require(receiver == original); + Send(sender, Bind(receiver)); + Register(epoll, receiver, 1, NewData); + } else if (operation == "delete" || operation == "replace") { + Require(sceNetEpollControl(epoll, 3, receiver, nullptr) == 0); + if (operation == "replace") Register(epoll, receiver, 1, NewData); + } else if (operation == "modify") { + Register(epoll, receiver, 2, NewData, 2); + } else if (operation == "abort") { + Require(sceNetEpollAbort(epoll, 0) == 0); + } else { + Require(operation == "destroy"); + Require(sceNetEpollDestroy(epoll) == 0); + } + release.set_value(); + Require(waiter.wait_for(std::chrono::seconds(2)) == std::future_status::ready); + const auto result = waiter.get(); + if (operation == "delete") { + Require(result.count == 0); + } else if (operation == "abort" || operation == "destroy") { + Require(result.count < 0 && result.error == (operation == "abort" ? 53 : 9)); + } else { + Require(result.count == 1); + CheckEvent(result.event, receiver, NewData, operation == "modify" ? 2 : 1); + } + if (operation != "destroy") Require(sceNetEpollDestroy(epoll) == 0); + Require(sceNetSocketClose(receiver) == 0 && sceNetSocketClose(sender) == 0); +} + +int main(int argc, char** argv) { + Require(argc == 2); + Require(sceNetInit_nid_postfix() == 0); + for (int iteration = 0; iteration != 3; ++iteration) { + if (std::string(argv[1]) == "reuse") CheckReuse(); + else if (std::string(argv[1]) == "control") CheckControl(); + else CheckWait(argv[1]); + } + std::printf("epoll lifetime %s: 3 rounds passed\n", argv[1]); +} diff --git a/core/libs/tests/numeric-locale/comma.charmap b/core/libs/tests/numeric-locale/comma.charmap new file mode 100644 index 0000000000..cbfea3c784 --- /dev/null +++ b/core/libs/tests/numeric-locale/comma.charmap @@ -0,0 +1,135 @@ + ANYPS5_COMMA + % + / + 1 + 1 +CHARMAP + /x00 + /x01 + /x02 + /x03 + /x04 + /x05 + /x06 + /x07 + /x08 + /x09 + /x0a + /x0b + /x0c + /x0d + /x0e + /x0f + /x10 + /x11 + /x12 + /x13 + /x14 + /x15 + /x16 + /x17 + /x18 + /x19 + /x1a + /x1b + /x1c + /x1d + /x1e + /x1f + /x20 + /x21 + /x22 + /x23 + /x24 + /x25 + /x26 + /x27 + /x28 + /x29 + /x2a + /x2b + /x2c + /x2d + /x2e + /x2f + /x30 + /x31 + /x32 + /x33 + /x34 + /x35 + /x36 + /x37 + /x38 + /x39 + /x3a + /x3b + /x3c + /x3d + /x3e + /x3f + /x40 + /x41 + /x42 + /x43 + /x44 + /x45 + /x46 + /x47 + /x48 + /x49 + /x4a + /x4b + /x4c + /x4d + /x4e + /x4f + /x50 + /x51 + /x52 + /x53 + /x54 + /x55 + /x56 + /x57 + /x58 + /x59 + /x5a + /x5b + /x5c + /x5d + /x5e + /x5f + /x60 + /x61 + /x62 + /x63 + /x64 + /x65 + /x66 + /x67 + /x68 + /x69 + /x6a + /x6b + /x6c + /x6d + /x6e + /x6f + /x70 + /x71 + /x72 + /x73 + /x74 + /x75 + /x76 + /x77 + /x78 + /x79 + /x7a + /x7b + /x7c + /x7d + /x7e + /x7f +END CHARMAP diff --git a/core/libs/tests/numeric-locale/comma.locale b/core/libs/tests/numeric-locale/comma.locale new file mode 100644 index 0000000000..3207685c62 --- /dev/null +++ b/core/libs/tests/numeric-locale/comma.locale @@ -0,0 +1,5 @@ +LC_NUMERIC +decimal_point "" +thousands_sep "" +grouping -1 +END LC_NUMERIC diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index fa78fbaa42..5c4b89dfa8 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -160,6 +160,16 @@ if(WIN32) add_test(NAME windows_dependency_diagnostics COMMAND windows_dependency_diagnostics_tests) endif() + if(NOT ANYPS5_RELINKER_ONLY) + add_test_executable(windows_import_identity_tests EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/tests/WindowsImportIdentityTests.cpp) + target_link_libraries(windows_import_identity_tests PRIVATE relinker_core) + if(BUILD_TESTING) + set_target_properties(windows_import_identity_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) + add_dependencies(windows_import_identity_tests libs) + add_test(NAME windows_import_identity COMMAND windows_import_identity_tests "${CMAKE_BINARY_DIR}/core/libs/libs") + endif() + endif() + add_test_executable(amd64_only_windows_tests EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/tests/Amd64OnlyWindowsTests.cpp ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/windows/WindowsTrampolineBuilder.cpp @@ -369,12 +379,15 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/StubBodyBuilder.cpp ${CMAKE_CURRENT_SOURCE_DIR}/io/src/ByteWriter.cpp ${CMAKE_CURRENT_SOURCE_DIR}/io/src/BufferUtils.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../libs/nid/src/NidCompute.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../libs/nid/src/Sha1.cpp ) target_include_directories(linux_load_pages_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/domain/include ${CMAKE_CURRENT_SOURCE_DIR}/codegen/include ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/include ${CMAKE_CURRENT_SOURCE_DIR}/io/include + ${CMAKE_CURRENT_SOURCE_DIR}/../libs/nid/include ) add_test(NAME linux_load_pages COMMAND linux_load_pages_tests) set_tests_properties(linux_load_pages PROPERTIES TIMEOUT 20) @@ -409,12 +422,15 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/general/SectionHeaderTableBuilder.cpp ${CMAKE_CURRENT_SOURCE_DIR}/io/src/ByteWriter.cpp ${CMAKE_CURRENT_SOURCE_DIR}/io/src/BufferUtils.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../libs/nid/src/NidCompute.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../libs/nid/src/Sha1.cpp ) target_include_directories(amd64_only_converter_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/domain/include ${CMAKE_CURRENT_SOURCE_DIR}/codegen/include ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/include ${CMAKE_CURRENT_SOURCE_DIR}/io/include + ${CMAKE_CURRENT_SOURCE_DIR}/../libs/nid/include ) if(APPLE AND (CMAKE_OSX_ARCHITECTURES STREQUAL "x86_64" OR (NOT CMAKE_OSX_ARCHITECTURES AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64"))) target_link_options(amd64_only_converter_tests PRIVATE "LINKER:-pagezero_size,0x10000") @@ -475,6 +491,9 @@ if(BUILD_TESTING) add_test(NAME overlapping_relocations COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_overlapping_relocations.py $) set_tests_properties(overlapping_relocations PROPERTIES TIMEOUT 30) + add_test(NAME guest_own_tls_relocations COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_own_tls_relocations.py $) + set_tests_properties(guest_own_tls_relocations PROPERTIES TIMEOUT 30) add_test(NAME optional_plt COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_optional_plt.py $) add_test(NAME optional_rela COMMAND ${Python3_EXECUTABLE} @@ -595,6 +614,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_host_libc.py $) add_test(NAME windows_named_imports COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_named_imports.py $) + add_test(NAME linux_named_imports COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_linux_named_imports.py $) add_library(guest_module_args_host SHARED elfpatcher/tests/GuestModuleArgsHost.cpp) target_include_directories(guest_module_args_host PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../libs) set_target_properties(guest_module_args_host PROPERTIES diff --git a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp index 08d527462d..a03384cea7 100644 --- a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp +++ b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp @@ -1811,7 +1811,13 @@ void sha1Execution() { else require(run.Xmm[reg][0] == xmmIn[reg][0] && run.Xmm[reg][1] == xmmIn[reg][1], "SHA-1 stub clobbered an xmm register"); } - require(run.Rax == rax && run.Rcx == 0x1122334455667788ull && (run.Flags & 0x8D5) == (0x8D7 & 0x8D5), "SHA-1 stub changed a general register or RFLAGS"); + if (run.Rax != rax || run.Rcx != 0x1122334455667788ull || (run.Flags & 0x8D5) != (0x8D7 & 0x8D5)) { + std::cerr << "SHA-1 sample=" << sample << " site=" << std::hex; + for (const auto byte : site) + std::cerr << static_cast(byte) << ' '; + std::cerr << "rax=" << run.Rax << " expected=" << rax << " rcx=" << run.Rcx << " flags=" << run.Flags << std::dec << '\n'; + throw std::runtime_error("SHA-1 stub changed a general register or RFLAGS"); + } } } } diff --git a/core/relinker/domain/include/domain/Types.hpp b/core/relinker/domain/include/domain/Types.hpp index dd181853e0..6757f8507a 100644 --- a/core/relinker/domain/include/domain/Types.hpp +++ b/core/relinker/domain/include/domain/Types.hpp @@ -106,6 +106,7 @@ struct SysVDynamicSection { std::vector GuestModules; std::map ImportModules; std::set FunctionImports; + std::set NamedImports; std::set UnmatchedImportModules; std::vector NeededLibraries; }; diff --git a/core/relinker/elfpatcher/include/elfpatcher/linux/LinuxElfPatcher.hpp b/core/relinker/elfpatcher/include/elfpatcher/linux/LinuxElfPatcher.hpp index c24b3114f0..58aebbcd67 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/linux/LinuxElfPatcher.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/linux/LinuxElfPatcher.hpp @@ -14,6 +14,8 @@ namespace Elfpatcher::Linux { +inline constexpr std::uint64_t DefaultFixedBase = 0x400000; + class LinuxElfPatcher : public IElfPatcher { public: LinuxElfPatcher( diff --git a/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp b/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp index df116a307c..7453d82aca 100644 --- a/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp +++ b/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp @@ -4,10 +4,13 @@ #include #include #include +#include #include +#include #include #include #include +#include #include #include #include @@ -190,6 +193,46 @@ std::vector TracedImports(const Domain::SysVDynamicSection& dynSec return imports; } +Domain::SysVDynamicSection ImportsByNid(Domain::SysVDynamicSection dynSection) { + RequireWholeEntries(dynSection.DynamicSegmentData, kDynEntrySize, "Executable dependency table"); + for (std::size_t entry = 0; entry < dynSection.DynamicSegmentData.size(); entry += kDynEntrySize) { + if (static_cast(Io::ReadU64(dynSection.DynamicSegmentData, entry)) != DT_NEEDED) + continue; + const auto offset = Io::ReadU64(dynSection.DynamicSegmentData, entry + 8); + if (offset >= dynSection.DynStrData.size()) + throw Domain::RelinkerException("DT_NEEDED string offset is out of bounds", offset); + const auto name = ReadDynamicString(dynSection.DynStrData, static_cast(offset)); + const auto library = Domain::NeededLibraryName(name); + if (library == name) + continue; + Io::WriteU64(dynSection.DynamicSegmentData, entry + 8, dynSection.DynStrData.size()); + Io::AppendString(dynSection.DynStrData, library); + } + for (auto& library : dynSection.NeededLibraries) + library = Domain::NeededLibraryName(library); + + std::set symbols; + for (const auto* table : {&dynSection.RelaData, &dynSection.RelaPltData}) { + RequireWholeEntries(*table, kRelaEntrySize, "Relocation table"); + for (std::size_t entry = 0; entry < table->size(); entry += kRelaEntrySize) + if (dynSection.NamedImports.contains(Io::ReadU64(*table, entry))) + symbols.insert(Io::ReadU64(*table, entry + 8) >> 32); + } + for (const auto symbol : symbols) { + if (symbol >= dynSection.DynSymData.size() / kSymEntrySize) + throw Domain::RelinkerException("Invalid import symbol index", symbol); + const auto symbolOffset = static_cast(symbol * kSymEntrySize); + const auto name = ReadDynamicString(dynSection.DynStrData, Io::ReadU32(dynSection.DynSymData, symbolOffset)); + if (name.empty() || name.find('#') != std::string::npos) + continue; + if (dynSection.DynStrData.size() > std::numeric_limits::max()) + throw Domain::RelinkerException("Dynamic string table too large"); + Io::WriteU32(dynSection.DynSymData, symbolOffset, static_cast(dynSection.DynStrData.size())); + Io::AppendString(dynSection.DynStrData, Nid::ComputeNid(name, "")); + } + return dynSection; +} + std::vector TracePrologue(const std::uint64_t vaddr, const std::uint64_t table, const std::uint64_t tableEnd, const std::uint64_t function) { std::vector code = {0x52, 0x53, 0x41, 0x54, 0x48, 0x83, 0xec, 0x08}; const auto rip = [&](std::initializer_list opcode, const std::uint64_t target) { @@ -297,13 +340,14 @@ void LinuxElfPatcher::_appendTrampoline( std::vector LinuxElfPatcher::Patch( std::vector sourceElf, const std::vector& originalHeaders, - const Domain::SysVDynamicSection& dynSection, + const Domain::SysVDynamicSection& sourceDynSection, const std::uint64_t originalPltGotVaddr, const std::string& runPath, const bool lazyBinding, const bool dependencyDiagnostics, const std::vector& trampolines) { + const auto dynSection = ImportsByNid(sourceDynSection); if (dependencyDiagnostics) throw Domain::RelinkerException("Linux target does not support --windows-diagnostics"); if (_fixedBase && lazyBinding) diff --git a/core/relinker/elfpatcher/tests/EntryStubTests.cpp b/core/relinker/elfpatcher/tests/EntryStubTests.cpp index 2dc67f2ec2..1fe8dc32e9 100644 --- a/core/relinker/elfpatcher/tests/EntryStubTests.cpp +++ b/core/relinker/elfpatcher/tests/EntryStubTests.cpp @@ -118,6 +118,23 @@ void windowsStubRejectsAnOverlongAllocatorPath() { throw std::runtime_error("an allocator path past the startup buffer was accepted"); } +void windowsLibSceAmprObjectImportsFallBackToTheLibcProvider() { + using namespace Elfpatcher::Windows; + const auto nativeImports = WindowsImportBuilder().Build(0x1000); + const std::vector libraries{"libSceAmpr.prx", "libc.prx"}; + const std::vector amprImport{{.Name = "-mLzBSk-VGs", .TargetRva = 0x2000, .Addend = 0, .Library = "libSceAmpr.prx"}}; + const auto amprStub = WindowsEntryStubBuilder().Build(0x40000, 0x1000, nativeImports, libraries, amprImport, "$ORIGIN/libs", false, false); + const std::vector fallbackOrder{0, 0, 0, 0, 1, 0, 0, 0}; + require(std::search(amprStub.Data.Data.begin(), amprStub.Data.Data.end(), fallbackOrder.begin(), fallbackOrder.end()) != amprStub.Data.Data.end(), + "a libSceAmpr-qualified locale object import does not fall back to libc"); + + const std::vector libcImport{{.Name = "-mLzBSk-VGs", .TargetRva = 0x2000, .Addend = 0, .Library = "libc.prx"}}; + const auto libcStub = WindowsEntryStubBuilder().Build(0x40000, 0x1000, nativeImports, libraries, libcImport, "$ORIGIN/libs", false, false); + const std::vector libcOrder{1, 0, 0, 0}; + require(std::search(libcStub.Data.Data.begin(), libcStub.Data.Data.end(), libcOrder.begin(), libcOrder.end()) != libcStub.Data.Data.end(), + "a libc-qualified locale object import does not resolve through libc"); +} + void windowsKernelVariantImportsSearchCanonicalKernelAfterTheRequestedVariant() { using namespace Elfpatcher::Windows; const auto nativeImports = WindowsImportBuilder().Build(0x1000); @@ -153,6 +170,7 @@ int main() { windowsStubLoadsTheAllocatorOnRequest("C:/Games/Title/libs", u"C:\\Games\\Title\\libs\\", {0x48, 0x8d, 0x0d}); windowsStubLoadsTheAllocatorOnRequest("$ORIGIN/libs", u"libs\\", {0x48, 0x8d, 0x35}); windowsStubRejectsAnOverlongAllocatorPath(); + windowsLibSceAmprObjectImportsFallBackToTheLibcProvider(); windowsKernelVariantImportsSearchCanonicalKernelAfterTheRequestedVariant(); windowsKernelVariantDependenciesLoadCanonicalKernel(); } catch (const std::exception& error) { diff --git a/core/relinker/elfpatcher/tests/WindowsImportIdentityTests.cpp b/core/relinker/elfpatcher/tests/WindowsImportIdentityTests.cpp new file mode 100644 index 0000000000..ffe753bfaf --- /dev/null +++ b/core/relinker/elfpatcher/tests/WindowsImportIdentityTests.cpp @@ -0,0 +1,162 @@ +#define WIN32_LEAN_AND_MEAN +#define NOMINMAX +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +namespace Fs = std::filesystem; +using namespace Elfpatcher::Windows; + +constexpr std::uint32_t GotRva = LoadRva + 0x1000; +constexpr std::array LocaleObjects{"-mLzBSk-VGs", "a54t8+k7KpY", "KfcTPbeaOqg"}; + +void require(const bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +Fs::path createFixtureDirectory(const Fs::path& parent) { + std::random_device random; + for (int attempt = 0; attempt < 100; ++attempt) { + auto name = Fs::path("windows-import-identity-"); + name += std::to_string(GetCurrentProcessId()) + "-" + std::to_string(random()); + const auto directory = parent / name; + if (Fs::create_directory(directory)) return directory; + } + throw std::runtime_error("Cannot create a unique import identity fixture directory"); +} + +void appendDisplacement(std::vector& code, const std::int32_t displacement) { + for (int i = 0; i < 4; ++i) code.push_back(static_cast((static_cast(displacement) >> (8 * i)) & 0xffu)); +} + +// For each pair of GOT slots: mov rax,[a]; cmp rax,[b]; jne fail. Then xor eax,eax; ret. fail: mov eax,1; ret. +std::vector compareCode(const std::vector>& pairs) { + std::vector code; + const auto failOffset = static_cast(pairs.size() * 16 + 3); + for (std::size_t i = 0; i < pairs.size(); ++i) { + const auto base = static_cast(i * 16); + code.insert(code.end(), {0x48, 0x8b, 0x05}); + appendDisplacement(code, static_cast(GotRva + 8u * static_cast(pairs[i].first)) - static_cast(LoadRva + base + 7)); + code.insert(code.end(), {0x48, 0x3b, 0x05}); + appendDisplacement(code, static_cast(GotRva + 8u * static_cast(pairs[i].second)) - static_cast(LoadRva + base + 14)); + code.push_back(0x75); + code.push_back(static_cast(failOffset - static_cast(base + 16))); + } + code.insert(code.end(), {0x31, 0xc0, 0xc3}); + code.insert(code.end(), {0xb8, 0x01, 0x00, 0x00, 0x00, 0xc3}); + return code; +} + +std::uint32_t slot(const int index) { + return GotRva + 8u * static_cast(index); +} + +void generateProbe(const Fs::path& file, const std::string& libraries, const std::vector& peImports, const std::vector>& pairs) { + const std::vector loaded{"libSceAmpr.prx", "libkernel.prx", "libc.prx"}; + PeSection entry{".guest", LoadRva, SectionRead | SectionExecute | 0x20u, compareCode(pairs)}; + PeSection got{".got", GotRva, SectionRead | SectionWrite | 0x40u, std::vector(8 * 8)}; + const auto imports = WindowsImportBuilder().Build(LoadRva + 0x2000); + const auto startup = WindowsEntryStubBuilder().Build(LoadRva + 0x3000, entry.Rva, imports, loaded, peImports, libraries, false, false); + std::array directories{}; + directories[1] = imports.Directory; + directories[3] = startup.ExceptionDirectory; + directories[12] = imports.AddressTable; + const auto image = WindowsPeWriter().Write({entry, got, imports.Section, startup.Data, startup.Code}, startup.Code.Rva, directories, false, ImageBase, false); + std::ofstream stream(file, std::ios::binary); + stream.write(reinterpret_cast(image.data()), static_cast(image.size())); + require(static_cast(stream), "Cannot write " + file.string()); +} + +DWORD runProbe(const Fs::path& probe, std::string& output) { + SECURITY_ATTRIBUTES security{sizeof(SECURITY_ATTRIBUTES), nullptr, TRUE}; + HANDLE input = nullptr; + HANDLE written = nullptr; + require(CreatePipe(&input, &written, &security, 0) && SetHandleInformation(input, HANDLE_FLAG_INHERIT, 0), "Cannot create the probe's pipe"); + STARTUPINFOW startup{}; + startup.cb = sizeof(startup); + startup.dwFlags = STARTF_USESTDHANDLES; + startup.hStdOutput = written; + startup.hStdError = written; + startup.hStdInput = GetStdHandle(STD_INPUT_HANDLE); + PROCESS_INFORMATION process{}; + const auto filename = probe.wstring(); + require(CreateProcessW(filename.c_str(), nullptr, nullptr, nullptr, TRUE, CREATE_NO_WINDOW, nullptr, probe.parent_path().wstring().c_str(), &startup, &process) != 0, + "Cannot start " + probe.filename().string() + ": " + std::to_string(GetLastError())); + CloseHandle(written); + if (WaitForSingleObject(process.hProcess, 120000) != WAIT_OBJECT_0) { + TerminateProcess(process.hProcess, 1); + throw std::runtime_error(probe.filename().string() + " timed out"); + } + DWORD status = 0; + require(GetExitCodeProcess(process.hProcess, &status) != 0, "Cannot read the probe's exit code"); + char buffer[4096]; + DWORD size = 0; + while (ReadFile(input, buffer, sizeof(buffer), &size, nullptr) && size != 0) output.append(buffer, size); + CloseHandle(input); + CloseHandle(process.hThread); + CloseHandle(process.hProcess); + return status; +} + +// The generated probes load libc.prx next to the other libraries; its compiler runtime DLLs sit next to this test. +void copyRuntime(const Fs::path& directory) { + wchar_t self[MAX_PATH]; + const auto length = GetModuleFileNameW(nullptr, self, MAX_PATH); + if (length == 0 || length >= MAX_PATH) return; + const auto own = Fs::path(self).parent_path(); + for (const char* name : {"libgcc_s_seh-1.dll", "libstdc++-6.dll", "libwinpthread-1.dll"}) { + std::error_code error; + if (Fs::exists(own / name, error)) Fs::copy_file(own / name, directory / name, Fs::copy_options::overwrite_existing, error); + } +} + +} + +int main(int argc, char** argv) { + if (argc != 2) { + std::cerr << "usage: windows_import_identity_tests \n"; + return 2; + } + try { + const auto libraries = Fs::absolute(argv[1]).string(); + const auto directory = createFixtureDirectory(Fs::temp_directory_path()); + copyRuntime(directory); + std::vector same; + for (int i = 0; i < 3; ++i) { + same.push_back({.Name = LocaleObjects[i], .TargetRva = slot(2 * i), .Addend = 0, .Library = "libSceAmpr.prx"}); + same.push_back({.Name = LocaleObjects[i], .TargetRva = slot(2 * i + 1), .Addend = 0, .Library = "libc.prx"}); + } + generateProbe(directory / "same.exe", libraries, same, {{0, 1}, {2, 3}, {4, 5}}); + const std::vector different{ + {.Name = LocaleObjects[0], .TargetRva = slot(0), .Addend = 0, .Library = "libSceAmpr.prx"}, + {.Name = "k+AXqu2-eBc", .TargetRva = slot(6), .Addend = 0, .Library = "libkernel.prx"}, + }; + generateProbe(directory / "different.exe", libraries, different, {{0, 6}}); + std::string output; + const auto sameStatus = runProbe(directory / "same.exe", output); + require(sameStatus == 0, "A libSceAmpr.prx-qualified import of the locale objects did not resolve to libc's object (exit " + std::to_string(sameStatus) + ")\n" + output); + output.clear(); + const auto differentStatus = runProbe(directory / "different.exe", output); + require(differentStatus == 1, "The probe did not tell two different addresses apart (exit " + std::to_string(differentStatus) + ")\n" + output); + std::error_code error; + Fs::remove_all(directory, error); + std::cout << "Windows import identity tests passed\n"; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } + return 0; +} diff --git a/core/relinker/main.cpp b/core/relinker/main.cpp index b5fab9f739..98bca27396 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -35,6 +35,7 @@ struct ConvertedExecutable { std::vector Bytes; std::string Diagnostics; std::vector AppliedPatches; + std::optional LinuxBase; }; ConvertedExecutable Convert(const Cli::Args& args, const std::string& inputPath, const std::string& absPath, const bool mainExecutable, const std::string& targetName, const std::vector& patches, Cli::ConsoleProgress& progress) { @@ -74,7 +75,7 @@ ConvertedExecutable Convert(const Cli::Args& args, const std::string& inputPath, progress.IncludeFile(Io::Utf8Path(input.OutputPath)); for (const auto& module : input.Modules) progress.IncludeFile(Io::Utf8Path(module.Path)); auto converted = Relinker::Conversion(progress).Convert(std::move(input), options); - return {inputPath, absPath, std::move(converted.Analysis), std::move(converted.Modules), std::move(converted.Executable), std::move(converted.WindowsDiagnostics), std::move(converted.AppliedPatches)}; + return {inputPath, absPath, std::move(converted.Analysis), std::move(converted.Modules), std::move(converted.Executable), std::move(converted.WindowsDiagnostics), std::move(converted.AppliedPatches), converted.LinuxBase}; } class OutputIdentityDirectory { @@ -333,7 +334,7 @@ int Run(const int argc, char* argv[]) { std::filesystem::path manifestPath; std::string manifestText; if (!appliedPatches.empty()) manifestPath = Io::NativePath(absPath).parent_path() / Io::NativePath(Io::Utf8Path(Io::NativePath(absPath).stem()) + Relinker::GamePatch::ManifestSuffix); - if (!manifestPath.empty()) manifestText = Relinker::GamePatch::WriteManifest(appliedPatches, mainTarget, Io::Utf8Path(patchFile)); + if (!manifestPath.empty()) manifestText = Relinker::GamePatch::WriteManifest(appliedPatches, mainTarget, Io::Utf8Path(patchFile), executables.front().LinuxBase); const Elfpatcher::Linux::LinuxDesktopEntryWriter desktopEntryWriter; const auto desktopEntry = args.toWindows || args.toMacos ? std::nullopt : desktopEntryWriter.Prepare(Io::NativePath(args.inputPath).parent_path() / "sce_sys", Io::NativePath(absPath)); diff --git a/core/relinker/relinker/include/relinker/patching/GamePatch.hpp b/core/relinker/relinker/include/relinker/patching/GamePatch.hpp index d893a7015e..6b76048dfb 100644 --- a/core/relinker/relinker/include/relinker/patching/GamePatch.hpp +++ b/core/relinker/relinker/include/relinker/patching/GamePatch.hpp @@ -40,6 +40,7 @@ struct PatchEntry { struct PatchFile { std::vector Entries; std::optional MainTarget; + std::optional LinuxBase; }; struct PatchedRange { @@ -114,7 +115,7 @@ void MatchTargets(const std::vector& entries, const std::vector AggregateManifest(const std::vector>& records); -std::string WriteManifest(const std::vector& patches, const std::string& mainTarget, const std::string& configPath); +std::string WriteManifest(const std::vector& patches, const std::string& mainTarget, const std::string& configPath, std::optional linuxBase = std::nullopt); std::vector VerifyPatches(const PatchFile& file, const std::vector& executables, const std::vector& modules, bool outputMode); diff --git a/core/relinker/relinker/include/relinker/pipeline/Conversion.hpp b/core/relinker/relinker/include/relinker/pipeline/Conversion.hpp index 194262b78e..6998e95873 100644 --- a/core/relinker/relinker/include/relinker/pipeline/Conversion.hpp +++ b/core/relinker/relinker/include/relinker/pipeline/Conversion.hpp @@ -46,6 +46,7 @@ struct ConversionOutput { RelinkResult Analysis; std::string WindowsDiagnostics; std::vector AppliedPatches; + std::optional LinuxBase; }; class Conversion { diff --git a/core/relinker/relinker/src/guest/GuestImageReader.cpp b/core/relinker/relinker/src/guest/GuestImageReader.cpp index a630010692..49a209c9d4 100644 --- a/core/relinker/relinker/src/guest/GuestImageReader.cpp +++ b/core/relinker/relinker/src/guest/GuestImageReader.cpp @@ -244,6 +244,7 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector const auto next = relocationTargets.lower_bound(target); if ((next != relocationTargets.end() && next->first < target + 8) || (next != relocationTargets.begin() && std::prev(next)->second > target)) fail("Overlapping guest relocations"); relocationTargets.emplace(target, target + 8); + bool ownTlsBlock = false; if (relocation == 8) { if ((info >> 32) != 0) { const auto& symbol = image.Symbols[info >> 32]; @@ -263,9 +264,12 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector if ((info >> 32) == 0 || (symbol.Info & 15) == 6 || (relocation != 1 && addend != 0) || (symbol.Section == 0 && symbol.Name.empty())) fail("Invalid guest symbol relocation"); } else { const auto& symbol = image.Symbols[info >> 32]; - if (((info >> 32) == 0 && tls == nullptr) || ((info >> 32) != 0 && (symbol.Info & 15) != 6) || (relocation == 16 && addend != 0)) fail("Invalid guest TLS relocation"); + ownTlsBlock = (info >> 32) != 0 && (relocation == 16 || relocation == 17) && symbol.Info == 3 && symbol.Section == 0 && symbol.Value == 0 && tls != nullptr; + if (((info >> 32) == 0 && tls == nullptr) || ((info >> 32) != 0 && (symbol.Info & 15) != 6 && !ownTlsBlock) || (relocation == 16 && addend != 0)) fail("Invalid guest TLS relocation"); } + const auto position = output.size(); output.insert(output.end(), bytes.begin() + offset + index, bytes.begin() + offset + index + 24); + if (ownTlsBlock) Io::WriteU64(output, position + 8, relocation); } }; copyRelocations(relaOffset, relaSize, image.Dynamic.RelaData, false); diff --git a/core/relinker/relinker/src/output/SysVDynamicSectionBuilder.cpp b/core/relinker/relinker/src/output/SysVDynamicSectionBuilder.cpp index 0d8bdd763f..a0044b4765 100644 --- a/core/relinker/relinker/src/output/SysVDynamicSectionBuilder.cpp +++ b/core/relinker/relinker/src/output/SysVDynamicSectionBuilder.cpp @@ -1,5 +1,7 @@ #include #include +#include +#include #include namespace Relinker { @@ -77,6 +79,9 @@ SysVDynamicSection SysVDynamicSectionBuilder::BuildDynamicSection( _appendElfSym(result.DynSymData, 0, 0, 0, 0, 0, 0); + const bool consoleFileNames = std::any_of(neededLibraries.begin(), neededLibraries.end(), + [](const std::string& name) { return Domain::NeededLibraryName(name) != name; }); + auto stripHashSuffix = [](const std::string& value) -> std::string { const auto hashPos = value.find('#'); if (hashPos == std::string::npos) return value; @@ -98,6 +103,7 @@ SysVDynamicSection SysVDynamicSectionBuilder::BuildDynamicSection( for (const auto& ref : nidReferences) { if (!ref.Library.empty()) result.ImportModules.emplace(ref.RelocationAddress, ref.Library); if (ref.Function) result.FunctionImports.insert(ref.RelocationAddress); + if (consoleFileNames && ref.Library.empty() && !ref.Nid.empty() && ref.Nid.find('#') == std::string::npos) result.NamedImports.insert(ref.RelocationAddress); std::uint32_t relType = ref.RelocationTypeValue; if (relType == 0) relType = R_X86_64_JUMP_SLOT; diff --git a/core/relinker/relinker/src/patching/GamePatch.cpp b/core/relinker/relinker/src/patching/GamePatch.cpp index 05d334868a..403264c79b 100644 --- a/core/relinker/relinker/src/patching/GamePatch.cpp +++ b/core/relinker/relinker/src/patching/GamePatch.cpp @@ -33,8 +33,8 @@ constexpr std::array kIntegerTypes{{ {"i8", 8, true}, {"i16", 16, true}, {"i32", 32, true}, {"i64", 64, true}, }}; -constexpr std::array kIgnoredTopLevelKeys{"relinker_version", "relinker_commit", "config", "config_sha256", "main_target", "settings", "settings_sha256"}; -constexpr std::array kReservedNames{"format", "relinker_version", "relinker_commit", "config", "config_sha256", "main_target", "settings", "settings_sha256"}; +constexpr std::array kIgnoredTopLevelKeys{"relinker_version", "relinker_commit", "config", "config_sha256", "main_target", "linux_base", "settings", "settings_sha256"}; +constexpr std::array kReservedNames{"format", "relinker_version", "relinker_commit", "config", "config_sha256", "main_target", "linux_base", "settings", "settings_sha256"}; constexpr std::array kEntryKeys{"name", "target", "va", "type", "expected", "value", "min", "max", "align", "description"}; std::string HexValue(const std::uint64_t value) { @@ -300,8 +300,8 @@ void RejectEntryOverlaps(const std::vector& entries, const std::vect } } -bool IsElfDyn(const std::vector& bytes) { - return bytes.size() >= 18 && bytes[0] == 0x7f && bytes[1] == 'E' && bytes[2] == 'L' && bytes[3] == 'F' && bytes[16] == Elfpatcher::ET_DYN && bytes[17] == 0; +bool IsElfOfType(const std::vector& bytes, const std::uint16_t type) { + return bytes.size() >= 18 && bytes[0] == 0x7f && bytes[1] == 'E' && bytes[2] == 'L' && bytes[3] == 'F' && bytes[16] == (type & 0xff) && bytes[17] == (type >> 8); } VerifyResult VerifyOne(const PatchEntry& entry, const EntryRange& resolved, const std::vector& bytes, const bool outputMode) { @@ -410,6 +410,10 @@ PatchFile ParsePatchFile(const std::string_view text) { if (!mainTarget->IsString || mainTarget->Text.empty()) FailLine(mainTarget->Line, "main_target must be a nonempty string", "main_target"); file.MainTarget = mainTarget->Text; } + if (const auto* linuxBase = Find(top, "linux_base")) { + if (linuxBase->IsString || linuxBase->Number.Negative) FailLine(linuxBase->Line, "linux_base must be a non-negative integer", "linux_base"); + file.LinuxBase = linuxBase->Number.Magnitude; + } return file; } @@ -505,7 +509,7 @@ std::vector AggregateManifest(const std::vector& patches, const std::string& mainTarget, const std::string& configPath) { +std::string WriteManifest(const std::vector& patches, const std::string& mainTarget, const std::string& configPath, const std::optional linuxBase) { const auto holdable = [](const std::string& text) { return std::none_of(text.begin(), text.end(), [](const char character) { return character == '"' || character == '\\' || (static_cast(character) < 0x20 && character != '\t') || character == 0x7f; @@ -519,6 +523,7 @@ std::string WriteManifest(const std::vector& patches, const std::s if (!config.empty() && !holdable(config)) config = std::filesystem::path(Io::NativePath(configPath)).filename().generic_string(); std::ostringstream manifest; manifest << "format = 1\nmain_target = " << quoted(mainTarget, "main_target") << '\n'; + if (linuxBase) manifest << "linux_base = " << HexValue(*linuxBase) << '\n'; if (!config.empty()) manifest << "config = " << quoted(config, "config") << '\n'; for (const auto& patch : patches) { manifest << "\n[[patch]]\n"; @@ -553,8 +558,12 @@ std::vector VerifyPatches(const PatchFile& file, const std::vector for (std::size_t index = 0; index < executables.size(); ++index) for (std::size_t other = index + 1; other < executables.size(); ++other) if (executables[index].Name == executables[other].Name) throw Domain::RelinkerException("ambiguous patch target: " + executables[index].Name); - for (const auto& executable : executables) - if (!IsElfDyn(executable.Bytes)) throw Domain::RelinkerException(executable.Name + " is not an ET_DYN Linux ELF output (ET_EXEC from --linux-fixed-base, PE and Mach-O outputs are not supported): verify the input instead"); + for (const auto& executable : executables) { + const bool fixed = IsElfOfType(executable.Bytes, Elfpatcher::ET_EXEC); + if (!fixed && !IsElfOfType(executable.Bytes, Elfpatcher::ET_DYN)) throw Domain::RelinkerException(executable.Name + " is not a Linux ELF output (PE and Mach-O outputs are not supported): verify the input instead"); + if (fixed && !file.LinuxBase) throw Domain::RelinkerException(executable.Name + " is an ET_EXEC Linux ELF output, but the patch file has no linux_base: verify the manifest written by its conversion"); + if (!fixed && file.LinuxBase) throw Domain::RelinkerException(executable.Name + " is an ET_DYN Linux ELF output, but the patch file has linux_base = " + HexValue(*file.LinuxBase) + ": verify the output of the conversion that wrote it"); + } } else { std::vector names; for (const auto& executable : executables) names.push_back(executable.Name); @@ -583,13 +592,19 @@ std::vector VerifyPatches(const PatchFile& file, const std::vector } continue; } + const std::uint64_t base = outputMode && IsElfOfType(target->Bytes, Elfpatcher::ET_EXEC) ? *file.LinuxBase : 0; std::vector own; - for (const auto other : indices) own.push_back(file.Entries[other]); + for (const auto other : indices) { + own.push_back(file.Entries[other]); + if (own.back().VirtualAddress > std::numeric_limits::max() - base) throw Domain::RelinkerException(Describe(own.back()) + " lies outside the address space at linux_base = " + HexValue(base)); + own.back().VirtualAddress += base; + } const ElfReader reader(target->Bytes); const auto resolved = ResolveEach(reader, own); RejectEntryOverlaps(own, resolved); for (std::size_t position = 0; position < indices.size(); ++position) { results[indices[position]] = VerifyOne(own[position], resolved[position], target->Bytes, outputMode); + results[indices[position]].VirtualAddress = file.Entries[indices[position]].VirtualAddress; done[indices[position]] = true; } } diff --git a/core/relinker/relinker/src/pipeline/Conversion.cpp b/core/relinker/relinker/src/pipeline/Conversion.cpp index ad008fa002..1cf1879466 100644 --- a/core/relinker/relinker/src/pipeline/Conversion.cpp +++ b/core/relinker/relinker/src/pipeline/Conversion.cpp @@ -125,6 +125,11 @@ ConversionOutput Conversion::Convert(ConversionInput input, const ConversionOpti auto byteWriter = std::make_shared(); + std::optional linuxBase; + if (!windows && !macos) + linuxBase = options.LinuxFixedBase || options.LazyBinding ? options.LinuxFixedBase : std::optional(Elfpatcher::Linux::DefaultFixedBase); + if (!windows && !macos && options.LazyBinding) + inputProgress.Message("Warning: with --lazy-binding the Linux executable stays position independent and is not placed at 0x400000; a title whose own sceLibcMspaceCreate checks the 40-bit address window stops at start-up\n"); std::shared_ptr patcher; if (macos) { patcher = std::make_shared(options.MacosFixedBase, [&](std::string_view text) { inputProgress.Message(text); }); @@ -139,7 +144,7 @@ ConversionOutput Conversion::Convert(ConversionInput input, const ConversionOpti ), std::make_shared(byteWriter), byteWriter, - options.LinuxFixedBase, + linuxBase, options.TraceImports ); } @@ -157,7 +162,7 @@ ConversionOutput Conversion::Convert(ConversionInput input, const ConversionOpti std::string diagnostics; if (windows) diagnostics = std::static_pointer_cast(patcher)->GetDiagnostics(); inputProgress.Report({Domain::ProgressStage::Executable, Io::Utf8Path(input.InputPath), 1, 1}); - return {std::move(executableBytes), std::move(guestArtifacts), std::move(result), std::move(diagnostics), std::move(appliedPatches)}; + return {std::move(executableBytes), std::move(guestArtifacts), std::move(result), std::move(diagnostics), std::move(appliedPatches), linuxBase}; } catch (Domain::RelinkerException& error) { if (error.InputPath.empty()) error.InputPath = Io::Utf8Path(input.InputPath); throw; diff --git a/core/relinker/relinker/tests/ConversionTests.cpp b/core/relinker/relinker/tests/ConversionTests.cpp index 468921cf4f..3357892f5e 100644 --- a/core/relinker/relinker/tests/ConversionTests.cpp +++ b/core/relinker/relinker/tests/ConversionTests.cpp @@ -132,14 +132,22 @@ void fixedBase() { const Relinker::Conversion converter(progress); auto source = input(); source.Modules.push_back({"virtual/title/sce_module/plugin.prx", fixture()}); - const auto plain = converter.Convert(source, {}); + const auto standard = converter.Convert(source, {}); + Relinker::ConversionOptions lazy; + lazy.LazyBinding = true; + const auto independent = converter.Convert(input(), lazy); Relinker::ConversionOptions options; options.LinuxFixedBase = 0x400000000; const auto fixed = converter.Convert(source, options); - require(Io::ReadU16(plain.Executable, 16) == 3 && Io::ReadU16(fixed.Executable, 16) == 2, "Fixed base did not select ET_EXEC"); - require(Io::ReadU64(fixed.Executable, 24) == Io::ReadU64(plain.Executable, 24) + *options.LinuxFixedBase, "Fixed entry was not rebased above 4 GiB"); - require(fixed.Modules.front().Bytes == plain.Modules.front().Bytes, "Executable fixed base changed a bundled module"); + require(Io::ReadU16(standard.Executable, 16) == 2 && Io::ReadU16(fixed.Executable, 16) == 2, "Fixed base did not select ET_EXEC"); + require(Io::ReadU16(independent.Executable, 16) == 3, "Lazy binding did not keep ET_DYN"); + require(standard.LinuxBase == 0x400000 && fixed.LinuxBase == *options.LinuxFixedBase && !independent.LinuxBase, "Conversion reported the wrong Linux base"); + require(Io::ReadU64(fixed.Executable, 24) == Io::ReadU64(standard.Executable, 24) + *options.LinuxFixedBase - 0x400000, "Fixed entry was not rebased above 4 GiB"); + require(fixed.Modules.front().Bytes == standard.Modules.front().Bytes, "Executable fixed base changed a bundled module"); for (const auto target : {Relinker::TargetSystem::Windows, Relinker::TargetSystem::MacOs}) { + Relinker::ConversionOptions other; + other.Target = target; + require(!converter.Convert(source, other).LinuxBase, "A non-Linux conversion reported a Linux base"); options.Target = target; reject([&] { converter.Convert(source, options); }); } diff --git a/core/relinker/relinker/tests/ElfFixture.hpp b/core/relinker/relinker/tests/ElfFixture.hpp index daa1f322cb..2ed1a52cae 100644 --- a/core/relinker/relinker/tests/ElfFixture.hpp +++ b/core/relinker/relinker/tests/ElfFixture.hpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -117,12 +118,17 @@ public: const auto phOffset = Read(elf, 32); const auto phSize = Read(elf, 54); const auto phCount = Read(elf, 56); + const auto page = static_cast(sysconf(_SC_PAGESIZE)); + linkBase = std::numeric_limits::max(); + std::uint64_t last = 0; for (std::uint16_t index = 0; index < phCount; ++index) { const auto header = phOffset + index * phSize; if (Read(elf, header) != 1) continue; - size = std::max(size, Read(elf, header + 16) + Read(elf, header + 40)); + linkBase = std::min(linkBase, Read(elf, header + 16) / page * page); + last = std::max(last, Read(elf, header + 16) + Read(elf, header + 40)); } - if (size == 0) throw std::runtime_error("ELF has no PT_LOAD segment"); + if (last == 0) throw std::runtime_error("ELF has no PT_LOAD segment"); + size = static_cast(last - linkBase); base = mmap(nullptr, size, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (base == MAP_FAILED) throw std::runtime_error("Cannot map executable memory for the ELF image"); for (std::uint16_t index = 0; index < phCount; ++index) { @@ -131,15 +137,14 @@ public: const auto offset = Read(elf, header + 8); const auto fileSize = Read(elf, header + 32); if (offset > elf.size() || fileSize > elf.size() - offset) throw std::runtime_error("PT_LOAD exceeds the ELF file"); - std::memcpy(static_cast(base) + Read(elf, header + 16), elf.data() + offset, fileSize); + std::memcpy(static_cast(base) + (Read(elf, header + 16) - linkBase), elf.data() + offset, fileSize); } if (mprotect(base, size, PROT_NONE) != 0) throw std::runtime_error("Cannot protect the ELF image"); - const auto page = static_cast(sysconf(_SC_PAGESIZE)); for (std::uint16_t index = 0; index < phCount; ++index) { const auto header = phOffset + index * phSize; if (Read(elf, header) != 1) continue; const auto flags = Read(elf, header + 4); - const auto vaddr = Read(elf, header + 16); + const auto vaddr = Read(elf, header + 16) - linkBase; const auto start = vaddr / page * page; const auto end = (vaddr + Read(elf, header + 40) + page - 1) / page * page; const int protection = ((flags & 4) != 0 ? PROT_READ : 0) | ((flags & 2) != 0 ? PROT_WRITE : 0) | ((flags & 1) != 0 ? PROT_EXEC : 0); @@ -163,6 +168,7 @@ public: private: void* base = nullptr; std::size_t size = 0; + std::uint64_t linkBase = 0; }; } // namespace RelinkerTests diff --git a/core/relinker/relinker/tests/GamePatchTests.cpp b/core/relinker/relinker/tests/GamePatchTests.cpp index f81bc1f2fe..bde267682d 100644 --- a/core/relinker/relinker/tests/GamePatchTests.cpp +++ b/core/relinker/relinker/tests/GamePatchTests.cpp @@ -385,7 +385,20 @@ void verify() { rejects([&] { VerifyPatches(ParsePatchFile(header("[[patch]]\ntarget = \"main.elf\"\nva = 0x1000\nexpected = \"00\"\nvalue = \"01\"\n")), outputs, {}, true); }, "no main_target"); auto exec = twoSegmentFixture(); Io::WriteU16(exec, 16, 2); - rejects([&] { VerifyPatches(file, {{"main.elf", exec}}, {}, true); }, "verify the input instead"); + rejects([&] { VerifyPatches(file, {{"main.elf", exec}}, {}, true); }, "has no linux_base"); + const auto based = ParsePatchFile(header("main_target = \"main.elf\"\nlinux_base = 0x400000\n[[patch]]\nname = \"one\"\ntarget = \"main.elf\"\nva = 0x1000\nexpected = \"0001\"\nvalue = \"0908\"\n" + "[[patch]]\ntarget = \"main.elf\"\nva = 0x1100\nexpected = \"ff\"\nvalue = \"01\"\n")); + require(based.LinuxBase == 0x400000, "linux_base was not read"); + auto rebased = patchedBytes; + Io::WriteU16(rebased, 16, 2); + for (const std::size_t field : {64 + 16, 64 + 24, 120 + 16, 120 + 24}) Io::WriteU64(rebased, field, Io::ReadU64(rebased, field) + 0x400000); + output = VerifyPatches(based, {{"main.elf", rebased}}, {}, true); + require(output.size() == 2 && output[0].Status == VerifyStatus::Applied && output[1].Status == VerifyStatus::Applied, "fixed-base output applied"); + require(output[0].VirtualAddress == 0x1000 && output[0].Offset == 0x1000 && output[1].Offset == 0x2000, "fixed-base output reported the rebased address"); + output = VerifyPatches(based, {{"main.elf", exec}}, {}, true); + require(output[0].Status == VerifyStatus::Mismatch && output[0].Detail.find("PT_LOAD") != std::string::npos, "linux_base was not applied"); + rejects([&] { VerifyPatches(based, outputs, {}, true); }, "has linux_base = 0x400000"); + rejects([&] { VerifyPatches(ParsePatchFile(header("main_target = \"main.elf\"\nlinux_base = 0x400000\n[[patch]]\ntarget = \"main.elf\"\nva = 0xffffffffffffff00\nexpected = \"00\"\nvalue = \"01\"\n")), {{"main.elf", rebased}}, {}, true); }, "lies outside the address space at linux_base"); rejects([&] { VerifyPatches(file, {{"main.elf", {'M', 'Z', 0x90, 0}}}, {}, true); }, "verify the input instead"); rejects([&] { VerifyPatches(file, {{"main.elf", {0xcf, 0xfa, 0xed, 0xfe, 7, 0, 0, 1}}}, {}, true); }, "verify the input instead"); rejects([&] { VerifyPatches(file, {{"main.elf", {}}}, {}, true); }, "verify the input instead"); @@ -424,10 +437,15 @@ void manifest() { require(parsed.Entries[1].Name.empty() && parsed.Entries[1].Target == "sce_module/libc.prx" && parsed.Entries[1].Description.empty(), "second entry"); require(text.find("name = \"alpha\"") != std::string::npos && text.find("main_target = \"main.elf\"") != std::string::npos, "manifest text"); require(WriteManifest(AggregateManifest({{plain}, {second}}), "main.elf", "") == text, "manifest is not deterministic"); + require(text.find("linux_base") == std::string::npos && !parsed.LinuxBase, "a manifest without a base wrote linux_base"); + const auto based = WriteManifest({plain}, "main.elf", "", 0x400000); + require(based.find("main_target = \"main.elf\"\nlinux_base = 0x400000\n") != std::string::npos && ParsePatchFile(based).LinuxBase == 0x400000, "linux_base did not round-trip"); + rejectsText(header("linux_base = \"0x400000\"\n"), 2, "linux_base must be a non-negative integer"); + rejectsText(header("linux_base = -1\n"), 2, "linux_base must be a non-negative integer"); } void reservedNames() { - for (const char* reserved : {"format", "relinker_version", "relinker_commit", "config", "config_sha256", "main_target", "settings", "settings_sha256"}) + for (const char* reserved : {"format", "relinker_version", "relinker_commit", "config", "config_sha256", "main_target", "linux_base", "settings", "settings_sha256"}) rejects([&] { ParsePatchFile(namedEntry(reserved)); }, "line 3: name is reserved"); ParsePatchFile(header("settings = \"a\"\nsettings_sha256 = \"b\"\n")); } diff --git a/core/relinker/relinker/tests/test_executable_tls_relocations.py b/core/relinker/relinker/tests/test_executable_tls_relocations.py index fd6ceff424..5ce918047b 100644 --- a/core/relinker/relinker/tests/test_executable_tls_relocations.py +++ b/core/relinker/relinker/tests/test_executable_tls_relocations.py @@ -20,6 +20,7 @@ PT_TLS = 7 DTPMOD64, DTPOFF64, TPOFF64, TPOFF32 = 16, 17, 18, 23 TLS_MEMSZ, TLS_ALIGN = 0x24, 16 TLS_BLOCK = 0x30 +DEFAULT_BASE = 0x400000 STRINGS = b"\0Tls\0Data\0ImportedTls\0" TLS_NAME, DATA_NAME, IMPORT_NAME = 1, 5, 10 @@ -107,8 +108,9 @@ def main(): elf = output.read_bytes() relocations = dynamic_relocations(elf) tls = sorted(relocation for relocation in relocations if relocation[1] in (DTPMOD64, DTPOFF64, TPOFF64)) - assert tls == [(0x1000, TPOFF64, "", 0, 4), (0x1008, DTPOFF64, "", 0, 8), (0x1010, TPOFF64, "", 0, 0x12), - (0x1018, DTPMOD64, "", 0, 0), (0x1020, TPOFF64, "ImportedTls", 6, 0)], relocations + assert tls == [(DEFAULT_BASE + 0x1000, TPOFF64, "", 0, 4), (DEFAULT_BASE + 0x1008, DTPOFF64, "", 0, 8), + (DEFAULT_BASE + 0x1010, TPOFF64, "", 0, 0x12), (DEFAULT_BASE + 0x1018, DTPMOD64, "", 0, 0), + (DEFAULT_BASE + 0x1020, TPOFF64, "ImportedTls", 6, 0)], relocations assert any(struct.unpack_from("= len(image), "The grown program header table must be moved past the input" assert "PT_GNU_EH_FRAME" not in result.stderr, result.stderr @@ -125,7 +127,7 @@ def main(): result, elf = relink(relinker, directory, "few-slots-" + str(with_frame), image) headers = program_headers(elf) assert len(headers) == (8 if with_frame else 7), headers - assert [header for header in headers if header[0] == PT_GNU_EH_FRAME] == ([EH_FRAME_HEADER] if with_frame else []), headers + assert [header for header in headers if header[0] == PT_GNU_EH_FRAME] == ([EH_FRAME_OUTPUT] if with_frame else []), headers assert struct.unpack_from("= len(image), "The grown program header table must be moved past the input" assert elf[64 + 5 * 56:64 + 6 * 56] == image[64 + 5 * 56:64 + 6 * 56], "Bytes after the input program header table changed" diff --git a/core/relinker/relinker/tests/test_linux_entry_argv.py b/core/relinker/relinker/tests/test_linux_entry_argv.py index 4dad8c6e07..87604a5743 100644 --- a/core/relinker/relinker/tests/test_linux_entry_argv.py +++ b/core/relinker/relinker/tests/test_linux_entry_argv.py @@ -12,6 +12,7 @@ from signal_run import run_without_core_dump from test_linux_load_alignment import PT_LOAD, PT_SCE_VERSION, fixture, loads ENTRY = 0x10 +DEFAULT_BASE = 0x400000 ARGV_CHECK = bytes.fromhex( "833f02" "751e" "48837f0800" "7417" @@ -79,7 +80,7 @@ def main(): capture_output=True, text=True, timeout=20) if error is None: assert result.returncode == 0, (address, result.stdout, result.stderr) - assert entry_call_target(far.read_bytes()) == ENTRY, hex(address) + assert entry_call_target(far.read_bytes()) == DEFAULT_BASE + ENTRY, hex(address) else: assert result.returncode == 2 and error in result.stderr and not far.exists(), (address, result) print("Linux entry argv test passed") diff --git a/core/relinker/relinker/tests/test_linux_fixed_base.py b/core/relinker/relinker/tests/test_linux_fixed_base.py index 84244f4533..ee31ad59ef 100644 --- a/core/relinker/relinker/tests/test_linux_fixed_base.py +++ b/core/relinker/relinker/tests/test_linux_fixed_base.py @@ -1,4 +1,4 @@ -"""Check that --linux-fixed-base places the executable at the requested address and rebases every address it holds.""" +"""Check that the Linux executable is placed at 0x400000 by default or at the --linux-fixed-base address, with every address it holds rebased, that --lazy-binding keeps it position independent and warns, and that the largest image the relinker accepts ends below the guest arena.""" from pathlib import Path import platform @@ -16,9 +16,13 @@ PT_LOAD = 1 PT_DYNAMIC = 2 PT_GNU_STACK = 0x6474E551 BASE = 0x400000 +HIGH_BASE = 0x400000000 +SHIFT = HIGH_BASE - BASE ENTRY = 0x10 SLOT = 0x800 ADDRESS_TAGS = {3, 4, 5, 6, 7, 23} +GUEST_ARENA_START = 0x200000000 +LAZY_WARNING = "with --lazy-binding the Linux executable stays position independent" def check_code(base): @@ -86,58 +90,83 @@ def main(): relinker = Path(sys.argv[1]).resolve() with tempfile.TemporaryDirectory(prefix="anyps5-fixed-base-") as directory: work = Path(directory) - result, plain_path = relink(relinker, work, "plain", fixture()) + result, fixed_path = relink(relinker, work, "default", fixture()) assert result.returncode == 0, (result.stdout, result.stderr) - result, fixed_path = relink(relinker, work, "fixed", fixture(), "--linux-fixed-base", hex(BASE)) + assert LAZY_WARNING not in result.stdout + result.stderr, (result.stdout, result.stderr) + result, explicit_path = relink(relinker, work, "explicit", fixture(), "--linux-fixed-base", hex(BASE)) assert result.returncode == 0, (result.stdout, result.stderr) - plain = plain_path.read_bytes() + result, high_path = relink(relinker, work, "high", fixture(), "--linux-fixed-base", hex(HIGH_BASE)) + assert result.returncode == 0, (result.stdout, result.stderr) + result, lazy_path = relink(relinker, work, "lazy", fixture(), "--lazy-binding") + assert result.returncode == 0, (result.stdout, result.stderr) + assert LAZY_WARNING in result.stdout + result.stderr, (result.stdout, result.stderr) fixed = fixed_path.read_bytes() + high = high_path.read_bytes() + lazy = lazy_path.read_bytes() - assert struct.unpack_from(" struct.unpack_from("\n" + f"int exported(void) __asm__(\"\\\"{symbol}\\\"\");\n" + f"int exported(void) {{ _exit({value}); }}\n") + output = work / (name + ".so") + subprocess.run([os.environ.get("CC", "cc"), "-shared", "-fPIC", str(source), "-o", str(output)], check=True) + return output.read_bytes() + + +def convert(relinker, case, image, module=None): + case.mkdir() + if module is not None: + (case / "sce_module").mkdir() + (case / "sce_module" / "bundled.prx").write_bytes(module) + source = case / "input.elf" + source.write_bytes(image) + output = case / "output.elf" + options = [] if module is not None else ["--skip-sce-module"] + result = subprocess.run([str(relinker), *options, str(source), str(output)], capture_output=True, text=True, timeout=30) + assert result.returncode == 0, (case.name, result.stdout, result.stderr) + return output + + +def run(output, library): + (output.parent / "libs").mkdir() + (output.parent / "libs" / "available.prx").write_bytes(library) + output.chmod(0o755) + return subprocess.run([str(output)], capture_output=True, text=True, timeout=30) + + +def main(): + relinker = Path(sys.argv[1]).resolve() + runnable = sys.platform.startswith("linux") and platform.machine() in ("x86_64", "AMD64") + with tempfile.TemporaryDirectory(prefix="anyps5-linux-named-imports-") as directory: + work = Path(directory) + cases = { + "sprx": convert(relinker, work / "sprx", executable("available.sprx", "shared")), + "prx": convert(relinker, work / "prx", executable("available.prx", "shared")), + "qualified": convert(relinker, work / "qualified", executable("available.sprx", "shared#A#B")), + "name": convert(relinker, work / "name", executable("available.sprx", "shared")), + "guest": convert(relinker, work / "guest", executable("bundled.prx", "shared", extra_dependencies=("available.sprx",)), + module_with_symbol(True)), + } + image = cases["sprx"].read_bytes() + assert imported_symbols(image) == {nid("shared")}, imported_symbols(image) + assert needed_libraries(image) == ["available.prx"], needed_libraries(image) + assert imported_symbols(cases["prx"].read_bytes()) == {"shared"} + assert imported_symbols(cases["qualified"].read_bytes()) == {"shared"} + image = cases["guest"].read_bytes() + assert imported_symbols(image) == {"shared#guest"}, imported_symbols(image) + assert "available.prx" in needed_libraries(image), needed_libraries(image) + if not runnable: + return + by_nid = library(work, "by-nid", nid("shared"), 22) + by_name = library(work, "by-name", "shared", 44) + result = run(cases["sprx"], by_nid) + assert result.returncode == 22, (result.returncode, result.stdout, result.stderr) + result = run(cases["prx"], by_name) + assert result.returncode == 44, (result.returncode, result.stdout, result.stderr) + result = run(cases["qualified"], by_nid) + assert result.returncode != 0 and "undefined symbol: shared" in result.stderr, (result.returncode, result.stderr) + result = run(cases["name"], by_name) + assert result.returncode != 0 and f"undefined symbol: {nid('shared')}" in result.stderr, (result.returncode, result.stderr) + print("Linux named import tests passed") + + +if __name__ == "__main__": + main() diff --git a/core/relinker/relinker/tests/test_linux_program_header_order.py b/core/relinker/relinker/tests/test_linux_program_header_order.py index 6d223a4135..3a8be9aca1 100644 --- a/core/relinker/relinker/tests/test_linux_program_header_order.py +++ b/core/relinker/relinker/tests/test_linux_program_header_order.py @@ -21,6 +21,7 @@ PT_OS_PROCPARAM = 0x61000001 PT_GNU_STACK = 0x6474E551 PT_SCE_VERSION = 0x6FFFFF01 PAGE = 0x1000 +DEFAULT_BASE = 0x400000 TEXT = (PT_LOAD, 5, 0x4000, 0x0000, 0x1000, 0x1000, 0x1000) RODATA = (PT_LOAD, 4, 0x5000, 0x1000, 0x0800, 0x0800, 0x1000) @@ -102,13 +103,13 @@ def main(): ordered = convert("ordered-loads", multi_load_fixture([TEXT, RODATA, PROCPARAM, DATA])) headers = check_order("ordered-loads", ordered.read_bytes()) - assert (PT_OS_PROCPARAM, PROCPARAM[3]) in [(h[0], h[3]) for h in headers], headers - assert [h[3] for h in headers if h[0] == PT_LOAD][:3] == [TEXT[3], RODATA[3], DATA[3]], headers + assert (PT_OS_PROCPARAM, DEFAULT_BASE + PROCPARAM[3]) in [(h[0], h[3]) for h in headers], headers + assert [h[3] for h in headers if h[0] == PT_LOAD][:3] == [DEFAULT_BASE + h[3] for h in (TEXT, RODATA, DATA)], headers run_expecting_trap("ordered-loads", ordered) unordered = convert("unordered-loads", multi_load_fixture([DATA, PROCPARAM, TEXT, RODATA])) headers = check_order("unordered-loads", unordered.read_bytes()) - assert [h[3] for h in headers if h[0] == PT_LOAD][:3] == [TEXT[3], RODATA[3], DATA[3]], headers + assert [h[3] for h in headers if h[0] == PT_LOAD][:3] == [DEFAULT_BASE + h[3] for h in (TEXT, RODATA, DATA)], headers assert kinds_between_loads(headers) == [PT_OS_PROCPARAM, PT_GNU_STACK], headers run_expecting_trap("unordered-loads", unordered) diff --git a/core/relinker/relinker/tests/test_none_relocations.py b/core/relinker/relinker/tests/test_none_relocations.py index 538b0247d8..ca10af8fd0 100644 --- a/core/relinker/relinker/tests/test_none_relocations.py +++ b/core/relinker/relinker/tests/test_none_relocations.py @@ -10,6 +10,13 @@ from test_dynamic_segment import fixture from test_linux_dynamic_strings import dynamic_strings, file_offset from test_linux_load_alignment import loads +DEFAULT_BASE = 0x400000 + + +def rebased(relocation): + offset, info, addend = relocation + return (offset + DEFAULT_BASE, info, addend + DEFAULT_BASE if info & 0xffffffff == 8 else addend) + def image_with_relocations(relocations, sce, imported): image = fixture() @@ -75,7 +82,7 @@ def main(): if tags.get(8, 0): start = file_offset(data, loads(data), tags[7]) actual = [struct.unpack_from("> 32 for _, info, _ in actual) return diff --git a/core/relinker/relinker/tests/test_sce_relro.py b/core/relinker/relinker/tests/test_sce_relro.py index 51de2a0953..42cbcb5bf8 100644 --- a/core/relinker/relinker/tests/test_sce_relro.py +++ b/core/relinker/relinker/tests/test_sce_relro.py @@ -12,6 +12,7 @@ from test_linux_load_alignment import loads PT_LOAD = 1 PT_DYNAMIC = 2 PT_SCE_RELRO = 0x61000010 +DEFAULT_BASE = 0x400000 PT_SCE_VERSION = 0x6FFFFF01 @@ -77,11 +78,11 @@ def main(): assert result.returncode == 0, (name, target, result.stdout, result.stderr) if target == "linux": data = output.read_bytes() - relro = [load for load in loads(data) if load[3] <= 0x1000 < load[3] + load[6]] - assert len(relro) == 1 and relro[0][1] == 6 and relro[0][3] == 0x1000 and relro[0][5:7] == (0x1000, 0x1000), (name, relro) + relro = [load for load in loads(data) if load[3] <= DEFAULT_BASE + 0x1000 < load[3] + load[6]] + assert len(relro) == 1 and relro[0][1] == 6 and relro[0][3] == DEFAULT_BASE + 0x1000 and relro[0][5:7] == (0x1000, 0x1000), (name, relro) tags, _ = dynamic_strings(data) start = file_offset(data, loads(data), tags[7]) - assert [struct.unpack_from("= swappc->userDataBaseRegister && targetRegister + 1u < swappc->userDataBaseRegister + swappc->userDataCount && - std::none_of(program.instructions.begin(), program.instructions.end(), [&](const RdnaInstruction& other) { + std::none_of(program.instructions.begin(), GcnGuestShaders() ? program.instructions.begin() + index : program.instructions.end(), [&](const RdnaInstruction& other) { return writesScalar(other, targetRegister) || writesScalar(other, targetRegister + 1u); }); return swappc != nullptr && swappc->fetchCallAllowed && positional && (userDataPair || outsideProgram); @@ -715,9 +715,10 @@ struct LinkEscape { template LinkEscape requireReturnAddressesStayInCalls(const RdnaProgram& program, const std::vector& calls, const std::vector& unlinked, std::uint32_t knownWaveSize, const TError& escapeError) { + const bool gcn = GcnGuestShaders(); std::set links; for (const auto& call : calls) { - if (!call.fetch) links.insert(call.linkRegister); + if (!call.fetch || gcn) links.insert(call.linkRegister); } const auto size = static_cast(program.instructions.size()); const auto successors = [&](std::uint32_t index) { @@ -776,7 +777,7 @@ LinkEscape requireReturnAddressesStayInCalls(const RdnaProgram& program, const s if (holds[index] == 0u || writesLink(index) || instruction.family == RdnaInstructionFamily::SOPC || findReturnAt(calls, index) != nullptr) continue; const bool inside = std::ranges::any_of(calls, [&](const SwappcCall& call) { return !call.fetch && call.linkRegister == link && index >= call.targetIndex && index <= call.returnIndex; }); if (inside || !(((holds[index] & 1u) != 0u && readsScalarDword(instruction, link, waveSize)) || ((holds[index] & 2u) != 0u && readsScalarDword(instruction, link + 1u, waveSize)))) continue; - const auto owner = std::ranges::find_if(calls, [&](const SwappcCall& call) { return !call.fetch && call.linkRegister == link; }); + const auto owner = std::ranges::find_if(calls, [&](const SwappcCall& call) { return (!call.fetch || gcn) && call.linkRegister == link; }); return escapeError(*owner, instruction); } } @@ -897,7 +898,7 @@ std::vector analyzeSwappcCalls(const RdnaProgram& program, const Swa continue; } const bool outsideRegion = regionEnd != InvalidControlFlowId && (index < call.targetIndex || index > regionEnd); - if (outsideRegion) continue; + if (outsideRegion || (call.fetch && GcnGuestShaders())) continue; if (touchesLinkRegister(instruction, call.linkRegister)) { throw escapeError(call, instruction); } @@ -1292,7 +1293,8 @@ std::set branchLabels(const RdnaProgram& program) { bool unchangedUntil(const RdnaProgram& program, std::uint32_t reg, std::uint32_t reader, std::uint32_t waveSize) { for (std::uint32_t index = 0; index < program.instructions.size(); ++index) { - if (!writesScalar(program.instructions[index], reg, waveSize)) continue; + const bool readerWritesAfterReading = index == reader && GcnGuestShaders(); + if (readerWritesAfterReading || !writesScalar(program.instructions[index], reg, waveSize)) continue; if (index <= reader) return false; for (std::uint32_t later = index; later < program.instructions.size(); ++later) { const auto& instruction = program.instructions[later]; @@ -1402,7 +1404,7 @@ std::vector FindUserDataCalls(const RdnaProgram& program, const Sw const auto source = scalarIndex(instruction.source0); if (link == NoScalarRegister || source == NoScalarRegister || source % 2u != 0u) continue; std::uint32_t pair = NoScalarRegister; - if (source != link && userData(source) && userData(source + 1u) && unchanged(source, index) && unchanged(source + 1u, index)) { + if ((source != link || GcnGuestShaders()) && userData(source) && userData(source + 1u) && unchanged(source, index) && unchanged(source + 1u, index)) { pair = source; } else { std::uint32_t low = 0, high = 0; diff --git a/core/shader/recompiler/Deserialize.cpp b/core/shader/recompiler/Deserialize.cpp index 6f1b3a0ca9..5bc2aec696 100644 --- a/core/shader/recompiler/Deserialize.cpp +++ b/core/shader/recompiler/Deserialize.cpp @@ -165,6 +165,7 @@ int main(int argc, char** argv) { translateOptions.stage = stageKind; translateOptions.waveSize = request.context.waveSize; translateOptions.userDataBaseRegister = request.context.userDataBaseRegister; + translateOptions.gcnLegacyVertexInputs = GcnGuestShaders() && request.context.userDataBaseRegister == 0u; translateOptions.userDataCount = static_cast(request.context.userData.size()); translateOptions.scratchDwords = request.context.compute.has_value() ? request.context.compute->scratchDwords : 0u; translateOptions.sharedMemoryBytes = request.context.compute.has_value() && static_cast(request.context.compute->ldsSizeDwords) * 4u + 4u > request.target.maxWorkgroupSharedMemoryBytes ? request.context.compute->ldsSizeDwords * 4u : 0u; diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index 16bbae5c22..45b3a7ea1e 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -4,6 +4,7 @@ #include "Optimization/ShaderStageInputInfo.hpp" #include "RdnaDecoder/RdnaDescriptorFormat.hpp" #include "RdnaDecoder/RdnaImageOpDecoder.hpp" +#include "RdnaDecoder/RdnaInstructionDecoder.hpp" #include "SpirvBackend/SpirvBufferFormat.hpp" #include "ThreadOwned.hpp" #include "ImageTableAbi.hpp" @@ -118,6 +119,15 @@ RdnaImageDimension descriptorDimension(const DescriptorValue& descriptor, RdnaIm } } +std::uint32_t imageCoordinates(RdnaImageDimension dimension) { + switch (dimension) { + case RdnaImageDimension::Dim1D: return 1u; + case RdnaImageDimension::Dim1DArray: case RdnaImageDimension::Dim2D: return 2u; + case RdnaImageDimension::Dim2DMsaaArray: return 4u; + default: return 3u; + } +} + bool validImageDescriptor(const DescriptorValue& descriptor, bool r128) { const auto type = rawImageType(descriptor); const auto format = rawImageFormat(descriptor); @@ -199,6 +209,9 @@ ImageDecode inspectImageDescriptor(const DescriptorValue& descriptor, const Imag decoded.dimension = RdnaImageDimension::Dim2DArray; } } + if (GcnGuestShaders() && imageCoordinates(decoded.dimension) != imageCoordinates(base.dimension)) { + throw std::runtime_error("GCN image instruction on a descriptor of type " + std::to_string(static_cast(rawImageType(descriptor))) + " needs a different address count than its inferred dimension; not implemented"); + } decoded.cube = descriptorIsCube(descriptor); const auto format = rawImageFormat(descriptor); if (base.atomic64 && format != IrBufferFormat::Format32_32UInt && format != IrBufferFormat::Format32_32SInt && format != IrBufferFormat::Format32_32Float) { @@ -215,7 +228,7 @@ ImageDecode inspectImageDescriptor(const DescriptorValue& descriptor, const Imag return rejectImage(PoisonReason::Fmask, "FMASK requires a direct sampled image load"); } if (decoded.fmask && format != IrBufferFormat::Fmask32_S8_F8) return rejectImage(PoisonReason::Fmask, "FMASK format is not implemented"); - if (decoded.fmask && (descriptor.dwords[6] & (3u << 20u)) != 0u) return rejectImage(PoisonReason::Fmask, "FMASK CMASK compression is not implemented"); + if (decoded.fmask && ((descriptor.dwords[6] & (1u << 20u)) != 0u || ((descriptor.dwords[6] & (1u << 21u)) != 0u && (descriptor.dwords[6] >> 24u) == 0u && descriptor.dwords[7] == 0u))) return rejectImage(PoisonReason::Fmask, "FMASK CMASK compression is not implemented without readable metadata"); if (decoded.fmask && (base.packed || base.byElements != 0u)) return rejectImage(PoisonReason::Fmask, "packed FMASK image reads are not implemented"); if (base.packed) { if (table || (!storage && descriptorImageSwizzle(descriptor) != ShaderImageIdentitySwizzle)) { @@ -281,7 +294,9 @@ ImageDecode inspectImageDescriptor(const DescriptorValue& descriptor, const Imag decoded.conversionFormat = format; } } else if (decoded.numericClass == IrTextureNumericClass::Unsupported || (base.depthCompare && decoded.numericClass != IrTextureNumericClass::Float)) { - return rejectImage(PoisonReason::NumericClass, "sampled image descriptor uses an unsupported format"); + char words[96]; + std::snprintf(words, sizeof(words), " (T# %08x %08x %08x %08x %08x %08x %08x %08x)", descriptor.dwords[0], descriptor.dwords[1], descriptor.dwords[2], descriptor.dwords[3], descriptor.dwords[4], descriptor.dwords[5], descriptor.dwords[6], descriptor.dwords[7]); + return rejectImage(PoisonReason::NumericClass, std::string("sampled image descriptor uses an unsupported format") + words); } return result; } @@ -1285,6 +1300,12 @@ void materializeSnapshot(const IrResourcePlan& plan, const SrtRuntime& runtime, throw std::runtime_error("materialization sources are missing buffer descriptors"); } snapshot.buffers.assign(values.begin(), values.begin() + plan.info.buffers.size()); + if (GcnGuestShaders()) { + for (auto& buffer : snapshot.buffers) { + buffer.dwords[1] &= GcnBufferWord1Mask; + buffer.dwords[3] = GcnBufferWord3(buffer.dwords[1], buffer.dwords[3]); + } + } cursor += plan.info.buffers.size(); snapshot.images.resize(plan.info.images.size()); @@ -1305,6 +1326,10 @@ void materializeSnapshot(const IrResourcePlan& plan, const SrtRuntime& runtime, if (descriptor.dwordCount != 8u) { throw std::runtime_error("image descriptor has an invalid width"); } + if (GcnGuestShaders()) { + const auto words = GcnImageWords(std::span(descriptor.dwords.data(), 8u)); + std::copy(words.begin(), words.end(), descriptor.dwords.begin()); + } const auto activity = plan.descriptorSources.size() + i; if (activity < activeSources.size() && activeSources[activity] == 0u) { descriptor = DescriptorValue{}; @@ -1848,6 +1873,12 @@ std::vector ResourceMaterializer::RuntimeImageModes(const ImageRe volume.dimension = RdnaImageDimension::Dim3D; modes.push_back(volume); } + const bool arrayStore = image.dimension == RdnaImageDimension::Dim3D && storage && image.written && !image.read && !image.atomic && image.mipMode != ImageMipMode::DynamicStorage; + if (arrayStore && !depth && packed == IrBufferFormat::Invalid && image.byElements == 0u) { + auto array = mode; + array.dimension = RdnaImageDimension::Dim2DArray; + modes.push_back(array); + } if (image.dimension == RdnaImageDimension::Dim2DMsaa && image.fmaskCompatible && image.cubeFaceCompatible && !table && !storage && !depth && conversion == IrBufferFormat::Invalid && packed == IrBufferFormat::Invalid && image.byElements == 0u) { auto plane = mode; plane.dimension = RdnaImageDimension::Dim2D; diff --git a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp index b7f35bb4c1..aa37749e51 100644 --- a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp +++ b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp @@ -285,6 +285,8 @@ void CollectPixelInputs(const IrProgram& program, const ShaderPixelInputInfo* pi std::array perVertex {}; std::array interpolated {}; std::array halfRead {}; + std::array readsP10 {}; + std::array readsP20 {}; for (const auto& block : program.Blocks()) { for (const IrValue* inst : block->Instructions()) { if (inst->Opcode() == IrOpcode::GetAttribute) { @@ -298,7 +300,9 @@ void CollectPixelInputs(const IrProgram& program, const ShaderPixelInputInfo* pi const auto input = inst->Argument(0)->Resolve()->ImmediateU32(); const auto mode = inst->Argument(2)->Resolve()->ImmediateU32(); read[input] = true; - perVertex[input] = perVertex[input] || mode == 1u || !IsPixelParameterFlat(*pixel, input); + readsP10[input] = readsP10[input] || mode == 0u; + readsP20[input] = readsP20[input] || mode == 1u; + perVertex[input] = perVertex[input] || (readsP10[input] && readsP20[input]) || !IsPixelParameterFlat(*pixel, input); } else if (inst->Opcode() == IrOpcode::GetInterpolationParameterF16) { const auto input = inst->Argument(0)->Resolve()->ImmediateU32(); const bool high = inst->Argument(3)->Resolve()->ImmediateU32() != 0u; diff --git a/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp b/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp index 5eb41904a0..ae3cbe0839 100644 --- a/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp +++ b/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp @@ -2,7 +2,9 @@ #define CORE_SHADER_RECOMPILIER_RDNADECODER_INCLUDE_RDNADECODER_RDNADESCRIPTORFORMAT_HPP #include "IntermediateRepresentation/IrMetadata.hpp" +#include #include +#include namespace ShaderRecompiler { @@ -24,6 +26,15 @@ enum class ImageType : std::uint32_t { [[nodiscard]] std::uint32_t DepthBitsTextureWidth(std::uint32_t word1, std::uint32_t word3); [[nodiscard]] bool IsDepthBitsTexture(std::uint32_t word1, std::uint32_t word3); +[[nodiscard]] const std::array& GcnBufferFormats(); +[[nodiscard]] std::uint32_t GcnBufferWord3(std::uint32_t word1, std::uint32_t word3); +inline constexpr std::uint32_t GcnBufferWord1Mask = 0xffff0fffu; +[[nodiscard]] std::uint32_t GcnImageFormat(std::uint32_t dataFormat, std::uint32_t numberFormat); +[[nodiscard]] std::array GcnImageWords(std::span words); +[[nodiscard]] constexpr std::uint32_t GcnImageTilingIndex(std::uint32_t word7) { return word7 & 0x1fu; } +[[nodiscard]] constexpr bool GcnImagePow2Pad(std::uint32_t word7) { return ((word7 >> 5u) & 1u) != 0u; } +[[nodiscard]] constexpr std::uint32_t GcnImagePitch(std::uint32_t word7) { return ((word7 >> 6u) & 0x3fffu) + 1u; } + [[nodiscard]] constexpr std::uint32_t SrgbDecodeBit(IrBufferFormat format) { switch (format) { case IrBufferFormat::Format8Srgb: return 1u; diff --git a/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaInstructionDecoder.hpp b/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaInstructionDecoder.hpp index 9456f38cd1..d52379a7c3 100644 --- a/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaInstructionDecoder.hpp +++ b/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaInstructionDecoder.hpp @@ -21,6 +21,10 @@ private: RdnaInstructionFamily GetRdnaInstructionFamily(std::uint32_t word); RdnaInstruction DecodeRdnaInstruction(std::uint32_t programCounter, std::span code, std::uint32_t wordIndex); +RdnaInstruction DecodeRdnaEncodedInstruction(std::uint32_t programCounter, std::span code, std::uint32_t wordIndex); +RdnaInstruction DecodeGcnInstruction(std::uint32_t programCounter, std::span code, std::uint32_t wordIndex); +void SetGcnGuestShaders(bool enabled); +bool GcnGuestShaders(); RdnaProgram DecodeRdnaFrontProgram(std::span front); void DecodeRdnaProgram(std::span code, RdnaProgram& program); RdnaOperand DecodeRdnaScalarSource(std::uint32_t code, std::uint32_t programCounter); diff --git a/core/shader/recompiler/RdnaDecoder/src/GcnInstructionDecoder.cpp b/core/shader/recompiler/RdnaDecoder/src/GcnInstructionDecoder.cpp new file mode 100644 index 0000000000..e5ccedb328 --- /dev/null +++ b/core/shader/recompiler/RdnaDecoder/src/GcnInstructionDecoder.cpp @@ -0,0 +1,349 @@ +#include "RdnaDecoder/RdnaInstructionDecoder.hpp" +#include "GcnOpcodeTables.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ShaderRecompiler { + +namespace { + +std::atomic g_gcnGuestShaders{false}; + +constexpr std::uint32_t LiteralSource = 0xffu; +constexpr std::uint32_t VccSdst = 106u; +constexpr std::uint32_t NullSoffset = 0x7du; +constexpr std::uint32_t LinkedCallSopk = 0x16u; +constexpr std::uint32_t CodeEndSopp = 0x1fu; +constexpr std::uint32_t SetRegImmSopk = 0x15u; + +template +std::optional mapOpcode(const std::array, N>& table, std::uint32_t opcode) { + const auto found = std::lower_bound(table.begin(), table.end(), opcode, [](const auto& entry, std::uint32_t value) { return entry.first < value; }); + if (found == table.end() || found->first != opcode) return std::nullopt; + return found->second; +} + +[[noreturn]] void unsupported(const char* family, std::uint32_t opcode, std::uint32_t programCounter) { + throw UnsupportedInstructionError(std::string("GCN ") + family + " opcode " + std::to_string(opcode) + " at program counter " + std::to_string(programCounter) + " is not implemented"); +} + +std::uint32_t replaceBits(std::uint32_t word, std::uint32_t shift, std::uint32_t width, std::uint32_t value) { + const std::uint32_t mask = ((1u << width) - 1u) << shift; + return (word & ~mask) | ((value << shift) & mask); +} + +bool hasLiteral(std::uint32_t source) { + return source == LiteralSource; +} + +struct Encoded { + std::array words{}; + std::uint32_t rdnaWords = 0; + std::uint32_t gcnWords = 0; +}; + +std::uint32_t literalAt(std::span code, std::uint32_t index, std::uint32_t programCounter) { + if (index >= code.size()) throw std::out_of_range("GCN literal constant at program counter " + std::to_string(programCounter) + " is past the end of the code"); + return code[index]; +} + +Encoded encodeScalar(std::span code, std::uint32_t index, std::uint32_t programCounter) { + const auto word = code[index]; + Encoded out; + out.gcnWords = 1; + std::uint32_t opcode = 0; + std::optional mapped; + bool literal = false; + const auto prefix9 = word >> 23u; + if (prefix9 == 0x17du) { + opcode = (word >> 8u) & 0xffu; + mapped = mapOpcode(GcnTables::Sop1, opcode); + if (!mapped) unsupported("SOP1", opcode, programCounter); + out.words[0] = replaceBits(word, 8, 8, *mapped); + literal = hasLiteral(word & 0xffu); + } else if (prefix9 == 0x17eu) { + opcode = (word >> 16u) & 0x7fu; + mapped = mapOpcode(GcnTables::Sopc, opcode); + if (!mapped) unsupported("SOPC", opcode, programCounter); + out.words[0] = replaceBits(word, 16, 7, *mapped); + literal = hasLiteral(word & 0xffu) || hasLiteral((word >> 8u) & 0xffu); + } else if (prefix9 == 0x17fu) { + opcode = (word >> 16u) & 0x7fu; + mapped = opcode == CodeEndSopp ? std::optional(CodeEndSopp) : mapOpcode(GcnTables::Sopp, opcode); + if (!mapped) unsupported("SOPP", opcode, programCounter); + out.words[0] = replaceBits(word, 16, 7, *mapped); + } else if ((word >> 28u) == 0xbu) { + opcode = (word >> 23u) & 0x1fu; + mapped = opcode == LinkedCallSopk ? std::optional(LinkedCallSopk) : mapOpcode(GcnTables::Sopk, opcode); + if (!mapped) unsupported("SOPK", opcode, programCounter); + out.words[0] = replaceBits(word, 23, 5, *mapped); + literal = opcode == SetRegImmSopk; + } else { + opcode = (word >> 23u) & 0x7fu; + mapped = mapOpcode(GcnTables::Sop2, opcode); + if (!mapped) unsupported("SOP2", opcode, programCounter); + out.words[0] = replaceBits(word, 23, 7, *mapped); + literal = hasLiteral(word & 0xffu) || hasLiteral((word >> 8u) & 0xffu); + } + out.rdnaWords = 1; + if (literal) { + out.words[1] = literalAt(code, index + 1, programCounter); + out.rdnaWords = out.gcnWords = 2; + } + return out; +} + +Encoded encodeSmrd(std::span code, std::uint32_t index, std::uint32_t programCounter) { + const auto word = code[index]; + const auto opcode = (word >> 22u) & 0x1fu; + const auto mapped = mapOpcode(GcnTables::Smrd, opcode); + if (!mapped) unsupported("SMRD", opcode, programCounter); + const auto sdst = (word >> 15u) & 0x7fu; + const auto sbase = (word >> 9u) & 0x3fu; + const bool immediate = ((word >> 8u) & 1u) != 0; + const auto offset = word & 0xffu; + if (!immediate && offset == LiteralSource) unsupported("SMRD literal-offset", opcode, programCounter); + Encoded out; + out.gcnWords = 1; + out.rdnaWords = 2; + out.words[0] = (0x3du << 26u) | (*mapped << 18u) | (sdst << 6u) | sbase; + out.words[1] = immediate ? (NullSoffset << 25u) | (offset * 4u) : (offset << 25u); + return out; +} + +Encoded encodeVop3Words(std::uint32_t word0, std::uint32_t word1, std::uint32_t rdnaOpcode, bool carryOut) { + Encoded out; + out.gcnWords = 2; + out.rdnaWords = 2; + const auto vdst = word0 & 0xffu; + std::uint32_t encoded = (0x35u << 26u) | (rdnaOpcode << 16u) | vdst; + if (carryOut) { + encoded |= ((word0 >> 8u) & 0x7fu) << 8u; + } else { + encoded |= ((word0 >> 8u) & 0x7u) << 8u; + encoded |= ((word0 >> 11u) & 1u) << 15u; + } + out.words[0] = encoded; + out.words[1] = word1; + return out; +} + +std::optional reversedShift(std::uint32_t gcnVop3Opcode) { + switch (gcnVop3Opcode) { + case 0x115u: return 0x116u; + case 0x117u: return 0x118u; + case 0x119u: return 0x11au; + case 0x161u: return 0x2ffu; + case 0x162u: return 0x300u; + case 0x163u: return 0x301u; + default: return std::nullopt; + } +} + +bool compareWritesExec(std::uint32_t gcnVop3Opcode) { + return gcnVop3Opcode < 0x100u && (gcnVop3Opcode & 0x10u) != 0; +} + +std::uint32_t swapSources(std::uint32_t word1) { + const auto src0 = word1 & 0x1ffu; + const auto src1 = (word1 >> 9u) & 0x1ffu; + const auto neg0 = (word1 >> 29u) & 1u; + const auto neg1 = (word1 >> 30u) & 1u; + word1 = (word1 & ~0x3ffffu & ~(3u << 29u)) | src1 | (src0 << 9u) | (neg1 << 29u) | (neg0 << 30u); + return word1; +} + +std::uint32_t swapAbsolute(std::uint32_t word0) { + const auto abs0 = (word0 >> 8u) & 1u; + const auto abs1 = (word0 >> 9u) & 1u; + return (word0 & ~(3u << 8u)) | (abs1 << 8u) | (abs0 << 9u); +} + +bool gcnVop3bCarryOut(std::uint32_t opcode) { + switch (opcode) { + case 0x125u: case 0x126u: case 0x127u: case 0x128u: case 0x129u: case 0x12au: case 0x16du: case 0x16eu: case 0x176u: case 0x177u: return true; + default: return false; + } +} + +Encoded encodeVop3(std::span code, std::uint32_t index, std::uint32_t programCounter) { + if (index + 1 >= code.size()) throw std::out_of_range("truncated GCN VOP3 instruction"); + const auto word0 = code[index]; + const auto opcode = (word0 >> 17u) & 0x1ffu; + if (compareWritesExec(opcode)) unsupported("VOP3 v_cmpx (writes both the scalar destination and EXEC)", opcode, programCounter); + if (const auto shift = reversedShift(opcode)) return encodeVop3Words(swapAbsolute(word0), swapSources(code[index + 1]), *shift, false); + const auto mapped = mapOpcode(GcnTables::Vop3, opcode); + if (!mapped) unsupported("VOP3", opcode, programCounter); + return encodeVop3Words(word0, code[index + 1], *mapped, gcnVop3bCarryOut(opcode)); +} + +Encoded promoteToVop3(std::uint32_t gcnVop3Opcode, std::uint32_t vdst, std::uint32_t src0, std::uint32_t src1, std::uint32_t src2, bool carryOut, std::span code, std::uint32_t index, std::uint32_t programCounter) { + const auto shift = reversedShift(gcnVop3Opcode); + const auto mapped = shift ? shift : mapOpcode(GcnTables::Vop3, gcnVop3Opcode); + if (!mapped) unsupported("VOP3-promoted", gcnVop3Opcode, programCounter); + const std::uint32_t word0 = carryOut ? (vdst | (VccSdst << 8u)) : vdst; + const std::uint32_t word1 = shift ? (src1 | (src0 << 9u) | (src2 << 18u)) : (src0 | (src1 << 9u) | (src2 << 18u)); + auto out = encodeVop3Words(word0, word1, *mapped, carryOut); + out.gcnWords = 1; + if (hasLiteral(src0)) { + out.words[2] = literalAt(code, index + 1, programCounter); + out.rdnaWords = 3; + out.gcnWords = 2; + } + return out; +} + +Encoded encodeVop2(std::span code, std::uint32_t index, std::uint32_t programCounter) { + const auto word = code[index]; + const auto opcode = (word >> 25u) & 0x3fu; + const auto src0 = word & 0x1ffu; + if (const auto mapped = mapOpcode(GcnTables::Vop2, opcode)) { + Encoded out; + out.words[0] = replaceBits(word, 25, 6, *mapped); + out.rdnaWords = out.gcnWords = 1; + const bool constantK = opcode == 0x20u || opcode == 0x21u; + if (hasLiteral(src0) || constantK) { + out.words[1] = literalAt(code, index + 1, programCounter); + out.rdnaWords = out.gcnWords = 2; + } + return out; + } + const auto vdst = (word >> 17u) & 0xffu; + const auto vsrc1 = (word >> 9u) & 0xffu; + const bool laneSelect = opcode == 0x01u || opcode == 0x02u; + const bool carryOut = opcode == 0x25u || opcode == 0x26u || opcode == 0x27u; + return promoteToVop3(0x100u + opcode, vdst, src0, laneSelect ? vsrc1 : 0x100u + vsrc1, 0, carryOut, code, index, programCounter); +} + +Encoded encodeVop1(std::span code, std::uint32_t index, std::uint32_t programCounter) { + const auto word = code[index]; + const auto opcode = (word >> 9u) & 0xffu; + const auto src0 = word & 0x1ffu; + if (const auto mapped = mapOpcode(GcnTables::Vop1, opcode)) { + Encoded out; + out.words[0] = replaceBits(word, 9, 8, *mapped); + out.rdnaWords = out.gcnWords = 1; + if (hasLiteral(src0)) { + out.words[1] = literalAt(code, index + 1, programCounter); + out.rdnaWords = out.gcnWords = 2; + } + return out; + } + return promoteToVop3(0x180u + opcode, (word >> 17u) & 0xffu, src0, 0, 0, false, code, index, programCounter); +} + +Encoded encodeVopc(std::span code, std::uint32_t index, std::uint32_t programCounter) { + const auto word = code[index]; + const auto opcode = (word >> 17u) & 0xffu; + if (compareWritesExec(opcode)) unsupported("VOPC v_cmpx (writes both VCC and EXEC)", opcode, programCounter); + const auto mapped = mapOpcode(GcnTables::Vopc, opcode); + if (!mapped) unsupported("VOPC", opcode, programCounter); + Encoded out; + out.words[0] = replaceBits(word, 17, 8, *mapped); + out.rdnaWords = out.gcnWords = 1; + if (hasLiteral(word & 0x1ffu)) { + out.words[1] = literalAt(code, index + 1, programCounter); + out.rdnaWords = out.gcnWords = 2; + } + return out; +} + +Encoded encodeDs(std::span code, std::uint32_t index, std::uint32_t programCounter) { + if (index + 1 >= code.size()) throw std::out_of_range("truncated GCN DS instruction"); + const auto word0 = code[index]; + const auto opcode = (word0 >> 18u) & 0xffu; + const auto mapped = mapOpcode(GcnTables::Ds, opcode); + if (!mapped) unsupported("DS", opcode, programCounter); + Encoded out; + out.words[0] = replaceBits(word0, 18, 8, *mapped); + out.words[1] = code[index + 1]; + out.rdnaWords = out.gcnWords = 2; + return out; +} + +Encoded encodeMubuf(std::span code, std::uint32_t index, std::uint32_t programCounter) { + if (index + 1 >= code.size()) throw std::out_of_range("truncated GCN MUBUF instruction"); + const auto word0 = code[index]; + const auto opcode = (word0 >> 18u) & 0x7fu; + const auto mapped = mapOpcode(GcnTables::Mubuf, opcode); + if (!mapped) unsupported("MUBUF", opcode, programCounter); + if (((word0 >> 15u) & 1u) != 0) unsupported("MUBUF addr64", opcode, programCounter); + Encoded out; + out.words[0] = replaceBits(replaceBits(word0, 18, 7, *mapped & 0x7fu), 25, 1, *mapped >> 7u); + out.words[1] = code[index + 1]; + out.rdnaWords = out.gcnWords = 2; + return out; +} + +Encoded encodeMimg(std::span code, std::uint32_t index, std::uint32_t programCounter) { + if (index + 1 >= code.size()) throw std::out_of_range("truncated GCN MIMG instruction"); + const auto word0 = code[index]; + const auto opcode = (word0 >> 18u) & 0x7fu; + const auto mapped = mapOpcode(GcnTables::Mimg, opcode); + if (!mapped) unsupported("MIMG", opcode, programCounter); + const bool array = ((word0 >> 14u) & 1u) != 0; + Encoded out; + out.words[0] = replaceBits(replaceBits(replaceBits(word0, 18, 7, *mapped & 0x7fu), 14, 1, 0), 0, 8, (*mapped >> 7u) | ((array ? 5u : 1u) << 3u)); + out.words[1] = code[index + 1] & 0x03ffffffu; + out.rdnaWords = out.gcnWords = 2; + return out; +} + +Encoded encodePassThrough(std::span code, std::uint32_t index, std::uint32_t words, const char* family) { + if (index + words > code.size()) throw std::out_of_range(std::string("truncated GCN ") + family + " instruction"); + Encoded out; + for (std::uint32_t i = 0; i < words; ++i) out.words[i] = code[index + i]; + out.rdnaWords = out.gcnWords = words; + return out; +} + +Encoded encodeGcn(std::span code, std::uint32_t index, std::uint32_t programCounter) { + const auto word = code[index]; + if ((word >> 31u) == 0) { + switch ((word >> 25u) & 0x3fu) { + case 0x3eu: return encodeVopc(code, index, programCounter); + case 0x3fu: return encodeVop1(code, index, programCounter); + default: return encodeVop2(code, index, programCounter); + } + } + if ((word >> 30u) == 2u) return encodeScalar(code, index, programCounter); + switch (word >> 26u) { + case 0x30u: case 0x31u: return encodeSmrd(code, index, programCounter); + case 0x32u: return encodePassThrough(code, index, 1, "VINTRP"); + case 0x34u: return encodeVop3(code, index, programCounter); + case 0x36u: return encodeDs(code, index, programCounter); + case 0x38u: return encodeMubuf(code, index, programCounter); + case 0x3eu: return encodePassThrough(code, index, 2, "EXP"); + case 0x37u: unsupported("FLAT", (word >> 18u) & 0x7fu, programCounter); + case 0x3au: unsupported("MTBUF", (word >> 16u) & 7u, programCounter); + case 0x3cu: return encodeMimg(code, index, programCounter); + default: break; + } + throw std::invalid_argument("unknown GCN instruction family at program counter " + std::to_string(programCounter) + " raw word " + std::to_string(word)); +} + +} + +void SetGcnGuestShaders(bool enabled) { + g_gcnGuestShaders.store(enabled, std::memory_order_release); +} + +bool GcnGuestShaders() { + return g_gcnGuestShaders.load(std::memory_order_acquire); +} + +RdnaInstruction DecodeGcnInstruction(std::uint32_t programCounter, std::span code, std::uint32_t wordIndex) { + if (wordIndex >= code.size()) throw std::out_of_range("word index is out of the code span bounds"); + const auto encoded = encodeGcn(code, wordIndex, programCounter); + auto instruction = DecodeRdnaEncodedInstruction(programCounter, std::span(encoded.words.data(), encoded.rdnaWords), 0); + instruction.wordCount = encoded.gcnWords; + return instruction; +} + +} diff --git a/core/shader/recompiler/RdnaDecoder/src/GcnOpcodeTables.hpp b/core/shader/recompiler/RdnaDecoder/src/GcnOpcodeTables.hpp new file mode 100644 index 0000000000..cbe30ec329 --- /dev/null +++ b/core/shader/recompiler/RdnaDecoder/src/GcnOpcodeTables.hpp @@ -0,0 +1,187 @@ +#ifndef CORE_SHADER_RECOMPILIER_RDNADECODER_SRC_GCNOPCODETABLES_HPP +#define CORE_SHADER_RECOMPILIER_RDNADECODER_SRC_GCNOPCODETABLES_HPP + +#include +#include +#include + +namespace ShaderRecompiler::GcnTables { + +inline constexpr std::array, 47> Sop1{{ + {0x3, 0x3}, {0x4, 0x4}, {0x5, 0x5}, {0x6, 0x6}, {0x7, 0x7}, {0x8, 0x8}, {0x9, 0x9}, {0xa, 0xa}, + {0xb, 0xb}, {0xc, 0xc}, {0xd, 0xd}, {0xe, 0xe}, {0xf, 0xf}, {0x10, 0x10}, {0x11, 0x11}, {0x12, 0x12}, + {0x13, 0x13}, {0x14, 0x14}, {0x15, 0x15}, {0x16, 0x16}, {0x17, 0x17}, {0x18, 0x18}, {0x19, 0x19}, {0x1a, 0x1a}, + {0x1b, 0x1b}, {0x1c, 0x1c}, {0x1d, 0x1d}, {0x1e, 0x1e}, {0x1f, 0x1f}, {0x20, 0x20}, {0x21, 0x21}, {0x22, 0x22}, + {0x24, 0x24}, {0x25, 0x25}, {0x26, 0x26}, {0x27, 0x27}, {0x28, 0x28}, {0x29, 0x29}, {0x2a, 0x2a}, {0x2b, 0x2b}, + {0x2c, 0x2c}, {0x2d, 0x2d}, {0x2e, 0x2e}, {0x2f, 0x2f}, {0x30, 0x30}, {0x31, 0x31}, {0x34, 0x34}, +}}; + +inline constexpr std::array, 42> Sop2{{ + {0x0, 0x0}, {0x1, 0x1}, {0x2, 0x2}, {0x3, 0x3}, {0x4, 0x4}, {0x5, 0x5}, {0x6, 0x6}, {0x7, 0x7}, + {0x8, 0x8}, {0x9, 0x9}, {0xa, 0xa}, {0xb, 0xb}, {0xe, 0xe}, {0xf, 0xf}, {0x10, 0x10}, {0x11, 0x11}, + {0x12, 0x12}, {0x13, 0x13}, {0x14, 0x14}, {0x15, 0x15}, {0x16, 0x16}, {0x17, 0x17}, {0x18, 0x18}, {0x19, 0x19}, + {0x1a, 0x1a}, {0x1b, 0x1b}, {0x1c, 0x1c}, {0x1d, 0x1d}, {0x1e, 0x1e}, {0x1f, 0x1f}, {0x20, 0x20}, {0x21, 0x21}, + {0x22, 0x22}, {0x23, 0x23}, {0x24, 0x24}, {0x25, 0x25}, {0x26, 0x26}, {0x27, 0x27}, {0x28, 0x28}, {0x29, 0x29}, + {0x2a, 0x2a}, {0x2c, 0x2c}, +}}; + +inline constexpr std::array, 16> Sopc{{ + {0x0, 0x0}, {0x1, 0x1}, {0x2, 0x2}, {0x3, 0x3}, {0x4, 0x4}, {0x5, 0x5}, {0x6, 0x6}, {0x7, 0x7}, + {0x8, 0x8}, {0x9, 0x9}, {0xa, 0xa}, {0xb, 0xb}, {0xc, 0xc}, {0xd, 0xd}, {0xe, 0xe}, {0xf, 0xf}, +}}; + +inline constexpr std::array, 6> Sopk{{ + {0x0, 0x0}, {0x2, 0x2}, {0x10, 0x10}, {0x12, 0x12}, {0x13, 0x13}, {0x15, 0x15}, +}}; + +inline constexpr std::array, 21> Sopp{{ + {0x0, 0x0}, {0x1, 0x1}, {0x2, 0x2}, {0x4, 0x4}, {0x5, 0x5}, {0x6, 0x6}, {0x7, 0x7}, {0x8, 0x8}, + {0x9, 0x9}, {0xa, 0xa}, {0xc, 0xc}, {0xd, 0xd}, {0xe, 0xe}, {0xf, 0xf}, {0x10, 0x10}, {0x11, 0x11}, + {0x12, 0x12}, {0x13, 0x13}, {0x14, 0x14}, {0x15, 0x15}, {0x16, 0x16}, +}}; + +inline constexpr std::array, 13> Smrd{{ + {0x0, 0x0}, {0x1, 0x1}, {0x2, 0x2}, {0x3, 0x3}, {0x4, 0x4}, {0x8, 0x8}, {0x9, 0x9}, {0xa, 0xa}, + {0xb, 0xb}, {0xc, 0xc}, {0x1d, 0x21}, {0x1e, 0x24}, {0x1f, 0x21}, +}}; + +inline constexpr std::array, 58> Vop1{{ + {0x0, 0x0}, {0x1, 0x1}, {0x2, 0x2}, {0x3, 0x3}, {0x4, 0x4}, {0x5, 0x5}, {0x6, 0x6}, {0x7, 0x7}, + {0x8, 0x8}, {0xa, 0xa}, {0xb, 0xb}, {0xc, 0xc}, {0xd, 0xd}, {0xe, 0xe}, {0xf, 0xf}, {0x10, 0x10}, + {0x11, 0x11}, {0x12, 0x12}, {0x13, 0x13}, {0x14, 0x14}, {0x15, 0x15}, {0x16, 0x16}, {0x17, 0x17}, {0x18, 0x18}, + {0x19, 0x19}, {0x1a, 0x1a}, {0x20, 0x20}, {0x21, 0x21}, {0x22, 0x22}, {0x23, 0x23}, {0x24, 0x24}, {0x25, 0x25}, + {0x27, 0x27}, {0x2a, 0x2a}, {0x2b, 0x2b}, {0x2e, 0x2e}, {0x2f, 0x2f}, {0x31, 0x31}, {0x33, 0x33}, {0x34, 0x34}, + {0x35, 0x35}, {0x36, 0x36}, {0x37, 0x37}, {0x38, 0x38}, {0x39, 0x39}, {0x3a, 0x3a}, {0x3b, 0x3b}, {0x3c, 0x3c}, + {0x3d, 0x3d}, {0x3e, 0x3e}, {0x3f, 0x3f}, {0x40, 0x40}, {0x41, 0x41}, {0x42, 0x42}, {0x43, 0x43}, {0x44, 0x44}, + {0x54, 0x54}, {0x55, 0x55}, +}}; + +inline constexpr std::array, 37> Vop2{{ + {0x0, 0x1}, {0x3, 0x3}, {0x4, 0x4}, {0x5, 0x5}, {0x6, 0x6}, {0x7, 0x7}, {0x8, 0x8}, {0x9, 0x9}, + {0xa, 0xa}, {0xb, 0xb}, {0xc, 0xc}, {0xf, 0xf}, {0x10, 0x10}, {0x11, 0x11}, {0x12, 0x12}, {0x13, 0x13}, + {0x14, 0x14}, {0x16, 0x16}, {0x18, 0x18}, {0x1a, 0x1a}, {0x1b, 0x1b}, {0x1c, 0x1c}, {0x1d, 0x1d}, {0x1f, 0x1f}, + {0x20, 0x20}, {0x21, 0x21}, {0x28, 0x28}, {0x29, 0x29}, {0x2a, 0x2a}, {0x2f, 0x2f}, {0x32, 0x32}, {0x33, 0x33}, + {0x34, 0x34}, {0x35, 0x35}, {0x39, 0x39}, {0x3a, 0x3a}, {0x3b, 0x3b}, +}}; + +inline constexpr std::array, 216> Vopc{{ + {0x0, 0x0}, {0x1, 0x1}, {0x2, 0x2}, {0x3, 0x3}, {0x4, 0x4}, {0x5, 0x5}, {0x6, 0x6}, {0x7, 0x7}, + {0x8, 0x8}, {0x9, 0x9}, {0xa, 0xa}, {0xb, 0xb}, {0xc, 0xc}, {0xd, 0xd}, {0xe, 0xe}, {0xf, 0xf}, + {0x10, 0x10}, {0x11, 0x11}, {0x12, 0x12}, {0x13, 0x13}, {0x14, 0x14}, {0x15, 0x15}, {0x16, 0x16}, {0x17, 0x17}, + {0x18, 0x18}, {0x19, 0x19}, {0x1a, 0x1a}, {0x1b, 0x1b}, {0x1c, 0x1c}, {0x1d, 0x1d}, {0x1e, 0x1e}, {0x1f, 0x1f}, + {0x20, 0x20}, {0x21, 0x21}, {0x22, 0x22}, {0x23, 0x23}, {0x24, 0x24}, {0x25, 0x25}, {0x26, 0x26}, {0x27, 0x27}, + {0x28, 0x28}, {0x29, 0x29}, {0x2a, 0x2a}, {0x2b, 0x2b}, {0x2c, 0x2c}, {0x2d, 0x2d}, {0x2e, 0x2e}, {0x2f, 0x2f}, + {0x30, 0x30}, {0x31, 0x31}, {0x32, 0x32}, {0x33, 0x33}, {0x34, 0x34}, {0x35, 0x35}, {0x36, 0x36}, {0x37, 0x37}, + {0x38, 0x38}, {0x39, 0x39}, {0x3a, 0x3a}, {0x3b, 0x3b}, {0x3c, 0x3c}, {0x3d, 0x3d}, {0x3e, 0x3e}, {0x3f, 0x3f}, + {0x40, 0x0}, {0x41, 0x1}, {0x42, 0x2}, {0x43, 0x3}, {0x44, 0x4}, {0x45, 0x5}, {0x46, 0x6}, {0x47, 0x7}, + {0x48, 0x8}, {0x49, 0x9}, {0x4a, 0xa}, {0x4b, 0xb}, {0x4c, 0xc}, {0x4d, 0xd}, {0x4e, 0xe}, {0x4f, 0xf}, + {0x50, 0x10}, {0x51, 0x11}, {0x52, 0x12}, {0x53, 0x13}, {0x54, 0x14}, {0x55, 0x15}, {0x56, 0x16}, {0x57, 0x17}, + {0x58, 0x18}, {0x59, 0x19}, {0x5a, 0x1a}, {0x5b, 0x1b}, {0x5c, 0x1c}, {0x5d, 0x1d}, {0x5e, 0x1e}, {0x5f, 0x1f}, + {0x60, 0x20}, {0x61, 0x21}, {0x62, 0x22}, {0x63, 0x23}, {0x64, 0x24}, {0x65, 0x25}, {0x66, 0x26}, {0x67, 0x27}, + {0x68, 0x28}, {0x69, 0x29}, {0x6a, 0x2a}, {0x6b, 0x2b}, {0x6c, 0x2c}, {0x6d, 0x2d}, {0x6e, 0x2e}, {0x6f, 0x2f}, + {0x70, 0x30}, {0x71, 0x31}, {0x72, 0x32}, {0x73, 0x33}, {0x74, 0x34}, {0x75, 0x35}, {0x76, 0x36}, {0x77, 0x37}, + {0x78, 0x38}, {0x79, 0x39}, {0x7a, 0x3a}, {0x7b, 0x3b}, {0x7c, 0x3c}, {0x7d, 0x3d}, {0x7e, 0x3e}, {0x7f, 0x3f}, + {0x80, 0x80}, {0x81, 0x81}, {0x82, 0x82}, {0x83, 0x83}, {0x84, 0x84}, {0x85, 0x85}, {0x86, 0x86}, {0x87, 0x87}, + {0x88, 0x88}, {0x89, 0x89}, {0x8a, 0x8a}, {0x8b, 0x8b}, {0x8c, 0x8c}, {0x8d, 0x8d}, {0x8e, 0x8e}, {0x90, 0x90}, + {0x91, 0x91}, {0x92, 0x92}, {0x93, 0x93}, {0x94, 0x94}, {0x95, 0x95}, {0x96, 0x96}, {0x97, 0x97}, {0x98, 0x98}, + {0xa0, 0xa0}, {0xa1, 0xa1}, {0xa2, 0xa2}, {0xa3, 0xa3}, {0xa4, 0xa4}, {0xa5, 0xa5}, {0xa6, 0xa6}, {0xa7, 0xa7}, + {0xa8, 0xa8}, {0xa9, 0xa9}, {0xaa, 0xaa}, {0xab, 0xab}, {0xac, 0xac}, {0xad, 0xad}, {0xae, 0xae}, {0xb0, 0xb0}, + {0xb1, 0xb1}, {0xb2, 0xb2}, {0xb3, 0xb3}, {0xb4, 0xb4}, {0xb5, 0xb5}, {0xb6, 0xb6}, {0xb7, 0xb7}, {0xb8, 0xb8}, + {0xc0, 0xc0}, {0xc1, 0xc1}, {0xc2, 0xc2}, {0xc3, 0xc3}, {0xc4, 0xc4}, {0xc5, 0xc5}, {0xc6, 0xc6}, {0xc7, 0xc7}, + {0xc8, 0xc8}, {0xc9, 0xc9}, {0xca, 0xca}, {0xcb, 0xcb}, {0xcc, 0xcc}, {0xcd, 0xcd}, {0xce, 0xce}, {0xcf, 0xcf}, + {0xd0, 0xd0}, {0xd1, 0xd1}, {0xd2, 0xd2}, {0xd3, 0xd3}, {0xd4, 0xd4}, {0xd5, 0xd5}, {0xd6, 0xd6}, {0xd7, 0xd7}, + {0xe0, 0xe0}, {0xe1, 0xe1}, {0xe2, 0xe2}, {0xe3, 0xe3}, {0xe4, 0xe4}, {0xe5, 0xe5}, {0xe6, 0xe6}, {0xe7, 0xe7}, + {0xf0, 0xf0}, {0xf1, 0xf1}, {0xf2, 0xf2}, {0xf3, 0xf3}, {0xf4, 0xf4}, {0xf5, 0xf5}, {0xf6, 0xf6}, {0xf7, 0xf7}, +}}; + +inline constexpr std::array, 324> Vop3{{ + {0x0, 0x0}, {0x1, 0x1}, {0x2, 0x2}, {0x3, 0x3}, {0x4, 0x4}, {0x5, 0x5}, {0x6, 0x6}, {0x7, 0x7}, + {0x8, 0x8}, {0x9, 0x9}, {0xa, 0xa}, {0xb, 0xb}, {0xc, 0xc}, {0xd, 0xd}, {0xe, 0xe}, {0x10, 0x10}, + {0x11, 0x11}, {0x12, 0x12}, {0x13, 0x13}, {0x14, 0x14}, {0x15, 0x15}, {0x16, 0x16}, {0x17, 0x17}, {0x18, 0x18}, + {0x19, 0x19}, {0x1a, 0x1a}, {0x1b, 0x1b}, {0x1c, 0x1c}, {0x1d, 0x1d}, {0x1e, 0x1e}, {0x20, 0x20}, {0x21, 0x21}, + {0x22, 0x22}, {0x23, 0x23}, {0x24, 0x24}, {0x25, 0x25}, {0x26, 0x26}, {0x27, 0x27}, {0x28, 0x28}, {0x29, 0x29}, + {0x2a, 0x2a}, {0x2b, 0x2b}, {0x2c, 0x2c}, {0x2d, 0x2d}, {0x2e, 0x2e}, {0x30, 0x30}, {0x31, 0x31}, {0x32, 0x32}, + {0x33, 0x33}, {0x34, 0x34}, {0x35, 0x35}, {0x36, 0x36}, {0x37, 0x37}, {0x38, 0x38}, {0x39, 0x39}, {0x3a, 0x3a}, + {0x3b, 0x3b}, {0x3c, 0x3c}, {0x3d, 0x3d}, {0x3e, 0x3e}, {0x80, 0x80}, {0x81, 0x81}, {0x82, 0x82}, {0x83, 0x83}, + {0x84, 0x84}, {0x85, 0x85}, {0x86, 0x86}, {0x87, 0x87}, {0x88, 0x88}, {0x89, 0x89}, {0x8a, 0x8a}, {0x8b, 0x8b}, + {0x8c, 0x8c}, {0x8d, 0x8d}, {0x8e, 0x8e}, {0x90, 0x90}, {0x91, 0x91}, {0x92, 0x92}, {0x93, 0x93}, {0x94, 0x94}, + {0x95, 0x95}, {0x96, 0x96}, {0x97, 0x97}, {0x98, 0x98}, {0xa0, 0xa0}, {0xa1, 0xa1}, {0xa2, 0xa2}, {0xa3, 0xa3}, + {0xa4, 0xa4}, {0xa5, 0xa5}, {0xa6, 0xa6}, {0xa7, 0xa7}, {0xa8, 0xa8}, {0xa9, 0xa9}, {0xaa, 0xaa}, {0xab, 0xab}, + {0xac, 0xac}, {0xad, 0xad}, {0xae, 0xae}, {0xb0, 0xb0}, {0xb1, 0xb1}, {0xb2, 0xb2}, {0xb3, 0xb3}, {0xb4, 0xb4}, + {0xb5, 0xb5}, {0xb6, 0xb6}, {0xb7, 0xb7}, {0xb8, 0xb8}, {0xc0, 0xc0}, {0xc1, 0xc1}, {0xc2, 0xc2}, {0xc3, 0xc3}, + {0xc4, 0xc4}, {0xc5, 0xc5}, {0xc6, 0xc6}, {0xc7, 0xc7}, {0xc8, 0xc8}, {0xc9, 0xc9}, {0xca, 0xca}, {0xcb, 0xcb}, + {0xcc, 0xcc}, {0xcd, 0xcd}, {0xce, 0xce}, {0xcf, 0xcf}, {0xd0, 0xd0}, {0xd1, 0xd1}, {0xd2, 0xd2}, {0xd3, 0xd3}, + {0xd4, 0xd4}, {0xd5, 0xd5}, {0xd6, 0xd6}, {0xd7, 0xd7}, {0xe0, 0xe0}, {0xe1, 0xe1}, {0xe2, 0xe2}, {0xe3, 0xe3}, + {0xe4, 0xe4}, {0xe5, 0xe5}, {0xe6, 0xe6}, {0xe7, 0xe7}, {0xf0, 0xf0}, {0xf1, 0xf1}, {0xf2, 0xf2}, {0xf3, 0xf3}, + {0xf4, 0xf4}, {0xf5, 0xf5}, {0xf6, 0xf6}, {0xf7, 0xf7}, {0x100, 0x101}, {0x101, 0x360}, {0x102, 0x361}, {0x103, 0x103}, + {0x104, 0x104}, {0x105, 0x105}, {0x106, 0x106}, {0x107, 0x107}, {0x108, 0x108}, {0x109, 0x109}, {0x10a, 0x10a}, {0x10b, 0x10b}, + {0x10c, 0x10c}, {0x10f, 0x10f}, {0x110, 0x110}, {0x111, 0x111}, {0x112, 0x112}, {0x113, 0x113}, {0x114, 0x114}, {0x116, 0x116}, + {0x118, 0x118}, {0x11a, 0x11a}, {0x11b, 0x11b}, {0x11c, 0x11c}, {0x11d, 0x11d}, {0x11e, 0x363}, {0x11f, 0x11f}, {0x120, 0x120}, + {0x121, 0x121}, {0x122, 0x364}, {0x123, 0x365}, {0x124, 0x366}, {0x125, 0x30f}, {0x126, 0x310}, {0x127, 0x319}, {0x128, 0x128}, + {0x12b, 0x362}, {0x12d, 0x368}, {0x12e, 0x369}, {0x12f, 0x12f}, {0x130, 0x36a}, {0x131, 0x36b}, {0x132, 0x132}, {0x133, 0x133}, + {0x134, 0x134}, {0x135, 0x135}, {0x139, 0x139}, {0x13a, 0x13a}, {0x13b, 0x13b}, {0x140, 0x140}, {0x141, 0x141}, {0x142, 0x142}, + {0x143, 0x143}, {0x144, 0x144}, {0x145, 0x145}, {0x146, 0x146}, {0x147, 0x147}, {0x148, 0x148}, {0x149, 0x149}, {0x14a, 0x14a}, + {0x14b, 0x14b}, {0x14c, 0x14c}, {0x14d, 0x14d}, {0x14e, 0x14e}, {0x14f, 0x14f}, {0x150, 0x150}, {0x151, 0x151}, {0x152, 0x152}, + {0x153, 0x153}, {0x154, 0x154}, {0x155, 0x155}, {0x156, 0x156}, {0x157, 0x157}, {0x158, 0x158}, {0x159, 0x159}, {0x15a, 0x15a}, + {0x15b, 0x15b}, {0x15c, 0x15c}, {0x15d, 0x15d}, {0x15e, 0x15e}, {0x15f, 0x15f}, {0x160, 0x160}, {0x164, 0x164}, {0x165, 0x165}, + {0x166, 0x166}, {0x167, 0x167}, {0x168, 0x168}, {0x169, 0x169}, {0x16a, 0x16a}, {0x16b, 0x16b}, {0x16c, 0x16c}, {0x16d, 0x16d}, + {0x16e, 0x16e}, {0x16f, 0x16f}, {0x170, 0x170}, {0x171, 0x171}, {0x172, 0x172}, {0x173, 0x173}, {0x174, 0x174}, {0x175, 0x175}, + {0x176, 0x176}, {0x177, 0x177}, {0x180, 0x180}, {0x181, 0x181}, {0x182, 0x182}, {0x183, 0x183}, {0x184, 0x184}, {0x185, 0x185}, + {0x186, 0x186}, {0x187, 0x187}, {0x188, 0x188}, {0x18a, 0x18a}, {0x18b, 0x18b}, {0x18c, 0x18c}, {0x18d, 0x18d}, {0x18e, 0x18e}, + {0x18f, 0x18f}, {0x190, 0x190}, {0x191, 0x191}, {0x192, 0x192}, {0x193, 0x193}, {0x194, 0x194}, {0x195, 0x195}, {0x196, 0x196}, + {0x197, 0x197}, {0x198, 0x198}, {0x199, 0x199}, {0x19a, 0x19a}, {0x1a0, 0x1a0}, {0x1a1, 0x1a1}, {0x1a2, 0x1a2}, {0x1a3, 0x1a3}, + {0x1a4, 0x1a4}, {0x1a5, 0x1a5}, {0x1a7, 0x1a7}, {0x1aa, 0x1aa}, {0x1ab, 0x1ab}, {0x1ae, 0x1ae}, {0x1af, 0x1af}, {0x1b1, 0x1b1}, + {0x1b3, 0x1b3}, {0x1b4, 0x1b4}, {0x1b5, 0x1b5}, {0x1b6, 0x1b6}, {0x1b7, 0x1b7}, {0x1b8, 0x1b8}, {0x1b9, 0x1b9}, {0x1ba, 0x1ba}, + {0x1bb, 0x1bb}, {0x1bc, 0x1bc}, {0x1bd, 0x1bd}, {0x1be, 0x1be}, {0x1bf, 0x1bf}, {0x1c0, 0x1c0}, {0x1c1, 0x1c1}, {0x1c2, 0x1c2}, + {0x1c3, 0x1c3}, {0x1c4, 0x1c4}, {0x1d4, 0x1d4}, {0x1d5, 0x1d5}, {0x303, 0x303}, {0x307, 0x307}, {0x308, 0x308}, {0x30d, 0x30d}, + {0x30e, 0x30e}, {0x314, 0x314}, {0x346, 0x346}, {0x347, 0x347}, {0x351, 0x351}, {0x354, 0x354}, {0x357, 0x357}, {0x35e, 0x35e}, + {0x36d, 0x36d}, {0x36f, 0x36f}, {0x371, 0x371}, {0x372, 0x372}, +}}; + +inline constexpr std::array, 56> Mubuf{{ + {0x0, 0x0}, {0x1, 0x1}, {0x2, 0x2}, {0x3, 0x3}, {0x4, 0x4}, {0x5, 0x5}, {0x6, 0x6}, {0x7, 0x7}, + {0x8, 0x8}, {0x9, 0x9}, {0xa, 0xa}, {0xb, 0xb}, {0xc, 0xc}, {0xd, 0xd}, {0xe, 0xe}, {0xf, 0xf}, + {0x18, 0x18}, {0x1a, 0x1a}, {0x1c, 0x1c}, {0x1d, 0x1d}, {0x1e, 0x1e}, {0x1f, 0x1f}, {0x30, 0x30}, {0x31, 0x31}, + {0x32, 0x32}, {0x33, 0x33}, {0x35, 0x35}, {0x36, 0x36}, {0x37, 0x37}, {0x38, 0x38}, {0x39, 0x39}, {0x3a, 0x3a}, + {0x3b, 0x3b}, {0x3c, 0x3c}, {0x3d, 0x3d}, {0x3e, 0x3e}, {0x3f, 0x3f}, {0x40, 0x40}, {0x50, 0x50}, {0x51, 0x51}, + {0x52, 0x52}, {0x53, 0x53}, {0x55, 0x55}, {0x56, 0x56}, {0x57, 0x57}, {0x58, 0x58}, {0x59, 0x59}, {0x5a, 0x5a}, + {0x5b, 0x5b}, {0x5c, 0x5c}, {0x5d, 0x5d}, {0x5e, 0x5e}, {0x5f, 0x5f}, {0x60, 0x60}, {0x70, 0x71}, {0x71, 0x71}, +}}; + +inline constexpr std::array, 124> Ds{{ + {0x0, 0x0}, {0x1, 0x1}, {0x2, 0x2}, {0x3, 0x3}, {0x4, 0x4}, {0x5, 0x5}, {0x6, 0x6}, {0x7, 0x7}, + {0x8, 0x8}, {0x9, 0x9}, {0xa, 0xa}, {0xb, 0xb}, {0xc, 0xc}, {0xd, 0xd}, {0xe, 0xe}, {0xf, 0xf}, + {0x10, 0x10}, {0x11, 0x11}, {0x12, 0x12}, {0x13, 0x13}, {0x14, 0x14}, {0x18, 0x18}, {0x19, 0x19}, {0x1a, 0x1a}, + {0x1b, 0x1b}, {0x1c, 0x1c}, {0x1d, 0x1d}, {0x1e, 0x1e}, {0x1f, 0x1f}, {0x20, 0x20}, {0x21, 0x21}, {0x22, 0x22}, + {0x23, 0x23}, {0x24, 0x24}, {0x25, 0x25}, {0x26, 0x26}, {0x27, 0x27}, {0x28, 0x28}, {0x29, 0x29}, {0x2a, 0x2a}, + {0x2b, 0x2b}, {0x2c, 0x2c}, {0x2d, 0x2d}, {0x2e, 0x2e}, {0x2f, 0x2f}, {0x30, 0x30}, {0x31, 0x31}, {0x32, 0x32}, + {0x33, 0x33}, {0x34, 0x34}, {0x35, 0x35}, {0x36, 0x36}, {0x37, 0x37}, {0x38, 0x38}, {0x39, 0x39}, {0x3a, 0x3a}, + {0x3b, 0x3b}, {0x3c, 0x3c}, {0x3d, 0x3d}, {0x3e, 0x3e}, {0x3f, 0x3f}, {0x40, 0x40}, {0x41, 0x41}, {0x42, 0x42}, + {0x43, 0x43}, {0x44, 0x44}, {0x45, 0x45}, {0x46, 0x46}, {0x47, 0x47}, {0x48, 0x48}, {0x49, 0x49}, {0x4a, 0x4a}, + {0x4b, 0x4b}, {0x4c, 0x4c}, {0x4d, 0x4d}, {0x4e, 0x4e}, {0x4f, 0x4f}, {0x50, 0x50}, {0x51, 0x51}, {0x52, 0x52}, + {0x53, 0x53}, {0x60, 0x60}, {0x61, 0x61}, {0x62, 0x62}, {0x63, 0x63}, {0x64, 0x64}, {0x65, 0x65}, {0x66, 0x66}, + {0x67, 0x67}, {0x68, 0x68}, {0x69, 0x69}, {0x6a, 0x6a}, {0x6b, 0x6b}, {0x6c, 0x6c}, {0x6d, 0x6d}, {0x6e, 0x6e}, + {0x6f, 0x6f}, {0x70, 0x70}, {0x71, 0x71}, {0x72, 0x72}, {0x73, 0x73}, {0x76, 0x76}, {0x77, 0x77}, {0x78, 0x78}, + {0x7e, 0x7e}, {0x80, 0x80}, {0x81, 0x81}, {0x82, 0x82}, {0x83, 0x83}, {0x84, 0x84}, {0x85, 0x85}, {0x86, 0x86}, + {0x87, 0x87}, {0x88, 0x88}, {0x89, 0x89}, {0x8a, 0x8a}, {0x8b, 0x8b}, {0x8d, 0x8d}, {0x92, 0x92}, {0x93, 0x93}, + {0xde, 0xde}, {0xdf, 0xdf}, {0xfe, 0xfe}, {0xff, 0xff}, +}}; + +inline constexpr std::array, 92> Mimg{{ + {0x0, 0x0}, {0x1, 0x1}, {0x2, 0x2}, {0x3, 0x3}, {0x4, 0x4}, {0x5, 0x5}, {0x8, 0x8}, {0x9, 0x9}, + {0xa, 0xa}, {0xb, 0xb}, {0xe, 0xe}, {0xf, 0xf}, {0x10, 0x10}, {0x11, 0x11}, {0x12, 0x12}, {0x14, 0x14}, + {0x15, 0x15}, {0x16, 0x16}, {0x17, 0x17}, {0x18, 0x18}, {0x19, 0x19}, {0x1a, 0x1a}, {0x1b, 0x1b}, {0x1c, 0x1c}, + {0x1d, 0x1d}, {0x1e, 0x1e}, {0x1f, 0x1f}, {0x20, 0x20}, {0x21, 0x21}, {0x22, 0x22}, {0x23, 0x23}, {0x24, 0x24}, + {0x25, 0x25}, {0x26, 0x26}, {0x27, 0x27}, {0x28, 0x28}, {0x29, 0x29}, {0x2a, 0x2a}, {0x2b, 0x2b}, {0x2c, 0x2c}, + {0x2d, 0x2d}, {0x2e, 0x2e}, {0x2f, 0x2f}, {0x30, 0x30}, {0x31, 0x31}, {0x32, 0x32}, {0x33, 0x33}, {0x34, 0x34}, + {0x35, 0x35}, {0x36, 0x36}, {0x37, 0x37}, {0x38, 0x38}, {0x39, 0x39}, {0x3a, 0x3a}, {0x3b, 0x3b}, {0x3c, 0x3c}, + {0x3d, 0x3d}, {0x3e, 0x3e}, {0x3f, 0x3f}, {0x40, 0x40}, {0x41, 0x41}, {0x44, 0x44}, {0x45, 0x45}, {0x46, 0x46}, + {0x47, 0x47}, {0x48, 0x48}, {0x49, 0x49}, {0x4c, 0x4c}, {0x4d, 0x4d}, {0x4e, 0x4e}, {0x4f, 0x4f}, {0x50, 0x50}, + {0x51, 0x51}, {0x54, 0x54}, {0x55, 0x55}, {0x56, 0x56}, {0x57, 0x57}, {0x58, 0x58}, {0x59, 0x59}, {0x5c, 0x5c}, + {0x5d, 0x5d}, {0x5e, 0x5e}, {0x5f, 0x5f}, {0x60, 0x60}, {0x68, 0x68}, {0x69, 0x69}, {0x6a, 0x6a}, {0x6b, 0x6b}, + {0x6c, 0x6c}, {0x6d, 0x6d}, {0x6e, 0x6e}, {0x6f, 0x6f}, +}}; + +} + +#endif diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp index c3d1a68701..f28258ca1e 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp @@ -1,6 +1,7 @@ #include "RdnaDecoder/RdnaDescriptorFormat.hpp" #include #include +#include namespace ShaderRecompiler { @@ -139,6 +140,76 @@ bool IsDepthBitsTexture(std::uint32_t word1, std::uint32_t word3) { return DepthBitsTextureWidth(word1, word3) != 0u; } +const std::array& GcnBufferFormats() { + static const auto table = [] { + std::array formats{}; + struct Family { std::uint32_t data; std::uint32_t first; bool normalized; bool floating; }; + constexpr std::array normalizedFamilies{{ + {1u, 1u, true, false}, {2u, 7u, true, true}, {3u, 14u, true, false}, {5u, 23u, true, true}, {6u, 30u, true, true}, + {7u, 37u, true, true}, {8u, 44u, true, false}, {9u, 50u, true, false}, {10u, 56u, true, false}, {12u, 65u, true, true}, + }}; + for (const auto& family : normalizedFamilies) { + for (std::uint32_t number = 0; number < 6u; ++number) formats[(family.data << 3u) | number] = static_cast(family.first + number); + if (family.floating) formats[(family.data << 3u) | 7u] = static_cast(family.first + 6u); + } + constexpr std::array, 4> wideFamilies{{{4u, 20u}, {11u, 62u}, {13u, 72u}, {14u, 75u}}}; + for (const auto& [data, first] : wideFamilies) { + formats[(data << 3u) | 4u] = static_cast(first); + formats[(data << 3u) | 5u] = static_cast(first + 1u); + formats[(data << 3u) | 7u] = static_cast(first + 2u); + } + return formats; + }(); + return table; +} + +std::uint32_t GcnImageFormat(std::uint32_t dataFormat, std::uint32_t numberFormat) { + constexpr std::uint32_t Unorm = 0u, Snorm = 1u, Float = 7u, Srgb = 9u; + if (dataFormat < 16u && numberFormat < 8u) return GcnBufferFormats()[(dataFormat << 3u) | numberFormat]; + switch (dataFormat) { + case 1u: return numberFormat == Srgb ? 128u : 0u; + case 3u: return numberFormat == Srgb ? 129u : 0u; + case 10u: return numberFormat == Srgb ? 130u : 0u; + case 16u: return numberFormat == Unorm ? 133u : 0u; + case 17u: return numberFormat == Unorm ? 134u : 0u; + case 18u: return numberFormat == Unorm ? 135u : 0u; + case 19u: return numberFormat == Unorm ? 136u : 0u; + case 34u: return numberFormat == Float ? 132u : 0u; + case 35u: return numberFormat == Unorm ? 169u : numberFormat == Srgb ? 170u : 0u; + case 36u: return numberFormat == Unorm ? 171u : numberFormat == Srgb ? 172u : 0u; + case 37u: return numberFormat == Unorm ? 173u : numberFormat == Srgb ? 174u : 0u; + case 38u: return numberFormat == Unorm ? 175u : numberFormat == Snorm ? 176u : 0u; + case 39u: return numberFormat == Unorm ? 177u : numberFormat == Snorm ? 178u : 0u; + case 40u: return numberFormat == Unorm ? 179u : numberFormat == Snorm ? 180u : 0u; + case 41u: return numberFormat == Unorm ? 181u : numberFormat == Srgb ? 182u : 0u; + default: return 0u; + } +} + +std::array GcnImageWords(std::span gcn) { + const auto format = GcnImageFormat((gcn[1] >> 20u) & 0x3fu, (gcn[1] >> 26u) & 0xfu); + const auto width = gcn[2] & 0x3fffu; + const auto height = (gcn[2] >> 14u) & 0x3fffu; + const auto type = gcn[3] >> 28u; + const auto lastLevel = (gcn[3] >> 16u) & 0xfu; + const bool layered = type == 11u || type == 12u || type == 13u || type == 15u; + std::array words{}; + words[0] = gcn[0]; + words[1] = (gcn[1] & 0x000fffffu) | (format << 20u) | ((width & 3u) << 30u); + words[2] = (width >> 2u) | (height << 14u); + words[3] = (gcn[3] & 0x000fffffu) | (type << 28u); + words[4] = (layered ? (gcn[5] >> 13u) & 0x1fffu : gcn[4] & 0x1fffu) | ((gcn[5] & 0x1fffu) << 16u); + words[5] = lastLevel << 4u; + words[7] = ((gcn[3] >> 20u) & 0x1fu) | (((gcn[3] >> 25u) & 1u) << 5u) | (((gcn[4] >> 13u) & 0x3fffu) << 6u); + return words; +} + +std::uint32_t GcnBufferWord3(std::uint32_t word1, std::uint32_t word3) { + const std::uint32_t format = GcnBufferFormats()[(word3 >> 12u) & 0x7fu]; + const std::uint32_t outOfBounds = ((word1 >> 16u) & 0x3fffu) != 0u ? 1u : 3u; + return (word3 & 0xc0e00fffu) | (format << 12u) | (outOfBounds << 28u); +} + IrTextureNumericClass VertexInputNumericClass(IrBufferFormat format) { switch (format) { case IrBufferFormat::Format8UInt: diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaInstructionDecoder.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaInstructionDecoder.cpp index abdb151c23..29cdbcc01b 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaInstructionDecoder.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaInstructionDecoder.cpp @@ -90,6 +90,11 @@ RdnaInstructionFamily GetRdnaInstructionFamily(std::uint32_t word) { } RdnaInstruction DecodeRdnaInstruction(std::uint32_t programCounter, std::span code, std::uint32_t wordIndex) { + if (GcnGuestShaders()) return DecodeGcnInstruction(programCounter, code, wordIndex); + return DecodeRdnaEncodedInstruction(programCounter, code, wordIndex); +} + +RdnaInstruction DecodeRdnaEncodedInstruction(std::uint32_t programCounter, std::span code, std::uint32_t wordIndex) { if (wordIndex >= code.size()) { throw std::out_of_range("word index is out of the code span bounds"); } diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index ad94823d64..dae9b85256 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -231,6 +231,7 @@ IrProgram PrepareResourceProgram(const RecompileRequest& request, ShaderPreparat translateOptions.shaderHash = request.shader.codeAddress; translateOptions.waveSize = request.context.waveSize; translateOptions.userDataBaseRegister = request.context.userDataBaseRegister; + translateOptions.gcnLegacyVertexInputs = GcnGuestShaders() && request.context.userDataBaseRegister == 0u; translateOptions.userDataCount = static_cast(request.context.userData.size()); translateOptions.scratchDwords = request.context.compute.has_value() ? request.context.compute->scratchDwords : 0u; translateOptions.sharedMemoryBytes = DeviceMemoryLdsBytes(request); diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 127ed88d5b..23c4658d37 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -377,6 +377,7 @@ struct FragmentParameter { bool custom = false; bool flatP10 = false; bool vertexOrder = false; + bool flatP20 = false; bool operator==(const FragmentParameter&) const = default; }; diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index bcda9f0c40..2362ce871a 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -1,4 +1,5 @@ #include "ShaderDiskCache.hpp" +#include "RdnaDecoder/RdnaInstructionDecoder.hpp" #include "CacheKey.hpp" #include "ShaderCachePack.hpp" #include "ShaderCacheDirectory.hpp" @@ -503,6 +504,7 @@ void encodeArtifact(Writer& writer, const CompiledShaderArtifact& result) { out.Value(parameter.custom); out.Value(parameter.flatP10); out.Value(parameter.vertexOrder); + out.Value(parameter.flatP20); }); writer.Value(result.barycentricEmulation.active); writer.Value(result.barycentricEmulation.smooth); @@ -561,6 +563,7 @@ void decodeArtifact(Reader& reader, CompiledShaderArtifact& result) { in.Value(parameter.custom); in.Value(parameter.flatP10); in.Value(parameter.vertexOrder); + in.Value(parameter.flatP20); }); reader.Value(result.barycentricEmulation.active); reader.Value(result.barycentricEmulation.smooth); @@ -1780,6 +1783,7 @@ void BuildKey(const RecompileRequest& request, std::uint32_t hostSubgroupSize, s writer.Value(FileMagic); writer.Value(FormatVersion); writer.Value(SourceVersion()); + writer.Value(static_cast(GcnGuestShaders())); thread_local std::vector* memoryKeySlot = nullptr; auto& memoryKey = ThreadOwned(memoryKeySlot); RecompileCacheKey::Build(request, memoryKey); @@ -1800,6 +1804,7 @@ void BuildPlanKey(const RecompileRequest& request, std::uint32_t hostSubgroupSiz writer.Value(FileMagic); writer.Value(FormatVersion); writer.Value(SourceVersion()); + writer.Value(static_cast(GcnGuestShaders())); writer.Value(PlanEntryKind); thread_local std::vector* memoryKeySlot = nullptr; auto& memoryKey = ThreadOwned(memoryKeySlot); diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index dc3a268ee3..92ea828d77 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -187,6 +187,7 @@ struct SpirvEmitterState { std::uint32_t loopGuardVisits = 0; std::uint32_t loopGuardPc = 0; std::uint32_t srgbTableVariable = 0; + std::uint32_t gcnBufferFormatVariable = 0; std::uint32_t gdsVariable = 0; std::uint32_t gdsLength = 0; std::uint32_t pushConstantVariable = 0; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp index d609c27286..0f4f8ac116 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -322,74 +323,126 @@ public: explicit EntryLaneWalk(IrShaderStage stage) : everyInvocationEnters(EveryInvocationEnters(stage)) {} bool ZeroBit(const IrValue* bit) { - bit = Resolved(bit); - if (bit == nullptr || !Spend()) return false; - if (bit->HasImmediate()) return bit->Type() == IrType::Bool && !bit->ImmediateBool(); - if (Is(bit, IrOpcode::LogicalAnd)) return HelperOrFalseConjunct(bit) || ZeroBit(bit->Argument(0)) || ZeroBit(bit->Argument(1)); - if (Is(bit, IrOpcode::SelectU1)) return ZeroBit(bit->Argument(1)) && ZeroBit(bit->Argument(2)); - if (Is(bit, IrOpcode::Phi)) return Incoming(bit, [this](const IrValue* incoming) { return ZeroBit(incoming); }); - if (Is(bit, IrOpcode::IEqual32)) return HelperTest(bit); - const auto words = LaneBitWords(bit); - return words && Word((*words)[0], 0u) && Word((*words)[1], 1u); + return Prove(bit, Proof::ZeroBit, [this](const IrValue* value) { + if (value->HasImmediate()) return value->Type() == IrType::Bool && !value->ImmediateBool(); + if (Is(value, IrOpcode::LogicalAnd)) return HelperOrFalseConjunct(value) || ZeroBit(value->Argument(0)) || ZeroBit(value->Argument(1)); + if (Is(value, IrOpcode::SelectU1)) return ZeroBit(value->Argument(1)) && ZeroBit(value->Argument(2)); + if (Is(value, IrOpcode::Phi)) return Incoming(value, [this](const IrValue* incoming) { return ZeroBit(incoming); }); + if (Is(value, IrOpcode::IEqual32)) return HelperTest(value); + const auto words = LaneBitWords(value); + return words && Word((*words)[0], 0u) && Word((*words)[1], 1u); + }); } private: + enum class Proof : std::uint8_t { ZeroBit, NotHelper, LowWord, HighWord, LowWord64, HighWord64 }; + using Key = std::pair; + struct Assumption { + std::size_t depth; + std::uint64_t order; + }; + static constexpr std::size_t Unbounded = static_cast(-1); + bool Spend() { if (budget == 0u) return false; --budget; return true; } - bool NotHelper(const IrValue* predicate) { - predicate = Resolved(predicate); - if (predicate == nullptr || !Spend()) return false; - if (predicate->HasImmediate()) return everyInvocationEnters && predicate->Type() == IrType::Bool && predicate->ImmediateBool(); - switch (predicate->Opcode()) { - case IrOpcode::LogicalAnd: return NotHelper(predicate->Argument(0)) || NotHelper(predicate->Argument(1)); - case IrOpcode::IEqual32: return HelperTest(predicate); - default: return ZeroBit(predicate); + template + bool Prove(const IrValue* value, Proof proof, TCheck&& check) { + value = Resolved(value); + if (value == nullptr) return false; + const Key key{value, proof}; + if (const auto found = settled.find(key); found != settled.end()) return found->second; + if (const auto found = active.find(key); found != active.end()) return Assume(found->second); + if (const auto found = assumed.find(key); found != assumed.end()) return Assume(found->second.depth); + if (!Spend()) return false; + const auto outer = dependency; + const auto depth = active.size(); + const auto mark = created; + dependency = Unbounded; + active.emplace(key, depth); + const bool result = check(value); + active.erase(key); + const auto inner = dependency; + const bool holds = result && inner >= depth; + for (auto entry = assumed.begin(); entry != assumed.end();) { + if (entry->second.order < mark) { + ++entry; + continue; + } + if (result && !holds) { + entry->second.depth = std::min(entry->second.depth, inner); + ++entry; + continue; + } + if (holds) settled.emplace(entry->first, true); + entry = assumed.erase(entry); } + if (result && !holds) assumed.emplace(key, Assumption{inner, created++}); + else settled.emplace(key, result); + dependency = result && !holds ? std::min(outer, inner) : outer; + return result; + } + + bool Assume(std::size_t depth) { + dependency = std::min(dependency, depth); + return true; + } + + bool NotHelper(const IrValue* predicate) { + return Prove(predicate, Proof::NotHelper, [this](const IrValue* value) { + if (value->HasImmediate()) return everyInvocationEnters && value->Type() == IrType::Bool && value->ImmediateBool(); + switch (value->Opcode()) { + case IrOpcode::LogicalAnd: return NotHelper(value->Argument(0)) || NotHelper(value->Argument(1)); + case IrOpcode::IEqual32: return HelperTest(value); + default: return ZeroBit(value); + } + }); } bool Word(const IrValue* word, std::uint32_t half) { - word = Resolved(word); - if (word == nullptr || !Spend()) return false; - if (word->HasImmediate()) return IsU32(word, 0u); - switch (word->Opcode()) { - case IrOpcode::CompositeExtractU32x4: { - const auto* ballot = Resolved(word->Argument(0)); - return IsU32(word->Argument(1), half) && Is(ballot, IrOpcode::Ballot) && NotHelper(ballot->Argument(0)); + return Prove(word, half == 0u ? Proof::LowWord : Proof::HighWord, [this, half](const IrValue* value) { + if (value->HasImmediate()) return IsU32(value, 0u); + switch (value->Opcode()) { + case IrOpcode::CompositeExtractU32x4: { + const auto* ballot = Resolved(value->Argument(0)); + return IsU32(value->Argument(1), half) && Is(ballot, IrOpcode::Ballot) && NotHelper(ballot->Argument(0)); + } + case IrOpcode::CompositeExtractU64: return IsU32(value->Argument(1), half) && Word64(value->Argument(0), half); + case IrOpcode::BitwiseAnd32: return Word(value->Argument(0), half) || Word(value->Argument(1), half); + case IrOpcode::BitwiseOr32: return Word(value->Argument(0), half) && Word(value->Argument(1), half); + case IrOpcode::Phi: return Incoming(value, [this, half](const IrValue* incoming) { return Word(incoming, half); }); + default: return false; } - case IrOpcode::CompositeExtractU64: return IsU32(word->Argument(1), half) && Word64(word->Argument(0), half); - case IrOpcode::BitwiseAnd32: return Word(word->Argument(0), half) || Word(word->Argument(1), half); - case IrOpcode::BitwiseOr32: return Word(word->Argument(0), half) && Word(word->Argument(1), half); - case IrOpcode::Phi: return Incoming(word, [this, half](const IrValue* incoming) { return Word(incoming, half); }); - default: return false; - } + }); } bool Word64(const IrValue* mask, std::uint32_t half) { - mask = Resolved(mask); - if (mask == nullptr || !Spend()) return false; - if (mask->HasImmediate()) return mask->Type() == IrType::U64 && static_cast(mask->ImmediateU64() >> (32u * half)) == 0u; - switch (mask->Opcode()) { - case IrOpcode::CompositeConstructU64: return Word(mask->Argument(half), half); - case IrOpcode::Phi: return Incoming(mask, [this, half](const IrValue* incoming) { return Word64(incoming, half); }); - default: return false; - } + return Prove(mask, half == 0u ? Proof::LowWord64 : Proof::HighWord64, [this, half](const IrValue* value) { + if (value->HasImmediate()) return value->Type() == IrType::U64 && static_cast(value->ImmediateU64() >> (32u * half)) == 0u; + switch (value->Opcode()) { + case IrOpcode::CompositeConstructU64: return Word(value->Argument(half), half); + case IrOpcode::Phi: return Incoming(value, [this, half](const IrValue* incoming) { return Word64(incoming, half); }); + default: return false; + } + }); } template - bool Incoming(const IrValue* phi, TCheck&& check) { - if (!phis.insert(phi).second) return true; + static bool Incoming(const IrValue* phi, TCheck&& check) { bool result = phi->ArgumentCount() != 0u; for (std::size_t index = 0; result && index < phi->ArgumentCount(); ++index) result = check(phi->Argument(index)); - phis.erase(phi); return result; } bool everyInvocationEnters; - std::unordered_set phis; + std::map active; + std::map assumed; + std::map settled; + std::size_t dependency = Unbounded; + std::uint64_t created = 0u; std::uint32_t budget = 2048u; }; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index 772f4f8f4c..de08a9338f 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -246,6 +246,10 @@ bool VolumeSliceAddress(const ImageEmitAccess& access) { return access.mem.imageDimension == RdnaImageDimension::Dim2DArray && access.image.dimension == RdnaImageDimension::Dim3D; } +bool ArraySliceAddress(const ImageEmitAccess& access) { + return access.mem.imageDimension == RdnaImageDimension::Dim3D && access.image.dimension == RdnaImageDimension::Dim2DArray; +} + std::uint32_t VolumeSliceInRange(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, std::uint32_t image, std::optional lod) { auto& state = ctx.state; state.module.EmitCapability(spv::CapabilityImageQuery); @@ -1073,8 +1077,8 @@ void EmitWriteOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access) { auto& state = ctx.state; const bool uintImage = access.image.numericClass == IrTextureNumericClass::Uint; auto condition = ctx.Arg(access.inst, 3); - if (VolumeSliceAddress(access)) { - if (access.image.mipMode == ImageMipMode::DynamicStorage) ctx.Fail(access.inst, "stores a 2D-array address to a 3D image with dynamic mips, which is not implemented"); + if (VolumeSliceAddress(access) || ArraySliceAddress(access)) { + if (access.image.mipMode == ImageMipMode::DynamicStorage) ctx.Fail(access.inst, "stores a 2D-array address to a 3D image or a 3D address to a 2D-array image with dynamic mips, which is not implemented"); const auto image = state.module.AllocateId(); state.module.AddFunction(spv::OpLoad, ImageType(state, access.image), image, StorageImageDescriptorPointer(state, access.mem.resource)); condition = Binary(state, spv::OpLogicalAnd, TypeBool(state), condition, VolumeSliceInRange(ctx, access, image, std::nullopt)); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvInputOutput.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvInputOutput.cpp index 4c05ffe094..645d3d1677 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvInputOutput.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvInputOutput.cpp @@ -49,11 +49,15 @@ namespace ShaderRecompiler const auto& pixel = *inputInfo.pixel; std::vector result; std::array readsP10{}; + std::array readsP20{}; for (const auto& block : program.Blocks()) { for (const auto* instruction : block->Instructions()) { - if (instruction->Opcode() != IrOpcode::GetInterpolationParameter || instruction->Argument(2)->Resolve()->ImmediateU32() != 0u) continue; + if (instruction->Opcode() != IrOpcode::GetInterpolationParameter) continue; + const auto mode = instruction->Argument(2)->Resolve()->ImmediateU32(); const auto attribute = instruction->Argument(0)->Resolve()->ImmediateU32(); - if (attribute < readsP10.size()) readsP10[attribute] = true; + if (attribute >= readsP10.size()) continue; + readsP10[attribute] = readsP10[attribute] || mode == 0u; + readsP20[attribute] = readsP20[attribute] || mode == 1u; } } for (const auto& input : program.Info().inputs) { @@ -61,14 +65,16 @@ namespace ShaderRecompiler const auto location = PixelInputLocation(pixel, input.location); const bool flat = PixelInputIsFlat(pixel, input.location); const bool flatP10 = flat && !input.perVertex && input.location < readsP10.size() && readsP10[input.location]; + const bool flatP20 = flat && !input.perVertex && input.location < readsP20.size() && readsP20[input.location]; const bool vertexOrder = input.perVertex && (input.location >= 32u || (program.Metadata().pixelVertexOrderInputs >> input.location & 1u) != 0u); const auto existing = std::find_if(result.begin(), result.end(), [&](const auto& parameter) { return parameter.location == location; }); if (existing != result.end()) { existing->flatP10 = existing->flatP10 || flatP10; + existing->flatP20 = existing->flatP20 || flatP20; existing->vertexOrder = existing->vertexOrder || vertexOrder; continue; } - result.push_back({location, location, flat, input.perVertex, PixelInputIsCustom(pixel, input.location), flatP10, vertexOrder}); + result.push_back({location, location, flat, input.perVertex, PixelInputIsCustom(pixel, input.location), flatP10, vertexOrder, flatP20}); } return result; } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index 4a8d6a526c..c82ab5fe76 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -840,6 +840,20 @@ std::uint32_t TessellationPointer(SpirvValueEmitContext& ctx, const IrValue& ins state.module.AddFunction(spv::OpAccessChain, type, pointer, variable, vertex, ConstantU32(state, constant % stride / 16u), ConstantU32(state, constant % stride % 16u / 4u)); return pointer; } + if (inst.Argument(1)->HasImmediate()) { + const auto value = inst.Argument(1)->ImmediateU32(); + const auto offset = patch ? value : value % stride; + const auto attribute = ConstantU32(state, offset >> 4u); + const auto component = ConstantU32(state, (offset >> 2u) & 3u); + if (kind == TessellationAttribute::LocalOutput || patch) { + state.module.AddFunction(spv::OpAccessChain, type, pointer, variable, attribute, component); + return pointer; + } + const bool own = kind == TessellationAttribute::ControlOutput || (kind == TessellationAttribute::ControlShared && !sharedRead); + const auto controlPoint = kind == TessellationAttribute::ControlInput ? std::min(value / stride, state.inputInfo.vertex->tess.inputControlPoints - 1u) : kind == TessellationAttribute::EvaluationInput ? std::min(value / stride, state.inputInfo.vertex->tess.outputControlPoints - 1u) : value / stride; + state.module.AddFunction(spv::OpAccessChain, type, pointer, variable, own ? EmitLaneId(state) : ConstantU32(state, controlPoint), attribute, component); + return pointer; + } const auto offset = patch ? address : EmitBinaryU32(state, spv::OpUMod, address, ConstantU32(state, stride)); const auto attribute = EmitBinaryU32(state, spv::OpShiftRightLogical, offset, ConstantU32(state, 4u)); const auto component = EmitBinaryU32(state, spv::OpBitwiseAnd, EmitBinaryU32(state, spv::OpShiftRightLogical, offset, ConstantU32(state, 2u)), ConstantU32(state, 3u)); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp index aa7dc2bbe8..b71a38a1c1 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp @@ -5,8 +5,11 @@ #include "SpirvBackend/SpirvMemory/SpirvSubgroup.hpp" #include "SpirvBackend/SpirvMemory/SpirvInputOutput.hpp" #include "PipelineSpecialization.hpp" +#include "RdnaDecoder/RdnaDescriptorFormat.hpp" +#include "RdnaDecoder/RdnaInstructionDecoder.hpp" #include #include +#include #include #include #include @@ -47,6 +50,23 @@ std::uint32_t nonzero(SpirvEmitterState& state, std::uint32_t value) { return Binary(state, spv::OpINotEqual, TypeBool(state), value, ConstantU32(state, 0u)); } +std::uint32_t gcnWord3(SpirvEmitterState& state, std::uint32_t word1, std::uint32_t word3) { + const auto u32 = TypeU32(state); + const auto& formats = GcnBufferFormats(); + std::array words{}; + for (std::size_t index = 0; index < words.size(); ++index) words[index] = ConstantU32(state, formats[index]); + const auto arrayType = state.module.Type(spv::OpTypeArray, u32, ConstantU32(state, static_cast(words.size()))); + if (state.gcnBufferFormatVariable == 0) state.gcnBufferFormatVariable = state.module.DefineGlobalVariable(TypePointer(state, spv::StorageClassPrivate, arrayType), spv::StorageClassPrivate); + state.module.AddFunction(spv::OpStore, state.gcnBufferFormatVariable, state.module.Constant(spv::OpConstantComposite, arrayType, std::span(words))); + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassPrivate, u32), pointer, state.gcnBufferFormatVariable, field(state, word3, 12u, 7u)); + const auto format = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, u32, format, pointer); + const auto outOfBounds = Select(state, u32, nonzero(state, field(state, word1, 16u, 14u)), ConstantU32(state, 1u << 28u), ConstantU32(state, 3u << 28u)); + const auto kept = Binary(state, spv::OpBitwiseAnd, u32, word3, ConstantU32(state, 0xc0e00fffu)); + return Binary(state, spv::OpBitwiseOr, u32, Binary(state, spv::OpBitwiseOr, u32, kept, Binary(state, spv::OpShiftLeftLogical, u32, format, ConstantU32(state, 12u))), outOfBounds); +} + void faultIf(SpirvValueEmitContext& context, const IrValue& instruction, std::uint32_t condition, std::uint32_t address, std::uint32_t bytes, BdaAbi::FaultReason reason) { auto& state = context.state; EmitIfCondition(state, condition, [&] { RecordBdaFault(state, address, ConstantU32(state, bytes), BdaInstructionPc(state, instruction), reason); }); @@ -60,14 +80,16 @@ RuntimeDescriptor descriptor(SpirvValueEmitContext& context, const IrValue& inst const auto& memory = context.Memory(instruction); const bool runtime = memory.gpuDescriptor || state.registrationModules; const auto first = PipelineSpecialization::BufferBase + memory.resource * PipelineSpecialization::BufferWords; - const auto word1 = runtime ? context.Arg(*handle, 1u) : state.module.SpecializationConstant(TypeU32(state), first, 0u); + auto word1 = runtime ? context.Arg(*handle, 1u) : state.module.SpecializationConstant(TypeU32(state), first, 0u); + if (runtime && GcnGuestShaders()) word1 = Binary(state, spv::OpBitwiseAnd, TypeU32(state), word1, ConstantU32(state, GcnBufferWord1Mask)); const bool runtimeFormat = memory.gpuDescriptor || (state.registrationModules && !state.program.Info().buffers.at(memory.resource).descriptorFormatted); - const auto word3 = runtimeFormat ? context.Arg(*handle, 3u) : state.module.SpecializationConstant(TypeU32(state), first + 1u, 0u); + auto word3 = runtimeFormat ? context.Arg(*handle, 3u) : state.module.SpecializationConstant(TypeU32(state), first + 1u, 0u); + if (runtimeFormat && GcnGuestShaders()) word3 = gcnWord3(state, word1, word3); const auto u32 = TypeU32(state); auto base = ConstantU32(state, 0u); if (memory.gpuDescriptor || BufferAccessOf(instruction.Opcode()) == BufferAccess::Atomic) { const auto u64 = TypeScalarU64(state); - const auto high = Binary(state, spv::OpShiftLeftLogical, u64, Unary(state, spv::OpUConvert, u64, field(state, context.Arg(*handle, 1u), 0u, 16u)), BdaConstant(state, 32u)); + const auto high = Binary(state, spv::OpShiftLeftLogical, u64, Unary(state, spv::OpUConvert, u64, field(state, GcnGuestShaders() ? Binary(state, spv::OpBitwiseAnd, u32, context.Arg(*handle, 1u), ConstantU32(state, GcnBufferWord1Mask)) : context.Arg(*handle, 1u), 0u, 16u)), BdaConstant(state, 32u)); base = Binary(state, spv::OpBitwiseOr, u64, Unary(state, spv::OpUConvert, u64, context.Arg(*handle, 0u)), high); } auto invalid = nonzero(state, field(state, word3, 30u, 2u)); @@ -526,7 +548,8 @@ std::uint32_t formattedDispatch(SpirvValueEmitContext& context, const IrValue& i } const auto* word = instruction.Argument(0)->Resolve()->Argument(3)->Resolve(); if (memory.gpuDescriptor && word->HasImmediate()) { - const auto format = static_cast((word->ImmediateU32() >> 12u) & 0x7fu); + const auto bits = (word->ImmediateU32() >> 12u) & 0x7fu; + const auto format = static_cast(GcnGuestShaders() ? GcnBufferFormats()[bits] : bits); if (format == IrBufferFormat::Invalid) context.Fail(instruction, "formatted buffer instruction has an invalid constant format"); return formatted(context, instruction, resource, GetFormatInfo(format), components, store, data); } diff --git a/core/shader/recompiler/Translation/include/Translation/InstructionTranslator.hpp b/core/shader/recompiler/Translation/include/Translation/InstructionTranslator.hpp index e54dfdec75..2c190b4239 100644 --- a/core/shader/recompiler/Translation/include/Translation/InstructionTranslator.hpp +++ b/core/shader/recompiler/Translation/include/Translation/InstructionTranslator.hpp @@ -36,6 +36,7 @@ struct TranslateOptions { bool interpolationFunctions = false; bool nativeFloat16 = false; bool float32FlushToZero = false; + bool gcnLegacyVertexInputs = false; std::optional floatMode; ShaderStageInputInfo inputInfo; const EmbeddedFetchPlan* embeddedFetch = nullptr; diff --git a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp index cbd1075512..4a3849fbd6 100644 --- a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp +++ b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp @@ -446,8 +446,13 @@ void emitEntryPrologue(IrProgram& program, IrBlock& entryBlock, const TranslateO entryIr.SetScalarReg(static_cast(2), entryIr.Constant((lanes << 12u) | (lanes << 22u))); entryIr.SetScalarReg(static_cast(3), entryIr.Constant(lanes | (lanes << 8u))); } - setVector(static_cast(5), drawIndex(StageInputKind::VertexIndex)); - setVector(static_cast(8), drawIndex(StageInputKind::InstanceIndex)); + if (options.gcnLegacyVertexInputs) { + setVector(static_cast(0), drawIndex(StageInputKind::VertexIndex)); + for (std::uint32_t reg = 1u; reg <= 3u; reg++) setVector(static_cast(reg), drawIndex(StageInputKind::InstanceIndex)); + } else { + setVector(static_cast(5), drawIndex(StageInputKind::VertexIndex)); + setVector(static_cast(8), drawIndex(StageInputKind::InstanceIndex)); + } } } diff --git a/docs/dev/BUILD.md b/docs/dev/BUILD.md index 4cebf79a20..7778788a16 100644 --- a/docs/dev/BUILD.md +++ b/docs/dev/BUILD.md @@ -12,7 +12,7 @@ ctest --test-dir build-relinker --output-on-failure Python must be found during CMake configuration for Python tests to be registered. List registered tests with `ctest --test-dir build-relinker -N` and look for `input_magic`, a Python-backed relinker test. If automatic discovery fails, add `-DPython3_EXECUTABLE="C:/path/to/python.exe"` to the configure command, using the path to your Python interpreter, then list the tests again. -This mode builds the conversion tool and its tests on Linux, Windows and macOS, including Apple Silicon. macOS uses AppleClang from the Xcode command-line tools. Windows uses MinGW-w64 GCC with C++20 support and accepts distributions with static or shared runtimes; the WinLibs requirement below applies to the full build. The executable is `build-relinker/core/relinker/relinker` (`relinker.exe` on Windows with Ninja). +This mode builds the conversion tool and its tests on Linux, Windows and macOS, including Apple Silicon. macOS uses AppleClang from the Xcode command-line tools. Windows uses MinGW-w64 GCC with C++20 support and accepts distributions with static or shared runtimes; the full build also requires the exception support described below. The executable is `build-relinker/core/relinker/relinker` (`relinker.exe` on Windows with Ninja). The output is x86-64: Linux ELF by default, Windows PE with `--windows` or Mach-O with `--macos` ([usage](../user/USAGE.md#input-and-conversion)). Converted games need system libraries built for the target OS and a compatible x86-64 host. This mode does not build those libraries, and releases ship them for Linux and Windows only. Tests inspect all three output formats; execution checks run only on their compatible hosts. @@ -30,7 +30,7 @@ git submodule update --init --recursive - Linux display refresh queries require SDL's XRandR support (`libxrandr-dev` on Debian/Ubuntu; `libXrandr-devel` on Fedora). Bundled SDL configuration fails without it. Keep `SDL_X11_XRANDR` enabled. - Linux: GCC, G++, binutils. SDL's X11 backend requires X11 and Xext development headers (`libx11-dev` and `libxext-dev` on Debian/Ubuntu). SDL's Wayland backend requires the Wayland, xkbcommon and EGL development files (`libwayland-dev`, `libxkbcommon-dev` and `libegl-dev` on Debian/Ubuntu; `wayland-devel`, `libxkbcommon-devel` and `mesa-libEGL-devel` on Fedora), and `libdecor-0-dev` (`libdecor-devel`) for window decorations on GNOME. SDL loads both backends at run time and prefers Wayland when `WAYLAND_DISPLAY` is set; `SDL_VIDEODRIVER=x11` selects X11 (XWayland). Titles that bundle their own SDL read `SDL_VIDEODRIVER` too, and some of them fail to start with `x11`. Without the Wayland files configuration still succeeds, and the build uses X11 only. Audio output needs SDL's PipeWire, PulseAudio or ALSA backend, built from their development files (`libpipewire-0.3-dev`, `libpulse-dev` and `libasound2-dev` on Debian/Ubuntu; `pipewire-devel`, `pulseaudio-libs-devel` and `alsa-lib-devel` on Fedora). SDL loads them at run time. Without any of them SDL keeps only its silent dummy and disk drivers, so configuration fails. - Fedora: if CMake reports `Could NOT find Threads`, install `glibc-static` and `libstdc++-static` with `sudo dnf install glibc-static libstdc++-static`, then rerun configuration. The project enables static executable linking, and FFmpeg's thread check needs these libraries. -- Windows: only MinGW-w64 GCC 15.2.0 (WinLibs `x86_64-ucrt-posix-seh`, release [`15.2.0posix-14.0.0-ucrt-r7`](https://github.com/brechtsanders/winlibs_mingw/releases/tag/15.2.0posix-14.0.0-ucrt-r7), archive `winlibs-x86_64-posix-seh-gcc-15.2.0-mingw-w64ucrt-14.0.0-r7.zip`) is currently supported. Add its `mingw64/bin` directory to `PATH` before configuring. +- Windows: x86-64 MinGW-w64 GCC with POSIX threads. The compiler must provide DWARF exceptions with `-fno-asynchronous-unwind-tables`, or SEH exceptions for System V ABI functions; CMake checks these capabilities. WinLibs GCC 15.2.0 (`x86_64-ucrt-posix-seh`, release [`15.2.0posix-14.0.0-ucrt-r7`](https://github.com/brechtsanders/winlibs_mingw/releases/tag/15.2.0posix-14.0.0-ucrt-r7)) uses DWARF. [MSYS2 UCRT64 GCC](https://packages.msys2.org/packages/mingw-w64-ucrt-x86_64-gcc) 16.2.0-6 uses SEH. Add the selected toolchain's `bin` directory to `PATH` before configuring. - Windows antivirus software can quarantine the executables that the relinker tests create in the temp directory, which fails `optional_plt`, `empty_tls`, `windows_address_space` and `windows_icon`. Set `TEMP` and `TMP` to a directory the scanner excludes before running `ctest`. - Bitdefender Advanced Threat Defense hooks Windows calls that `sceKernelRaiseException` makes while its target thread is suspended. When the target is suspended inside ntdll's exception dispatch while it holds an ntdll lock, the hook waits for that lock and `guest_raise_exception` hangs until its timeout. A scanning exception for the build directory does not prevent it; add `guest_raise_exception_tests.exe` to the Advanced Threat Defense exceptions. - FFmpeg binaries are downloaded during configuration unless `FFMPEG_PREBUILT_DIR` is set. With the WinLibs CMake, the download fails with status 60 (`SSL peer certificate or SSH remote key was not OK`) unless `SSL_CERT_FILE` names a CA bundle, for example `C:\Program Files\Git\ucrt64\etc\ssl\certs\ca-bundle.crt` from Git for Windows 2.56 and later (`mingw64` instead of `ucrt64` before 2.56), as in CI. @@ -64,6 +64,12 @@ therefore tests the previous binaries and can show no effect at all. The full build also requires Python 3 for glslang and Git for Windows with Bash for libusb's version-header generation. A minimal Git installation without Bash is insufficient for the full build. +For MSYS2, install `mingw-w64-ucrt-x86_64-gcc` with `pacman -S mingw-w64-ucrt-x86_64-gcc` and use `C:\msys64\ucrt64\bin` in PowerShell. Adjust the prefix for a Scoop installation of MSYS2. Use a separate build directory for each toolchain, and select both `gcc` and `g++` from the same installation. + +The [Windows MSYS2 workflow](../../.github/workflows/windows-msys2.yml) builds and tests the full runtime nightly at 08:23 UTC and can be started manually with `workflow_dispatch`. The regular Windows build uses WinLibs. + +Scoop's standalone GCC 15.2.0 package cannot build the runtime: disabling SEH selects SjLj exceptions, and its SEH backend cannot compile System V ABI catch handlers. Use MSYS2 UCRT64 or a DWARF-capable WinLibs distribution. This restriction does not apply to relinker-only builds. + In PowerShell, prepend the supported WinLibs toolchain and Git for Windows to the current session's `PATH`. Adjust the paths for the installed locations; a Portable Git installation can be used in place of `C:\Program Files\Git`. ```powershell @@ -119,7 +125,7 @@ Build configuration parameters: SDL and FreeType settings forced by the root `CMakeLists.txt` cannot be overridden with `-D`. -With `ANYPS5_USE_SYSTEM_DEPS=ON` those libraries come from the system (on Debian and Ubuntu: `libsdl2-dev`, `libfreetype-dev`, `libvulkan-dev`, `spirv-headers`, `glslang-dev`, `glslang-tools` and `spirv-tools`). A missing package, Vulkan-Headers older than 1.4.310 (the agc driver uses `VK_KHR_maintenance8`), or an SDL2 without the HIDAPI joystick driver (sdl2-compat over SDL3, or a build with HIDAPI disabled, which breaks the pad and disk-audio tests), fails the configure with an error and never falls back to the submodule. The forced SDL and FreeType settings from the root `CMakeLists.txt` do not apply with the flag ON: the system SDL2 comes with whatever its distribution enabled. The prx libraries are shared objects, so SDL2 is linked from the system as a shared library; the default build keeps its own static SDL2. `ffmpeg-core`, `libjpeg-turbo`, `stb`, `VulkanMemoryAllocator` and `LibAtrac9` always stay bundled and still need `git submodule update --init`. +With `ANYPS5_USE_SYSTEM_DEPS=ON` those libraries come from the system (on Debian and Ubuntu: `libsdl2-dev`, `libfreetype-dev`, `libvulkan-dev`, `spirv-headers`, `glslang-dev`, `glslang-tools` and `spirv-tools`). A missing package, Vulkan-Headers older than 1.4.310 (the agc driver uses `VK_KHR_maintenance8`), or an SDL2 without the HIDAPI joystick driver (sdl2-compat over SDL3, or a build with HIDAPI disabled, which breaks the pad and disk-audio tests), fails the configure with an error and never falls back to the submodule. The forced SDL and FreeType settings from the root `CMakeLists.txt` do not apply with the flag ON: the system SDL2 comes with whatever its distribution enabled. The prx libraries are shared objects, so SDL2 is linked from the system as a shared library; the default build keeps its own static SDL2. `ffmpeg-core`, `libjpeg-turbo`, `stb`, `VulkanMemoryAllocator`, `LibAtrac9`, `ogg` and `vorbis` always stay bundled and still need `git submodule update --init`. ## Quick syntax checks diff --git a/docs/dev/GAME_TESTING.md b/docs/dev/GAME_TESTING.md index 73d6271fc2..6e74085f0e 100644 --- a/docs/dev/GAME_TESTING.md +++ b/docs/dev/GAME_TESTING.md @@ -7,6 +7,7 @@ For user options see [USAGE](../user/USAGE.md); for internals see [ARCHITECTURE] ## Before a run - Build `main` or the PR head in a clean worktree, including the `libs` target (it is not in the default build). If a new test breaks the build, `cmake --build build --target libs relinker` is enough to convert and run a title. +- `tools/check_main_sync.py --fetch` in the worktree reports whether the branch is in step with upstream `main`, so a result isn't taken on an old base by mistake. - Convert the title from plain ELF files (`tools/unwrap_self.py` unwraps a dump's `eboot.bin` and `.prx`): - Linux: `relinker --registry src/eboot.elf app.elf` - Windows: `relinker --windows --registry src/eboot.elf app.exe`. Without `--windows` the output is an ELF file whatever its name, and Windows refuses to start it (exit code 216, "not compatible with the version of Windows"). @@ -50,7 +51,12 @@ cmd /c '.\app.exe > run.log 2>&1' - The driver's own: Mesa (`MESA_SHADER_CACHE_DIR`, or `MESA_SHADER_CACHE_DISABLE=true`), AMD on Windows (`%LOCALAPPDATA%\AMD\VkCache`). Back a driver cache up instead of deleting it; it isn't AnyPS5's. Say in the report whether the caches were empty or warm. A first run that ends in device lost (`VK_ERROR_DEVICE_LOST`) or a GPU wait timeout while large pipelines are still being built, seen on AMD, often is the cache and not a regression: run again with the caches warm and report both runs. -- **Power and display:** on a laptop, run on AC power; a run on battery can make a neutral change look like a regression. Note the display's refresh rate: a title that asks for 120 Hz output can stop at start on a 60 Hz display. +- **Power and display:** on a laptop, run on AC power; a run on battery can make a neutral change look like a regression. Note the display's refresh rate: a title that asks for 120 Hz output can stop at start on a 60 Hz display. Keep the display awake for long or unattended runs: a title started while it sleeps stops with `SDL_GetWindowDisplayIndex failed: Couldn't find any displays`. +- **Memory budgets:** two budgets come from the machine's memory and can change the frame rate several times over, so check them before comparing results from different machines. + - Host imports (`APS5_HOST_IMPORT_MIB`, see [ENVIRONMENT](ENVIRONMENT.md)): the default follows the physical memory and the host heap the driver reports, so two machines with the same RAM can get different budgets. Refused imports print `[gpu] host import budget: X MiB live of Y MiB (...); N imports (M MiB) refused so far`, at most every 10 s; without such a line nothing was refused. A refused range is copied on every later use instead, and those copies can take device memory the texture caches need. + - Texture caches (`APS5_TEXTURE_CACHE_MIB`, see [USAGE](../user/USAGE.md#texture-cache-budget)): `[gpu] sampled texture cache budget N MiB` at start and when it changes, and with `APS5_PROFILE_DRAW=1` a `[texture-cache] (10 s)` line with the evictions and the budget. Thousands of evictions per 10 s with the budget at its 2048 MiB floor mean textures are rebuilt every frame. + + Report these lines. Set either variable only for a comparison, and say so. ## What to read @@ -80,7 +86,8 @@ cmd /c '.\app.exe > run.log 2>&1' - Measure the same spot every time: load a save, wait for the scene to settle, then measure a fixed window with no input. - Compare builds on the same machine, alternating them (A, B, A, B), at least two runs each. Throw away a run that was disturbed (another program, a sleeping display) and say so. - `APS5_TRACE_SUMMARY_EVERY=10` writes one line per 10 s to `aps5-summary.jsonl` without slowing the title; `tools/trace_digest.py` turns it into a table, and `tools/performance_compare.py` compares two captures ([BUILD](BUILD.md#performance-trace)). Pull requests labelled `perf` need a capture of `main` and one with the change. -- `APS5_DUMP_FRAMES=` with `APS5_DUMP_FRAMES_EVERY=` saves frames as `frame_.bmp` to check what was on screen. A second process of the same title writes to the same names and overwrites the first one's frames. +- `APS5_DUMP_FRAMES=` with `APS5_DUMP_FRAMES_EVERY=` saves frames as `frame_.bmp` to check what was on screen. A second process of the same title writes to the same names and overwrites the first one's frames. While 64 MiB of frames wait to be written, the presenter waits for the + writer, so a slow disk or `APS5_DUMP_SCALE=1` slows the title down instead of stopping it. ## Title notes @@ -104,7 +111,7 @@ Include, in this order: 3. Host: CPU, OS (and kernel on Linux), the GPU as the `Selected GPU name=` line names it, the driver version, AC or battery, and the display refresh rate. 4. The exact command and every `APS5_*` / `ANYPS5_*` variable, the in-game settings if they differ from the defaults, and whether the caches were empty or warm. 5. How far the title got (menu, a save, a scene) and what stopped it: the exit code and the last 200 log lines, plus the kernel line (Linux) or crash report (Windows) for a crash. -6. Presents per 10 s, and storage-image lines if memory is involved. +6. Presents per 10 s, and the storage-image and budget lines (`[gpu] host import budget`, `[gpu] sampled texture cache budget`, `[texture-cache]`) if memory is involved. 7. How many runs and how long each lasted. One run is an observation, not a reproduction. 8. The date of the test. Leave unknown fields empty rather than filling them from another report. diff --git a/docs/dev/debt/libSceAgc.md b/docs/dev/debt/libSceAgc.md index ab5eaacf4e..e83a029f5e 100644 --- a/docs/dev/debt/libSceAgc.md +++ b/docs/dev/debt/libSceAgc.md @@ -21,7 +21,7 @@ - [sceAgcDcbSet{Cx,Sh,Uc}RegistersIndirectGetSize](../../../core/libs/prx/libSceAgc/DcbState/src/Registers.cpp) (libSceAgc) - the count argument is ignored: PPSA28416 calls them with leftover register contents (0xf5f5039c), so the real functions likely take none; the size is 20 bytes either way - [sceAgcUpdateInterpolantMapping_0100](../../../core/libs/prx/libSceAgc/Misc/src/ShaderFusion.cpp) (libSceAgc) - name from its NID (`k0E7vkgqAuE`); the `(regs, vs, ps)` arguments are the declared ones, behaviour unknown - [sceAgcWriteDataPatchSetDst, sceAgcWriteDataPatchSetCachePolicy and their Async forms](../../../core/libs/prx/libSceAgc/Patch/src/WaitRegMem.cpp) (libSceAgc) - the signatures `(packet, uint8 value)` are inferred from the other patch functions, as no public declaration was found; the value is taken in the encoding of the `dst` and `cachePolicy` arguments of `sceAgcDcbWriteData` (graphics) and `sceAgcAcbWriteData` (Async, compute), and the patched packet is the one those calls would write. `sceAgcAsyncWriteDataPatchSetAddressOrOffset` is assumed to patch the address like `sceAgcWriteDataPatchSetAddressOrOffset`, as the compute WRITE_DATA packet keeps it in the same dwords -- [sceAgcCreateInterpolantMapping_10000](../../../core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp) (libSceAgc) - name from its NID (`dbOlWdppb4o`); same arguments as `sceAgcCreateInterpolantMapping_0100`; PPSA26344 passes three pointers at each of nine observed call sites (`0x9F914E`, `0x9F91E2`, `0x9F9216`, `0x1604BBD`, `0x16122D7`, `0x1618285`, `0x161838C`, `0x16184EE`, `0x1618607`), consistent with that argument assumption, but does not read the return value there, so its return type is not established. The `SPI_PS_INPUT_CNTL` bits for `is_f16` 2 inputs follow KytyPS5, not confirmed on hardware +- [sceAgcCreateInterpolantMapping_10000](../../../core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp) (libSceAgc) - name from its NID (`dbOlWdppb4o`); same arguments as `sceAgcCreateInterpolantMapping_0100`; PPSA26344 passes three pointers at each of nine observed call sites (`0x9F914E`, `0x9F91E2`, `0x9F9216`, `0x1604BBD`, `0x16122D7`, `0x1618285`, `0x161838C`, `0x16184EE`, `0x1618607`), consistent with that argument assumption, but does not read the return value there, so its return type is not established. - [sceAgcGetDataPacketPayloadRange](../../../core/libs/prx/libSceAgc/Misc/src/PacketInfo.cpp) (libSceAgc) - signature, the `{base, size in bytes}` output and the type 0 range (header + 1, one dword longer than the type 1 payload) from KytyPS5 only, matching `sceAgcGetDataPacketPayloadAddress_0090`; prosper returns header + 2 for both types from that address function - [sceAgcGetDataPacketPayloadAddress](../../../core/libs/prx/libSceAgc/Misc/src/PacketInfo.cpp) (libSceAgc) - forward to `sceAgcGetDataPacketPayloadAddress_0090` and the inferred (addr, cmd, type) parameters follow SharpProspero and decrypted firmware 3.20 NID mapping; not verified on console hardware - [sceAgcCbMemSemaphore](../../../core/libs/prx/libSceAgc/Cb/src/Sync.cpp) (libSceAgc) - signature (cb, semaphoreAddr, operation, modifier, value) follows SharpProspero; writes 4-dword PM4 0x39 packet with control word 0x80100000 following console oracle measurements in prosper (cases agcbuild_CbMemSemaphore_a/b/c); operation, modifier and value are unused @@ -29,9 +29,7 @@ - [sceAgcAcbAtomicGds_0900](../../../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) - name from its NID (`gQkqkLttcpw`), signature unknown - [sceAgcDcbPrimeUtcl2](../../../core/libs/prx/libSceAgc/DcbState/src/Display.cpp) (libSceAgc) - assumed to write the packet `sceAgcAcbPrimeUtcl2` writes: PRIME_UTCL2 has the same 5 dwords on the graphics and compute rings. Not confirmed on a title. Both write a NOP of that size, as there is no TLB to prime - [sceAgcDcbQueueEndOfShaderActionGetSize](../../../core/libs/prx/libSceAgc/DcbState/src/ContextState.cpp) (libSceAgc) - the builder is inline in the SDK and not exported; the size is assumed to be that of the RELEASE_MEM packet `sceAgcCbReleaseMem` writes (32 bytes), the same as `sceAgcAcbQueueEndOfShaderActionGetSize`, as in [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c) and [REmu](https://github.com/xnetcat/REmu/blob/main/crates/remu-runtime/src/agc_dcb.rs) -- [sceAgcUpdateInterpolantMapping_10000](../../../core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp) (libSceAgc) - name from its NID (`vieBRwlh1Lw`), not in the aerolib NID list; the `(regs, gs, ps)` arguments of `sceAgcCreateInterpolantMapping_0100` come from OpenAGC only - [sceAgcDcbSetZPassPredicationEnableGetSize, sceAgcDcbSetPredicationDisableGetSize, sceAgcDcbSetBoolPredicationEnableGetSize](../../../core/libs/prx/libSceAgc/DcbState/src/Predication.cpp) (libSceAgc) - the builders are inline in the SDK and not exported; the size is assumed to be that of the SET_PREDICATION packet `sceAgcDcbSetPredication` writes (16 bytes) -- [sceAgcUpdateInterpolantMapping_10000](../../../core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp) (libSceAgc) follows [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c) only, not confirmed on hardware: the `sceAgcCreateInterpolantMapping_10000` transform without the identity tail, so it writes one register per pixel shader input and none for a pixel shader without inputs, and a null pixel shader throws. For an `is_f16` 1 or 3 input whose `default_value` and `default_value_hi` differ it throws: OpenAGC takes `DEFAULT_VAL` from `default_value_hi`, while KytyPS5, sharpemu and `sceAgcCreateInterpolantMapping_10000` here take it from `default_value` - [sceAgcAcbWaitUntilSafeForRendering](../../../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) - `(buffer, videoOutHandle, displayBufferIndex)`, the declared arguments, which PPSA12544 (01.000.010) passes inside `waitUntilSafeForRendering` and `waitUntilSafeForRendering VR` markers on an asynchronous compute command buffer, ignoring the result. It writes the same rendering-wait packet as `sceAgcDcbWaitUntilSafeForRendering`, which the driver now accepts on compute queues too; the wait blocks only that queue's worker. Whether the console encodes the compute variant differently is not known - [sceAgcGetDefaultCxStateFlat](../../../core/libs/prx/libSceAgc/Misc/src/RegisterDefaults.cpp) (libSceAgc) - copies up to `size` bytes (clamped to 2048 bytes, the 512-dword `AgcContextState`) from the default context state initialized from the version 8 public register default groups (CX, SH, UC), indexed by register offset, following OpenAGC `src/context_state.c`; returns `0x80890001` on null destination; version selection without `sceAgcInit` is unverified - [sceAgcDcbSetIndexIndirectArgs, sceAgcDcbSetIndexIndirectArgsGetSize](../../../core/libs/prx/libSceAgc/DcbDraw/src/IndexBuffer.cpp) (libSceAgc) - the `(buffer, attributeAddress, attributeIndex)` signature, a 64-bit address and a 32-bit index, is [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L1742-L1757)'s. The console oracle in prosper measured the packet, an INDEX_ATTRIBUTES_INDIRECT (opcode 0x91) holding the low address dword, the high address dword and the index ([measured](https://github.com/mattias800/prosper/blob/3ea5cb9a846b6dcd00f5073360462bbda4f72288/prosper/tests/data/console_oracle/agcbuild.golden.tsv#L172-L174)), and a size of 16 bytes ([measured](https://github.com/mattias800/prosper/blob/3ea5cb9a846b6dcd00f5073360462bbda4f72288/prosper/tests/data/console_oracle/agc.golden.tsv#L67)). The console clears the low four address bits and keeps the low 16 bits of the index; here a nonzero address that is not 16-byte aligned, or an index above 0xffff, throws, as `sceAgcDcbSetIndexBuffer` throws for a misaligned address. A null address is written as 0, as the console writes it. No title is known to call them. diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 8b92e54ef7..496f0cae85 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -9,7 +9,11 @@ - [sceAgcDriverSubmitCommandBuffer, sceAgcDriverSubmitMultiCommandBuffers](../../../core/libs/prx/libSceAgcDriver/Submit/src/CommandBuffer.cpp) (libSceAgcDriver) - `queue_context` is treated as a pointer to context whose queue identifier is stored 4 bytes in, matching KytyPS5 (`src/libs/agc.cpp` `AgcDriverSubmitCommandBuffer` / `get_driver_queue`). A null context returns `EINVAL` (22). Whether the console validates other context fields before queue dispatch is unverified -- [sceAgcDriverAcquireHDRScopesACQ, sceAgcDriverGetCoredumpAddress, sceAgcDriverHp3dLockEnd, sceAgcDriverHp3dLockStart, sceAgcDriverIsEsvmProcess, sceAgcDriverNopDataWritePacket, sceAgcDriverReleaseHDRScopesACQ, sceAgcDriverSetTargetRingForDiag, sceAgcDriverSetValueToSharedGds, sceAgcDriverSetWorkloadStreamInactive, sceAgcDriverSubmitReprojectionAcb, sceAgcDriverSuspendPointSubmitCdbg, sceAgcDriverSysGetGfxAppGpuResetStatus](../../../core/libs/prx/libSceAgcDriver/Unimplemented.cpp) (libSceAgcDriver) - signatures and behavior are unverified; these parameterless declarations only resolve imports and throw through `NotImplemented_nid_no_patch`. +- [sceAgcDriverSubmitAcb, sceAgcDriverSubmitMultiAcbs](../../../core/libs/prx/libSceAgcDriver/Submit/src/Acb.cpp) (libSceAgcDriver) accept compute queues 0x20 to 0x57 and throw for any other. On a PS5 (firmware 13.40, measured 2026-10-19 from inside a retail title) `sceAgcDriverSubmitAcb` returned 0 and ran a label-only buffer on queues 0x20, 0x30, 0x40, 0x50, 0x57 and also 0x58, which throws here; queue 0x1F faulted inside the console's driver (SIGSEGV in libkernel on the calling thread), which throws here too. Queues above 0x58 and `sceAgcDriverSubmitMultiAcbs` were not measured. Closes when: the console's highest accepted compute queue is measured + +- [sceAgcDriverAcquireHDRScopesACQ, sceAgcDriverHp3dLockEnd, sceAgcDriverHp3dLockStart, sceAgcDriverIsEsvmProcess, sceAgcDriverNopDataWritePacket, sceAgcDriverReleaseHDRScopesACQ, sceAgcDriverSetValueToSharedGds, sceAgcDriverSetWorkloadStreamInactive, sceAgcDriverSubmitReprojectionAcb, sceAgcDriverSuspendPointSubmitCdbg, sceAgcDriverSysGetGfxAppGpuResetStatus](../../../core/libs/prx/libSceAgcDriver/Unimplemented.cpp) (libSceAgcDriver) - signatures and behavior are unverified; these parameterless declarations only resolve imports and throw through `NotImplemented_nid_no_patch`. +- [sceAgcDriverGetCoredumpAddress](../../../core/libs/prx/libSceAgcDriver/Unimplemented.cpp) (libSceAgcDriver) returns the driver's shared coredump-area pointer, which is null when no coredump area is allocated. AnyPS5 reports coredump mode disabled and does not allocate this area; the export returns null. Enabling and populating an AGC coredump area is not implemented. +- [sceAgcDriverSetTargetRingForDiag](../../../core/libs/prx/libSceAgcDriver/Unimplemented.cpp) (libSceAgcDriver) returns `0x8A6D0001` (`DRIVER_FEATURE_UNAVAILABLE`), matching the retail PS5 export. The retail diagnostic log message is not reproduced. - [Initial point context](../../../core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp) - new queues and CLEAR_STATE seed PA_SU_POINT_SIZE 0x00080008 and PA_SU_POINT_MINMAX 0xffff0000, matching every public context table in libSceAgc/Command/include/RegisterDefaultsData.hpp. Cocoon (PPSA08766) reaches geometry point output without writing these registers in the observed startup. Applying the SDK context defaults to initial/CLEAR_STATE driver context is inferred; GPU power-on reset state is not measured and is not the source of these values. Explicit writes and pushed/restored contexts retain their own values. @@ -23,7 +27,7 @@ - [Native multisampled depth clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) accept the recognized HTILE mask kernel's direct full-range dispatch after its GPU writes complete. Every metadata byte needs tracked driver-store coverage since notification, no intervening CPU or depth write, and HTILE words proving the current clear value of zero or one. Partial, overlapping, indirect and unproven producers remain unsupported. Existing sampled views retain the native image; guest depth publication and metadata-fill transitions remain unimplemented. Windows shared imports retain write tracking through separate aliases; private host imports that lose tracking and copied or mirrored HTILE uploads remain unsupported. Clear-word encoding and console equivalence remain unmeasured on PS5 hardware. - [Native depth allocation retirement](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) covers sampling-eligible multisampled owners only. Successful release notifications must cumulatively cover both complete depth and HTILE ranges; protection changes, failed releases and partial coverage keep the original guards. Retired owners leave lookup and metadata routing, while held views and recorded work retain their images. Retirement of single-sample or sampling-ineligible depth attachments and console equivalence remain unimplemented or unmeasured. The regression uses real allocation-registry operations with a no-op host-release callback, so it does not validate the host unmap backend. - [Native MSAA depth attachment feedback](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) is not implemented. A draw sampling a native multisampled depth view while attaching its source image throws before pipeline creation or render-pass recording, including read-only attachment feedback. Sampling that image in a separate draw or dispatch remains supported. The refusal compares the held native image views and their device and attachment identity; single-sample depth behavior is retained. -- [Native multisampled depth sampling](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) accepts numeric D16 or D32, two/four/eight samples, one Z_X slice and mip, and pipe-aligned HTILE without stencil or VRS, with ITERATE_256 on or off: the initial import reads no compressed Z planes, and Mesa's HTILE clear words (`radv_get_htile_fast_clear_value`) and Addrlib's HTILE footprint do not depend on ITERATE_256, which `ac_descriptors.c` ties to TC reads of compressed MSAA HTILE. Sampling eligibility is recorded before the attachment's first host clear: every HTILE word must prove the same initial clear of zero or one, and the complete sample-aware depth and HTILE ranges must be accessible and write-watched. Unsupported initial state or device sampling support retains attachment rendering but sampling throws; eligibility is never retried. Sampling rejects later guest writes, queued external GPU writes, and unproven HTILE clear/fill transitions. Guest publication, arbitrary guest depth import, stencil/raw-bit sampling, arrays and storage access remain unimplemented. The block footprint follows main's AMD Addrlib thin-block rule; the accepted HTILE clear words and PS5 equivalence are not measured on console hardware. PPSA99902's historical resident eight-sample D32 reader is documented in [#419](https://github.com/boykopovar/AnyPS5/pull/419#issuecomment-6105597729). +- [Native multisampled depth sampling](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) accepts numeric D16 or D32, two/four/eight samples, one Z_X slice and mip, and pipe-aligned HTILE without stencil or VRS, with ITERATE_256 on or off: the initial import reads no compressed Z planes, and Mesa's HTILE clear words (`radv_get_htile_fast_clear_value`) and Addrlib's HTILE footprint do not depend on ITERATE_256, which `ac_descriptors.c` ties to TC reads of compressed MSAA HTILE. Sampling eligibility is recorded before the attachment's first host clear: every HTILE word must prove the same initial clear of zero or one, and the complete sample-aware depth and HTILE ranges must be accessible and write-watched. Unsupported initial state or device sampling support retains attachment rendering but sampling throws; eligibility is never retried. Sampling rejects later guest writes, queued external GPU writes, and unproven HTILE clear/fill transitions. A guest read over the depth or HTILE range of a surface the host has not written since import (draws with depth or stencil writes, depth/stencil copies, storage write-back; not clears after a proven HTILE clear) retires it instead of throwing; that a draw without those writes leaves the console's Z planes and HTILE unchanged is inferred, not measured. Guest publication of a written surface, arbitrary guest depth import, stencil/raw-bit sampling, arrays and storage access remain unimplemented. The block footprint follows main's AMD Addrlib thin-block rule; the accepted HTILE clear words and PS5 equivalence are not measured on console hardware. PPSA99902's historical resident eight-sample D32 reader is documented in [#419](https://github.com/boykopovar/AnyPS5/pull/419#issuecomment-6105597729). - [sceAgcDriverSetSubmitValidationMode, sceAgcDriverGetSubmitValidationMode, sceAgcDriverSetSubmitValidationConfig, sceAgcDriverGetSubmitValidationConfig, sceAgcDriverSetValidationErrorOutputFrequency, sceAgcDriverGetShaderDebuggingStatus](../../../core/libs/prx/libSceAgcDriver/Unimplemented.cpp) (libSceAgcDriver) - the five validation calls return `0x8A6C1000` without reading their arguments or writing caller memory, and `sceAgcDriverGetShaderDebuggingStatus` returns 1, as [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L250-L291) reads them from Sony's libSceAgcDriver.sprx: each validation export is `mov eax, 0x8a6c1000; ret` in the copy Dragon Quest VII Reimagined bundles (0x7bb0-0x7bf0), and the debugging status is 1 in the firmware 5.50 library (0x6740). The parameters (a 32-bit mode or frequency, a mode output, a configuration pointer) follow OpenAGC and are never read. Whether later retail firmware enables submit validation or shader debugging is unverified; no title in this repository is known to call them - [sceAgcDriver retail constant results](../../../core/libs/prx/libSceAgcDriver/Unimplemented.cpp) (libSceAgcDriver) - the debug, capture, GPU printf, register access, GPU reset, suspend point and user data exports taken from the retail `libSceAgcDriver.sprx`, and `sceAgcDriverRegisterOwnerForSystem`, `sceAgcDriverInitResourceRegistrationInterface` and `sceAgcDriverSetResourceRegistrationInfo` in [Registration.cpp](../../../core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp), are declared without parameters: the console bodies return a constant without reading any argument, so their signatures are unknown. `sceAgcDriverAllocateToolMemoryForGpuReset` and `sceAgcDriverGetAllocatedToolMemoryForGpuReset` read only the output pointer (fourth and second argument, a 12-byte range the console zeroes); the other arguments are declared as `uint64_t`. - [Indices far past every vertex fetch](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - when a draw's highest index is at least 64Ki vertices past the largest per-vertex record count, every index past that count is moved to it in a copy of the indices, so the zero-padded vertex copies are not sized to the original index (PPSA04159 draws a list of `0xffffffff` with primitive restart off, which asked for 160 GiB). Each attribute fetched by vertex index reads zeros there as at the original index (out-of-bounds select 0 or 1); a draw with another out-of-bounds select keeps its indices. The vertex index the shader sees changes for the moved indices, which a shader that reads it directly would notice @@ -32,9 +36,9 @@ - [Point-list geometry output](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - a geometry program with `VGT_GS_OUT_PRIM_TYPE = 0` (point list) runs as a mesh shader with `OutputPoints` and scalar `PrimitivePointIndicesEXT`. Each primitive export emits one point whose vertex index is extracted from bits [8:0] of the packed word. A subgroup gets up to `maxVertices` points, with provoking vertex 0 and point raster primitive topology. Rectangle lists, per-stream types and point or line strips from a vertex program on the mesh path still throw. - [Line-strip geometry output](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - a geometry program with `VGT_GS_OUT_PRIM_TYPE = 1` (line strip) runs as a mesh shader with `OutputLinesEXT`: each primitive export is one line whose two vertex indices are packed like the three of a triangle, as Mesa's NGG lowering exports line strips (`ac_nir_lower_ngg_gs.c`), and each input primitive gets `VGT_GS_MAX_VERT_OUT - 1` lines where a triangle strip gets `- 2` triangles. A subgroup gets at least `GE_MAX_OUTPUT_PER_SUBGROUP` line slots; whether the console does the same is unmeasured. A fast-launch subgroup takes its line count from `GE_NGG_SUBGRP_CNTL.PRIM_AMP_FACTOR`. Line output counts as lines for polygon offset decode. The lines are drawn 1 pixel wide with Vulkan default rasterization; `PA_SU_LINE_CNTL` is not read, and console line rules are not measured. - [Alpha-to-coverage](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - only `DB_ALPHA_TO_MASK = 0xaa01` is accepted: the enable bit with the four quad offsets at 2 and no `OFFSET_ROUND`, the undithered setting [radeonsi](https://gitlab.freedesktop.org/mesa/mesa/-/blob/bffd91947efc1932da99aca69fc540d6d4201b0c/src/gallium/drivers/radeonsi/si_state.c#L371-L381) programs. Draws are single-sampled, so the pixel shader [writes](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) `SampleMask` 0 when its MRT0 alpha is below one half or NaN and all ones otherwise, and runs with late fragment tests. That rule was measured on a Radeon 660M (gfx1035, RADV with `RADV_DEBUG=noatocdithering`, which programs the same register) for 0, -0, 0.25, the two floats below one half, one half, the two above, 0.75, 1, 2, -1, both infinities and both NaNs, and is pinned in [AlphaToCoverage.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp); it is not measured on gfx1013. Vulkan's `alphaToCoverageEnable` is not used: the NVIDIA driver 615.78.08 dithers single-sample coverage (an alpha of 0.25 writes half the pixels of an RTX 3050 Laptop target). Other offsets, `OFFSET_ROUND`, a sample-mask export, and an MRT0 that is missing, swizzled, packed or exported without a floating-point alpha throw. A draw without a color target on MRT0 keeps alpha-to-coverage from its MRT0 export, as [radeonsi exports MRT0 as `32_AR` for alpha-to-coverage without a color buffer](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-24.0.0/src/gallium/drivers/radeonsi/si_state_shaders.cpp#L2425-L2431). A draw that enables alpha-to-coverage with no MRT0 export still throws: what the console covers then is not measured. Default dithered `DB_ALPHA_TO_MASK = 0x18701` additionally uses a per-quad sample-mask table [measured on gfx1036](https://github.com/boykopovar/AnyPS5/pull/4451#issuecomment-6124848921) for eight standard samples with FP16_ABGR or 32_ABGR exports; other sample counts, custom locations, EQAA combinations and dithered UNORM16/SNORM16 exports throw. The dithered rule is not measured on gfx1013. -- [A one-sample target under a multisampled rasterizer without MSAA_ENABLE](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - `PA_SC_AA_CONFIG.MSAA_NUM_SAMPLES` > 0 with `PA_SC_MODE_CNTL_0.MSAA_ENABLE` clear, one-sample color targets and no depth target, or a one-sample depth target with no color target or one-sample color targets, is drawn as a one-sample draw at the pixel center. Without the bit every sample is taken to be at the pixel center (see the entry for a multisampled target rasterized without MSAA_ENABLE below), and a one-sample target is taken to keep the coverage of sample 0, so a one-sample depth target is tested and written once per pixel with the depth at the pixel center. That is the rule [`VK_AMD_mixed_attachment_samples`](https://github.com/KhronosGroup/Vulkan-Docs/blob/193ebe195f47ffc5004683d1659e71c725658c4d/chapters/fragops.adoc#L2510-L2516) gives a color target with fewer samples than the rasterizer, but Vulkan reaches it only beside a depth or stencil target with the rasterizer's sample count ([VUID-VkGraphicsPipelineCreateInfo-subpass-01505](https://github.com/KhronosGroup/Vulkan-Docs/blob/193ebe195f47ffc5004683d1659e71c725658c4d/chapters/pipelines.adoc#L3033-L3041)), so here it is an analogy, and for a one-sample depth target an extension of it. [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp#L127-L128) also rasterizes with the largest sample count of the bound depth and color targets ([L416-L417](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp#L416-L417), [L477-L480](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp#L477-L480)), and reads `PA_SC_AA_CONFIG` only for a `SAMPLE_COVERAGE` input ([L174-L177](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp#L174-L177)). This is not measured on hardware. An `ANCILLARY` or `SAMPLE_COVERAGE` pixel input and a sample-mask export throw in a pixel program that runs, and so does `PS_ITER_SAMPLE` with one-sample color targets and no depth target, because what the console gives them under a multisampled configuration is not known; a depth-only draw that skips its pixel program is drawn with the null program, and its `PS_ITER_SAMPLE` and pixel input bits are ignored. With a one-sample depth target, a pixel program with `PS_ITER_SAMPLE` and neither input runs once per pixel; if the console runs it once per sample, its atomics and append or consume counters count differently. Points, lines and alpha-to-coverage throw as for a multisampled target. With a one-sample depth target, `DB_EQAA` Z interpolation (`INTERPOLATE_COMP_Z`, `INTERPOLATE_SRC_Z`) or overrasterization (`OVERRASTERIZATION_AMOUNT`, `ENABLE_POSTZ_OVERRASTERIZATION`) also throws. METAL GEAR SOLID: Peace Walker (PPSA18216) draws a one-sample color target under a 4x configuration this way; the first draw of its opening movie binds a one-sample depth target and no color target under the same configuration. +- [A one-sample target under a multisampled rasterizer without MSAA_ENABLE](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - `PA_SC_AA_CONFIG.MSAA_NUM_SAMPLES` > 0 with `PA_SC_MODE_CNTL_0.MSAA_ENABLE` clear, one-sample color targets and no depth target, or a one-sample depth target with no color target or one-sample color targets, is drawn as a one-sample draw at the pixel center. Without the bit every sample is taken to be at the pixel center (see the entry for a multisampled target rasterized without MSAA_ENABLE below), and a one-sample target is taken to keep the coverage of sample 0, so a one-sample depth target is tested and written once per pixel with the depth at the pixel center. That is the rule [`VK_AMD_mixed_attachment_samples`](https://github.com/KhronosGroup/Vulkan-Docs/blob/193ebe195f47ffc5004683d1659e71c725658c4d/chapters/fragops.adoc#L2510-L2516) gives a color target with fewer samples than the rasterizer, but Vulkan reaches it only beside a depth or stencil target with the rasterizer's sample count ([VUID-VkGraphicsPipelineCreateInfo-subpass-01505](https://github.com/KhronosGroup/Vulkan-Docs/blob/193ebe195f47ffc5004683d1659e71c725658c4d/chapters/pipelines.adoc#L3033-L3041)), so here it is an analogy, and for a one-sample depth target an extension of it. [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp#L127-L128) also rasterizes with the largest sample count of the bound depth and color targets ([L416-L417](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp#L416-L417), [L477-L480](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp#L477-L480)), and reads `PA_SC_AA_CONFIG` only for a `SAMPLE_COVERAGE` input ([L174-L177](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp#L174-L177)). This is not measured on hardware. An `ANCILLARY` or `SAMPLE_COVERAGE` pixel input and a sample-mask export throw in a pixel program that runs, and so does `PS_ITER_SAMPLE` with one-sample color targets and no depth target, because what the console gives them under a multisampled configuration is not known; a depth-only draw that skips its pixel program is drawn with the null program, and its `PS_ITER_SAMPLE` and pixel input bits are ignored. With a one-sample depth target, a pixel program with `PS_ITER_SAMPLE` and neither input runs once per pixel; if the console runs it once per sample, its atomics and append or consume counters count differently. Points and alpha-to-coverage throw as for a multisampled target. With a one-sample depth target, line and point lists (`VGT_PRIMITIVE_TYPE` 1-3, even when a geometry program turns them into triangles), `DB_EQAA` Z interpolation (`INTERPOLATE_COMP_Z`, `INTERPOLATE_SRC_Z`) and overrasterization (`OVERRASTERIZATION_AMOUNT`, `ENABLE_POSTZ_OVERRASTERIZATION`) also throw. METAL GEAR SOLID: Peace Walker (PPSA18216) draws a one-sample color target under a 4x configuration this way; the first draw of its opening movie binds a one-sample depth target and no color target under the same configuration. - [MSAA_ENABLE on a single-sample target](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - `PA_SC_MODE_CNTL_0.MSAA_ENABLE` with `PA_SC_AA_CONFIG.MSAA_NUM_SAMPLES = 0` is drawn like the same state without the bit when sample 0 of the four `PA_SC_AA_SAMPLE_LOCS_PIXEL_*_0` registers is at the pixel center and the draw rasterizes filled triangles or rectangles (`VGT_PRIMITIVE_TYPE` 7 or 17 as well as the triangle types) with alpha-to-coverage and conservative rasterization off. [RADV](https://gitlab.freedesktop.org/mesa/mesa/-/blob/745709a000482a4a24c91faef5c5a40bd653a863/src/amd/vulkan/radv_cmd_buffer.c#L13606-13609) programs this state for one-sample custom sample locations on GFX10 and later, where the bit makes the rasterizer use the programmed location of sample 0 ([061bc615](https://gitlab.freedesktop.org/mesa/mesa/-/commit/061bc6151a11597f720535f00e79a5752e24aff6)); that the bit changes nothing else for such a draw is inferred from that and is not measured on hardware. EA Sports UFC 5 (PPSA03541) reaches a rect-list draw in this state. Other locations of sample 0, absent location registers, points, lines, alpha-to-coverage and conservative rasterization throw -- [A multisampled target rasterized without MSAA_ENABLE](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - `PA_SC_AA_CONFIG.MSAA_NUM_SAMPLES` > 0 with `PA_SC_MODE_CNTL_0.MSAA_ENABLE` clear is drawn with the target's sample count and every sample at the pixel center, as a single-sample scan into a multisampled target already is. On GFX10 and later the bit is what makes the rasterizer use the programmed sample locations ([RADV 061bc615](https://gitlab.freedesktop.org/mesa/mesa/-/commit/061bc6151a11597f720535f00e79a5752e24aff6)), and with multisampling disabled GL rasterizes at the pixel center and gives the fragment full coverage, which radeonsi programs by clearing the bit; that the hardware does the same with `MSAA_NUM_SAMPLES` > 0 is inferred from these and is not measured on hardware. A depth target with the same sample count also has every sample at the pixel center, so each depth and stencil sample is tested and written with the depth at the pixel center, as GL allows when multisampling is disabled ([L185-L190](https://github.com/KhronosGroup/OpenGL-Registry/blob/6af574a14089ccfee87efe230ebcdd8742859813/extensions/ARB/ARB_multisample.txt#L185-L190), [L422-L425](https://github.com/KhronosGroup/OpenGL-Registry/blob/6af574a14089ccfee87efe230ebcdd8742859813/extensions/ARB/ARB_multisample.txt#L422-L425)). GL also lets an implementation test only a centermost sample and set every sample from it ([L425-L434](https://github.com/KhronosGroup/OpenGL-Registry/blob/6af574a14089ccfee87efe230ebcdd8742859813/extensions/ARB/ARB_multisample.txt#L425-L434)), which differs when a pixel's stored samples differ; which one the console does, and how its occlusion queries count such a draw, is not measured on hardware. `DB_EQAA` Z interpolation or overrasterization and a sample-mask export with a depth target throw. Points, lines and alpha-to-coverage throw. SONIC SUPERSTARS (PPSA06888) draws such a 2x target, and METAL GEAR SOLID: Peace Walker (PPSA18216) a 4x depth and color pair. +- [A multisampled target rasterized without MSAA_ENABLE](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - not measured on hardware: `PA_SC_AA_CONFIG.MSAA_NUM_SAMPLES` > 0 with `PA_SC_MODE_CNTL_0.MSAA_ENABLE` clear puts every color, depth and stencil sample at the pixel center, as in GL without multisampling ([ARB_multisample](https://github.com/KhronosGroup/OpenGL-Registry/blob/6af574a14089/extensions/ARB/ARB_multisample.txt#L185), [RADV 061bc615](https://gitlab.freedesktop.org/mesa/mesa/-/commit/061bc6151a11)). Lines cover whole pixels by the host's line rule, not the console's aliased one ([D3D11.3](https://github.com/microsoft/DirectX-Specs/blob/f37b0d6033a1/d3d/archive/D3D11_3_FunctionalSpec.htm#L13705)). Depth sample tests and occlusion counts are unknown. Rasterized points, alpha-to-coverage, and Z interpolation, overrasterization or a sample-mask export with depth throw. PPSA06888 draws a 2x target, PPSA18216 a 4x depth and color pair and 4x lines. - [sceAgcDriverGetEqContextId](../../../core/libs/prx/libSceAgcDriver/Eq/src/Query.cpp) (libSceAgcDriver) - returns `ident`: an end-of-pipe interrupt is delivered only to the registration whose id equals the raising queue, so `ident` is that context's id (titles register 0 or 1 for graphics and `0x20 + 8 * pipe + queue` for compute). The console's value is not known; KytyPS5 returns the event's `data`, which it fills with the interrupt's context id, whereas `data` here is the EV_CLEAR trigger count. Other event filters throw - [User data registers](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp) (AGC driver) - a `SPI_SHADER_USER_DATA_*` or `COMPUTE_USER_DATA_*` register the title never wrote reads as 0 when a stage's RSRC2 count covers it, as Kyty (zeroed `UserSgprInfo`) and SharpEmu (missing registers read 0) do, and as AMD PAL and Mesa initialize them; the console's value is not confirmed - [SET_PREDICATION](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (AGC driver) - boolean predication follows the AMD PM4 encoding [Mesa](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/common/sid.h) uses: operation 3 reads 64 bits and 4 reads 32 bits, and bit 8 runs predicated packets when the value is non-zero (clear: when it is zero); the bit 12 hint is ignored. The address must have its low 4 bits clear, as `sceAgcDcbSetPredication` writes it. Query predication (Z-pass, primitive count), a predicated COND_EXEC, predicated command buffer chains and flips in predicated command buffers throw @@ -126,7 +130,7 @@ - A [COND_INDIRECT_BUFFER](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) (`sceAgcCbBranch`) runs only with the always-pass compare function; a comparison or a predicated packet throws. Modes 1 and 2 run the first buffer, assumed to be the if-then and if-then-else modes of AMD's PM4 packet. [sceAgcCbBranch](../../../core/libs/prx/libSceAgc/Cb/src/Branch.cpp) does not check the compare address of an always-pass branch or the address of an empty buffer, as PPSA14632 passes an uninitialised compare address and a null else buffer of 0 dwords. - A [label or store](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (`RELEASE_MEM`, `WRITE_DATA`, `DMA_DATA`) into guest memory the device has no host import of waits for all recorded work and is then written by the CPU, so the queue worker stalls once per such label. The console's CP stores it in stream order without that stall. - [WRITE_DATA to thread-trace user-data registers](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver) updates the queue's UCONFIG values for register destinations `0xc340..0xc347`, including repeated writes to one address; these are not guest-memory labels. The range follows AMD's GFX10 `SQ_THREAD_TRACE_USERDATA_0..7` definitions in [the Linux register header](https://github.com/torvalds/linux/blob/master/drivers/gpu/drm/amd/include/asic_reg/gc/gc_10_1_0_offset.h) and the packets emitted by PPSA21564's bundled AGC driver before its EOP flip. Other register destinations throw. GPU thread-trace capture and its user-data stream are not emitted; only the final register state is retained. Console trace behavior is unverified. -- [WRITE_DATA with DST_SEL 3](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver) writes guest memory like DST_SEL 5. AMD's GFX10 header names 3 the GDS, but Killzone: Liberation (PPSA11142) writes its frame fence with `sceAgcDcbWriteData(dst 6, cache policy 3, write confirm)`, which the SPRX-confirmed encoding ([OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/cb_builders.c)) turns into DST_SEL 3 with a 40-bit label address in the title's own memory that `RELEASE_MEM` and `WAIT_REG_MEM` then use; the console must write that label or the title would wait on it forever. Whether the console routes it through the GDS or a cache is unknown +- [WRITE_DATA with DST_SEL 3](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver) writes guest memory like DST_SEL 5, which a PS5 does not: on firmware 13.40 (measured 2026-10-19 with buffers submitted through `sceAgcDriverSubmitDcb` inside a retail title) a DST_SEL 3 `WRITE_DATA` (control 0x00100300, or 0x06100300 with cache policy 3) left its label in direct memory unwritten, also after a `RELEASE_MEM` with GL2 write-back and invalidate (0x00300514), and a GDS `COPY_DATA` (SRC_SEL 3) at that address read 0. The console's own `sceAgcDcbWriteData(dst 6)` builds DST_SEL 3 (0x00110300) as here, and Killzone: Liberation (PPSA11142) fences its frame with it and waits on that label with `WAIT_REG_MEM`, so the write is kept until the path that lands the label is known. Closes when: that path is measured - [SET_*_REG_INDIRECT](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) (libSceAgcDriver) reads its register list when the command buffer is submitted, with the packets. The console's graphics PFP reads it when it reaches the packet, so a list written after the submission (by the CPU, or by GPU work ahead of the packet behind PFP_SYNC_ME) is read here before that write. When the list read at submission holds an offset dword that is not a register offset (bits 16-27 set, or the 0xffffffff sentinel), the worker reads the list again where the packet executes and writes that copy. PPSA23566 (UFC 6) intermittently submits a SET_CONTEXT_REG_INDIRECT whose first entry is 0x038f038f or 0x03900390 in both copies; what the PFP does with such an entry is unknown, so a copy that still holds one throws unless `APS5_SKIP_INVALID_INDIRECT_REGISTERS=1`, which skips its invalid entries and writes its valid ones. - The [shader float mode](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (`FLOAT_MODE`, `DX10_CLAMP`, `IEEE_MODE` and `FP16_OVFL` of `COMPUTE_PGM_RSRC1` and `SPI_SHADER_PGM_RSRC1_PS/GS/HS`, bit positions from Mesa's `gfx10.json` and `gfx103.json`) is decoded into `GuestContext::floatMode` and is part of the shader cache keys and the serialized request (#981). `IEEE_MODE` is read by `v_min_f32`, `v_max_f32`, `v_min3_f32`, `v_max3_f32` and `v_med3_f32`, their f16 forms, `v_min_f64` and `v_max_f64`, `IEEE_MODE` and the `FLOAT_MODE` output denormal bits by the output modifier, `DX10_CLAMP` by the clamp modifier, `FP16_OVFL` by every instruction with an f16 result (see its entry), and the whole mode by the 16-bit interpolation instructions (see their entries). With IEEE=1 a signaling NaN operand returns that NaN quieted (the first operand checked first, min3/max3 as two nested pairs, med3 as min3 when any operand is NaN), measured on RDNA2 (gfx1035) over every combination of ±0, ±1, ±inf, denormals, quiet and signaling NaNs with f32 denormals kept and flushed. IEEE=0 and unknown modes keep the previous f32 lowering, which matches IEEE=0. The f16 forms choose their NaN results on the bits: a NaN operand returns the other operand unchanged (src0 when both are NaN), and with IEEE=1 a signaling NaN operand returns that NaN quieted, in the same order as f32. `v_min_f64` and `v_max_f64` quiet a signaling NaN operand the same way with IEEE=1; with IEEE=0 they return src0 unchanged when src1 is NaN, otherwise src1 when src0 is NaN. Both were measured on RDNA2 (gfx1036) at IEEE=0 and IEEE=1 with f32 denormals kept and flushed and f16/f64 denormals kept; RDNA2 has no f64 min3, max3 or med3. Unknown modes follow IEEE=0. `v_min_f16` and `v_max_f16` flush each denormal input to its signed zero when bit 0 of the f16/f64 denormal field is clear (0 and 2); fields 1 and 3 preserve inputs, even when output flushing is enabled. Measured on RDNA2 (gfx1036) at IEEE=0 and IEEE=1, including signed zeros, NaNs and neg/abs modifiers. Unknown modes keep f16 denormals. `v_min_f64` and `v_max_f64` flush subnormal inputs to signed zero when bit 0 of the f16/f64 denormal field is clear (0 and 2); fields 1 and 3, and an unknown mode, preserve inputs. The fully flushed and preserved f64 behavior was [measured on gfx1036](https://github.com/boykopovar/AnyPS5/pull/2036); split-mode selection follows [LLVM's denormal-mode enumeration](https://llvm.org/docs/AMDGPUUsage.html#floating-point-denorm-mode-enumeration-values). Other f64 arithmetic does not model these denormal controls. NaN quieting in f16 arithmetic (the unary instructions and the f32 arithmetic follow it, see below) and `FLOAT_MODE` (f32 denormals flushed before the min/max compare in Astro's Playroom's 0xc0) still follow a single mode, as the entries below record. The mode comes from the registered shader header's `sh_registers`, the same source at registration and at draw/dispatch so the prepared artifact keys match. A title that rewrites RSRC1 at run time, or a raw compute program with no header, compiles with that mode or none. - A [WAIT_REG_MEM](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp) (libSceAgcDriver) that a compute queue passes on another queue's recorded label store, before the GPU has executed it, is landed before the queue's next dispatch, draw, COND_EXEC or predicated packet reads guest memory: the worker polls until memory satisfies the wait or no unfinished batch writes the label. The graphics queue (queue 0) is exempt, as landing its waits costs PPSA01341 about a quarter of its frame rate; a title that fills memory a graphics command buffer reads after it submits the buffer behind such a wait reads it early there. `APS5_LAND_WAITS_ALL_QUEUES=1` lands the graphics queue's waits too. @@ -134,8 +138,9 @@ - The [dispatches that follow a compute queue's wait](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp) (libSceAgcDriver), up to the next wait that memory does not satisfy, a packet that writes guest memory other than a label, or a conditional packet, are resolved (memory captured, shader recompiled) when the wait passes, as the CP reads them right after it; the graphics queue is exempt (`APS5_GROUP_CAPTURE_ALL_QUEUES=1` includes it). The device still reads guest memory when the dispatch is issued; when that memory no longer matches the capture, the dispatch is resolved again from current memory. - [PA_CL_CLIP_CNTL](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - `CLIP_DISABLE` (bit 16) and `DX_LINEAR_ATTR_CLIP_ENA` (bit 24) are accepted without changing pipeline state: Vulkan clips primitives against viewport guard bands and clips `NoPerspective` outputs linearly; what clearing bit 24 or setting bit 16 changes on the console beyond standard clipping is undocumented. - [10_11_11 unorm color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into R32_UINT attachments: the pixel shader packs each export, rounded to nearest even like format-30 image stores, which is assumed to match the CB's conversion. -- [11_11_10 float color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into R32_UINT attachments: the pixel shader packs each export toward zero like format-43 image stores (overflow to the largest finite value, negatives to zero, NaN kept), which is assumed to match the CB's conversion; only the reversed component order (COMP_SWAP 2) is accepted. -- [PA_SU_SC_MODE_CNTL.PROVOKING_VTX_LAST](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp) (libSceAgcDriver) is not measured on the console: bit 19 follows opengnm's `flatshade_first` and Vulkan's vertex selection; rect lists take flat inputs from the third vertex (first mode: first); a geometry program gets odd strip triangle i as (i+1, i, i+2) in both modes (an RX 6700 XT, first mode: (i, i+2, i+1)); in last mode its primitives take flat inputs and layer from the last index, per-vertex ones in first-mode order; a flat input without pass-through read only through `v_interp_mov_f32` P10 gives the provoking vertex's value (ps5-opengl, #1413; first mode throws). Barycentric tessellation stages give each corner the last vertex's flat values. Else last mode throws without `VK_EXT_provoking_vertex`, for tessellation, geometry-program fans, explicit per-vertex flat inputs, and strips whose pixel shader reads a vertex slot (`v_interp_mov_f32`; 16-bit interpolation unchecked). +- [11_11_10 float color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into R32_UINT attachments: the pixel shader packs each export toward zero like format-43 image stores (overflow to the largest finite value, negatives to zero, NaN kept), which is assumed to match the CB's conversion; only the standard (COMP_SWAP 0) and reversed (COMP_SWAP 2) component orders are accepted, putting red and blue in the 10-bit low field. That follows from the format's layout and from the standard swap Mesa pairs with COLOR_10_11_11 for R11G11B10_FLOAT (ac_formats.c); it was not measured on the console. +- [PA_SU_SC_MODE_CNTL.PROVOKING_VTX_LAST](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp) (libSceAgcDriver) is not measured on the console: bit 19 follows opengnm's `flatshade_first` and Vulkan's vertex selection; rect lists take flat inputs from the third vertex (first mode: first); a geometry program gets odd strip triangle i as (i+1, i, i+2) in both modes (an RX 6700 XT, first mode: (i, i+2, i+1)); in last mode its primitives take flat inputs and layer from the last index, per-vertex ones in first-mode order; a flat input without pass-through read only via `v_interp_mov_f32` P10 (P20) is the provoking vertex's (ps5-opengl, #1413; first (last) mode throws). Barycentric tessellation stages give each corner the last vertex's flat values. Else last mode throws without `VK_EXT_provoking_vertex`, for tessellation, geometry-program fans, explicit per-vertex flat inputs, and strips whose pixel shader reads a vertex slot (`v_interp_mov_f32`; 16-bit interpolation unchecked). +- [Triangle strips through the barycentric tessellation stages](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - without `geometryShader`, a triangle strip without primitive restart is expanded on the CPU into one three-point patch per triangle in Vulkan's strip order: (i, i+1, i+2), and for odd triangles (i, i+2, i+1), or (i+1, i, i+2) with `PROVOKING_VTX_LAST`. That is the order the geometry stage receives, so both routes give the same weights and per-vertex values; which order the console uses for odd triangles is not measured. Indirect strips throw. - [Dual-source blending](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) takes the MRT1 export as the second source of the MRT0 blend, as RADV and radeonsi program it; what the CB does with other export or target layouts is unknown, so they throw. - [Blending into reversed-order color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - 8_8_8_8 and 16_16_16_16 targets with `COMP_SWAP` 2 or 3 keep the exported alpha in their first channel and red or blue in their fourth, which Vulkan would blend with the color and the alpha equation respectively. The color block is taken to blend in shader export order and apply `COMP_SWAP` afterwards: the alpha equation and every source-alpha factor use the exported alpha, and the destination alpha is the first channel. This follows the `CB_TARGET_MASK` text of the AMD [SI](https://xorg.freedesktop.org/docs/AMD/old/SI_3D_registers.pdf) (p. 229) and [CIK](https://xorg.freedesktop.org/docs/AMD/old/CIK_3D_registers_v2.pdf) (p. 238) register references ("the order of components after blending and before COMP_SWAP is applied"). RADV builds `CB_BLENDn_CONTROL` without regard to the swap ([radv_pipeline_graphics.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/vulkan/radv_pipeline_graphics.c#L650-L709)) and renders R4G4B4A4, B4G4R4A4, R5G5B5A1 and B5G5R5A1 with `SWAP_STD_REV` and `SWAP_ALT_REV` ([vk_format.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/vulkan/util/vk_format.c#L43-L48), [ac_formats.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/common/ac_formats.c#L609-L624)). Mesa CI's Navi10 (gfx1010) CTS run ([job 112381553](https://gitlab.freedesktop.org/mesa/mesa/-/jobs/112381553), [112381554](https://gitlab.freedesktop.org/mesa/mesa/-/jobs/112381554)) passes all 400 `blend.format..states` cases of each of those formats, including separate alpha and destination-alpha factors. It is not measured on gfx1013 or on 8_8_8_8 and 16_16_16_16 targets. Factor pairs Vulkan applies per channel are used directly. Otherwise the pixel shader only copies values: it writes the exported alpha, each channel's own export or the constants 0 and 1 into a second color at location 0 index 1 (and 1 into a channel of the first color whose source factor is ONE, moving its export into the factor), and the blend forms every factor, `1 -` and product from those copies through `SRC1_COLOR`/`SRC1_ALPHA` factors. The blend unit therefore rounds them as it rounds a plain Vulkan blend of the same export; Vulkan does not require a second-source color to be converted like the first, but on an RTX 5080 and lavapipe every reversed-order case of the execution test is bit-identical to the same draw into a `SWAP_STD` target, which the test checks. Factor combinations that copies cannot express (for example `ONE_MINUS_SRC_ALPHA` in the alpha equation with `SRC_COLOR` in the color equation), sRGB, SNORM and integer targets, constant and second-source factors, destination-alpha and `SRC_ALPHA_SATURATE` factors in the color equation, different color and alpha operations, MIN or MAX with factors other than ONE, other color exports when the blend needs the second color, and devices without `dualSrcBlend` throw. Gran Turismo 7 (PPSA01317) blends `CB_BLEND0_CONTROL` 0x65010504 into such a target (#4782). - [DB_SHADER_CONTROL.CONSERVATIVE_Z_EXPORT](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp) (libSceAgcDriver) becomes `DepthLess`/`DepthGreater`. For a pixel shader that breaks the promise, the host driver decides whether its interpolated Z is depth-tested before shading; what the console does then was not measured. @@ -153,6 +158,7 @@ - [Cube texture descriptors](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) with equal `BASE_ARRAY` and `DEPTH` expose six faces starting at `BASE_ARRAY`. Measured with `image_load`, `image_sample` and `image_get_resinfo` on RX 6600 (gfx1032), using 64x64 R32_UINT faces and base slices 0, 5 and 6. Other ranges containing fewer than six faces still throw: hardware load bounds and filtered-sample addressing differ. A range of six faces or more that does not end on a whole cube keeps every slice up to `DEPTH`, the bound loads use on hardware; sampling the cube it cuts short is out-of-range cube-array sampling. Out-of-range cube-array sampling remains unimplemented. Samplers with `DISABLE_CUBE_WRAP` are created with `VK_SAMPLER_CREATE_NON_SEAMLESS_CUBE_MAP_BIT_EXT`, the flag RADV maps to that bit (Mesa `src/amd/vulkan/radv_sampler.c`, `src/amd/common/ac_descriptors.c`); without `VK_EXT_non_seamless_cube_map` a cube texture sampled through such a sampler throws. PS5 equivalence has not been measured on console hardware. - [Array layer views](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) (libSceAgcDriver) - `ArrayLayersServeView` lets a 2D texture at a 2D array image's base address, or a 2D array texture over its first layers, view that image (`Texture::CanCopyFrom`). Mesa-built homebrew (ps5-opengl) writes the view's last slice plus one to `CB_COLOR_ATTRIB3.MIP0_DEPTH`, so a slice-0 render target is a two-layer array image, and the title samples it as a plain 2D texture (SuperTuxKart PPSA69421 drew a black 3D scene and minimap without this). The per-layer layout of a mipmapped array is assumed equal to a standalone surface's; it is not measured on the console. A 2D texture at a later slice's address still gets an image of its own. - [Multisampled CMASK](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a multisampled color target's CMASK is decoded under CB_COLOR_INFO FAST_CLEAR (bit 13, as for a single-sample target) or COMPRESSION (bit 14). Which bit a multisampled target sets is inferred: its fast clears were seen on GTA V's multisampled targets through their CMASK fills, not in a register dump. A COMPRESSION-only target's zero CMASK is not read as cleared (inferred: zero means cleared only under FAST_CLEAR), so only a fill after its first use clears it. A slice of a multisampled array is its own image at its own address with its own CB_COLOR0_CMASK (inferred from the register layout). Its CMASK is main's whole-metablock size. +- [Compressed FMASK reads](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) without an exact resident native owner throw. Native reads require eight fragments, one slice and mip, pipe-aligned DCC/CMASK, Z_64KB_X FMASK and unchanged, disjoint watched planes. The host stores independent samples and exposes their expanded indices (`0x76543210`, AMD PAL `c5e80007`, `Gfx9Fmask::GetPackedExpandedValue`); console equivalence is unmeasured. Other layouts, standalone compressed decoding, guest publication and recipe reuse remain unsupported. Metadata writes invalidate the native view, including same-value stores. - [Multisampled CMASK state](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - a multisampled target has no guest texels, so its CMASK state comes from fills rather than from a read at each draw as a single-sample target's does. A fill of the whole CMASK with 0 clears the host image at the target's next use, one with 0xffffffff marks it expanded, and any other pattern, or a fill that covers only part of the CMASK, throws at the next use (inferred: main's rule that a mixed CMASK is unmodeled, applied to the one host image). A FAST_CLEAR target starts from its CMASK keys at its first use, and a COMPRESSION-only target starts expanded. Copies by DMA into the CMASK are not seen, and a later use with another CMASK address throws. The clear does not rewrite the CMASK bytes as expanded, so a new target over the same CMASK (a different extent, so a different image) seeds from them again and clears once more. - [Multisampled color fills](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - a multisampled target's samples are on the host, so a fill of its color memory is applied at its next use: a uniform fill over the whole of it clears the host image to that value, and a pending CMASK fast clear takes precedence over the fill and over a fill that would throw. Its color memory is a single mip and slice of addrlib's block dimensions for its sample count (ComputeThinBlockDimension). A fill over part of it, a fill reaching the start of a target whose footprint is not computed (linear), and a fill value with no exact clear value in the format (sRGB channels other than 0 and 1) throw at the next use. A fill before the target's first use is not seen (the host image starts at 0), nor are copies or shader writes into its color memory. A uniform compute fill over exactly the whole color memory with an exact clear value is not stored into guest memory, since only the host image holds the samples. A target is retired only when its whole guarded range is unmapped or its allocation removed, so if a title reuses that memory for other data while it stays mapped, a read the guard does not check (a buffer read through a V# or a pointer) sees the bytes from before the fill. - [Multisampled texture no draw rendered to](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - sampling a multisampled texture whose surface no draw rendered to creates the host target from the T# and clears it to the color a single 32-bit value repeated over the whole footprint encodes, padding up to whole tile blocks included. Inferred, not measured on the console: that the samples of such a surface are the guest color memory, as a title that clears its canvas on the CPU (ps5-opengl `ps5_clear_msaa4_color`) relies on. Memory that is not one repeated value, tile modes other than Standard4KB, Standard64KB and RenderTarget, unmapped memory and values with no exact clear color throw. CPU writes to the surface after its host target exists are not seen by the host. @@ -161,7 +167,8 @@ - [DB depth copies](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a draw with `DB_RENDER_CONTROL.DEPTH_COPY` is run as a compute pass that copies one sample of the `Z_32_FLOAT` depth surface into a single-sample R32 float color target over the whole surface. With `COPY_CENTROID` the sample is the first one the draw covers, which is sample 0 under the full `PA_SC_AA_MASK` the driver requires: [radeonsi copies one sample per draw with `COPY_CENTROID` and a one-sample mask](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-24.0.0/src/gallium/drivers/radeonsi/si_state.c#L1679-1681); the console is not measured. Stencil copies, depth tests, other depth formats, multisampled or other-format targets and copies over part of the surface throw. - [CB resolve sample count](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a draw with CB_COLOR_CONTROL in resolve mode is a CB resolve when either PA_SC_AA_CONFIG or CB_COLOR0_ATTRIB.NUM_SAMPLES is multisampled, and takes its sample count from CB0. Inferred, not from documentation: PPSA36082 resolves its 4x targets with no sample count in PA_SC_AA_CONFIG, and radeonsi sets PA_SC_AA_CONFIG to 0 for a multisampled framebuffer when rasterizer multisampling and line smoothing are off. PA_SC_AA_CONFIG is only compared with CB0 while PA_SC_MODE_CNTL_0.MSAA_ENABLE is set: PPSA08270 resolves a 4x 512x512 target with MSAA_ENABLE off and PA_SC_AA_CONFIG still at 2x (0x00108001) from earlier draws. - [Custom MSAA sample locations](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) support a uniform 1x1 pattern for 2, 4 or 8 samples through VK_EXT_sample_locations. Patterns varying across the guest's 2x2 pixel grid use a 2x2 sample location grid, anchored at the framebuffer origin like the guest's while the window offset is zero, on devices whose `maxSampleLocationGridSize` for the sample count is a multiple of 2x2. On other devices every pixel takes the pattern's pixel (0,0) locations (`PA_SC_AA_SAMPLE_LOCS_PIXEL_X0Y0_*`, its first registers and the corner the grid is anchored at), and the first such pipeline of each sample count logs `[gpu] no 2x2 sample location grid`. The RTX 5070 reports 2x4 for 2 samples, 2x2 for 4 and 1x2 for 8; an AMD Radeon integrated GPU reports 2x2 for each count, per count and for every count; an RTX 3070 Laptop GPU (driver 617.42) reports `maxSampleLocationGridSize` 1x1 for every count (`VkPhysicalDeviceSampleLocationsPropertiesEXT`), but per count (`VkMultisamplePropertiesEXT`, which the device setup reads and logs) a 2x4 grid at 2 samples, 2x2 at 4 and 1x2 at 8. So the 3070's 2x patterns, the 2x checkerboard among them, take the native grid, and the stand-in plus the exact path serve its 8-sample patterns and devices without a grid. A pattern that only reorders one set of positions between pixels, as a 2x checkerboard pattern puts its samples at x = +4/16 and -4/16 and swaps them on odd rows, is drawn exactly through that stand-in: before every render pass that draws with it each pixel's samples are put in the stand-in's order and after it back in the guest's, through a storage view of a multisampled color target and through a copy and a per-sample pass for depth (writing stencil through VK_EXT_shader_stencil_export, or one pass per bit without it), while the pixel shader reads SampleID, applies the sample mask and exports coverage in the guest's order, and the second log line names the reordering. A uniform partial sample mask that the order moves is applied in the pixel shader. With early Z, a depth or stencil test and memory writes in the pixel program, the draw throws, where #6451's stand-in alone ran it with odd rows' sample indices swapped. A fix would choose the stand-in after compilation. That costs two passes over each multisampled attachment per render pass, and on those devices every multisampled color target is created with a mutable format and storage usage for its view, which a driver may answer by not compressing it. It needs shaderStorageImageMultisample with a storage view of the color format at the sample count, sampleRateShading, and a depth format a FragDepth write gives back exactly (16-bit, or 32-bit float; not 24-bit depth, the stand-in for 16-bit depth with stencil where the device has neither); without them, and for patterns whose pixels hold different positions, the stand-in alone is used, and the log line says why. A device without shaderStorageImageMultisample takes the stand-in alone for every pass, depth-only passes included, so a depth target that a depth-only pass and a color pass both draw keeps one order. On a device that has it, a pass is still refused for a color format without a storage view at its sample count, while a depth-only pass on the same depth target is reordered, so that target's samples change order between those passes; a fix would decide the order per target. Then the top-left pixel of every 2x2 quad matches the console and the other three use its positions. A same-set pattern keeps every position and the number of samples a primitive covers in each pixel, so a resolve that averages a pixel's samples, or takes their minimum or maximum, sees the console's values, but sample i of an odd row holds what the console stores in sample 1 - i: a pass that reads the samples by index, as checkerboard reconstruction does, exchanges the two half-pixels of odd rows, which can show as combing along vertical edges, and per-sample shading, sample-mask exports and coverage inputs see the swapped indices too. A pattern whose pixels hold different positions draws pixel (0,0)'s positions in each pixel whose own differ. A device's pipelines and render pass begins take the same stand-in for a given pattern, so a sample-location-compatible depth image is not drawn with one set of locations and transitioned or tested with another. A multisampled rasterizer drawing into one-sample color targets, and a one-sample depth target if any, with MSAA_ENABLE set, no alpha-to-coverage and no coverage pixel input is drawn with one sample at sample 0's location in each pixel of the grid, as the targets keep only sample 0's coverage. Without PS_ITER_SAMPLE such a draw also refuses centroid inputs, and keeps PERSP_SAMPLE, LINEAR_SAMPLE and ANCILLARY: a pixel shaded once is taken to get sample 0's barycentrics and sample index 0. With PS_ITER_SAMPLE only sample 0's invocation reaches the targets, so its sample and centroid barycentrics are taken to be sample 0's and the draw is shaded once. Both are inferred, not measured. The pixel shader runs once per pixel with sample number 0, so ANCILLARY reads sample 0 and POS_X_FLOAT and POS_Y_FLOAT keep the position `SPI_BARYC_CNTL` names. PERSP_CENTER and LINEAR_CENTER inputs on this path are interpolated at sample 0's location on the host, where the console uses the pixel center; one-sample depth images are created with `SAMPLE_LOCATIONS_COMPATIBLE_DEPTH` when the device supports it, as multisampled ones already were; this is how VK_AMD_mixed_attachment_samples defines the case, and it is not measured on PS5 hardware. Other mixed sample counts throw. Devices without the required sample count, four subpixel bits or the full guest coordinate range refuse custom patterns. Depth images remain in GENERAL between draws, and render passes cannot continue across pattern changes. A pass that draws depth at custom sample locations ends with a transition to TRANSFER_SRC_OPTIMAL and back that names them. Guest offsets retain the existing decoder's signed four-bit, sixteenth-pixel interpretation; PS5 coverage and interpolation equivalence have not been measured on hardware. -- [Single-sample scan into a multisampled depth target](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a draw with `PA_SC_AA_CONFIG.MSAA_NUM_SAMPLES` = 0 and `PA_SC_MODE_CNTL_0.MSAA_ENABLE` clear into a multisampled depth target rasterizes the depth target's sample count with every sample at the pixel center, as a single-sample scan into a multisampled color target already does. radeonsi programs this `MSAA_ENABLE` and `PA_SC_AA_CONFIG` with the depth buffer's own sample count when GL multisampling is disabled on a multisampled framebuffer ([L1066-L1068](https://github.com/openbsd/xenocara/blob/1b39033df8d928ec247ca64448c4a63c72302db7/lib/mesa/src/gallium/drivers/radeonsi/si_state.c#L1066-L1068), [L3485-L3512](https://github.com/openbsd/xenocara/blob/1b39033df8d928ec247ca64448c4a63c72302db7/lib/mesa/src/gallium/drivers/radeonsi/si_state.c#L3485-L3512)), although its comment lists Z samples as at most the coverage samples ([L3439](https://github.com/openbsd/xenocara/blob/1b39033df8d928ec247ca64448c4a63c72302db7/lib/mesa/src/gallium/drivers/radeonsi/si_state.c#L3439)) and it sets `DB_EQAA.MAX_ANCHOR_SAMPLES` to the Z sample count, where PPSA18216 sets `DB_EQAA` to 0. GL then lets every sample take the single-sample depth ([L185-L190](https://github.com/KhronosGroup/OpenGL-Registry/blob/6af574a14089ccfee87efe230ebcdd8742859813/extensions/ARB/ARB_multisample.txt#L185-L190)) and the fragment be treated as in multisampling with full coverage ([L422-L425](https://github.com/KhronosGroup/OpenGL-Registry/blob/6af574a14089ccfee87efe230ebcdd8742859813/extensions/ARB/ARB_multisample.txt#L422-L425)), which is what this draws: each depth and stencil sample is tested and written with the depth at the pixel center. GL also lets an implementation test only a centermost sample and set every sample from it ([L425-L434](https://github.com/KhronosGroup/OpenGL-Registry/blob/6af574a14089ccfee87efe230ebcdd8742859813/extensions/ARB/ARB_multisample.txt#L425-L434)), which differs when a pixel's stored samples differ; which one the console does, whether it instead evaluates the depth plane at each sample position, and how its occlusion queries count such a draw, is not measured on hardware. Points, lines, alpha-to-coverage, `DB_EQAA` Z interpolation or overrasterization, a pixel program reading the sample index, a sample-mask export and a single-sample color target next to the multisampled depth target throw +- [Single-sample scan into a multisampled depth target](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a draw with `PA_SC_AA_CONFIG.MSAA_NUM_SAMPLES` = 0 and `PA_SC_MODE_CNTL_0.MSAA_ENABLE` clear into a multisampled depth target rasterizes the depth target's sample count with every sample at the pixel center, as a single-sample scan into a multisampled color target already does. radeonsi programs this `MSAA_ENABLE` and `PA_SC_AA_CONFIG` with the depth buffer's own sample count when GL multisampling is disabled on a multisampled framebuffer ([L1066-L1068](https://github.com/openbsd/xenocara/blob/1b39033df8d928ec247ca64448c4a63c72302db7/lib/mesa/src/gallium/drivers/radeonsi/si_state.c#L1066-L1068), [L3485-L3512](https://github.com/openbsd/xenocara/blob/1b39033df8d928ec247ca64448c4a63c72302db7/lib/mesa/src/gallium/drivers/radeonsi/si_state.c#L3485-L3512)), although its comment lists Z samples as at most the coverage samples ([L3439](https://github.com/openbsd/xenocara/blob/1b39033df8d928ec247ca64448c4a63c72302db7/lib/mesa/src/gallium/drivers/radeonsi/si_state.c#L3439)) and it sets `DB_EQAA.MAX_ANCHOR_SAMPLES` to the Z sample count, where PPSA18216 sets `DB_EQAA` to 0. GL then lets every sample take the single-sample depth ([L185-L190](https://github.com/KhronosGroup/OpenGL-Registry/blob/6af574a14089ccfee87efe230ebcdd8742859813/extensions/ARB/ARB_multisample.txt#L185-L190)) and the fragment be treated as in multisampling with full coverage ([L422-L425](https://github.com/KhronosGroup/OpenGL-Registry/blob/6af574a14089ccfee87efe230ebcdd8742859813/extensions/ARB/ARB_multisample.txt#L422-L425)), which is what this draws: each depth and stencil sample is tested and written with the depth at the pixel center. GL also lets an implementation test only a centermost sample and set every sample from it ([L425-L434](https://github.com/KhronosGroup/OpenGL-Registry/blob/6af574a14089ccfee87efe230ebcdd8742859813/extensions/ARB/ARB_multisample.txt#L425-L434)), which differs when a pixel's stored samples differ; which one the console does, whether it instead evaluates the depth plane at each sample position, and how its occlusion queries count such a draw, is not measured on hardware. Points, lines, alpha-to-coverage, `DB_EQAA` Z interpolation or overrasterization, a pixel program reading the sample index and a sample-mask export throw +- [One-sample color targets beside a multisampled depth target](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a one-sample color target drawn with a depth target of the rasterizer's sample count is taken to keep the coverage of sample 0 after the depth and stencil tests. That is the rule of [`VK_AMD_mixed_attachment_samples`](https://github.com/KhronosGroup/Vulkan-Docs/blob/193ebe195f47ffc5004683d1659e71c725658c4d/chapters/fragops.adoc#L2510-L2516), and it is not measured on hardware. The draw needs `VK_AMD_mixed_attachment_samples`, or `VK_NV_framebuffer_mixed_samples` with truncated coverage reduction, for its sample count and throws without them. With `PS_ITER_SAMPLE` the color is the output of sample 0's invocation. Overrasterization and color targets of different sample counts throw. - [DCC display buffers](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) are presented only when all keys of the console's DCC extent agree on uncompressed or a fast-clear code, padding keys included; other keys, and register-clear keys over a pending image, throw. - [Storage image write-back](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) stores the GPU results over a CPU-stamped range whose guest bytes equal the copy taken when the image's layers got their generation (images up to 1 MiB whose guest bytes were settled at that point, with no unit shadow, pending image results or recorded GPU store over them; other images keep the previous rule: the CPU's bytes win). A CPU store that rewrites the baseline value after the GPU changed it is not told apart from no store, and the GPU result wins there. The 1 MiB limit is a choice, not a console value. A range a queued draw commit still has to read is left pending whatever the stamps say ([`DrawPipeline::HeldReads`](../../../core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp)), so a title that recycles one allocation between an index buffer and a storage image sees the draw's own indices; the trade is a title that reads a surface as index data and means the image gets the pre-image bytes instead, since the reservation does not compare the command-stream positions of the draw and of the image's write. `APS5_NO_QUEUED_DRAW_READ_ORDER=1` restores the old behaviour, and the `[storage]` line counts the listings the hold turned away (`held for a queued draw read`) and the `[draw] pipelined` line the stores it held. The hold covers what the commit reads of guest memory: the index range, the vertex input ranges as their descriptor extents ([`QueuedDrawReadRanges`](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WrittenBuffers.cpp), since the bytes a draw fetches depend on the highest index, which only the commit knows) and the source of a queued DMA copy. Every other commit-time read goes through the flush hook, so it sees the bytes the hold protects. Two commit-time reads are outside the hold: the texels a register or cmask clear materialization reads from the draw's own colour target (through the hook, so ordered, but the hold does not cover a render target), and a CPU read whose store the hold turned away, which sees the pre-image bytes. Nothing retries a refused store: the image reaches guest memory with the next flush of that range (the queue worker's idle store, a `WAIT_REG_MEM` poll over it, a later read or write of it). A kept 64 KiB unit that lies outside every (slice, level) window stores nothing: the tracking grid is flat over the guest range while 3D slices taper in depth with the mip level (`HasLayer`), so the higher depth slices' higher-level blocks belong to no window. The console's tracking of such units is not measured. - [16-bit float display buffers](../../../core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp) (libSceAgcDriver, libSceVideoOut) - pixel formats `0xC001000622000000` and `0xC001000600000000` are taken as R16G16B16A16 and B16G16R16A16 float, eight bytes per texel, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/59183dbfa81ed7efdff6069177fed50de0552725/src/graphics/host_gpu/renderer/image/imageInfo.h) maps them and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/VideoOut/VideoOutTypes.cs) names them (`Rgba16Float`, `Bgra16Float`); PPSA11386 registers 3840x2160 tiled buffers of the first. The tiled layout is assumed to be the 8-byte SW_64KB_R_X equation of `TextureSwizzleEquations.hpp` applied to full coordinates, as addrlib applies it, by analogy with the 32-bit scanout; `ColorTargetLayout` applies the equations to in-block coordinates, which for 8-byte texels differs in every odd 64-row block row (address bit 11 takes y bit 6) and for 16-byte texels also differs, so a float display buffer written through `WriteColorTarget` and presented from guest memory would show those rows misplaced. The values are assumed to be linear light and are encoded with the sRGB curve for the 8-bit output. Bits 32-39 of these formats are `0x06`, as in the 10-bit formats SharpProspero lists without sRGB transfer, while its sRGB formats have `0x00`, its 10-bit PQ formats `0x04` (with `0x07` in bits 40-47) and its float BT.2100 PQ formats `0x07`: they show only that these formats are not the sRGB ones. NaN and negative values give 0, values of 1 and above give 255, alpha is linear. Neither the layout nor the transfer is verified against console or title frames, and HDR output is not modelled. DCC metadata on a float buffer throws. @@ -174,7 +181,7 @@ - [8_SRGB color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) on such a device throw unless they are single-mip 2D surfaces with a resident image; linear targets have none. - [5_6_5 color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into R5G6B5. The field holding X is in bits 0-4, Y in 5-10 and Z in 11-15, as for the other AMD formats; the component swap picks which export fills them, following `ac_translate_colorswap` in [Mesa 26.2.2 ac_formats.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/common/ac_formats.c), which programs `SWAP_STD_REV` (2) for a surface with red in bits 11-15. With `SWAP_STD_REV` red goes to bits 11-15, R5G6B5's red, so the exports are written as they are; with `SWAP_STD` (0) red goes to bits 0-4 and the exports are written with X and Z exchanged, as for 1_5_5_5. The decoded format, component mapping and write mask are tested, and an execution test draws 32_ABGR exports into tiled and linear targets in both orders and compares every stored texel with that layout on the host device; the export direction is not measured on a PS5. Only UNORM with those two component orders is accepted; `SWAP_ALT` and `SWAP_ALT_REV` keep throwing. Destination alpha reads as 1, as Vulkan defines it for a format without alpha, which is not measured on the console. Constant blend colors with `SWAP_STD` go through the `0xc6` component-swap handling, which is not tested for this format. R5G6B5 has no entry in the DCC and multisample clear-encoding tables, so a CB DCC decompress or eliminate of DCC clear codes other than 0000 and a fast clear of a multisampled 5_6_5 target throw, as for 1_5_5_5. PPSA27305 renders into this format with `SWAP_STD_REV` (discussion #6434). - [1_5_5_5 color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into A1R5G5B5. AMD formats keep the first component in the least significant bits (X in bits 0-4, Y in 5-9, Z in 10-14, W in bit 15, as in the [fpPS4](https://github.com/red-prig/fpPS4) `kSurfaceFormat1_5_5_5` doc comment and as measured for textures in the packed 16-bit textures entry), while A1R5G5B5 keeps R in bits 10-14 and B in bits 0-4, so the pixel exports are written with X and Z exchanged, which makes the stored bits follow the guest layout. The decoded format, component mapping and write mask are tested, and an execution test draws 32_ABGR exports into tiled and linear targets and compares every stored texel with the guest layout on the host device; the export direction is not measured on a PS5. Only UNORM with the standard component swap is accepted. Constant blend colors go through the same component-swap handling as other targets with that export mapping (`0xc6`), which is not tested for this format. PPSA06699 renders into this format. -- [COPY_DATA from the GPU clock](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, `SRC_SEL` 9) writes the host steady clock in 10 ns ticks when the packet runs, the same counter as the `RELEASE_MEM` timestamp, not the console's GPU clock. The `COPY_DATA` cache policy fields are accepted and have no effect. +- [COPY_DATA from the GPU clock](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, `SRC_SEL` 9) writes the host steady clock in 10 ns ticks when the packet runs, the same counter as the `RELEASE_MEM` timestamp. On a PS5 (firmware 13.40, measured 2026-10-19 on the GPU through `sceAgcDriverSubmitDcb` from inside a retail title) `SRC_SEL` 9 and the `RELEASE_MEM` DATA_SEL 3 timestamp were the same counter (0x1C and 0x5C ticks apart within one buffer), and it ran at 98.0 and 99.8 MHz over two 1.1 s intervals timed by the CPU, i.e. 100 MHz within the submission jitter, as the 10 ns ticks here; only its starting value is the host's. The `COPY_DATA` cache policy fields are accepted and have no effect. - [COPY_DATA from a register](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, `SRC_SEL` 0) copies the value the queue holds for a shader, context or user config register (`COUNT_SEL` two consecutive ones) and throws for a register with no value. A dispatch records `COMPUTE_DISPATCH_INITIATOR` and, for `DISPATCH_DIRECT`, `COMPUTE_DIM_X/Y/Z` in the shader registers; `DISPATCH_INDIRECT` clears the three counts. Neither the register values the CP writes nor the copy's ordering against in-flight dispatches have been measured on hardware. - [EVENT_WRITE stream-out statistics events](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, opcode 0x46) - `SAMPLE_STREAMOUTSTATS1`-`3` and `SAMPLE_STREAMOUTSTATS` (event types 0x01-0x03 and 0x20 in [navi10_enum.h](https://github.com/torvalds/linux/blob/0fcb05b72344f40806cbe810de8872f1698bf053/drivers/gpu/drm/amd/include/navi10_enum.h#L14155-L14188)) are accepted on the graphics queue as a 2-dword packet with event index 0 and do nothing. [nvd.h](https://github.com/torvalds/linux/blob/0fcb05b72344f40806cbe810de8872f1698bf053/drivers/gpu/drm/amd/amdgpu/nvd.h#L326-L347) documents these events only with the sampling index (3, or `EVENT_INDEX__SAMPLE_STREAMOUTSTATS*` 8-11 on gfx10); the index-0 packet has no destination dwords, and what the CP does with it is neither documented nor measured on hardware. AnyPS5 does not emulate the VGT stream-out counters, so there is nothing to sample. The sampling indices throw that sampling to memory is not implemented, and other indices, other sizes and the compute queue throw. [sceAgcDcbEventWrite](../../../core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp) ignores the address for every event type except 0x38 and 0x39, so a call with one of these event types and an address builds the index-0 packet: nothing is written and nothing throws. Whether the console's builder emits a sampling packet for an address is not known. - [INDEX_ATTRIBUTES_INDIRECT](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, opcode 0x91) loads INDEX_BASE, INDEX_BUFFER_SIZE (in indices) and INDEX_TYPE from the 16-byte record at `attribute_base + attribute_index * 16`, read when the packet runs. The record layout (address low, address high, size, type) is PAL's [`IndexAttribIndirect`](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/hw/gfxip/gfx9/gfx9Chip.h#L60-L67) and the packet is the one [`BuildIndexAttributesIndirect`](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/hw/gfxip/gfx9/gfx9CmdUtil.cpp#L2586-L2613) writes. The console's `sceAgcDcbSetIndexIndirectArgs` writes this packet, with `indirect_mode` 0, the attribute base in the address dwords and the attribute index in bits 0-15 ([measured](https://github.com/mattias800/prosper/blob/3ea5cb9a846b6dcd00f5073360462bbda4f72288/prosper/tests/data/console_oracle/agcbuild.golden.tsv#L172-L174) by the prosper console oracle); the record layout is not measured on a PS5, and neither is seen in a title. `indirect_mode` 1 (`addr_offset`, which only PAL's ExecuteIndirect path emits) throws, and so does a record whose index type is above 3, as INDEX_TYPE does. @@ -227,5 +234,7 @@ - [2D reads of 2D arrays](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - a sampled image whose shader shape is 2D and whose T# is a 2D array is built from its base slice alone: the T# is narrowed to that slice (`FirstLayerDescriptor`) before its decode, cache key and image, in stage A records, stage B bindings, revalidation and image tables. A whole-array read of the same T# keeps its own cache entry, so both images exist when a title uses both. Storage images read or written through a 2D shape are not narrowed and still build every slice the T# names. PPSA26344 reads a 3328x1872 R_X T# of 1537 slices this way: the whole array was a 12.4 GiB image that did not fit the device-local budget of an RTX 4090. - [Sampled and storage views of 1 GiB or more](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) that stay whole are only logged (sampled views by `cachedTexture`, storage views by `cachedStorageTexture`), with the registered guest range holding their base and how many of their slices fit in it; nothing is capped to that range. PPSA26344 samples a 1467 MiB cube array window of which only 838 of 2934 slices lie inside its 3380 MiB range: the rest is read from whatever follows the range. Whether such slices are ever sampled is not measured. - [Color export type against the target format](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - the export-format check refuses a `UINT16_ABGR` export (`SPI_SHADER_COL_FORMAT` 7) into a target whose color-buffer number format is not unsigned integer, but not the reverse. A 32-bit export (`32_R`, `32_GR`, `32_AR` or `32_ABGR`) into an unsigned-integer target leaves `uintExport` clear, so the recompiler declares the fragment output `float4`; `DecodeColorBuffer` maps a number format of 4 to `R32_UINT` from the color buffer alone, independent of the export format, so the float output reaches an integer attachment. Desktop drivers keep the raw bits, but the Khronos validation layer reports `Undefined-Value-ShaderFragmentOutputMismatch` and MoltenVK's Metal compiler refuses the pipeline. Which export format a title pairs with a 32-bit integer target is not measured, so the output type is left following the export format rather than the attachment. +- [Depth mip levels](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a depth view of mip `DB_DEPTH_VIEW.MIPID` in a chain of `DB_Z_INFO.MAXMIP` levels renders into that level as a surface of its own, at the level's offset and extent from `ComputeElementMipLayout` for the 64 KiB Z_X swizzle, the layout color target mips and sampled depth already use; slices of a chain step by the size of the whole chain. Taken from LEGO Builder's Journey (PPSA05496), which renders depth mip 1 of 64 KiB Z_X surfaces without HTILE; the placement was not compared with a console capture. Multisampled depth mips, other swizzle modes, levels in the packed mip tail and HTILE on a mip chain throw - [Long Windows pipeline-cache paths](../../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp): `readHeader` and the failed-read existence check still use ordinary paths. A filename beyond MAX_PATH can prevent header reuse or be reported missing; the shared content reader and journal normalization do not cover these calls. +- [DB_RENDER_CONTROL.STENCIL_CLEAR_ENABLE without a stencil plane](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver): a stencil clear on a depth target whose `DB_STENCIL_INFO` format is `STENCIL_INVALID` is dropped; the depth clear and color writes proceed. AMD PAL ([`gfx9RsrcProcMgr.cpp`](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32/src/core/hw/gfxip/rpm/gfx9/gfx9RsrcProcMgr.cpp)) and radeonsi (`si_clear.c`) never set the bit without a stencil plane, and shadPS4 drops it the same way (`vk_rasterizer.cpp`, `StencilValid`). No source states what the hardware does with the bit then; the console was not measured. A read-only stencil plane still throws diff --git a/docs/dev/debt/libSceAjm.native.md b/docs/dev/debt/libSceAjm.native.md index 2f7798c679..65ee52f866 100644 --- a/docs/dev/debt/libSceAjm.native.md +++ b/docs/dev/debt/libSceAjm.native.md @@ -7,11 +7,11 @@ - [sceAjmInstanceCreate](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - a full 14-bit instance slot namespace returns `0x80930007` (`OUT_OF_RESOURCES`), as [shadPS4's InstanceCreate](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/ajm/ajm_context.cpp) does for its own instance pool (PS4). The 16384 slots per codec prefix are this runtime's existing ID layout, not a measured console capacity; the PS5 capacity and exhaustion result are unverified. - [ATRAC9 vibration decoding](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native): channel configurations 6 (mono) and 7 (dual mono) use the pinned [vgmstream decoder](https://github.com/vgmstream/vgmstream/blob/7c520e8f9b078dfde2f84f1b5bcbad0c6c415806/src/coding/libs/atrac9_dec.c), including its 12/24/48 kHz profile limits. Synthetic fixtures cover interleaving, reset after malformed input, partial superframes and PCM16/PCM32/float output; console PCM rounding and physical haptic output are unverified. Ordinary channel configurations and discrete multichannel streams still use LibAtrac9. Decoder creation and decoding run under the existing AJM instance lock because the dependency rebuilds shared Huffman tables during initialization. - [sceAjmBatchJobGetInfo](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - writes the result and format sideband of a run job without buffers with the `SIDEBAND_FORMAT` flag (0x400000000000), as [KytyPS5](https://github.com/KytyPS5/KytyPS5) does, not confirmed on a PS5 title; the format reports bitrate 0 for ATRAC9 as in KytyPS5, while [shadPS4](https://github.com/shadps4-emu/shadPS4) derives it from the superframe size; before the instance is initialized it returns the not-initialized result (1) and no format, like the other run jobs here, while KytyPS5 returns result 0 with the format of the uninitialized decoder -- [sceAjmDecMp3ParseFrame](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - signature, result layout and rate tables taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) reimplementation of the PS4 library, not confirmed on a PS5 title: the layer field is not checked and MPEG-2.5 stops at 64 kbps; the original file length (`parse_ofl`) layout (Xing/Info + LAME tag, VBRI, Fraunhofer `0xB4` block with its CRC, encoder delay = samples per frame + LAME delay + 529) also comes from shadPS4 only, and the Fraunhofer block has no other public description +- [sceAjmDecMp3ParseFrame](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - signature, result layout and rate tables taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) reimplementation of the PS4 library, not confirmed on a PS5 title: Layer I/II headers are accepted when no original-file-length (`parse_ofl`) lookup is requested, which is supported only for Layer III; MPEG-2.5 stops at 64 kbps. The `parse_ofl` layout (Xing/Info + LAME tag after side information, with the standard fixed Xing/Info offset retained for encoders such as [LAME](https://github.com/lameproject/lame/blob/master/libmp3lame/VbrTag.c#L854-L862), VBRI at byte 36 or after side information, Fraunhofer `0xB4` block with its CRC, encoder delay = samples per frame + LAME delay + 529) also comes from shadPS4 only, and the Fraunhofer block has no other public description. - [sceAjmBatchJobGetCodecInfo, sceAjmBatchJobGetGaplessDecode](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - the `RUN_GET_CODEC_INFO` flag (0x800), the ATRAC9 codec info layout and its next frame size are taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) and [RPCSX](https://github.com/RPCSX/rpcsx) reimplementations, not confirmed on a PS5 title; a run job without buffers returns result 0, as in shadPS4. The gapless sideband reports the total and skip counts the title set and does not count them down: RPCSX does the same, shadPS4 counts them down and BryKytyPS5 returns its current counts - [AJM Opus decoder](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native, codec 24) - the initialize parameter layout and little-endian `u16` packet byte count come from The Smurfs Dreams (`2, 48000, 0`) and Lost Soul Aside (`4, 48000, 1`). The third word is interpreted as the mapping family, as in [KytyPS5's initialize structure](https://github.com/KytyPS5/KytyPS5/blob/984cfcebd141b211aa9e0a8cde63b0ebe652d07a/src/libs/ajm/opus_decoder.h#L11-L15). Family 1 uses the conventional stream counts, coupled counts and channel map from [libopus 1.5.2's surround layout](https://github.com/xiph/opus/blob/v1.5.2/src/opus_multistream_encoder.c#L47-L55), passed to FFmpeg through an [RFC 7845 section 5.1](https://datatracker.ietf.org/doc/html/rfc7845#section-5.1) header with zero pre-skip and gain; gapless skipping remains controlled by AJM sideband parameters. FFmpeg returns PCM in its native speaker order. The implicit stream layout, PCM channel order and invalid family-0 channel count result are not verified on a console. NHL 27 (PPSA34063) passes `1, 48000, 255`. Family 255 with one channel is decoded as one mono stream, the same as family 0; this is assumed, since the stream count, coupled stream count and channel mapping that a family-255 Opus header carries ([RFC 7845 section 5.1.1](https://datatracker.ietf.org/doc/html/rfc7845#section-5.1.1)) are not passed. Only 48000 Hz, mapping families 0 and 1, and family 255 with one channel are implemented; other rates and families, and family 255 with more channels, throw - [AJM codec 23](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - taken as interleaved `s16` PCM that only passes through the resampler, from Broken Sword - Shadow of the Templars: Reforged (PPSA21480); no source names a codec 23 ([shadPS4's PS4 header](https://github.com/shadps4-emu/shadPS4/blob/5aa7ffa3f4f21bba827d7865ed8f69f98f993542/src/core/libraries/ajm/ajm.h#L19-L25) ends its codec list with `Max = 23`) and the name `CODEC_PCM` is ours. The title decodes its Ogg Opus and Vorbis audio itself and, for every block, initializes the instance (flags 0x1002) with `0, 2, 44100`, sets the ratio 0.91875 (44100/48000) and decodes 235 stereo frames into a 256-frame output. The 12 initialize bytes are taken as input format (0 is `s16`, other values throw), channel count and sample rate; the rate is only reported in the format sideband; other sizes throw; zero or more than 8 channels or a zero rate return the invalid-parameter result as for Opus, not verified. A run consumes its whole input and emits every output frame whose position (output index × ratio) is below the input frame count, with the 4-point Catmull-Rom of the other codecs' resampler and the first and last input frames repeated past the edges, so each run restarts at position 0; `totalDecodedSamples` counts input frames; `s32` and float outputs are accepted although only `s16` was seen. Throwing because the console's behaviour is unknown: a run after a resampled run without an initialize or context clear in between (whether the console carries the resampler position across runs), a ratio change per sample, gapless counts, an empty input or one that is not a whole number of frames, an output that cannot hold the resampled input, run flags other than `SIDEBAND_STREAM` and initialization through `sceAjmBatchJobControl` -- [sceAjmBatchJobSetResampleParametersEx](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - the `(info, instance, float ratio_start, float ratio_change_per_sample, flags, result)` signature is inferred from the name and from `sceAjmBatchJobSetResampleParameters`; with a zero change per sample it sets the ratio as `sceAjmBatchJobSetResampleParameters` does (Watch Dogs: Legion, PPSA01500, calls it so). How a non-zero change ramps the ratio is not confirmed on a console (no source: KytyPS5 ignores it, prosper stubs it, the PS5 firmware does not export the batch builders to a payload): it is assumed to be added to the ratio after each emitted output frame, clamped at zero, and kept across batches. RIDE 4 (PPSA01599) passes one call with a change of 4.6e-7, so the exact rule matters little there. +- [sceAjmBatchJobSetResampleParametersEx](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - the `(info, instance, float ratio_start, float ratio_change_per_sample, flags, result)` signature is inferred from the name and from `sceAjmBatchJobSetResampleParameters`, and PPSA02225's one call agrees with that layout (`rdi`, `esi`, `xmm0` and `xmm1` as two single floats, `edx`, `rcx`) without showing `flags` or the float roles. With a zero change per sample it sets the ratio as `sceAjmBatchJobSetResampleParameters` does (Watch Dogs: Legion, PPSA01500, calls it so). How a non-zero change ramps the ratio is not confirmed on a console (no source: KytyPS5 ignores it, prosper stubs it, the PS5 firmware does not export the batch builders to a payload): it is assumed to be added to the ratio after each emitted output frame, clamped at zero, and kept across batches. RIDE 4 (PPSA01599) passes one call with a change of 4.6e-7, so the exact rule matters little there. - [sceAjmBatchJobControl](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - RESET (bit 13), INITIALIZE (bit 14) and the sideband input order (gapless decode, then initialize parameters) come from [shadPS4](https://github.com/shadps4-emu/shadPS4) (PS4); other flags throw - [sceAjmBatchJobControl](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - RESET is assumed to clear the context as sceAjmBatchJobClearContext does - [sceAjmBatchJobControl](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - initialize parameters are 8 bytes for ATRAC9 and M4AAC, 12 for Opus and none for MP3, as one title passes them; other codecs and sizes, and an output other than the 8-byte result, throw @@ -20,9 +20,9 @@ ### Functional +- [sceAjmDecWVorbisCreateHeaderPacket](../../../core/libs/prx/libSceAjm.native/Export.cpp) (libSceAjmWVorbis, implemented in libSceAjm.native) - refuses an output smaller than the whole packet (56 bytes plus the setup packet) with `0x8A6E0003`. The PS5 12.00 bit writer only checks that each write starts inside the buffer, so the console can accept a buffer a few bytes short and write past it - [AJM batch contexts](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native): start, wait and cancel reject context IDs not allocated by initialization; batches belong to their creating context, as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0ebea15c24253b60f514fc37a348dabc8d0bcb90/src/core/libraries/ajm/ajm.cpp). This is not verified on PS5. Finalization still does not invalidate contexts or release their instances and outstanding batches; jobs execute synchronously and cancellation does not interrupt them. -- [sceAjmDecMp3ParseFrame](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) throws when the original file length is requested (`parse_ofl` non-zero) for a layer other than III, for a CRC-protected frame, or for a VBRI header outside MPEG-1 stereo: shadPS4 looks for the tag after the CRC and after the side information, while LAME and FFmpeg place it without the CRC and VBRI always at byte 36. - [ATRAC9 gapless segments](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - a run job stops after the gapless total of output samples counted from the start of the segment, leaving the rest of its input unconsumed, and the next run starts the next segment with the decoder as it is and without a second skip. FMOD in PPSA29141 sets a total shorter than a sound's frames and reads the frames after it on later runs; with the output held at the total its stream reader waited for them forever and the last buffer repeated. The console's handling of the frames after the total is not documented; a reset (`SetGaplessDecode` with the reset flag, `ClearContext`, initialization) starts a new segment. An ATRAC9 run job without `RUN_MULTIPLE_FRAMES` decodes its whole input as one with it: FMOD in PPSA29141 puts two run jobs in one batch with the second's input right after the whole of the first's. Whether the console stops after one frame there is not verified; MP3 and Opus run jobs still stop after one frame - [AJM M4AAC decoder](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native, codec 2) - Ghost of Yotei (PPSA26344) initializes it through `sceAjmBatchJobControl`. The initialize parameters (`u32` config type, 1 ADTS or 2 raw, then `u32` sampling frequency index), instance flags (channels in bits 3-6, `ENABLE_SBR_DECODE` bit 32, `ENABLE_NONDELAY_OUTPUT` bit 33, surround interleave order bits 34-35), `{heaac, reserved}` codec info and decoder error result (`CODEC_ERROR | FATAL`) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/443f022450f6857256e780daa3d5a14029a748f7/src/core/libraries/ajm/ajm_aac.cpp) (PS4, fdk-aac) and are unverified on PS5; decoding uses FFmpeg's AAC decoder. Without `ENABLE_NONDELAY_OUTPUT`, the first two frames after initialization or reset are consumed without output. Raw input takes its channel configuration from the instance (0 means stereo) and is decoded as one access unit per run job. The format reports the last frame's bitrate. Invalid config type or raw sampling frequency index above 11 returns invalid-parameter result 4. The implementation throws for ADTS without a frame header, profiles other than AAC LC, multiple raw data blocks per ADTS frame, unsupported channel configurations, more channels than the instance supports, HE-AAC without `ENABLE_SBR_DECODE`, raw input with `RUN_MULTIPLE_FRAMES`, output room smaller than the previous frame length, and unknown codec flags. - [AJM](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp), [APR](../../../core/libs/prx/libkernel/Apr/src/Apr.cpp) and [NGS2](../../../core/libs/prx/libSceNgs2.native/src/System.cpp) state is allocated with `new` and never freed on purpose: title threads still call into these libraries while `std::exit` runs static destructors, so ordinary statics would be destroyed under them. Keep them leaked; `guest_ajm_teardown` covers the AJM case on Unix. diff --git a/docs/dev/debt/libSceAppContent.md b/docs/dev/debt/libSceAppContent.md index 371a027f51..9976365de2 100644 --- a/docs/dev/debt/libSceAppContent.md +++ b/docs/dev/debt/libSceAppContent.md @@ -6,10 +6,10 @@ - [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) - 17 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) - 16 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 -- [Temporary app-content storage](../../../core/libs/prx/libSceAppContent/Export.cpp) tracks one `/temp0` mount, preserves its files on unmount, and rejects new path resolutions until remount. Mount, format, space-query and unmount errors follow [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/sys/sys_path.pas); the pointer ABI is also declared by [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/AppContent/AppContent.cs). Existing open host descriptors remain usable after unmount; console behaviour with open files is unverified. Host filesystem failures still throw. +- [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/libSceAudio3d.md b/docs/dev/debt/libSceAudio3d.md index e83c9da9a2..9c17a838c6 100644 --- a/docs/dev/debt/libSceAudio3d.md +++ b/docs/dev/debt/libSceAudio3d.md @@ -20,11 +20,7 @@ - [sceAudio3dPortGetStatus](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature - [sceAudio3dReportRegisterHandler](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature - [sceAudio3dReportUnregisterHandler](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature -- [sceAudio3dGetSpeakerArrayMemorySize](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature -- [sceAudio3dCreateSpeakerArray](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature -- [sceAudio3dDeleteSpeakerArray](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature -- [sceAudio3dGetSpeakerArrayMixCoefficients](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature -- [sceAudio3dGetSpeakerArrayMixCoefficients2](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature +- [sceAudio3dCreateSpeakerArray](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - the speaker-array calls return the AudioOut2 result unchanged: the console translates AudioOut2 speaker-array errors into Audio3d codes, but this project's AudioOut2 speaker-array calls throw instead of returning error codes, so there is nothing to translate yet - [Audio3d bed downmix coefficients](../../../core/libs/prx/libSceAudio3d/Mixer.hpp) (libSceAudio3d) - the centre and the surrounds fold into stereo with a 0.7071 gain and the LFE channel is dropped, which follows [shadPS4](https://github.com/shadps4-emu/shadPS4) (PS4): its `DOWNMIX_CENTER` and `DOWNMIX_SURROUND` are 0.7071 and its `DOWNMIX_LFE` is 0, so the `if constexpr` that adds the LFE is compiled out. [prosper](https://github.com/mattias800/prosper) disagrees on the LFE only: it folds it into both sides with a 0.5 gain, in its AudioOut2 main bed path, which is not an Audio3d path. KytyPS5 has no Audio3d mixing at all. The absolute level is unmeasured: fpPS4 scales the front by 1 + 3/sqrt(2) instead of 1 - [Audio3d bed channel order](../../../core/libs/prx/libSceAudio3d/Mixer.hpp) (libSceAudio3d) - the buffer is read as L, R, C, LFE, Ls, Rs, Lrs, Rrs, the standard order, which three references agree on: [shadPS4](https://github.com/shadps4-emu/shadPS4) reads the front, centre, LFE and surround samples at those indices, [prosper](https://github.com/mattias800/prosper) documents FL, FR, FC, LFE and then surround pairs, and fpPS4 names them `_FL=0 _FR=1 _FC=2 _LF=3 _SL=4 _SR=5 _BL=6 _BR=7`. fpPS4's `STD` layout swaps the side and back pairs, which this library does not model - [sceAudio3dAudioOutOutput](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - the return is the number of frames per channel in the queued block, which follows [KytyPS5](https://github.com/KytyPS5/KytyPS5) and [prosper](https://github.com/mattias800/prosper), whose `sceAudioOutOutput` returns the grain, and this project's own `sceAudioOutOutput`, which returns the port's `samplesNum` (the buffer length in frames). [shadPS4](https://github.com/shadps4-emu/shadPS4) returns the total samples instead (`len * channels`) and fpPS4 returns 0; no console measurement settles it diff --git a/docs/dev/debt/libSceAudioIn.md b/docs/dev/debt/libSceAudioIn.md index 5f2f688821..abb06131ee 100644 --- a/docs/dev/debt/libSceAudioIn.md +++ b/docs/dev/debt/libSceAudioIn.md @@ -6,7 +6,6 @@ ### Unknown function info -- [sceAudioInIsPadEmbeddedMic](../../../core/libs/prx/libSceAudioIn/Export.cpp) - the PS5 module uses the low handle byte as a port index and returns whether its resolved device type equals 8, propagating negative device errors. Host capture devices have no corresponding controller-microphone classification; the protected throwing body remains unchanged. - [sceAudioInDeviceIdOpen](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature - [sceAudioInDeviceIdAsyncOpen](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature - [sceAudioInSetUsbGain](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature @@ -17,14 +16,12 @@ - [sceAudioInSetPortConnections](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature - [sceAudioInSetPortStatuses](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature - [sceAudioInGetHandleStatusInfo](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature -- [sceAudioInOpenEx](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature -- [sceAudioInHqOpenEx](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature - [sceAudioInDeviceOpenEx](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature - [sceAudioInChangeAppModuleState](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature - [sceAudioInSetSparkParam](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature - [sceAudioInSetSparkSideTone](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature -- [sceAudioInSetCompressorPreGain](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature -- [sceAudioInSetNoiseGateThreshold](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInSetCompressorPreGain](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature; the PS5 12.00 module returns 0 without reading any argument, and so does this one +- [sceAudioInSetNoiseGateThreshold](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature; the PS5 12.00 module returns 0 without reading any argument, and so does this one - [sceAudioInSetMicLevel](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature - [sceAudioInSetDeviceMuteStatus](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature - [sceAudioInSetDevConnection](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature @@ -42,5 +39,6 @@ ### Functional +- [sceAudioInIsPadEmbeddedMic](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - returns 0 for every open port: on the console it returns 1 when the port captures from the controller's built-in microphone and 0 for another input device, but ports here capture from the host's default input device - [sceAudioInHqOpen, sceAudioInAsyncOpen](../../../core/libs/prx/libSceAudioIn/Export.cpp) - PS5 wrapper validation requires 48000 Hz followed by exactly 128 samples for both calls; HqOpen then requires format 2. The host uses the same SDL capture path for both, so native asynchronous scheduling and high-quality processing are not reproduced. Common port validation, device selection and host capture pacing remain incomplete. - [sceAudioInGetGain, sceAudioInGetRerouteCount](../../../core/libs/prx/libSceAudioIn/Export.cpp) - GetGain writes 1.0f before handle validation and returns 0x80260105 for a null output, as confirmed by the PS5 module. Native handle bit checks and uninitialized/inactive port errors differ from the host port table. Native reroute counters follow device reassignment; the host opens each SDL device once and reports zero. diff --git a/docs/dev/debt/libSceAudioOut.md b/docs/dev/debt/libSceAudioOut.md index 792d6e1a9e..227ade843d 100644 --- a/docs/dev/debt/libSceAudioOut.md +++ b/docs/dev/debt/libSceAudioOut.md @@ -1,5 +1,9 @@ # libSceAudioOut technical debt +### Silent stubs + +- [sceAudioOut2LoTerminate](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Low.cpp) (libSceAudioOut) - returns 0, as the PS5 12.00 module (0x248d0) does whether or not the low-latency service runs. The module disconnects from the audio server and clears the service tables; here `sceAudioOut2LoInit` starts no service and open contexts and ports are left as they are + ### Unknown function info - [sceAudioOut2ContextAdvance and sceAudioOut2ContextPush](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp) (libSceAudioOut) - with an SDL device, Advance snapshots each port's PCM and gain; Push consumes completed grains in order and a blocking Push waits for Advance when none is ready. Inferred from PPSA26358, which rewrites the same 512-frame, 12-channel float buffer on a producer thread and pushes on another thread. Console buffer ownership, snapshot timing and empty-queue errors are unverified. Completed grains remain queued until Push consumes them, so memory use grows with an unconsumed PCM backlog. The console backlog limit is unverified. A nonblocking Push without a completed grain throws; this is a runtime restriction, not a console measurement. Context destruction concurrent with active calls remains unsupported. Without an SDL device the existing wall-clock path remains. @@ -90,18 +94,17 @@ - [sceAudioOut2MbusSetSparkParam](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2AllAudioMainToHeadphoneForDiag](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2LoPortGetStateEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature -- [sceAudioOut2LoTerminate](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) throws. It takes no arguments and returns an int, and stops the low-latency service ([SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioOut2.cs#L497-L498)), but what it does to open contexts and ports and what it returns when the service is not started are unknown, and its counterpart `sceAudioOut2Terminate` is unimplemented. - [sceAudioOut2ReportRegisterHandler](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2ReportUnregisterHandler](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2GetTvCorrectionInfo](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2GetIpcHandle](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2GetSystemStateEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature -- [sceAudioOut2MasteringInitEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2MasteringInitEx](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) (libSceAudioOut) accepts only flag bit 31, following the shared Init/InitEx validation routine in the decrypted PS5 12.00 `libSceAudioOut` analyzed for #9027. The mode is not applied because this implementation has no mastering stage; other firmware versions are unverified. - [sceAudioOut2SetDebugHeadphoneMode](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2DebugStateCtrl](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2ContextBedWrite](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2PortCreateEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature -- [sceAudioOut2InitializeEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2InitializeEx](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) (libSceAudioOut) - the PS5 12.00 module (0x2a640) stores its 32-bit argument and runs `sceAudioOut2Initialize`; the argument's meaning is unknown and it is ignored - [sceAudioOut2Terminate](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2PortRegister](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2PortUnregister](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature diff --git a/docs/dev/debt/libSceAudioPropagation.md b/docs/dev/debt/libSceAudioPropagation.md index cff417667f..ee209da65f 100644 --- a/docs/dev/debt/libSceAudioPropagation.md +++ b/docs/dev/debt/libSceAudioPropagation.md @@ -8,4 +8,4 @@ - [sceAudioPropagationSourceSetAudioPath](../../../core/libs/prx/libSceAudioPropagation/Export.cpp): the native path lookup and update confirm a 64-bit path handle, a read-only 224-byte path description and a float gain in XMM0. The description contains four 48-byte points, a point count and flags; each point contains a descriptor, room handle, gain and three position floats. The last eight bytes of each point and of the description are not consumed by these paths and remain opaque. Protected implementation remains unchanged; allocation, ownership and rendering are not implemented. - [libSceAudioPropagation](../../../core/libs/prx/libSceAudioPropagation/Export.cpp): system options use a recovered 56-byte record; the field at byte 24 and final eight bytes remain semantically unresolved. CPU memory requirements and option limits are recovered; GPU memory queries throw because their platform-specific runtime condition is unresolved. Descriptor validation, ray ownership and path generation are unimplemented. Native rendering accepts multiple mutable records; the host supports only format 2 and writes one silent frame without native accumulation. Frame capacity and source-count limits follow the recovered options. Other formats and native validation precedence remain unimplemented. -- [Initialization exports](../../../core/libs/prx/libSceAudioPropagation/Export.cpp) ending in `Init`, plus `sceAudioPropagationSystem`, are absent from the supplied native module and retain unresolved signatures. Query, locking, enumeration and report callback signatures are recovered; diagnostic layouts are recovered, but source attributes 2/3 and the constant-one fields still lack semantic names. The final system word and the fourth position/direction words have no confirmed semantics. +- [Initialization exports](../../../core/libs/prx/libSceAudioPropagation/Export.cpp) ending in `Init`, plus `sceAudioPropagationSystem`, are not exported: the SDK declares the helpers `static inline` and none is in the SDK stub library or the supplied native module. Query, locking, enumeration and report callback signatures are recovered; diagnostic layouts are recovered, but source attributes 2/3 and the constant-one fields still lack semantic names. The final system word and the fourth position/direction words have no confirmed semantics. diff --git a/docs/dev/debt/libSceCommonDialog.md b/docs/dev/debt/libSceCommonDialog.md index 0e7ac021bf..f46aaf0394 100644 --- a/docs/dev/debt/libSceCommonDialog.md +++ b/docs/dev/debt/libSceCommonDialog.md @@ -3,3 +3,7 @@ ### Silent stubs - [libSceCommonDialog](../../../core/libs/prx/libSceCommonDialog/Export.cpp) + +### 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. diff --git a/docs/dev/debt/libSceFontFt.md b/docs/dev/debt/libSceFontFt.md index 3aa05e385d..c5c609e43f 100644 --- a/docs/dev/debt/libSceFontFt.md +++ b/docs/dev/debt/libSceFontFt.md @@ -3,6 +3,7 @@ ### Functional - [Font format support](../../../core/libs/prx/libSceFontFt/Export.cpp): format queries now take the font library, validate its magic and lock it while checking its actual FreeType modules. The host registers its default modules at creation rather than enabling formats individually, so format-selection state and guest allocator failures during format registration are not reproduced. Unsupported foreign drivers throw. +- [TrueType GX support](../../../core/libs/prx/libSceFontFt/Export.cpp): the GX query checks the bundled FreeType TrueType module, whose configuration enables GX variation support. The console's response for a FreeType build without GX variation support has not been measured. ### Unknown function info diff --git a/docs/dev/debt/libSceFreeType.md b/docs/dev/debt/libSceFreeType.md index 6d4f98438d..d8ac3130a9 100644 --- a/docs/dev/debt/libSceFreeType.md +++ b/docs/dev/debt/libSceFreeType.md @@ -2,7 +2,9 @@ ### Unknown function info -- [FT_Select_Charmap, FT_Set_Charmap, FT_Get_First_Char and FT_Get_Next_Char](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - console signatures and errors are unverified; the LP64 guest adapter follows the bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftobjs.c). Character iteration uses the low 32 bits of the input code, as that version does internally, and updates the guest's selected charmap pointer without refreshing unrelated glyph or outline data. Guest charmap fields are treated as read-only copies of the native records. +- [FT_Get_Sfnt_Name_Count, FT_Get_Sfnt_Name](../../../core/libs/prx/libSceFreeType/src/Names.cpp) (libSceFreeType) - console signatures, record layout and errors are unverified; the adapter follows bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftsnames.c) and assumes the public LP64 `FT_SfntName` layout (24 bytes, string pointer at 8, byte length at 16). Strings retain their original encoding, borrow native face storage and remain valid only while the face is alive + +- [FT_Select_Charmap, FT_Set_Charmap, FT_Get_First_Char, FT_Get_Next_Char](../../../core/libs/prx/libSceFreeType/Export.cpp), [FT_Get_Glyph_Name, FT_Get_Name_Index, FT_Get_Postscript_Name](../../../core/libs/prx/libSceFreeType/src/Names.cpp) (libSceFreeType) - console signatures, naming rules and errors are unverified; the LP64 adapter follows bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftobjs.c). Iteration uses the low 32 input bits and updates only the selected guest charmap; charmap fields are read-only copies. Naming preserves LP64 invalid-index errors on Windows. PostScript names borrow native face storage and remain valid only while the face is alive - [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 @@ -13,3 +15,5 @@ - [libSceFreeType](../../../core/libs/prx/libSceFreeType/src/GuestObjects.cpp) (libSceFreeType) - private fields (`driver`, `memory`, `stream`, `sizes_list`, `extensions`) and `subglyphs` stay null. Face, size and glyph-slot records are copies. Only edits to outline points and flags are written back before `FT_Render_Glyph`; replacing arrays or exceeding the host `FT_Pos` range throws. `FT_New_Face` reads the whole file into memory and throws for files of 2 GiB or more on Windows. On macOS, native FreeType symbols remain exported and could resolve guest imports without adaptation; Windows and Linux hide them with `--exclude-libs`. - [FT_New_Memory_Face](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - on Windows, the host FT_Long is 32 bits, so buffer sizes of 2 GiB or more or below -2 GiB throw before the font buffer is read + +- [FT_Set_Char_Size](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - console ABI, FreeType version and error compatibility are unverified. The name and NID `xuY99Qcb108` are in the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceFreeTypeOt.c); the LP64 signature and behavior follow bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftobjs.c), not a console measurement. After applying its defaults and minimum-size clamp, requests whose `dimension * dpi + 36` exceeds the host's signed `FT_Long` range throw before the native call. This is a 32-bit limit on Windows; larger console requests remain unsupported. diff --git a/docs/dev/debt/libSceGameLiveStreaming.md b/docs/dev/debt/libSceGameLiveStreaming.md index e85a882199..75299aa071 100644 --- a/docs/dev/debt/libSceGameLiveStreaming.md +++ b/docs/dev/debt/libSceGameLiveStreaming.md @@ -2,10 +2,10 @@ ### Functional -- [sceGameLiveStreamingEnableLiveStreaming](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - imported by PS4 titles, which do not load without it, and added without an implementation: declared without parameters, every call throws +- [sceGameLiveStreamingEnableLiveStreaming](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) - a call outside `sceGameLiveStreamingInitialize` and `sceGameLiveStreamingTerminate` throws, as the module's error for it is not confirmed (`sceGameLiveStreamingTerminate` and `sceGameLiveStreamingGetCurrentStatus2` return `0x80A00004`; [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/src/ps4_libscegamelivestreaming.pas#L18) does not return its `SCE_GAME_LIVE_STREAMING_ERROR_NOT_INITIALIZED` from this call). Otherwise both values are accepted and change nothing, as no broadcast is ever on air. The signature, `int sceGameLiveStreamingEnableLiveStreaming(const bool)`, follows [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json#L810) and [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/src/ps4_libscegamelivestreaming.pas#L111) - [libSceGameLiveStreaming](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) - native initialization requires exactly 0x4000 heap bytes before checking repeated initialization. Host lifecycle and pointer errors now match the wrapper, but service registration, allocation failures and asynchronous service readiness are not modelled. The host becomes ready immediately after initialization. - [sceGameLiveStreamingGetCurrentStatus2](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) - the supplied module confirms the 72-byte layout and inactive status (user -1, remaining bytes zero). Live service counters, active-user changes and broadcast-resolution conversion are unimplemented. -- [sceGameLiveStreamingGetProgramInfo](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) - consumes one output pointer and uses a 4940-byte record. Field semantics remain unresolved; the protected body remains unimplemented. +- [sceGameLiveStreamingGetProgramInfo](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - the PS5 12.00 module clears the 0x134C-byte program info, asks the system service for its first 0xFD4 bytes and returns `0x80A00002` for a null pointer and `0x80A00004` before initialization. With nothing broadcast here the info stays zero, which assumes the service reports an empty program when nothing is on air; the field layout is unknown ### Unknown function info diff --git a/docs/dev/debt/libSceGnmDriverForNeoMode.md b/docs/dev/debt/libSceGnmDriverForNeoMode.md index 726f106af0..46f92c14e9 100644 --- a/docs/dev/debt/libSceGnmDriverForNeoMode.md +++ b/docs/dev/debt/libSceGnmDriverForNeoMode.md @@ -2,6 +2,8 @@ ### Unknown function info -- [libSceGnmDriverForNeoMode pending exports](../../../core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp) - 118 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceGnmDriverForNeoMode.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`. +- [libSceGnmDriverForNeoMode pending exports](../../../core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp) - 70 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceGnmDriverForNeoMode.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`. -- [libSceGnmDriverForNeoMode pending exports](../../../core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp) - 45 named callable exports were identified in the PS5 3.20 export table and cross-checked against firmware 13.60. Signatures and behavior remain unverified; declarations use the unknown-signature convention and throw through `NotImplemented_nid_no_patch`. Evidence: [3.20 export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceGnmDriverForNeoMode.c). +### Functional + +- [retail-only answers](../../../core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp) (libSceGnmDriverForNeoMode) - the debug resets, the resource and owner registration calls, the SDMA calls and the SQ counter calls return `0x8EEE00FF`, the five `sceGnmDriverInternalRetrieveGnmInterface*` calls return `0x80000000`, `sceGnmGetDbgGcHandle` returns -1, `sceGnmDriverTraceInProgress` and `sceGnmValidateOnSubmitEnabled` return 0 and the other validation calls return `0x80D13FFF`. These are the values [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/gnmdriver/gnmdriver.cpp) returns for calls it marks as not available in retail firmware; they are not measured on a console, and the arguments are not read diff --git a/docs/dev/debt/libSceHttp.md b/docs/dev/debt/libSceHttp.md index e37b75bc79..cc4d80e4e2 100644 --- a/docs/dev/debt/libSceHttp.md +++ b/docs/dev/debt/libSceHttp.md @@ -4,6 +4,7 @@ - [libSceHttp](../../../core/libs/prx/libSceHttp/Export.cpp) - no request reaches the network, so `sceHttpSetResponseHeaderMaxSize` has no response header to limit, `sceHttpSetRecvBlockSize` (`(int id, u32 blockSize)` as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.h)) no response body to read in blocks and `sceHttpRedirectCacheFlush` no redirect to forget; `sceHttpsUnloadCert` returns success like `sceHttpsLoadCert`, which keeps no certificate; `sceHttpSetRedirectCallback` and `sceHttpSetCookieRecvCallback` accept callbacks that are never called, `sceHttpsSetSslVersion` has no TLS connection to configure and `sceHttpsGetSslError` reports error 0 with detail 0; `sceHttpSetAuthInfoCallback` accepts a callback that is never called, as no server asks for authentication; `sceHttpsGetCaList` and `sceHttpsFreeCaList` report an empty CA list (`certs` null, `certsNum` 0) and return 0, as no CA store is kept, and a null list returns `ERROR_INVALID_VALUE`; unlike shadPS4 they do not check that `http_ctx_id` is a live context nor that the library is initialised, and `sceHttpsFreeCaList` also nulls `certs`, which shadPS4 leaves untouched. The signatures and the `{ void* certs; int certsNum; }` layout follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.h) (PS4), not confirmed on a PS5 title - [HTTP options without a network](../../../core/libs/prx/libSceHttp/src/Options.cpp) (libSceHttp) - `sceHttpCacheRedirectedConnectionEnabled`, `sceHttpsEnableOptionPrivate` and `sceHttpsDisableOptionPrivate` return 0 for a live template, connection or request and keep nothing, since no request reaches the network; the private options refuse flags outside `0x2dff` with `ERROR_INVALID_VALUE` before they look at the id, and `sceHttpAbortRequestForce` does what `sceHttpAbortRequest` does. Signatures, the mask and the order of the checks follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/network/http.cpp) (PS4), not measured on a PS5 +- [sceHttpSetProxy](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - returns 0 for a live template, connection or request and keeps nothing, since no request reaches the network. The signature `(id, HTTP proxy conf, WLAN proxy conf, host, port)` and the order of the checks (`ERROR_BEFORE_INIT`, then `ERROR_INVALID_VALUE` for a null host, then `ERROR_INVALID_ID`) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/network/http.cpp) (PS4), which stores the proxy; not measured on a PS5 ### Unknown function info @@ -13,7 +14,8 @@ - [sceHttpSetCookieEnabled](../../../core/libs/prx/libSceHttp/Export.cpp) - the two-argument signature and 0/1 enable values follow [VitaSDK](https://github.com/vitasdk/vita-headers/blob/master/include/psp2/net/http.h) and [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp); PPSA01521 passes 0 to disable cookies. Disabling succeeds because the offline HTTP backend has no cookie engine; enabling throws. Invalid enable values use the same error as `sceHttpSetInflateGZIPEnabled`. Like the existing offline HTTP setters, the handle is not validated; requests still fail with the network error. Console behavior is unverified - [sceHttpParseResponseHeader](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Net/Http.cs); the result, value bounds and error codes follow a payload process on a PS5 (firmware 13.60). Unmeasured there: a bare carriage return, and vertical tab or form feed around the value, which are skipped as blanks before it as before. A value end above `INT_MAX` throws instead of wrapping - [sceHttpParseStatusLine](../../../core/libs/prx/libSceHttp/src/Parse.cpp) (libSceHttp) - follows a payload process on a PS5 (firmware 13.60) for 124 lines and lengths. As on the console, `lineLen` is only checked to be at least 8 and to leave the 3 code digits (`lineLen - index < 3` in unsigned arithmetic): the version digits and the `.` and space after them are read past `lineLen`, and the line feed is searched up to and including the byte at `lineLen`, so a length that stops right before the line feed still parses. When the version ends past `lineLen` both subtractions wrap and the search goes on until a line feed; what the console does when none follows in readable memory was not measured -- [sceHttpUriMerge](../../../core/libs/prx/libSceHttp/src/Uri.cpp) (libSceHttp) - signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Net/Http.cs); validation, size formula, absolute/relative split and path join follow the disassembly-like body in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp), not confirmed on a PS5 title or hardware. The base URL goes through our `sceHttpUriParse`, which removes dot segments from the base path, while the relative part is appended unswept; base query and fragment are dropped. The relative reference is only classified by its scheme and a leading `//`, and only references with `//` are validated; as in shadPS4, `//host/x` is returned unchanged instead of taking the base scheme, a reference with a scheme but no `//` (`mailto:x`) is joined onto the base directory, and `?q` and `#f` are appended to the base directory instead of the base path. +- [sceHttpUriMerge](../../../core/libs/prx/libSceHttp/src/Uri.cpp) (libSceHttp) - signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Net/Http.cs); results, errors and `require` (the base's `sceHttpUriParse` size plus 2 and twice the two string lengths, also for a reference copied unchanged) follow a payload process on a PS5 (firmware 13.60). A reference with an authority (`//` after an optional scheme) is copied unchanged; any other, including `mailto:x`, `http:d`, `?q` and `#f`, is appended unswept to the base path up to its last `/`, and the base query and fragment are dropped. A reference that `sceHttpUriParse` rejects returns that error, which was not measured. +- [sceHttpUriParse](../../../core/libs/prx/libSceHttp/src/Uri.cpp) (libSceHttp) - follows a payload process on a PS5 (firmware 13.60) for every ASCII byte inside a scheme, userinfo and hostname, and for letters, digits, `+`, `-` and `.` at the start of a scheme, where a digit ends it; other bytes at the start of a scheme are taken as they are inside one. The userinfo ends at the first `@` after bytes from the RFC 3986 userinfo set; a hostname is letters, digits, `-`, `_` and `.` without a leading `.`, and the first byte outside it starts the path instead of failing (`http://bad host/` has the host `bad` and the path ` host/`). A port is up to 5 digits that end at `/` or the end, at most 65536, which reads as 0; an empty port takes 443 for a scheme starting with `https`, 80 for one starting with `http` (case-insensitive) and 0 otherwise. The path is swept as `sceHttpUriSweepPath` does, while the pool keeps room for the unswept path. With a pool too small the console returns `ERROR_OUT_OF_MEMORY`, leaves `require` alone and has already written part of the pool; how much is not known, so nothing is written here. - [sceHttpUriSweepPath](../../../core/libs/prx/libSceHttp/src/Uri.cpp) (libSceHttp) - follows a payload process on a PS5 (firmware 13.60), which agrees with the [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp) reimplementation of the PS4 library: it is not RFC 3986 5.2.4, a path that doesn't start with `/` is copied unchanged and a trailing `.` or `..` is kept. Like the console, the byte at `srcSize - 1` is read as the `/` that ends a `..` segment, and `srcSize == 1` with a leading `/` writes `/`; whether it also ends a `.` segment was not measured, so a `.` there is still kept as before - [sceHttpSetResponseHeaderMaxSize, sceHttpRedirectCacheFlush, sceHttpsUnloadCert](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - signatures follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp) (PS4); no PS5 reference has them - [sceHttpSetRequestStatusCallback](../../../core/libs/prx/libSceHttp/Export.cpp) - the three-argument ABI follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp). Registrations and user data are retained and removed on request deletion; the offline backend never dispatches callbacks. Invalid ids return `0x80431100`, an uncited assumption, not a verified console error. Handle hierarchy, inheritance and deleted-handle validation remain incomplete; callback statuses and dispatch timing are unknown. @@ -30,7 +32,7 @@ ### Functional -- [sceHttpUriParse, sceHttpUriBuild, sceHttpUriMerge](../../../core/libs/prx/libSceHttp/src/Uri.cpp) - console behavior for non-ASCII scheme and hostname bytes remains unmeasured. ASCII character classification and scheme case folding follow [RFC 3986 sections 3.1 and 3.2](https://www.rfc-editor.org/rfc/rfc3986#section-3.1), independently of the host locale; parsing retains the existing C-locale rejection of those bytes. +- [sceHttpUriParse, sceHttpUriBuild, sceHttpUriMerge](../../../core/libs/prx/libSceHttp/src/Uri.cpp) - bytes 0x80 to 0xFF were measured only as 0x80, 0xC3 and 0xFF in a hostname; the others, and any of them in a scheme or userinfo, are taken as the measured ASCII bytes outside those sets are. ASCII classification and scheme case folding are independent of the host locale. A bracketed host such as `[::1]` is parsed, merged and built without its brackets, as a PS5 (firmware 13.60) does; a PS4 (firmware 10.71) takes no bracketed host (`http://[::1]x/` has an empty host and the path `[::1]x/`) and keeps `[::1]:80` in a merge, as measured in #9058, so this follows the PS5 only. - [sceHttpUriEscape](../../../core/libs/prx/libSceHttp/src/Uri.cpp) escapes every byte except the ASCII unreserved set in [RFC 3986 section 2.3](https://www.rfc-editor.org/rfc/rfc3986#section-2.3), independently of the host locale. Console behaviour for non-ASCII input remains unmeasured. diff --git a/docs/dev/debt/libSceImeDialog.md b/docs/dev/debt/libSceImeDialog.md index 58f32f8d68..84726f50bc 100644 --- a/docs/dev/debt/libSceImeDialog.md +++ b/docs/dev/debt/libSceImeDialog.md @@ -6,7 +6,8 @@ ### Unknown function info -- [sceImeDialogForTestFunction, sceImeDialogGetCurrentStarState, sceImeDialogInitInternal, sceImeDialogSetPanelPosition](../../../core/libs/prx/libSceImeDialog/Export.cpp) (libSceImeDialog) - their signatures follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/ime/ime_dialog.h#L34-L51), not measured on a console. They are declared solely to resolve known imports, `sceImeDialogForTestFunction` without parameters and the others with shadPS4's parameters; this is not an ABI claim, and every call throws. +- [sceImeDialogForTestFunction](../../../core/libs/prx/libSceImeDialog/Export.cpp) (libSceImeDialog) - declared without parameters to resolve a known import, following [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/ime/ime_dialog.h#L34-L51); not measured on a console and not an ABI claim. +- [sceImeDialogGetCurrentStarState, sceImeDialogInitInternal, sceImeDialogSetPanelPosition](../../../core/libs/prx/libSceImeDialog/Export.cpp) (libSceImeDialog) - not measured on a console; signatures and checks follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/ime/ime_dialog.cpp). `sceImeDialogInitInternal` runs `sceImeDialogInit`, as shadPS4's two share one path apart from user flags this library does not keep. There is no movable panel, so `sceImeDialogSetPanelPosition` never succeeds: `DIALOG_NOT_IN_USE` without a dialog and `IME_SUSPENDING` while one is running or finished, the states in which shadPS4 refuses it too. The star state is always 0 - [sceImeDialogForceClose](../../../core/libs/prx/libSceImeDialog/Export.cpp) (libSceImeDialog) - not measured on a PS5: its `int (void)` signature and behaviour follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/ime/ime_dialog.cpp#L571-L601) (PS4). It closes a running or finished dialog, as `sceImeDialogTerm` does, without writing the typed text to the title's buffer, which shadPS4 updates while typing and this library only on Enter, and returns `0x80bc0107` with no dialog open, the `NotInUse` code of [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Dialog/ImeDialog.cs#L224-L225), while this library's `sceImeDialogTerm` returns `0x80bc0002` in the same state. Closes when: measured on a PS5 - [sceImeDialogInit, sceImeDialogGetStatus, sceImeDialogGetResult, sceImeDialogAbort, sceImeDialogTerm](../../../core/libs/prx/libSceImeDialog/Export.cpp) (libSceImeDialog) - the statuses (none 0, running 1, finished 2), the end statuses (ok 0, user canceled 1, aborted 2) and the errors of `sceImeDialogInit` for an open dialog (`0x80bc0001`), a null parameter (`0x80bc0031`), a maximum text length outside 1 to 2048 (`0x80bc0016`) and a null text buffer (`0x80bc0017`) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/b2d7ec7d31f9ad9426db93afe079db8679b3e1e0/src/core/libraries/ime/ime_dialog.cpp); none is measured on a PS5. A result, abort or term on a dialog that is not finished, running or open returns `0x80bc0002`, where shadPS4 returns `0x80bc0105` to `0x80bc0107`, and `sceImeDialogTerm` also closes a running dialog, which shadPS4 refuses. The text reaches the title's buffer only when Enter accepts it diff --git a/docs/dev/debt/libSceJson2.md b/docs/dev/debt/libSceJson2.md index 4868db14d0..706f8c4594 100644 --- a/docs/dev/debt/libSceJson2.md +++ b/docs/dev/debt/libSceJson2.md @@ -2,6 +2,7 @@ ### Unknown function info +- [JSON real-number conversion](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) uses locale-independent `to_chars` with 17 significant digits and `from_chars`, following JSON's decimal-point grammar in RFC 8259 section 6. Out-of-range input falls back to a classic-locale stream so underflow retains the prior signed-zero behavior and overflow throws. Console rounding and formatting are unverified. - [sce::Json::Parser::parse](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - a null text pointer with nonzero size throws instead of being dereferenced. A number that overflows a double (e.g. `1e309`) throws: whether the console rejects the document or stores an infinite real is unknown. Syntax validation for number tokens and destination state after a failed parse still differ from the system parser - [sce::Json::InitParameter2, InitParameterRtti and InitParameterRtti2](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - RTTI allocator adaptation and the `InitParameterRtti` layout remain unverified. `setAllocatorRtti` stores the raw allocation-parameter pointer as the allocator; `initialize` ignores allocator, buffer-size and special-float settings. Special-float enum meanings and parser support for non-finite numbers remain unresolved; serialization of non-finite values throws. Setter return types remain unverified. - [sce::Json::Object::Pair, sce::Json::Array::iterator and sce::Json::Object::iterator members](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - the signatures follow the mangled names, with `Array::iterator::operator++(int)` and `Object::iterator::operator++(int)` returning their copy through a result pointer before `this` as `begin` does, since the iterators have non-trivial destructors. `Pair(const String&, const Value&)` and default `Pair()` deep-copy or initialize into the layout `Object` iteration already hands out (key at 0, value at 0x10) and zeroes the 8 bytes between them; what the console keeps there is unknown. diff --git a/docs/dev/debt/libSceJxr.md b/docs/dev/debt/libSceJxr.md index 20faabeefb..0fbc29eb6c 100644 --- a/docs/dev/debt/libSceJxr.md +++ b/docs/dev/debt/libSceJxr.md @@ -2,4 +2,8 @@ ### Unknown function info -- [libSceJxr pending exports](../../../core/libs/prx/libSceJxr/Export.cpp) - 4 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceJxr.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`. +- [libSceJxr pending exports](../../../core/libs/prx/libSceJxr/Export.cpp) - 4 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceJxr.c) and were cross-checked against the same module in PS5 firmware 13.60. `PixelFormatLookup`, `readIS` and `writeIS` have unverified signatures and behaviour; their declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. + +### Functional + +- [PKFree](../../../core/libs/prx/libSceJxr/Export.cpp) (libSceJxr) follows Microsoft's jxrlib [JXRGlue.c](https://github.com/4creators/jxrlib/blob/f7521879862b9085318e814c6157490dd9dbbdb4/jxrgluelib/JXRGlue.c#L53-L62), whose four exported names the module shares: a non-null `ppv` has `*ppv` released with the guest `free` and set to null, and the result is always `WMP_errSuccess` (0) as a 64-bit `ERR` (`long`). That the console's module is built from jxrlib, and that its `PKFree` releases through libc's `free`, is inferred from the export names and not checked against the console's code. Closes when: the console's `PKFree` is disassembled or measured on a PS5 diff --git a/docs/dev/debt/libSceLibreSsl.md b/docs/dev/debt/libSceLibreSsl.md index b8330484a5..bcaab509a1 100644 --- a/docs/dev/debt/libSceLibreSsl.md +++ b/docs/dev/debt/libSceLibreSsl.md @@ -2,4 +2,5 @@ ### Unknown function info -- [libSceLibreSsl pending exports](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - 399 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) - 377 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); the module's LibreSSL version is unknown and nothing is checked on the console. Behaviour that differs between LibreSSL versions throws: `SHA256` with a null digest buffer (older versions return a static buffer) and `SHA512_Final` with the SHA-512/224 or SHA-512/256 digest length. 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 diff --git a/docs/dev/debt/libSceLibreSsl3.md b/docs/dev/debt/libSceLibreSsl3.md index 3fbd00e901..dba672f91f 100644 --- a/docs/dev/debt/libSceLibreSsl3.md +++ b/docs/dev/debt/libSceLibreSsl3.md @@ -2,4 +2,9 @@ ### Unknown function info -- [libSceLibreSsl3 pending exports](../../../core/libs/prx/libSceLibreSsl3/Export.cpp) - 87 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibreSsl3.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`. +- [libSceLibreSsl3 pending exports](../../../core/libs/prx/libSceLibreSsl3/Export.cpp) - 84 named callable exports, excluding `CRYPTO_memcmp`, `MD5` and `SHA1`, 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 + +- [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 throws: LibreSSL up to 3.8 returns a static buffer, later versions do not check it. Nothing is checked on the console diff --git a/docs/dev/debt/libSceNKWebKit.md b/docs/dev/debt/libSceNKWebKit.md new file mode 100644 index 0000000000..e515ec10a7 --- /dev/null +++ b/docs/dev/debt/libSceNKWebKit.md @@ -0,0 +1,5 @@ +# libSceNKWebKit technical debt + +### Known export names; ABI prototypes unverified + +- The 150 `sceNKWebKitUnknown_*_nid_no_patch` exports in [Export.cpp](../../../core/libs/prx/libSceNKWebKit/Export.cpp) are present in the PS5 3.20 `libSceNKWebKit` export inventory from [PS5-3.20_Libs](https://github.com/DNNDHH/PS5-3.20_Libs). Their NIDs are derived from exported mangled symbols. 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. diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index f10d1eb873..f9f697287e 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -12,11 +12,11 @@ - [Resolver-connect, thread creation, interface and diagnostic queries, WLAN configuration and DHCP queries](../../../core/libs/prx/libSceNet/Export.cpp) retain unsupported implementations. Their restored argument lists still reference incomplete connection, thread, interface, route, WLAN and statistics types. Kernel ioctl payloads need reconstruction from their handlers or consumers; buffer lengths alone do not establish field types. - [sceHttp2GetMemoryPoolStats, sceSslGetMemoryPoolStats, sceNetGetMemoryPoolStats](../../../core/libs/prx/libSceNet/Export.cpp) (libSceHttp2, libSceSsl, libSceNet) - the signatures `(int id, stats*)`, the layouts (pool size, peak and current use, and for Http2 and Ssl a 32-bit reserved field) and the error codes were measured on a PS5 (firmware 13.60). The pool size is the one given at init or pool creation. Http2 and Net allocate nothing from these pools. Ssl counts its certificate backend allocations and returned data against its pool; C++ bookkeeping is outside that budget. All three initially report zero use, where the console already reports 0x15C0 bytes for Ssl, 0x1640 for Http2 and 4 for a Net pool. Ssl's allocation sizes and peak usage do not reproduce the console allocator - [sceNetClearDnsCache](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the library has no DNS cache to clear because lookups use the host resolver. The host resolver cache is left unchanged. -- [sceNetEtherStrton](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - its signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Socket.cs#L737-L913) and its [Net bindings](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Http.cs#L19), not measured on a console; the throwing declaration only resolves the import. +- [sceNetEtherStrton](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - strings shorter than 17 bytes return EINVAL without changing the output, rather than reading past the terminating NUL as in the [firmware 13.60 probe](https://gist.github.com/orezraey/789290577123af3c45b340d6db6f395c/71e0a6215c7ee86f49fdb31de989dd2b50720b7a). Non-ASCII digit bytes throw because their decoding is unmeasured; other firmware versions remain unverified. Fixed character pairs, ignored separators, alphabetic values beyond f, non-alphanumeric ASCII values of 0x7f, and null-pointer EINVAL follow those measurements, not the strict [shadPS4 parser](https://github.com/shadps4-emu/shadPS4/blob/d615b3887ba29025a59242e1a26f5b68bb176158/src/core/libraries/net/net.cpp#L1274-L1297) - [sceNetGetDnsInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(info, int flags)` signature, the two IPv4 server slots in network byte order and the server count as the return value follow the PS4 [OpenOrbis musl resolver](https://github.com/OpenOrbis/musl/blob/2b0bb8ce2fa64ac255cb656785729d159a5f074c/src/network/resolvconf.c) and [OpenOrbis Net.h](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/0a1aaf9dd4a92695538bdeb09fb056d06dd11725/include/orbis/Net.h); unverified on PS5 hardware. It reports the host DNS servers of the interface `sceNetCtlGetInfo` reports, never servers set through `sceNetSetDnsInfo`. Zeroing the unused slots and returning 0 without a host IPv4 interface are unverified. Nonzero flags and a null info throw because their console results are unknown - [sceNetGetRandom](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - generates values with the host random device; the system library maintains its own seeded PRNG state. - [sceNetInetPtonEx](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(int af, const char* src, void* dst, int flags)` signature follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/net/net.cpp); IPv4 conversion and the ignored flags follow PS4 firmware 10.71, not yet confirmed on PS5. There IPv4 text is read as `inet_aton` reads it (`a`, `a.b`, `a.b.c`, hex and octal parts, values wrapping at 32 bits, the text ending at the first space, tab or VT), except that a part starting with `09` is decimal (`"0989"` is 989) and an octal part with `8` or `9` fails. Flags 0, each single bit up to 0x100, 0x10000, 0x80000000 and -1 gave the same results; other values were not measured. LF, FF and CR as terminators, and `NET_EINVAL` for a null `src` or `dst`, follow `sceNetInetPton` and were not measured for this function. IPv6 converts as `sceNetInetPton` does. -- [sceNetMemoryAllocate and sceNetMemoryFree](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - allocations use the guest heap instead of the system network pool. Pool limits, its allocation accounting and free-time fill are not reproduced. Allocation flag 4 bypasses the system pool reserve threshold; it throws here because the pool reserve is not implemented. Flag 2 zero-fills and other bits are ignored, matching the system allocator. +- [sceNetMemoryAllocate and sceNetMemoryFree](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - allocations use the guest heap instead of the system network pool. Pool limits, its allocation accounting and free-time fill are not reproduced: a reused block allocated without flag 2 holds 0xdd bytes on the console and its previous contents here. Flag 4 bypasses the system pool reserve threshold; with no pool there is no threshold, so it allocates as other flags do. What the console returns once the pool is exhausted is unmeasured; AnyPS5 returns null without setting an errno. Measured on a PS5 (firmware 13.60): a size of 0 or less returns null and leaves `sceNetErrnoLoc` unchanged, flag 2 zero-fills, flags 1, 4, 8, 0x10, 0x100 and 0x80000000 had no visible effect, and every block of 1 to 65536 bytes was 64-byte aligned - [sceNetThreadExit](../../../core/libs/prx/libSceNet/Export.cpp) is a zero-argument, non-returning void export in the native module. The protected host stub still declares int and contains `return 0`; changing it to void requires changing that protected body, so the return-type mismatch is retained. - [sceNetConfigCleanUpAllInterfaces](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the native routine consumes no arguments; the throwing implementation remains unchanged. - [sceNetDhcpdStart](../../../core/libs/prx/libSceNet/Export.cpp): six arguments and the three-argument address callback are confirmed. The 24-byte parameter contains five network-order addresses and a client limit. Individual address roles and the two unused thread-option meanings remain unresolved. @@ -26,17 +26,20 @@ ### Functional +- [sceNetUsleep](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) prints nothing for a negative count, where the console prints `[LIBNET] usleep ` to standard output before failing with `EINVAL`. What an interrupted sleep returns is unmeasured: AnyPS5 returns the result of libkernel's `sceKernelUsleep`, a cancellation point. The signed 32-bit count (the upper half of the register is ignored) and the 0 result were measured on a PS5 (firmware 13.60) - [sceNetResolverStartAton6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) reverse-resolves a 16-byte IPv6 address through the host resolver (`getnameinfo` with `NI_NAMEREQD`) into the caller's buffer of `len` bytes, as `sceNetResolverStartAton` does for IPv4. The `(rid, addr, hostname, len, timeout, retry, flags)` signature comes from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Socket.cs#L992-L997). An address with no name, or a name longer than `len - 1` bytes, fails with `RESOLVER_ENODNS` (`0x804101E1`) and leaves the buffer untouched; whether the console truncates instead is not verified. Abort flag 2 (ATON preservation) fails the next call with `EINTR` as it does `sceNetResolverStartAton`. Timeout and retry are ignored, and any nonzero flags throw because their meaning is unknown - [sceNetAccept](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) inherits the listener's non-blocking mode through the existing `SO_NBIO` setter, as the libkernel socket path does and FreeBSD's [`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L571) copies the socket state. The mode is sampled when accept starts; changes between connection creation and accept are not modelled. Not measured on a console. - [sceNetConnect to AF_UNSPEC](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) 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 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; one bound to port 0 is bound again to the wildcard address with the port it had -- [sceNetSetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) sets `SO_RCVTIMEO` and `SO_SNDTIMEO` as the host socket's timeouts. Windows sockets stay non-blocking and the library waits for readiness until the stored timeout. A stream receive that times out still reports `ETIMEDOUT` rather than FreeBSD's `EAGAIN`; a datagram receive reports `EAGAIN`. Blocking stream sends return the queued byte count on timeout, or `EAGAIN` when no bytes were queued. +- [sceNetSetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) sets `SO_RCVTIMEO` and `SO_SNDTIMEO` as the host socket's timeouts. Windows sockets stay non-blocking and the library waits for readiness until the stored timeout. Blocking stream sends return the queued byte count on timeout, or `EAGAIN` when no bytes were queued. - [sceNetSetsockopt, sceNetGetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) take `SO_SNDTIMEO` and `SO_RCVTIMEO` (0x1005, 0x1006) as a 16-byte `struct timeval`, as FreeBSD 11.4's [`sosetopt` and `sogetopt`](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/uipc_socket.c#L2657-L2690) do, and Sony's 0x1105 and 0x1106 as an `int` of microseconds. A [console log of ProsperoLight](https://github.com/blackbearreloaded/ProsperoLight/issues/49) (firmware 12.20) shows receives timing out every 100 ms after it gave 0x1006 a `{0, 100000}` timeval. The errors for a value shorter than a timeval (`EINVAL`) or out of range (`EDOM`), and `sceNetGetsockopt` cutting the timeval to the caller's length, follow FreeBSD's `sooptcopyin` and `sooptcopyout` and are not measured on a console - [sceNetSetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) stores `SO_ONESBCAST` (`0x10000`, [shadPS4's `ORBIS_NET_SO_ONESBCAST`](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/net/net_types.h)) per socket and `sceNetGetsockopt` reads back the value that was set, as shadPS4 does; PPSA34501 sets it and crashes later when it fails with `EOPNOTSUPP`. Sends ignore it. FreeBSD 11.4's [ip(4)](https://man.freebsd.org/cgi/man.cgi?manpath=FreeBSD+11.4-RELEASE&query=ip&sektion=4) documents the like-named `IPPROTO_IP` option `IP_ONESBCAST`: a datagram sent to an interface's directed broadcast address, such as 192.168.2.255, leaves through that interface with its destination rewritten to 255.255.255.255, and only on a socket allowed to set `SO_BROADCAST`. Here host routing selects the outgoing interface and the destination is not rewritten. shadPS4 also leaves sends unchanged, and its [TODO](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/net/net.cpp#L2467-L2470) reads the option the other way round: a send to 255.255.255.255 would be converted to the interface's broadcast address. Whether the console's `SOL_SOCKET` option behaves like `IP_ONESBCAST`, and what it reads back, is not verified on hardware. FreeBSD 11.4's `sogetopt()` reads several boolean socket options back as their flag bit (`so_options & optname`, `sys/kern/uipc_socket.c`), not as the value set; whether the console does so for `0x10000` is unknown - [sceNetGetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) reads `SO_ERROR` from the host socket. Winsock does not clear a socket's pending error when it is read, so on Windows the library reports a pending error once and shows it again only after a new `sceNetConnect`; a second failure with the same code on the same socket without a `sceNetConnect` in between would be hidden. - [sceNetSetsockopt, sceNetGetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) set `TCP_NODELAY` (`IPPROTO_TCP`, 1) on the host socket of a stream socket and read it back as `TF_NODELAY` (4) or 0, as FreeBSD 11.4's [`tcp_default_ctloutput`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/netinet/tcp_usrreq.c#L1626-L1650) does. Every other `IPPROTO_TCP` option (`TCP_MAXSEG`, `TCP_NOPUSH` and `TCP_KEEPIDLE` among them, which FreeBSD implements) fails with `ENOPROTOOPT`, and the `IPPROTO_TCP` level on a socket that is not a stream socket fails with `EINVAL`, as [`ip_ctloutput`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/netinet/ip_output.c#L943-L944) answers it. A set on a connection that FreeBSD's [`tcp_ctloutput`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/netinet/tcp_usrreq.c#L1498-L1502) refuses with `ECONNRESET` (refused, reset or in `TIME_WAIT`) succeeds here because the host accepts it. A null value with a nonzero length fails with `EINVAL`, where FreeBSD's `kern_setsockopt` returns `EFAULT`, and a get into less than an `int` fails with `EINVAL`, where `sooptcopyout` truncates. None of this was measured on a console. - [sceNetGetSockInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) reports 0 for the name, pid, policy, priority, queue lengths, virtual ports, state, bandwidth, drops and waits, sets no status flag except `SELF` (0x1) and `NONBLOCK` (0x200000), and reports the host's buffer sizes where none was set. An IPv6 socket fails with `EAFNOSUPPORT` and is not counted or listed; shadPS4 lists it with zero addresses. `flags` bits other than 0x40 fail with `EINVAL`, where shadPS4 ignores `flags`; 0x40 adds sockets in TIME_WAIT ([SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Socket.cs)), but closed sockets are not kept. Otherwise the `s` below 0 count and list, the return value and `EINVAL` for a null `info` or an `n` below 1 follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/06e813ff8c8e2a6d81b02cabfcc9543c3debf78e/src/core/libraries/net/net.cpp#L1498-L1522). Not measured on a console. +- [SO_SNDBUF and SO_RCVBUF validation](../../../core/libs/prx/libSceNet/Export.cpp) remains unverified on PS5, and positive sizes remain subject to host buffer limits. Nonpositive sizes fail with `EINVAL` before changing the native socket, following FreeBSD 11.4 [sosetopt](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/kern/uipc_socket.c#L2494-L2509). - [sceNetSendmsg and sceNetRecvmsg](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) copy the scatter/gather list through one host buffer and accept only the flags `sceNetSend`/`sceNetRecv` accept. Sending control data throws; a receive never returns control data (`msg_controllen` 0), since no socket option that queues it is supported +- [sceNetSocketClose, sceNetEpollControl and sceNetEpollWait](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - close and concurrent registration ordering are unmeasured on a console. Closed descriptors retire their registrations, and a wait discards snapshots invalidated by close, DEL or MOD; descriptor identity follows [Linux epoll(7)](https://man7.org/linux/man-pages/man7/epoll.7.html). ADD, MOD and DEL reject closed or invalid socket descriptors with `EBADF`, as [Linux epoll_ctl(2)](https://man7.org/linux/man-pages/man2/epoll_ctl.2.html) does; this error ordering is unmeasured on a console. Duplicate-descriptor alias lifetime is not modeled by this epoll backend. - [MSG_DONTWAIT](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - console ordering between competing readers and writers is unmeasured. The per-call behavior follows FreeBSD 11.4's `soreceive_generic` and `sosend_generic` (`sys/kern/uipc_socket.c`); `0x80` is FreeBSD's flag value (`sys/sys/socket.h`). On Windows native sockets stay non-blocking. `MSG_DONTWAIT` and guest `SO_NBIO` return one host transfer's result. Blocking stream sends queue the whole buffer with one deadline and return progress after a timeout or host error. SSL uses this path through the native owner, retaining the socket, its mode and timeouts. Readiness waits poll at most every 10 ms for local close, so idle blocked operations wake periodically. - [sceNetRecv, sceNetRecvfrom and sceNetRecvmsg](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) on a datagram socket with no local address wait on Windows until a bind, connect or send gives it one, checking the host socket every 10 ms. Winsock fails such a receive with `WSAEINVAL`, where FreeBSD 11.4's `soreceive_dgram` waits for a datagram (`sys/kern/uipc_socket.c`). A non-blocking socket fails with `EAGAIN` at once, and the receive timeout ends the wait with `EAGAIN`; when the address comes first, the call waits for a datagram only for what is left of the timeout. A `sceNetShutdown` of the receive side ends the wait and the receive returns 0, and a zero-length receive returns 0 without waiting, as `soreceive_dgram` does when the socket can't receive more or nothing was asked for. An abort or close ends the wait, and the host receive then fails as it did before - [sceNetResolverAbort and sceNetResolverStartNtoaMultipleRecords](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) follow shadPS4 ([net.cpp](https://github.com/shadps4-emu/shadPS4/blob/811bfcd7d17ed8ae975c477d9ff28abc744e1ca8/src/core/libraries/net/net.cpp), [net_resolver.cpp](https://github.com/shadps4-emu/shadPS4/blob/811bfcd7d17ed8ae975c477d9ff28abc744e1ca8/src/core/libraries/net/net_resolver.cpp)); the `(int rid, int flags)` signature of the abort also matches [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Socket.cs). Unverified on hardware. Lookups run synchronously over the host resolver, which cannot be interrupted: an abort while a lookup runs on another thread returns at once, and that lookup fails with `EINTR` without writing its output only when the host call returns; the abort flags are then ignored, as shadPS4 does for a running lookup. With nothing running, abort flag 1 (NTOA preservation) or 2 (ATON preservation) makes the next `sceNetResolverStartNtoa*` or `sceNetResolverStartAton` call on that resolver fail with `EINTR`, reported by `sceNetResolverGetError`, after its arguments are checked. Unlike shadPS4, which ignores them, other abort flag bits throw, and `sceNetResolverStartNtoaMultipleRecords` throws for any flag, including async (1) and 0x10000 (IP address literals disabled), as `sceNetResolverStartNtoaMultipleRecordsEx` does diff --git a/docs/dev/debt/libSceNetCtl.md b/docs/dev/debt/libSceNetCtl.md index 2e936ee5bc..aaa9cf2f9d 100644 --- a/docs/dev/debt/libSceNetCtl.md +++ b/docs/dev/debt/libSceNetCtl.md @@ -15,6 +15,8 @@ ### 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. Initialization, 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 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. +- [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. diff --git a/docs/dev/debt/libSceNgs2.md b/docs/dev/debt/libSceNgs2.md index 50b1203e89..a3bd2a6776 100644 --- a/docs/dev/debt/libSceNgs2.md +++ b/docs/dev/debt/libSceNgs2.md @@ -3,4 +3,4 @@ ### Unknown function info - [sceNgs2FftInit, sceNgs2FftProcess, sceNgs2FftQuerySize](../../../core/libs/prx/libSceNgs2/PendingExports.cpp) (libSceNgs2) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNgs2.c), but no credible prototypes were found in the cited sources. They are declared without parameters solely to resolve known imports and make no ABI claim. -- [sceNgs2CustomRackGetModuleInfo, sceNgs2GetWaveformFrameInfo, sceNgs2ParseWaveformUser, sceNgs2SystemEnumHandles, sceNgs2SystemEnumRackHandles, sceNgs2VoiceGetMatrixInfo](../../../core/libs/prx/libSceNgs2/PendingExports.cpp) (libSceNgs2) - names and NIDs are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNgs2.c); signatures follow [SharpProspero's Ngs2 bindings](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/Ngs2.cs), not measured on a console. Opaque structure pointers use `void*`; these stubs throw for import resolution. +- [sceNgs2CustomRackGetModuleInfo, sceNgs2ParseWaveformUser, sceNgs2SystemEnumHandles, sceNgs2SystemEnumRackHandles, sceNgs2VoiceGetMatrixInfo](../../../core/libs/prx/libSceNgs2/PendingExports.cpp) (libSceNgs2) - names and NIDs are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNgs2.c); signatures follow [SharpProspero's Ngs2 bindings](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/Ngs2.cs), not measured on a console. Opaque structure pointers use `void*`; these stubs throw for import resolution. diff --git a/docs/dev/debt/libSceNgs2.native.md b/docs/dev/debt/libSceNgs2.native.md index f5a4bd1856..3aae65845b 100644 --- a/docs/dev/debt/libSceNgs2.native.md +++ b/docs/dev/debt/libSceNgs2.native.md @@ -24,6 +24,7 @@ - [NGS2 reverb rack](../../../core/libs/prx/libSceNgs2.native/src/Reverb.cpp) (libSceNgs2.native, rack 0x2001) - follows the PS4 firmware 11.00 `libSceNgs2.sprx`, read and checked against its output at 11.025 to 48 kHz, including parameter changes during playback (differences at float rounding level); not yet compared with the PS5 library. Structure layouts come from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_reverb.h` and the param ids 0x20010000 (setup) and 0x20010001 (I3DL2) from PPSA22520. Only the default `reverb_size` of 1 is handled, with its 14400-sample reflection buffer; other sizes and the library's half-rate mode throw. System sample rates below about 7.5 kHz, where an allpass delay would have no samples, throw. Setup only accepts equal input and output channel counts - [sceNgs2ReportRegisterHandler, sceNgs2ReportUnregisterHandler](../../../core/libs/prx/libSceNgs2.native/src/System.cpp) (libSceNgs2.native, NIDs `uBIN24Tv2MI`, `nPzb7Ly-VjE`) keep a registry of report handlers. Register rejects a null handler with an invalid argument and returns `INVALID_OUT_ADDRESS` for a null out handle, then stores the report type, handler and user data under a freshly allocated opaque handle; unregister removes and frees the matching record and throws for a handle that is not registered. The signatures (report type, handler, user data, out handle) and (report handle) follow [SharpProspero](https://github.com/SvenGDK/SharpProspero) and [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_report.h`, and the 24-byte `Ngs2ReportDataHeader` passed to a handler is its `OrbisNgs2ReportDataHeader`. No report is produced yet, so no registered handler is ever invoked and PPSA04540, which imports both and registers a CPU load handler (type 5), still receives none. - [sceNgs2SystemSetSampleRate, sceNgs2SystemSetUserData, sceNgs2SystemGetUserData, sceNgs2SystemLock, sceNgs2SystemUnlock](../../../core/libs/prx/libSceNgs2.native/src/System.cpp) (libSceNgs2.native) - shadPS4 only checks the system handle (`INVALID_SYSTEM_HANDLE`) and SharpProspero names what each call does; neither gives the other limits. A sample rate of 0 and a null user data pointer throw, any other rate is accepted, and changing the rate while a sampler filter is enabled throws because the filter coefficients were computed for the old rate. `sceNgs2SystemLock` and `sceNgs2SystemUnlock` follow [prosper's notes](https://github.com/mattias800/prosper/blob/d61eb2c0ad2de879ef74df9c4aa050c512804a18/prosper/src/hle/audio/hle_audio.cpp#L4367-L4384) on the PS5 module: Lock takes a per-system lock and counts nested holds, Unlock releases one hold from any thread and returns 0 without a hold, and `sceNgs2SystemRender` only tries the lock: while another thread holds it, Render clears one grain of each output, leaves the render count and voices unchanged and returns 0. The other calls do not wait for the system lock. Peace Walker (PPSA18216) holds it while it spins on a flag that its audio thread clears only after `sceNgs2SystemRender` and eight `sceNgs2VoiceGetStateFlags` calls, so on the console neither can wait for it. Whether the console's voice and rack calls take the lock at all is not known. `sceNgs2RackLock` still holds the library's lock until `sceNgs2RackUnlock`, as before +- [sceNgs2GetWaveformFrameInfo](../../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) (libSceNgs2.native) - not measured on a console; shadPS4 only stubs it, and prosper does not cover the call. The outputs are taken to be the frame fields of the parsed waveform info, which [prosper's reading of `libSceNgs2.native.sprx`](https://github.com/mattias800/prosper/blob/30572df43ae20ff28f995db56149f7af42235741/prosper/src/hle/audio/ngs2_waveform.cpp#L224-L231) fills for ATRAC9 with the superframe bytes and samples, the frames per superframe and a one-frame delay, whatever the `fact` chunk says. `PCM_I16L` answers channels * 2 bytes, 1 sample, 1 unit and no delay, as for parsed 16-bit PCM, itself assumed. Other types throw. Null outputs return `INVALID_OUT_ADDRESS` and a bad format `INVALID_WAVEFORM_FORMAT`, as in `sceNgs2CalcWaveformBlock`; the codes are unverified - [sceNgs2RackSetUserData, sceNgs2RackGetUserData](../../../core/libs/prx/libSceNgs2.native/src/Rack.cpp) (libSceNgs2.native) - the signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/Ngs2.cs#L418-L421) and [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/ngs2/ngs2.cpp#L86-L111), which only logs them; neither gives an error. A handle that is not a live rack answers `INVALID_RACK_HANDLE` before a null user data pointer is checked, and the null pointer throws, as in `sceNgs2SystemGetUserData`. A new rack starts with 0. Not measured on a console - [sceNgs2RackGetInfo](../../../core/libs/prx/libSceNgs2.native/src/Rack.cpp) (libSceNgs2.native) - field order from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `OrbisNgs2RackInfo` with the 64-byte name of the other info structures (216 bytes); `type` is assumed to be the rack id divided by 0x1000, `min_grain_samples` 64 (the system minimum) and `state_flags` 1; `uid` comes from a per-process rack counter, `render_count` is the owning system's render count and `active_voice_count` counts voices that are not empty (paused and stopped voices included). The last process ratio and tick and `active_channel_work_count` are left zero - [sceNgs2VoiceQueryInfo](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - only info id 0x4001 is known: PPSA22520 passes an 8-byte buffer and branches on the first word being 1 or 2, so it is assumed to be the voice channel count (0 before the voice is set up); the second word is zeroed and other ids throw @@ -31,12 +32,12 @@ - [sceNgs2VoiceControl patch](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - a destination rack handle with input id 0 resolves to that rack's first voice; this matches the Dead Cells call in #4334, but selection for racks with multiple voices has not been checked against a console. Nonzero destination input ids still throw. `sceNgs2VoiceGetPortInfo` reports the resolved voice handle and zero delay. - [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 - [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) - follow the PS4 firmware 11.00 `libSceNgs2.sprx`, read and checked against its output for 2, 4, 5 and 7 speakers into 1, 2, 6 and 8 channels; not yet confirmed on the PS5 library. Speakers are taken in the order L R, L R Ls Rs, L R C Ls Rs or L R C Ls Rs Lb Rb and kept in a fixed circular order, not sorted; without angles the defaults are 90/270, 40/110/250/320, 0/40/110/250/320 and 0/40/110/150/210/250/320 degrees. As in that library, defaults converted to radians keep the wrap-around entry in degrees, so angles past the last speaker pan almost fully to it. Error codes are not known for the PS5, so invalid arguments throw, including a speaker angle equal to the unit angle, on which that library loops forever. Angles so large that adding or subtracting a turn leaves them unchanged throw for the same reason +- [sceNgs2PanInit / sceNgs2PanGetVolumeMatrix](../../../core/libs/prx/libSceNgs2.native/src/Pan.cpp) (libSceNgs2.native) - follow the PS4 firmware 11.00 `libSceNgs2.sprx`, read and checked against its output for 2, 4, 5 and 7 speakers into 1, 2, 6 and 8 channels; not yet confirmed on the PS5 library. Speakers are taken in the order L R, L R Ls Rs, L R C Ls Rs or L R C Ls Rs Lb Rb and kept in a fixed circular order, not sorted; without angles the defaults are 90/270, 40/110/250/320, 0/40/110/250/320 and 0/40/110/150/210/250/320 degrees. As in that library, defaults converted to radians keep the wrap-around entry in degrees, so angles past the last speaker pan almost fully to it. A speaker count of 0 without angles, which the firmware accepts, a non-finite speaker angle and a speaker angle equal to the unit angle, on which that library loops forever, throw. Angles so large that adding or subtracting a turn leaves them unchanged throw for the same reason - [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 -- [sceNgs2GeomCalcListener](../../../core/libs/prx/libSceNgs2.native/src/Geom.cpp) builds an orthonormal world-to-listener transform. The row-major matrix, front-axis priority and flag 1 for right-handed input follow [PS5PCEM](https://github.com/iStark/PS5PCEM/blob/d2534bac6325a2c61c0fa88977e5321074304484/src/hle/ngs2_geom.zig); they have not been measured on PS5 hardware. Velocity stays in world coordinates and positive finite sound speed is preserved. Orientation vectors are normalised and up is made perpendicular to front, so non-unit and slightly tilted vectors are accepted. When front and up are parallel, up is replaced by `(0,1,0)` or `(1,0,0)` so the basis stays defined: PPSA15552 (Dead Cells) passes `orient_front={0,0,1}` with `orient_up={0,0,1}` after moving the listener, which PS5PCEM rejects; the console error code is unknown. Unknown flags, zero orientation vectors, nonfinite input and an unrepresentable translation still throw without changing the output; console error codes are unknown, and a nonpositive sound speed throws where PS5PCEM and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/ngs2.cpp) default it to 343. KytyPS5 always writes the identity matrix, so the transform rests on PS5PCEM plus that Dead Cells case. Dead Cells (PPSA15552) reaches this call after the custom sampler rack in #4334. This does not implement source attenuation, panning or Doppler in `sceNgs2GeomApply`. +- [sceNgs2GeomCalcListener](../../../core/libs/prx/libSceNgs2.native/src/Geom.cpp) builds an orthonormal world-to-listener transform. The row-major matrix, front-axis priority and flag 1 for right-handed input follow [PS5PCEM](https://github.com/iStark/PS5PCEM/blob/d2534bac6325a2c61c0fa88977e5321074304484/src/hle/ngs2_geom.zig); they have not been measured on PS5 hardware. Velocity stays in world coordinates and positive finite sound speed is preserved. Orientation vectors are normalised and up is made perpendicular to front, so non-unit and slightly tilted vectors are accepted. When front and up are parallel, up is replaced by `(0,1,0)` or `(1,0,0)` so the basis stays defined: PPSA15552 (Dead Cells) passes `orient_front={0,0,1}` with `orient_up={0,0,1}` after moving the listener, which PS5PCEM rejects; the console error code is unknown. Zero orientation vectors, nonfinite input and an unrepresentable translation still throw without changing the output; console error codes are unknown, and a nonpositive sound speed throws where PS5PCEM and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/ngs2.cpp) default it to 343. KytyPS5 always writes the identity matrix, so the transform rests on PS5PCEM plus that Dead Cells case. Dead Cells (PPSA15552) reaches this call after the custom sampler rack in #4334. This does not implement source attenuation, panning or Doppler in `sceNgs2GeomApply`. - [NGS2 custom sampler rack](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, rack 0x4001) - implements bounded support for the custom sampler rack and its built-in module 0x13 (Custom Voice Filter), unblocking PPSA15552 (Dead Cells) startup. Structure layouts come from [shadPS4's ngs2_custom.h](https://raw.githubusercontent.com/shadps4-emu/shadPS4/main/src/core/libraries/ngs2/ngs2_custom.h) (OrbisNgs2CustomVoiceFilterParam, 40 bytes) and live trace correlation from [prosper's hle_audio.cpp](https://raw.githubusercontent.com/mattias800/prosper/main/prosper/src/hle/audio/hle_audio.cpp) (0x40001300 command and parameters like 10000.0f and 0.707f). Routes parameter updates via the 0x40001300 command family into the Biquad filter engine. Filter type 0 clears the destination filter; type 1 configures the RBJ low-pass biquad frequency, Q and level. Other filter types and unmodeled configurations throw. - [sceNgs2VoiceGetState custom sampler](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native, rack 0x4001) - only the size and one field come from a title. PPSA15552 (Dead Cells) calls it on custom sampler voices with a 48-byte buffer at two sites (eboot `0x1740f90`, `0x1741009`) and reads only the 64-bit value at offset 16: the first site keeps it and the second subtracts the kept value, so it is filled with the voice's decoded sample count. Other sizes return `INVALID_OUT_SIZE` as for the other racks. The state flags fill the common voice state; the remaining fields stay zero. shadPS4's `OrbisNgs2CustomSamplerVoiceState` is about 80 bytes and does not match the size this title passes, so the field names and the console's value are unverified -- [sceNgs2CalcWaveformBlock](../../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) (libSceNgs2.native) - VAG (0x1c) throws. ATRAC9 keeps AnyPS5's own mapping: it checks the config against the channels and rate (0x804A8431), leaves out the decoder delay from the skip and size, leaves both 0 for zero samples and computes the size in 64 bits, while [prosper reads the PS5 module](https://github.com/mattias800/prosper/blob/40d95db32efe1ad00106a5b9a84e018fffa9ecff/prosper/src/hle/audio/ngs2_waveform.cpp#L348-L396) without the check, with the delay, filling both and modulo 2^32. Linear PCM and float (0x10 to 0x1b), the null checks and the 0x804A8432 of other types follow that reading and [its entry point](https://github.com/mattias800/prosper/blob/40d95db32efe1ad00106a5b9a84e018fffa9ecff/prosper/src/hle/audio/ngs2_waveform.cpp#L444-L466). Closes when: VAG is mapped and ATRAC9 follows the module +- [sceNgs2CalcWaveformBlock](../../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) (libSceNgs2.native) - ATRAC9 keeps AnyPS5's own mapping: it checks the config against the channels and rate (0x804A8431), leaves out the decoder delay from the skip and size, leaves both 0 for zero samples and computes the size in 64 bits, while [prosper reads the PS5 module](https://github.com/mattias800/prosper/blob/40d95db32efe1ad00106a5b9a84e018fffa9ecff/prosper/src/hle/audio/ngs2_waveform.cpp#L348-L396) without the check, with the delay, filling both and modulo 2^32. Linear PCM and float (0x10 to 0x1b), VAG (0x1c: whole 28-sample frames of 16 bytes per channel, the frame-rounded sample count and no skip), the null checks and the 0x804A8432 of other types follow that reading and [its entry point](https://github.com/mattias800/prosper/blob/40d95db32efe1ad00106a5b9a84e018fffa9ecff/prosper/src/hle/audio/ngs2_waveform.cpp#L444-L466). Closes when: ATRAC9 follows the module ### Functional diff --git a/docs/dev/debt/libSceNpAuth.md b/docs/dev/debt/libSceNpAuth.md index 9f41ec750a..98b7bde5c9 100644 --- a/docs/dev/debt/libSceNpAuth.md +++ b/docs/dev/debt/libSceNpAuth.md @@ -4,6 +4,8 @@ - [sceNpAuthAbortRequest, sceNpAuthGetAuthorizationCode, sceNpAuthGetAuthorizationCodeA, sceNpAuthGetAuthorizationCodeV3, sceNpAuthGetIdToken, sceNpAuthGetIdTokenA, sceNpAuthGetIdTokenV3, sceNpAuthPollAsync, sceNpAuthWaitAsync](../../../core/libs/prx/libSceNpAuth/Export.cpp) (libSceNpAuth) - request tracking, `ORBIS_NP_AUTH_ERROR_REQUEST_NOT_FOUND` (0x80550306) on unknown IDs, `ORBIS_NP_AUTH_ERROR_INVALID_ID` (0x80550307) on non-async or unprocessed requests in Poll/Wait, `ORBIS_NP_AUTH_ERROR_INVALID_ARGUMENT` (0x80550301) on null arguments, and `ORBIS_NP_AUTH_ERROR_ABORTED` (0x80550304) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_auth/np_auth.cpp) (PS4); the A calls forward to their V3 counterparts, which in shadPS4 ([AuthorizationCode A](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_auth/np_auth.cpp#L189-L193), [V3](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_auth/np_auth.cpp#L195-L199); [IdToken A](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_auth/np_auth.cpp#L375-L378), [V3](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_auth/np_auth.cpp#L380-L383)) call the same helper and differ only in a version flag; async requests defer `SCE_NP_ERROR_SIGNED_OUT` to Poll/Wait while sync requests fail immediately; PSN is not contacted. Parameter size validation and PS5 behavior are unverified +- [sceNpAuthPollAsync and sceNpAuthWaitAsync](../../../core/libs/prx/libSceNpAuth/Export.cpp) (libSceNpAuth) - PS5 abort-before-start behavior is unmeasured. Poll/Wait keep returning `ORBIS_NP_AUTH_ERROR_INVALID_ID` without changing output after a pre-start abort, including subsequent rejected getter calls, following [shadPS4 StartRequest and FindAsyncRequest](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_auth/np_auth.cpp). + ### Functional - [sceNpAuthSetTimeout](../../../core/libs/prx/libSceNpAuth/Export.cpp): native validation and storage are implemented: retry must be nonnegative, the all-zero tuple is invalid, resolve timeout is zero or at least 1000000, and other timeouts are zero or at least 10000000. Invalid arguments precede request lookup. Stored settings currently have no transport to configure because authentication remains offline. diff --git a/docs/dev/debt/libSceNpCppWebApi.md b/docs/dev/debt/libSceNpCppWebApi.md index be7f650ac6..f18925c392 100644 --- a/docs/dev/debt/libSceNpCppWebApi.md +++ b/docs/dev/debt/libSceNpCppWebApi.md @@ -2,10 +2,11 @@ ### Silent stubs -- [sce::Np::CppWebApi::Common](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - the `finish` overrides of `TransactionBase<...>` (8 instantiations), `DownStreamTransaction<...>` (2 instantiations) and `Vector>::pushBack` return success without doing work; a failed request never produces a transaction to finish +- [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::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](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - `DataApi::ParameterToDownloadData::{initialize,terminate}` succeed without storing state +- [sce::Np::CppWebApi::UserProfile::V1, ProfanityFilter::V2, CommunicationRestrictionStatus::V3](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - parameter bookkeeping succeeds without storing state: `FriendsApi::ParameterToGetFriends`, `PresenceApi::ParameterToGetBasicPresences`, `BlocksApi::ParameterToGetBlockingUsers`, `ProfanityApi::ParameterToFilterProfanity` and `CommunicationRestrictionStatusApi::ParameterToGetCommunicationRestrictionStatus`, each `{ctor,dtor,initialize,terminate}` ### Unknown function info @@ -14,4 +15,5 @@ - [sce::Np::CppWebApi::Common::String](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - the 0x20-byte layout (32-bit reference count, buffer at 0x08, buffer size at 0x10, `LibContext*` at 0x18) comes from the module bundled with one title (PPSA21564); other SDK versions are not checked. `append` grows the buffer with the host allocator and the destructor frees it, instead of going through the `LibContext` allocator. `pushBack` into `Vector` stores a deep copy, so a destroyed temporary cannot invalidate the stored element; a title that byte-copies a `String` with a live buffer would still double-free, as nothing increments the reference count - [sce::Np::CppWebApi::Common::Vector](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - imported by PPSA03671, whose dump ships no `libSceNpCppWebApi.prx`. A `Vector` handle is an `IntrusivePtr` over a shared body; bodies created by `pushBack` use a host-side layout with the reference count at 0x30, matching the adopted-body convention measured on PPSA21564. The pointer/deleter constructor adopts a title-provided body released through the stored deleter; `pushBack` on such a body throws, since its element layout is unknown. `begin`, `end`, iterators and `operator[]` throw: the only `Vector`s the title reads back come from failed transactions, which never produce one - [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 diff --git a/docs/dev/debt/libSceNpManager.md b/docs/dev/debt/libSceNpManager.md index b7d03f1c33..a484f93b3a 100644 --- a/docs/dev/debt/libSceNpManager.md +++ b/docs/dev/debt/libSceNpManager.md @@ -6,7 +6,7 @@ ### Unknown function info -- [libSceNpManager pending exports](../../../core/libs/prx/libSceNpManager/Export.cpp) - 150 named callable exports are present in the local `libSceNpManager.sprx` export table and the public PS5 3.20 symbol list. Their signatures and behavior are unverified; each declaration uses the no-parameter unknown-signature convention and throws through `NotImplemented_nid_no_patch`. +- [libSceNpManager pending exports](../../../core/libs/prx/libSceNpManager/Export.cpp) - 143 named callable exports are present in the local `libSceNpManager.sprx` export table and the public PS5 3.20 symbol list. Their signatures and behavior are unverified; each declaration uses the no-parameter unknown-signature convention and throws through `NotImplemented_nid_no_patch`. - [sceNpGetSandboxInfo](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager, NID `ZVZ++68nNI8`) - the name hashes to the imported NID (found with nidhunt, confirmed with `tools/nid_names.py`). STAR WARS: Galactic Racer (PPSA28416) imports it; its OnlineSubsystemPS5 calls it with one pointer to a 0x50-byte stack buffer and reads the buffer as a string (discussions/3086). The returned string, the exact parameter struct and console errors are unknown, so every call throws without reading or writing caller memory. A working implementation belongs with that title's bring-up. - [sceNpUnknown_8weiQyIdI3A](../../../core/libs/prx/libSceNpManager/Export.cpp): the original name remains unknown. The native wrapper takes a user ID and a read-only 20-byte online-ID record, validates its first byte and sends the record through IPC. The implementation remains a throwing stub. @@ -18,6 +18,7 @@ ### Functional - [sceNpGetAccountCountry, sceNpGetAccountDateOfBirth, sceNpGetAccountLanguage, sceNpGetParentalControlInfo](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - PS4 forms taking an online id, not measured on a console. Signatures and checks follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/81be949167d0060b08f26dc3e055a0b640d129c8/src/core/libraries/np/np_manager.cpp): a null pointer is `INVALID_ARGUMENT`; no user is signed in, so no online id matches one and the rest return `USER_NOT_FOUND` without writing, like `sceNpCheckNpAvailability`. Request ids are not checked +- [sceNpCheckNpAvailabilityA, sceNpGetAccountDateOfBirthA, sceNpGetAccountLanguageA, sceNpGetGamePresenceStatus, sceNpGetGamePresenceStatusA, sceNpGetParentalControlInfoA](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - answer as if no PSN account is signed in; the signed-in behaviour is not implemented and none of it is measured on a console. Checks follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/c2814041e546bf75c12756dbec9da911c721ce1c/src/core/libraries/np/np_manager.cpp): a null pointer or user id -1 is `INVALID_ARGUMENT`, then request id 0 is `INVALID_ARGUMENT` and an unknown one `REQUEST_NOT_FOUND`. Other users get `SIGNED_OUT` with nothing written, as from `sceNpGetAccountCountryA`; shadPS4's `sceNpGetAccountDateOfBirthA` returns `USER_NOT_FOUND` for a user it does not know. An async request gets `SIGNED_OUT` from the call; shadPS4 returns 0 and leaves it to `sceNpPollAsync`. The presence queries write `OFFLINE` (0) and return 0 - [sceNpGetUserIdByOnlineId](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - the `(online id, user id output)` signature follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/81be949167d0060b08f26dc3e055a0b640d129c8/src/core/libraries/np/np_manager.cpp#L703): a null online id or output returns `SCE_NP_ERROR_INVALID_ARGUMENT`, and a signed-out NP returns `SCE_NP_ERROR_SIGNED_OUT` without writing the output, as `sceNpGetUserIdByAccountId` does - [sceNpCheckPlus](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - the `(request id, parameter, result)` signature follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/81be949167d0060b08f26dc3e055a0b640d129c8/src/core/libraries/np/np_manager.cpp#L246): request id 0 or a null parameter or result returns `SCE_NP_ERROR_INVALID_ARGUMENT`, and every other call returns `SCE_NP_ERROR_SIGNED_OUT` without writing the result, as `sceNpCheckPremium` does. The parameter's size and feature fields, which shadPS4 checks, are not read: the errors the console returns for them are not known - [sceNpGetAccountId, sceNpSetGamePresenceOnline](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - PS4 forms; the `(online id, account id output)` and `(request id, online id, presence)` signatures follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/81be949167d0060b08f26dc3e055a0b640d129c8/src/core/libraries/np/np_manager.cpp#L1092-L1096). A null online id or output, or request id 0 for `sceNpSetGamePresenceOnline`, returns `SCE_NP_ERROR_INVALID_ARGUMENT`. `sceNpSetGamePresenceOnline` then returns `SCE_NP_ERROR_REQUEST_NOT_FOUND` for a request that was never created or was deleted, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/c1b26558352b863f80142256e4df38585aa7d727/src/core/libraries/np/np_manager.cpp#L1098-L1104) does. Otherwise both return `SCE_NP_ERROR_SIGNED_OUT` without writing, as `sceNpGetAccountIdA` does. The console's result for a signed-out user is not measured @@ -25,5 +26,6 @@ - [sceNpCheckNpAvailability](../../../core/libs/prx/libSceNpManager/Export.cpp): native NP initialization and online-ID lookup are not modeled. The offline host returns user-not-found for valid arguments after clearing the optional one-byte availability output; request ID 0 and a null online-ID pointer return invalid-argument before writing the output. - [NP state callbacks](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - the registration rules are [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L857-L901)'s: `sceNpRegisterStateCallbackA` keeps up to 8 callbacks and returns the slot plus 1, a null callback returns `INVALID_ARGUMENT`, a callback already registered `CALLBACK_ALREADY_REGISTERED` and a ninth one `CALLBACK_MAX` (0x8055001D, [np_error.h](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_error.h#L38)); `sceNpUnregisterStateCallbackA` returns `INVALID_ARGUMENT` for an id outside 1..8 and `CALLBACK_NOT_REGISTERED` for an empty slot; the legacy [sceNpRegisterStateCallback](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L981-L1005) keeps one callback with the same errors. Each registration gets `SIGNED_OUT` for the initial user (0x10000000) once, from the next `sceNpCheckCallback` on the calling thread. For the A form this follows [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/network.cpp#L3959-L3974), whose [sceNpRegisterStateCallbackA](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/network.cpp#L4021-L4034) marks the registration pending. KytyPS5 never calls the legacy callback ([sceNpRegisterStateCallback](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/network.cpp#L4009-L4019) stores nothing); it is called as `(user, SIGNED_OUT, nullptr, userdata)`, as in shadPS4, which passes an NpId only for `SIGNED_IN` ([L823](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L823), [L938-L940](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L938-L940)). shadPS4 instead queues `SIGNED_OUT` from the user service login event and delivers it from `sceNpCheckCallback` ([DispatchPendingNpStateCallbacks](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L903-L962)); the reachability callback is not given an initial `UNAVAILABLE`, unlike that function ([L922-L961](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L922-L961)). A callback being delivered when another thread unregisters it is still called once. When and how often the console calls the state callbacks, and the errors, are not measured - [sceNpRegisterGamePresenceCallback](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - accepts any callback, null included, and returns nothing; no game presence event is delivered because the NP model remains signed out. [Kyty](https://github.com/InoriRus/Kyty/blob/4733b7e1c91b10554a52007903d74dc76c39a230/source/emulator/src/Network.cpp#L1549-L1552) ignores the callback the same way, and [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/8d50471b7dc141dd700b51276b8edd1f293bce26/src/core/libraries/np/np_manager.cpp#L1121-L1126) keeps the last one without calling it. Not measured on a PS5 +- [sceNpIsPlusMember and sceNpSetContentRestriction](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - the signatures and checks follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/np/np_manager.cpp): a null output or restriction is `INVALID_ARGUMENT`, a restriction size other than 0x18 is `INVALID_SIZE` (0x80550011), and a negative default age, more than 256 age restrictions or a count without an array is `INVALID_ARGUMENT`. Any user is reported as not a Plus member, and a valid restriction is accepted and not stored. Not measured on a PS5 -- The following pending declarations have unverified signatures and behavior; they are parameterless import-resolution declarations and throw through `NotImplemented_nid_no_patch`: [`sceNpCheckCallbackForLib`](../../../core/libs/prx/libSceNpManager/Export.cpp). +- [sceNpCheckCallbackForLib](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - not measured on a PS5: it delivers the pending state callbacks like `sceNpCheckCallback`, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/np/np_manager.cpp) does with its pending state events; shadPS4's `sceNpCheckCallback` also runs other registered NP callbacks, which this library does not keep diff --git a/docs/dev/debt/libSceNpTrophy.md b/docs/dev/debt/libSceNpTrophy.md new file mode 100644 index 0000000000..c445f4e8d8 --- /dev/null +++ b/docs/dev/debt/libSceNpTrophy.md @@ -0,0 +1,5 @@ +# libSceNpTrophy technical debt + +### Unknown function info + +- [sceNpTrophyRegisterContext, sceNpTrophyGetTrophyUnlockState, sceNpTrophyUnlockTrophy](../../../core/libs/prx/libSceNpTrophy/Export.cpp) (libSceNpTrophy, PS4) - the trophy definitions in `TROPCONF.ESFM` are encrypted, so the trophy count is the number of consecutive `TROPnnn.PNG` icons in the title's `/app0/sce_sys/trophy/trophyNN.trp`, and the calls that return names and details (`sceNpTrophyGetGameInfo`, `sceNpTrophyGetGroupInfo`, `sceNpTrophyGetTrophyInfo`) throw. Unlocks are kept per user and service label in `_trophy/trophyNN.` under the working directory, no platinum trophy is reported (`platinumId` is -1), and a locked trophy's icon fails with `TROPHY_NOT_UNLOCKED`. None of it is checked on a console. diff --git a/docs/dev/debt/libSceNpTrophy2.md b/docs/dev/debt/libSceNpTrophy2.md index 4576f12de6..e6cb5cd197 100644 --- a/docs/dev/debt/libSceNpTrophy2.md +++ b/docs/dev/debt/libSceNpTrophy2.md @@ -14,7 +14,7 @@ - [libSceNpTrophy2 pending exports](../../../core/libs/prx/libSceNpTrophy2/Export.cpp) - 23 named callable exports were identified in the PS5 3.20 export table and cross-checked against firmware 13.60. Signatures and behavior remain unverified; declarations use the unknown-signature convention and throw through `NotImplemented_nid_no_patch`. Evidence: [3.20 export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpTrophy2.c). ### Functional -- [Trophy queries](../../../core/libs/prx/libSceNpTrophy2/src/TrophyInfo.cpp) report the title's [trophy data](../../../core/libs/prx/libSceNpTrophy2/src/TrophyPackage.cpp) (`sce_sys/trophy2/trophyNN.ucp`) and the unlocks in the [trophy store](../../../core/libs/prx/libSceNpTrophy2/src/TrophyStore.cpp). Without the file, or when it is malformed, they report the old placeholders and `sceNpTrophy2GetTrophyInfoArray` lists only trophy 0. Icons are not read. Hidden trophies report their names and descriptions; whether the console blanks them before the unlock is not known. `progress_percentage` weighs gold 6, silver 2 and bronze 1 and leaves the platinum out, as KytyPS5 does (the ratio of the published PSN trophy points); the console's formula is not known. A trophy or group that is not in the file throws. A context that was not created or was destroyed reports placeholders and no unlocks and returns success; the console's errors for it are unknown. +- [Trophy queries](../../../core/libs/prx/libSceNpTrophy2/src/TrophyInfo.cpp) - not confirmed on the console: `sceNpTrophy2GetTrophyInfo` of a trophy id that is not in the title's [trophy data](../../../core/libs/prx/libSceNpTrophy2/src/TrophyPackage.cpp) (`sce_sys/trophy2/trophyNN.ucp`) returns `0x8055390A` (KytyPS5's `NP_TROPHY2_ERROR_INVALID_TROPHY_ID`) and writes nothing; a context not created or destroyed reports placeholders and no unlocks and returns success; hidden trophies report their names and descriptions before the unlock; `progress_percentage` weighs gold 6, silver 2 and bronze 1 and leaves the platinum out, as KytyPS5 does (the ratio of the published PSN trophy points). A group that is not in the data throws. Without the file, or when it is malformed, the queries report the old placeholders and `sceNpTrophy2GetTrophyInfoArray` lists only trophy 0. Icons are not read. Unlocks come from the [trophy store](../../../core/libs/prx/libSceNpTrophy2/src/TrophyStore.cpp). - [Trophy data limits](../../../core/libs/prx/libSceNpTrophy2/src/TrophyPackage.cpp): a `trophyNN.ucp` or `udsNN.ucp` larger than 256 MiB or listing more than 4096 files, and a JSON file in it that nests deeper than 64 levels or holds more than 1048576 values, are not used, and the title runs as without that file. The limits keep the memory the reader takes to the file and about 140 MiB of parsed JSON; the console's limits are not known. - [Text language](../../../core/libs/prx/libSceNpTrophy2/src/TrophyPackage.cpp): texts come from `tropmeta_.json` for the console language, then `en-US`, then the file's `defaultLanguage`. The tags for languages 0 to 29 are KytyPS5's; `uk-UA` for 30 (Ukrainian) is an assumption. - [Platinum](../../../core/libs/prx/libSceNpTrophy2/src/TitleTrophies.cpp): an unlock through any path unlocks the platinum that the trophy names in `platinumTrophyId` once every trophy naming that platinum is unlocked, as KytyPS5 does; trophies without `platinumTrophyId` are not required. With the title's trophy data loaded, a posted `_UnlockTrophy` of a trophy that is not in the data, of a platinum or of a progressive trophy is not stored and prints a `[trophy] ignored` line, as KytyPS5 refuses them, so a progressive trophy unlocks only through its statistic; without trophy data any id of 0 or more is stored, including ids the data would make the platinum, a progressive trophy or no trophy, and the platinum is not unlocked when the trophies it needs are. The console's behaviour is not known. diff --git a/docs/dev/debt/libSceNpTus.md b/docs/dev/debt/libSceNpTus.md index 141e50a1cc..cb413bbf34 100644 --- a/docs/dev/debt/libSceNpTus.md +++ b/docs/dev/debt/libSceNpTus.md @@ -4,88 +4,19 @@ - [TUS variable outputs](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - the layouts of the variable arrays of the `sceNpTusGetMultiSlotVariable*` calls and of `outVariable` in `sceNpTusAddAndGetVariableAAsync` are not verified, so the output is never written and `variableArraySize` / `outVariableSize` are not checked; shadPS4, and RPCS3 in its PS3 `sceNpTusGetMultiSlotVariable*` and `sceNpTusAddAndGetVariable*` calls, return `INVALID_ALIGNMENT` unless the size is `arrayLen` variables of the call's type, or one variable for `outVariable`. The signatures and the community error codes follow [shadPS4](https://github.com/shadps4-emu/shadPS4/tree/main/src/core/libraries/np/np_tus), which fpPS4 agrees with for the context and request calls; no console measurement. - [Multi-slot variable calls](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - the argument checks of the multi-slot get, set and delete calls are not measured on a console; they follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/c1b26558352b863f80142256e4df38585aa7d727/src/core/libraries/np/np_tus/np_tus.cpp), where they differ between functions: `arrayLen < 1` is `INSUFFICIENT_ARGUMENT` in the NpId and virtual-user delete calls and in `sceNpTusGetMultiSlotVariableVUserAsync`, and `INVALID_ARGUMENT` in the others; a null NpId is accepted except in delete; an empty virtual user ID is `INVALID_ARGUMENT`. RPCS3's [PS3 module](https://github.com/RPCS3/rpcs3/blob/1a9dca3a3a84a18df866e3021918322bd295160a/rpcs3/Emu/Cell/Modules/sceNpTus.cpp) has only the NpId and virtual-user calls; it returns `INVALID_ARGUMENT` for an empty array in all of them, rejects a null target in get and set and does not check for an empty virtual user ID. +- [Variable, data, data status and TSS requests](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - the argument checks of the 82 add-and-get, try-and-set, data, data status, multi-user, friends and `sceNpTssGetSmallStorage` calls are not measured on a console; their signatures, checks and order follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/np/np_tus/np_tus.cpp), including its differences between forms: the NpId try-and-set calls return `INVALID_ARGUMENT` for an operation type outside 1-6 where the others return `INVALID_TYPE`, and the NpId and virtual-user multi-slot data status calls return `TOO_MANY_NPID` for more than 64 slots. As for the calls above, the output and structure sizes are not checked, and every request finishes with `SCE_NP_ERROR_SIGNED_OUT` - [sceNpTusSetThreadParam](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTssCreateNpTitleCtx](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTssCreateNpTitleCtxA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature - [sceNpTusCreateTitleCtx](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature - [sceNpTusSetTimeout](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature - [sceNpTusChangeModeForOtherSaveDataOwners](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiUserVariable](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiUserVariableVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiUserVariableAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiUserVariableVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetFriendsVariable](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetFriendsVariableAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusAddAndGetVariable](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusAddAndGetVariableVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusAddAndGetVariableAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusAddAndGetVariableVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusTryAndSetVariable](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusTryAndSetVariableVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusTryAndSetVariableAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusTryAndSetVariableVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusDeleteMultiSlotData](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature - [sceNpTusDeleteMultiSlotDataVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusDeleteMultiSlotDataAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature - [sceNpTusDeleteMultiSlotDataVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusSetDataAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusSetDataVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetDataAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetDataVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiSlotDataStatus](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiSlotDataStatusVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiSlotDataStatusAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiSlotDataStatusVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiUserDataStatus](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiUserDataStatusVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiUserDataStatusAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiUserDataStatusVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetFriendsDataStatus](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetFriendsDataStatusAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTssGetSmallStorage](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTssGetSmallStorageAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature - [sceNpTssGetStorage](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature - [sceNpTssGetStorageAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature - [sceNpTssGetDataAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusAddAndGetVariableA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusAddAndGetVariableAVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusAddAndGetVariableAVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusAddAndGetVariableForCrossSave](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusAddAndGetVariableForCrossSaveAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusAddAndGetVariableForCrossSaveVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusAddAndGetVariableForCrossSaveVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiSlotDataStatusA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiSlotDataStatusAAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiSlotDataStatusAVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiSlotDataStatusAVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiSlotDataStatusForCrossSave](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiSlotDataStatusForCrossSaveAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiSlotDataStatusForCrossSaveVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiSlotDataStatusForCrossSaveVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetDataA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetDataAAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetDataAVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetDataAVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetDataForCrossSave](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetDataForCrossSaveAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature - [sceNpTusGetDataForCrossSaveVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature - [sceNpTusGetDataForCrossSaveVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiUserVariableA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiUserVariableAAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiUserVariableAVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiUserVariableAVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature - [sceNpTusGetMultiUserVariableForCrossSave](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiUserVariableForCrossSaveAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiUserVariableForCrossSaveVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiUserVariableForCrossSaveVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiUserDataStatusA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiUserDataStatusAAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiUserDataStatusAVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiUserDataStatusAVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiUserDataStatusForCrossSave](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiUserDataStatusForCrossSaveAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiUserDataStatusForCrossSaveVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusGetMultiUserDataStatusForCrossSaveVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature - [sceNpTusGetFriendsVariableA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature - [sceNpTusGetFriendsVariableAAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature - [sceNpTusGetFriendsVariableForCrossSave](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature @@ -94,23 +25,10 @@ - [sceNpTusGetFriendsDataStatusAAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature - [sceNpTusGetFriendsDataStatusForCrossSave](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature - [sceNpTusGetFriendsDataStatusForCrossSaveAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusSetDataA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusSetDataAAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusSetDataAVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusSetDataAVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusDeleteMultiSlotDataA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusDeleteMultiSlotDataAAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusTryAndSetVariableA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusTryAndSetVariableAAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusTryAndSetVariableAVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusTryAndSetVariableAVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusTryAndSetVariableForCrossSave](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusTryAndSetVariableForCrossSaveAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusTryAndSetVariableForCrossSaveVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature -- [sceNpTusTryAndSetVariableForCrossSaveVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature - [sceNpTusDeleteNpTitleCtx](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - the PS5 signature, error codes and child-request lifetime have not been measured on a console. Deletion invalidates the context and all its requests, following [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e7626e3c7de8d0f0fda6a91f620b42f6c762/src/core/libraries/np/np_tus/np_tus.cpp#L158-L180). ### Functional - [sceNpTusCreateNpTitleCtxA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - creates a title context without checking sign-in, up to 32 contexts (`TOO_MANY_OBJECTS` beyond); shadPS4 returns the `sceNpGetNpId` error instead. Requests are limited to 256, and the limits are not measured on a console. +- [sceNpTssCreateNpTitleCtx and sceNpTssCreateNpTitleCtxA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - that TSS title contexts share the TUS context table and its 32-context limit is inferred from [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/615bcba5e21b82dc59588575f7ecbce8da09ca2c/src/core/libraries/np/np_tus/np_tus.cpp#L146-L168), which redirects both to the TUS creators, and from TSS requests coming from `sceNpTusCreateRequest`; nothing is measured on a console. The `A` form is `sceNpTusCreateNpTitleCtxA`. The `NpId` form returns `INSUFFICIENT_ARGUMENT` for a null `npId` and `INVALID_ARGUMENT` for service label `0xFFFFFFFF` before the limit, as shadPS4's [sceNpTusCreateNpTitleCtx](https://github.com/shadps4-emu/shadPS4/blob/615bcba5e21b82dc59588575f7ecbce8da09ca2c/src/core/libraries/np/np_tus/np_tus.cpp#L122-L143) does; the signatures are shadPS4's and fpPS4's, and Orbital's for the `A` form. - [sceNpTusPollAsync, sceNpTusWaitAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - there is no PSN, so every implemented `*Async` call whose arguments pass the checks starts a request that finishes at once with `SCE_NP_ERROR_SIGNED_OUT`, which poll and wait report and the blocking calls return; shadPS4 returns `SIGNED_OUT` from the `*Async` call itself when no shadNet client is signed in, and RPCS3 returns `INVALID_ONLINE_ID` while offline, marked there as probable behaviour. An abort stays set, so every later operation on the request, blocking ones included, finishes with `ABORTED`; in shadPS4 an abort only ends the operation in progress. Until an operation has started, poll returns 1 and wait returns `INVALID_ARGUMENT` as in shadPS4; RPCS3 [blocks until a result is set](https://github.com/RPCS3/rpcs3/blob/1a9dca3a3a84a18df866e3021918322bd295160a/rpcs3/Emu/NP/np_contexts.cpp#L40-L52). diff --git a/docs/dev/debt/libSceNpWebApi.md b/docs/dev/debt/libSceNpWebApi.md new file mode 100644 index 0000000000..95fb314c74 --- /dev/null +++ b/docs/dev/debt/libSceNpWebApi.md @@ -0,0 +1,6 @@ +# libSceNpWebApi technical debt + +### Unknown function info + +- [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. shadPS4 also rejects an invalid service label for titles built with SDK 2.00 or later; AnyPS5 has no compiled-SDK version for libraries yet, so such a call returns `SIGNED_IN_USER_NOT_FOUND` instead of `INVALID_ARGUMENT`. `sceNpWebApiDeleteServicePushEventFilter` therefore never finds a filter. diff --git a/docs/dev/debt/libScePad.md b/docs/dev/debt/libScePad.md index d533ccb116..c8fa44cf9d 100644 --- a/docs/dev/debt/libScePad.md +++ b/docs/dev/debt/libScePad.md @@ -17,6 +17,7 @@ - [scePadVrControllerReadCombinedState](../../../core/libs/prx/libScePad/Export.cpp) - the two handle arguments and 128-byte output are recovered; output field semantics remain incomplete. - [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 - [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 @@ -26,4 +27,4 @@ ### Functional -- [Touchpad resolution](../../../core/libs/prx/libScePad/include/PadState.hpp) (libScePad) - the library reports a DualSense to every title, so the controller information reports its touchpad, 1920 by 1080 at 44.86 pixels per cm, and the host controller's fingers and the keyboard touch contacts are placed in that range. [duaLib](https://github.com/WujekFoliarz/duaLib/blob/0b39f92261e2ff0e77eb0dac00943be6970e0dbf/src/source/duaLib.cpp#L847-L849) reports these values for a DualSense and 1920 by 943 at 44 for a DualShock 4, and the Linux kernel's [hid-playstation](https://github.com/torvalds/linux/blob/cd4f494ba535e848e2d299707d363de84e61a518/drivers/hid/hid-playstation.c#L177-L178) holds 1920 by 1080 against 1920 by 942 for the DualShock 4. A DualShock 4 on the host reports through the DualSense range too, so its last touchpad rows are never reached. The console's values are not measured +- [Touchpad resolution](../../../core/libs/prx/libScePad/include/PadState.hpp) (libScePad) - the library reports a DualSense to every title, so the controller information reports its touchpad, 1920 by 1080 at 44.86 pixels per cm, and the host controller's fingers and the keyboard touch contacts are placed in that range. [duaLib](https://github.com/WujekFoliarz/duaLib/blob/0b39f92261e2ff0e77eb0dac00943be6970e0dbf/src/source/duaLib.cpp#L847-L849) reports these values for a DualSense and 1920 by 943 at 44 for a DualShock 4, and the Linux kernel's [hid-playstation](https://github.com/torvalds/linux/blob/cd4f494ba535e848e2d299707d363de84e61a518/drivers/hid/hid-playstation.c#L177-L178) holds 1920 by 1080 against 1920 by 942 for the DualShock 4. A DualShock 4 on the host reports through the DualSense range too, so its last touchpad rows are never reached. A PS5 on firmware 13.40 reported 1920 by 1080 at 44.86 and stick dead zones of 13 to a retail title diff --git a/docs/dev/debt/libScePosixForWebKit.md b/docs/dev/debt/libScePosixForWebKit.md index 681a24a3d5..39e20c0542 100644 --- a/docs/dev/debt/libScePosixForWebKit.md +++ b/docs/dev/debt/libScePosixForWebKit.md @@ -3,7 +3,8 @@ ### Unknown function info - [gethostbyname](../../../core/libs/prx/libScePosixForWebKit/Resolver.cpp) (libScePosixForWebKit) - numeric IPv4 forms, trailing whitespace and 64-bit accumulation follow FreeBSD 11.4 [inet_aton](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libc/inet/inet_addr.c); clearing the resolver error on numeric success follows [fakeaddr](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libc/net/gethostnamadr.c). Exact PS5 behavior is unverified; hostname lookup still uses the host resolver -- [__wrap_access, __wrap_clock_gettime, __wrap_fchown, __wrap_getcwd, __wrap_getenv, __wrap_link, __wrap_lstat, __wrap_mmap, __wrap_readlink, __wrap_statvfs, __wrap_symlink, clock_gettime_np, getenv_np, gethostbyname_r (`scePosixForWebKit_gethostbyname_r`), mmap_np, msetname, pthread_setschedparam_np, setenv_np, sysinfo (`scePosixForWebKit_sysinfo`)](../../../core/libs/prx/libScePosixForWebKit/Export.cpp) (libScePosixForWebKit) - names/NIDs appear in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libScePosixForWebKit.c) and [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv); credible prototypes are unknown. These exports use renamed helpers; defining `gethostbyname_r` would interpose on the host function this library calls itself. All are parameterless import stubs, not ABI claims; callers must not use them until signatures are verified. +- [clock_gettime_np, getenv_np, gethostbyname_r (`scePosixForWebKit_gethostbyname_r`), mmap_np, msetname, pthread_setschedparam_np, setenv_np, sysinfo (`scePosixForWebKit_sysinfo`)](../../../core/libs/prx/libScePosixForWebKit/Export.cpp) (libScePosixForWebKit) - names/NIDs appear in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libScePosixForWebKit.c) and [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv); credible prototypes are unknown. These exports use renamed helpers; defining `gethostbyname_r` would interpose on the host function this library calls itself. All are parameterless import stubs, not ABI claims; callers must not use them until signatures are verified. +- [__wrap_fchown, __wrap_link, __wrap_readlink, __wrap_statvfs, __wrap_symlink](../../../core/libs/prx/libScePosixForWebKit/Export.cpp) (libScePosixForWebKit) - parameterless throwing stubs. Under the `--wrap` convention assumed for the forwarding wrappers (see Functional) each would take the POSIX signature of the function it wraps, but the targets are in libkernel_sys (`fchown`, `link`, `readlink`, `symlink`) and libSceLibcInternal (`statvfs`), which no prx links; libkernel_sys's `fchown`, `readlink` and `symlink` also still throw for some arguments ### Functional @@ -11,3 +12,4 @@ - [arc4random](../../../core/libs/prx/libScePosixForWebKit/Export.cpp) (libScePosixForWebKit) returns four bytes of host OS entropy (`getentropy`, `rand_s` on Windows), as `arc4random_buf` does, uniform over the 32-bit range like FreeBSD 11.4's RC4 keystream in [arc4random.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/arc4random.c). The console's generator and its reseeding are unknown; `arc4random_uniform`, `arc4random_stir` and `arc4random_addrandom` are not in the module's export set and are absent - [fnmatch](../../../core/libs/prx/libScePosixForWebKit/Export.cpp) (libScePosixForWebKit) follows FreeBSD 11 [fnmatch.c](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libc/gen/fnmatch.c) in the C locale: each byte is one character, `FNM_CASEFOLD` folds ASCII letters only, and bracket ranges compare byte values. `[:class:]` names are not character classes: as in FreeBSD 11 through 14, `[[:digit:]]` is the set of `[`, `:`, `d`, `i`, `g`, `t` followed by a literal `]` (classes were added to `fnmatch.c` after 14). Multibyte characters and locale collation are not modelled. A null pattern or string throws - [getservbyname](../../../core/libs/prx/libScePosixForWebKit/Resolver.cpp) (libScePosixForWebKit) searches a built-in copy of 37 lines of FreeBSD 11.4's [etc/services](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/etc/services) (ftp-data, ftp, ssh, telnet, smtp, domain, tftp, http, pop3, ntp, imap, https, rtsp, submission, imaps, pop3s with their aliases and protocols, in file order) the way [getservent.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/net/getservent.c) searches the file: case-sensitive name or alias, a null protocol takes the first match, the port in network byte order, the result in per-thread storage. The console's `/etc/services` is unknown, so any other service returns null without setting `errno`, as for a name the file lacks. `getaddrinfo` still resolves service names through the host's service database +- [__wrap_access, __wrap_clock_gettime, __wrap_getcwd, __wrap_getenv, __wrap_lstat and __wrap_mmap](../../../core/libs/prx/libScePosixForWebKit/Export.cpp) (libScePosixForWebKit) assume, from the export names alone, the GNU ld [`--wrap`](https://sourceware.org/binutils/docs/ld/Options.html) convention (a reference to `` resolves to `__wrap_`): each takes its target's POSIX signature and forwards to libc's (`access`, `getcwd`, `getenv`) or libkernel's (`clock_gettime`, `lstat`, `mmap`) implementation, which sets `errno`. No PS5 module or WebKit call site was read to confirm that WebKit imports `__wrap_` in place of `` or that the console wrappers only forward. Any sandbox or path handling the console module adds is not modelled, and how the wrappers relate to `getenv_np`, `setenv_np`, `mmap_np` and `clock_gettime_np` (such as a separate environment that `setenv_np` fills) is unknown diff --git a/docs/dev/debt/libSceScreenShot.md b/docs/dev/debt/libSceScreenShot.md index 2e1f886b38..a109ca5bb9 100644 --- a/docs/dev/debt/libSceScreenShot.md +++ b/docs/dev/debt/libSceScreenShot.md @@ -1,5 +1,11 @@ # libSceScreenShot technical debt +### Silent stubs + +- [sceScreenShotEnableNotification and sceScreenShotDisableNotification](../../../core/libs/prx/libSceScreenShot/Export.cpp) (libSceScreenShot) - return 0 without effect: on the console they ask the system software to show or hide the capture notification, which this project does not display +- [sceScreenShotSetOverlayImage and sceScreenShotSetOverlayImageWithOrigin](../../../core/libs/prx/libSceScreenShot/Export.cpp) (libSceScreenShot) - check the arguments and the file as the PS5 module does, then return 0 without keeping the image, since this project takes no screenshots to overlay +- [sceScreenShotIsDisabled](../../../core/libs/prx/libSceScreenShot/Export.cpp) (libSceScreenShot) - reports one process-wide flag set by sceScreenShotDisable and cleared by sceScreenShotEnable for any id other than -1; on the console the system software answers, and what the id selects is unknown + ### Unknown function info -- [sceScreenShotCapture, sceScreenShotDisable, sceScreenShotDisableNotification, sceScreenShotEnable, sceScreenShotEnableNotification, sceScreenShotGetAppInfo, sceScreenShotGetDrcParam, sceScreenShotIsDisabled, sceScreenShotIsVshScreenCaptureDisabled, sceScreenShotSetDrcParam and sceScreenShotSetParam](../../../core/libs/prx/libSceScreenShot/Export.cpp) (libSceScreenShot) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceScreenShot.c), 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. +- [sceScreenShotCapture, sceScreenShotGetAppInfo, sceScreenShotGetDrcParam, sceScreenShotIsVshScreenCaptureDisabled, sceScreenShotSetDrcParam and sceScreenShotSetParam](../../../core/libs/prx/libSceScreenShot/Export.cpp) (libSceScreenShot) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceScreenShot.c), 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. diff --git a/docs/dev/debt/libSceSsl.md b/docs/dev/debt/libSceSsl.md index 23190fe3ef..20e9e19b1d 100644 --- a/docs/dev/debt/libSceSsl.md +++ b/docs/dev/debt/libSceSsl.md @@ -4,7 +4,7 @@ - [CA_MGMT_extractKeyBlobEx, CA_MGMT_extractPublicKeyInfo, CA_MGMT_freeKeyBlob, CRYPTO_initAsymmetricKey, CRYPTO_uninitAsymmetricKey, RSA_verifySignature, VLONG_freeVlongQueue, sceSslCheckRecvPending, sceSslCreateSslConnection and sceSslDeleteSslConnection](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - the list is the intersection of the export table of the console's libSceSsl (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo. Names and NIDs appear in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSsl.c) and [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv); no credible prototypes were found. They are declared without parameters solely to resolve known imports, not as an ABI claim; callers must not call them until signatures are verified. - [sceSslDisableOption, sceSslDisableOptionInternal, sceSslDisableOptionInternalInsecure, sceSslEnableOption, sceSslEnableOptionInternal, sceSslGetNanoSSLModuleId, sceSslGetPeerCert, sceSslGetSslError, sceSslLoadRootCACert and sceSslSetSslVersion](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - the list is the intersection of the export table of the console's libSceSsl (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo. Names and NIDs appear in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSsl.c) and [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv); no credible prototypes were found. They are declared without parameters solely to resolve known imports, not as an ABI claim; callers must not call them until signatures are verified. -- [sceSslRecv, sceSslReuseConnection and sceSslSend](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - the list is the intersection of the export table of the console's libSceSsl (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo. Names and NIDs appear in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSsl.c) and [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv). Their parameter shapes follow [SharpProspero's pinned bindings](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Http.cs#L222-L262), and are not measured on a PS5. +- [sceSslReuseConnection](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - its name and NID appear in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSsl.c) and [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv); its parameter shape follows [SharpProspero's pinned binding](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Http.cs#L2231-L2235). No console observation or reliable implementation source for session reuse was found. - [sceSslCreateConnection, sceSslDeleteConnection, sceSslSetVerifyCallback, sceSslConnect, sceSslRead, sceSslWrite](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - imported by `ffmpeg.prx`. Signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L195-L252), which also says the socket stays open after `sceSslDeleteConnection`. The behaviour below was measured on a PS5 on system software 13.40 with a ps5-payload-dev payload (every call printed with its return, a TLS 1.2 test server on the LAN presenting leaves issued by the test fixture root loaded with `sceSslLoadCert`, from an elfldr process rather than a title); what differs for a title process is unknown. An unknown context returns `SSL_ERROR_INVALID_ID` (0x8095F006) from `sceSslCreateConnection`, and every connection call on an unknown or deleted connection id or on a connection whose context was terminated returns `SSL_ERROR_INVALID_ID`. `sceSslCreateConnection` returns `SSL_ERROR_INVALID_VALUE` (0x8095F007) for a null host and does not check the socket: `sceSslConnect` on a descriptor that is not a socket returns `SCE_NET_ERROR_EBADF` (0x80410109). `sceSslConnect` runs a TLS 1.2 client handshake with Mbed TLS 3.6.7 over the host socket that the guest number maps to in the libkernel or libSceNet socket table, and `sceSslRead` and `sceSslWrite` carry application data over it. TLS 1.3 is compiled out: the console offers TLS 1.2 only (its ClientHello carries 12 suites and no supported_versions extension) and a TLS 1.3-only server gets `SSL_ERROR_FATAL_ALERT` (0x8095F010) from it, as from this build. Connection state is allocated from the context's pool and returned to it by `sceSslDeleteConnection`; a connection keeps the pool alive after `sceSslTerm`, whose connections return `SSL_ERROR_INVALID_ID` from then on. The console's pool use for one connection with a two-certificate chain peaked at 80,544 bytes in a 256 KiB pool (5,568 after `sceSslInit`, 12,000 after loading one CA); a 16 KiB pool fails the connect with `SSL_ERROR_OUT_OF_MEMORY` (0x809517D5) there and here. The server's chain is checked against the CAs loaded with `sceSslLoadCert`, the host name the connection was created with (case-insensitively; an IP literal sends no SNI and matches an IP subject alternative name; an empty host sends an empty SNI and fails `CN_CHECK`) and, when enabled, its validity dates, and `sceSslConnect` returns the first failure the connection's verify options select (the verify option entry below lists the bits, codes and initial options); no root bundle is shipped and the console's built-in roots are unknown, so with no CA loaded a connection with the initial options returns `SSL_ERROR_UNKNOWN_CA` (0x8095F00C), as on the console. A client certificate loaded with `sceSslLoadCert` is sent when the server asks for one. A failed verification is final: close_notify is sent, later `sceSslConnect` calls return the same code (also after the `WANT_POLLIN` rounds of a non-blocking socket), and `sceSslRead` and `sceSslWrite` throw, as the console's codes for them were not measured. With a verify callback set it is called once after the handshake, outside the connection's lock, with the context id, the bits of the enabled checks that failed (`KNOWN_CA_CHECK` 0x20, `CN_CHECK` 0x4, the date checks when enabled, 0 when the chain passed), handles to a copy of the peer chain as received (the leaf first, the root last when the server sent it) that the certificate readers of the connection's context accept until the connection is deleted (the handles stay registered in the context until it is terminated; the console frees them at the delete), the chain length and the user argument. A chain fault that no option names (`SSL_ERROR_INVALID_CERT` without a callback) throws when a callback is set, as the bits the console passes for it were not measured. A non-negative return accepts the connection whatever the bits were; a negative return fails the connect with `SSL_ERROR_APPLICATION_VERIFICATION_CALLBACK_CHECK` (0x8095F0FD) and the callback runs again, with the same chain, from every later `sceSslConnect`, `sceSslRead` and `sceSslWrite` of the connection, which return 0x8095F0FD while it keeps refusing and proceed once it accepts. A `sceSslConnect` from inside the callback, or from another thread while it runs, throws. `sceSslRead` and `sceSslWrite` before `sceSslConnect` run the handshake and verification themselves and return its code when it fails. A non-blocking socket that would block returns `SSL_ERROR_WANT_POLLIN` (0x8095F013) or `SSL_ERROR_WANT_POLLOUT` (0x8095F014) from `sceSslConnect`, `sceSslRead` and `sceSslWrite`; the next `sceSslConnect` resumes the handshake, and `sceSslWrite` returns the part it sent before blocking (the console returned 32 KiB of a 64 KiB write). A blocking libSceNet socket whose `SO_RCVTIMEO` expires returns `SSL_ERROR_WANT_POLLIN` as well, and the call can be repeated; `SSL_ERROR_READ_TIMEOUT` (0x80951710) is never returned. libkernel keeps its timeouts above the host socket, so they do not bound SSL reads and writes on a libkernel socket. `sceSslRead` with flags 0 reads until `len` bytes arrived or the stream ended and with flags 1 or 2 returns what the first record delivers. Native flags 3 peek without consuming data; values outside 0 through 3 return `SSL_ERROR_INVALID_VALUE` before connection lookup. A non-blocking read that got some bytes returns them. It returns 0 after the server's close_notify and after a transport that ends without one, every later read 0 again, and `SSL_ERROR_EOF` (0x8095177C) and `SSL_ERROR_SOCKET_CLOSED` (0x8095170D) never from a read. A reset (`ECONNRESET`, `EPIPE`, `WSAECONNRESET`, `WSAECONNABORTED`, `WSAENETRESET`) returns `SCE_NET_ERROR_ECONNRESET` (0x80410136) from `sceSslRead` and `SCE_NET_ERROR_EPIPE` (0x80410120) from `sceSslWrite`, on every later call too; a record that fails its MAC returns 0x80951D55 from `sceSslRead` after the fatal alert goes out. `sceSslWrite` accepts flags 0 only, `SSL_ERROR_INVALID_VALUE` otherwise, sends all of `len`, capped at `INT_MAX`, and returns the count; it keeps writing after the peer's close_notify or a stream that ended without one, as the console does. A fatal alert during the handshake returns `SSL_ERROR_FATAL_ALERT` and ends the transport: later `sceSslConnect` calls return 0, `sceSslRead` 0 and `sceSslWrite` `SSL_ERROR_SOCKET_CLOSED`, as measured. A transport that ends without close_notify or a reset during the handshake returns `SSL_ERROR_SOCKET_CLOSED`, the session is dropped and the next `sceSslConnect` starts over; those two handshake cases and `SSL_ERROR_INVALID_CERT` (0x8095F00A) for a verification failure no option names were not measured. Other handshake and socket failures, a read or write after a failed verification, a write after `sceSslClose`, a null buffer and a read or write from inside the callback throw, as their console codes are unknown. A connection serializes its reads and writes, so a read blocked in one thread holds back a write on the same connection from another. `sceSslDeleteConnection` sends no close_notify and returns `SSL_ERROR_INVALID_ID` when repeated. `sceSslTerm` leaves the context's connections to `sceSslDeleteConnection`, which then returns `SSL_ERROR_INVALID_ID`; a second `sceSslTerm` returns `SSL_ERROR_INVALID_ID` and `sceSslInit` with a pool size of 0 returns `SCE_KERNEL_ERROR_EINVAL` (0x80020016). `sceSslSetVerifyCallback` accepts any callback, null included, and returns 0. The console's first `sceSslConnect` in a process took 23.8 s before its ClientHello left, a one-time initialisation not emulated. `SSL_ERROR_BEFORE_INIT` (0x8095F001) is never returned, as the library keeps no state apart from its contexts - [sceSslClose](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - takes the connection id, as in [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L195-L252). Measured on 13.40 as the connection entry describes: it sends close_notify once if the handshake completed and returns 0, 0 again when repeated and 0 on a connection that never connected, already failed verification or lost its transport to a fatal alert; a close before the handshake changes nothing, so the connection can still connect. After the close `sceSslConnect` returns 0 and `sceSslRead` 0, as on the console; `sceSslWrite` after it throws, as the console's code was not measured. A close_notify that a non-blocking socket cannot take at once returns `SSL_ERROR_WANT_POLLOUT` (0x8095F014) and the close can be repeated; after a reset it returns `SCE_NET_ERROR_ECONNRESET` (0x80410136) every time. The socket is left to the title; an unknown connection returns `SSL_ERROR_INVALID_ID` (0x8095F006). Whether the console closes the socket is unknown diff --git a/docs/dev/debt/libSceSystemService.md b/docs/dev/debt/libSceSystemService.md index a5b01ed7ab..176b34aa6a 100644 --- a/docs/dev/debt/libSceSystemService.md +++ b/docs/dev/debt/libSceSystemService.md @@ -17,7 +17,8 @@ - [sceSystemServiceLaunchApp](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - AnyPS5 runs one title and cannot start another, so every call throws without reading its arguments. The (title ID, null-terminated argv, launch parameters) signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/SystemService/SystemService.cs#L141-L148), whose `AppLauncher` Prospero Explorer (PPSA99109) and Prospero Multi Tools (PPSA99110) import it through and which passes a 0x20-byte launch context or null; the console's argument checks and errors are unknown - [sceSystemServiceLaunchWebBrowser](../../../core/libs/prx/libSceSystemService/Export.cpp): the native export takes one URI argument. Null input, the 6015-byte length limit and supported scheme checks are recovered. The host still returns kernel `EOPNOTSUPP` for valid input; a missing browser service's native error is unresolved. - [sceSystemServiceParamGetString](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - the system name (parameter 6) is the fixed `PS5`, not the console's name. As in a payload process on a PS5 (firmware 13.60), the name is written NUL-padded to the whole buffer, and a buffer under 65 bytes, a null buffer or any other parameter returns `SYSTEM_SERVICE_ERROR_PARAMETER` without writing. Parameters 0 to 40, 100 to 110, 1000 to 1010, -1 and `INT_MAX` were tried there; the others are assumed to fail the same way. PPSA26344 at `0xA87935` passes parameter 6 with a 65-byte buffer -- [sceSystemServiceParamGetInt](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - console language numbers 0 to 29 follow the PS4 SDK enum in [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/06fcf7fd108fa080035cdd3259ed22eb07e0508c/include/orbis/_types/sys_service.h#L54-L83) (21 Arabic). 30 (Ukrainian) is not in it and follows PPSA99505 ([Jelly5](https://github.com/02dnot/Jelly5/blob/552b866d89b2a58b27f7f7b0484dc743ce235526/app/src/app/i18n.cpp#L29-L60)) and PPSA99002 ([ProsperoLight](https://github.com/blackbearreloaded/ProsperoLight/blob/b708bbde3457bef1073a9ab6959957cb691e8e2f/include/i18n.hpp#L252-L309)), whose numbers agree with the enum. Not confirmed on a console, and neither title starts on `main` yet. Parameter 400 returns the same number as parameter 1: PPSA01325 asks for 400 first, asks for 1 when that fails, and uses either answer as an index into the same 31 languages; in the PS5 10.01 and 12.60 `libSceSystemService` modules parameter 400 reads the same registry value as parameter 1, without parameter 1's mapping of newer language numbers by SDK version, and returns 0x80a10003 when an imported check that is not identified returns 1; the 4.03 module has no parameter 400. Neither the mapping nor that error is modelled, and the parameter's name is not known. Without `ANYPS5_LANGUAGE` the language is English (United States); the date format, time format and enter button stay fixed +- [sceSystemServiceParamGetInt](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - console language numbers 0 to 29 follow the PS4 SDK enum in [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/06fcf7fd108fa080035cdd3259ed22eb07e0508c/include/orbis/_types/sys_service.h#L54-L83) (21 Arabic). 30 (Ukrainian) is not in it and follows PPSA99505 ([Jelly5](https://github.com/02dnot/Jelly5/blob/552b866d89b2a58b27f7f7b0484dc743ce235526/app/src/app/i18n.cpp#L29-L60)) and PPSA99002 ([ProsperoLight](https://github.com/blackbearreloaded/ProsperoLight/blob/b708bbde3457bef1073a9ab6959957cb691e8e2f/include/i18n.hpp#L252-L309)), whose numbers agree with the enum. Not confirmed on a console, and neither title starts on `main` yet. Parameter 400 returns the same number as parameter 1: PPSA01325 asks for 400 first, asks for 1 when that fails, and uses either answer as an index into the same 31 languages; in the PS5 10.01 and 12.60 `libSceSystemService` modules parameter 400 reads the same registry value as parameter 1, without parameter 1's mapping of newer language numbers by SDK version, and returns 0x80a10003 when an imported check that is not identified returns 1; the 4.03 module has no parameter 400. Neither the mapping nor that error is modelled, and the parameter's name is not known. Without `ANYPS5_LANGUAGE` the language is English (United States); the date format and time format stay fixed +- [sceSystemServiceParamGetInt](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - parameters 1 to 5, 100 and 400 answer; 101 to 111, 200 to 213 and 300 throw (see the integer system parameters entry); every other parameter, and a null value pointer, returns 0x80A10003 without writing the value. Inside a retail title on a PS5 (firmware 13.40, measured 2026-10-19 through ptrace remote calls in the game process) parameters 0, 6, 7 (game parental level), 8 to 20, 401, 999, 1000 (enter button assignment) and 1001 to 1008 returned 0x80A10003, as did a null value pointer; 1 to 5 and 400 answered, and 100 answered the value 0, which is returned here too, although what the parameter means is not known. Whether the console writes the value on that error is not measured - [sceSystemServiceParamGetInt](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - the time zone (parameter 4) and summer time (parameter 5) follow the host's local time zone at the moment of the call. Titles compute the local time as UTC plus the time zone in minutes plus 60 minutes when summer time is 1 ([Apollo](https://github.com/bucanero/apollo-ps4/blob/252a9d5a187c463994263009851a179f4f910c49/source/exec_cmd.c#L70-L92), [GoldHEN's frame logger](https://github.com/GoldHEN/GoldHEN_Plugins_Repository/blob/6c3a06f53d9caebadfb0aeed9bb94119012010a7/plugin_src/frame_logger/source/main.c#L60-L82), and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/413d2e3518fb435f40971d238fe0ecae7ee551da/src/SharpProspero/Platform/SystemParameters.cs#L75-L79) calls them the offset from UTC in minutes and whether summer time is in effect), so the time zone is the host's current offset east of UTC less the summer time shift while summer time is in effect: 60 minutes on Linux, and on Windows the C runtime's own summer time bias (`_get_dstbias`), which is also what shifted its local time. For a host zone whose summer time is not one hour, Linux keeps the sum right and the standard offset wrong, and Windows the opposite; how the console reports such a zone is unknown. libkernel's `sceKernelConvertUtcToLocaltime`, `sceKernelConvertLocaltimeToUtc` and `sceKernelGettimezone` still use UTC - [sceSystemServiceInitializePlayerDialogParam](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - a null param throws: what the console does with it is not measured. The console run in [prosper](https://github.com/mattias800/prosper/blob/40d95db32efe1ad00106a5b9a84e018fffa9ecff/prosper/tests/data/console_oracle/nethttp.golden.tsv#L46) shows only the 0x40 bytes written (size 0x40, user ID -1 at 0xC, zeros), so the field names and the mode and target account fields follow [SharpProspero's struct](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/SystemService/SystemService.cs#L637-L655), and the `void` return [its declaration](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/SystemService/SystemService.cs#L419-L426); the console left 0x800693020 in `rax`, not an SCE error code. Closes when: the null case is measured on a PS5 @@ -149,6 +150,7 @@ - [Assembly-only shell and clock queries](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): format/fsck progress returns a float in XMM0. Impose-menu, accessibility-zoom and suspend-confirmation queries take an application ID and return a boolean. Suspend-blacklist lookup forwards a 32-bit ID to SysCore. Clock changes take a string and 32-bit value; CPU pstate takes a 32-byte input structure whose fields remain unresolved, and memory pstate takes one 32-bit value. -- [Integer system parameters](../../../core/libs/prx/libSceSystemService/Export.cpp): registry-backed parameters 100–111, 200–213 and 300 throw because their settings are not modeled. Language results omit the console SDK-version compatibility remapping, and parameters 7, 400 and 1000 do not reproduce platform-dependent availability. +- [Integer system parameters](../../../core/libs/prx/libSceSystemService/Export.cpp): registry-backed parameters 101–111, 200–213 and 300 throw because their settings are not modeled; 100 answers the value 0 measured on a PS5. Language results omit the console SDK-version compatibility remapping, and parameter 400 does not reproduce platform-dependent availability. - [Tournament launch parameters](../../../core/libs/prx/libSceSystemService/Export.cpp): occurrence parameters occupy 32 bytes and contain two string pointers and a user ID. The final string semantics are unresolved. Event-details and team-profile launches retain an unused first argument slot of unknown declared type. The daemon blocking-event receive argument is passed unchanged as 64 bits to IPC; its meaning remains unresolved. Protected bodies in [PendingExports.cpp](../../../core/libs/prx/libSceSystemService/PendingExports.cpp) remain unchanged. +- [sceSystemServiceIsBgmPlaying, sceShellCoreUtilIsBgmPlaying](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - not measured on a PS5: both return 0. In [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/system/systemservice.cpp) they report whether its libSceMusicPlayerService is playing; there is no libSceMusicPlayerService here, so no background music can be playing diff --git a/docs/dev/debt/libSceUlt.md b/docs/dev/debt/libSceUlt.md index 12002369d2..d539ce9772 100644 --- a/docs/dev/debt/libSceUlt.md +++ b/docs/dev/debt/libSceUlt.md @@ -6,6 +6,7 @@ - [libSceUlt ulthreads](../../../core/libs/prx/libSceUlt/Export.cpp) - Runtime option defaults still omit the native header, stack size, worker affinity and priority fields; affinity depends on unresolved process configuration queries. Concurrent join ownership is not modeled: the native runtime distinguishes an ordinary running thread from one with another joiner. Host threads do not use the guest context or worker scheduler. - [libSceUlt queues](../../../core/libs/prx/libSceUlt/Export.cpp) - Queue-data pool destruction and shared slot limits still require comparison with the native node allocator. +- [`sceUltConditionVariableCreate` and `sceUltConditionVariableOptParamInitialize`](../../../core/libs/prx/libSceUlt/Export.cpp) (libSceUlt) are absent from the console export list, where only the underscored variants `_sceUltConditionVariableCreate` and `_sceUltConditionVariableOptParamInitialize` appear; their non-underscored signatures are assumed. Condition variable option parameter initialization only zeroes 128 bytes, and its layout without the common header remains unverified on hardware. - [Ult factory and creation exports](../../../core/libs/prx/libSceUlt/Export.cpp) without a leading underscore are absent from the supplied module; their signatures remain unresolved. Diagnostic signatures and synchronization argument lists are recovered. - [sceUltMutexTryLock, the reader-writer lock and sceUltUlthreadGetSelf, sceUltUlthreadYield and sceUltUlthreadExit](../../../core/libs/prx/libSceUlt/src/ReaderWriterLock.cpp) (libSceUlt) - the signatures follow the PS Vita libult in [RPCS3](https://github.com/RPCS3/rpcs3/blob/13aa88c3c4d66ea8799e987f7d762d90943ab17b/rpcs3/Emu/PSP2/Modules/sceUlt.cpp) (a 256-byte lock and 128-byte option structures), not a PS5 binary. The rules are assumed: readers may take the lock while a writer waits, a thread that holds the write lock gets `ULT_ERROR_STATE` for any further lock instead of waiting on itself, a reader asking for the write lock waits on its own read lock, and the try forms return `ULT_ERROR_BUSY`. Outside an ulthread, `sceUltUlthreadGetSelf`, `sceUltUlthreadYield` and `sceUltUlthreadExit` return `ULT_ERROR_STATE`; inside one they act on the guest thread it runs on, since ulthreads are not scheduled cooperatively here @@ -13,4 +14,3 @@ - [libSceUlt queues](../../../core/libs/prx/libSceUlt/Export.cpp) use preallocated host storage instead of the supplied guest work area; the guest object layouts remain incomplete. Waiting-queue pool thread limits and waiter priority ordering are not implemented. Finalization wakes blocked queue callers with `ULT_ERROR_STATE`. - [sceUltWaitingQueueResourcePoolDestroy](../../../core/libs/prx/libSceUlt/Export.cpp) refuses with `ULT_ERROR_STATE` only when a thread is currently waiting on a pool-backed mutex, semaphore or queue, matching the PS5 libSceUlt disassembly (0x473a in or55417wcDk); bound but idle objects do not block pool destruction. Waiting-queue pool guest work area node accounting is not implemented. -- The following pending declarations have unverified signatures and behavior; they are parameterless import-resolution declarations and throw through `NotImplemented_nid_no_patch`: [`_sceUltConditionVariableOptParamInitialize`](../../../core/libs/prx/libSceUlt/Export.cpp). diff --git a/docs/dev/debt/libSceVdecsw.md b/docs/dev/debt/libSceVdecsw.md index 0bf65cb433..4818e0bfea 100644 --- a/docs/dev/debt/libSceVdecsw.md +++ b/docs/dev/debt/libSceVdecsw.md @@ -2,10 +2,6 @@ ### Unknown function info -- [libSceVdecsw](../../../core/libs/prx/libSceVdecsw/Export.cpp) (libSceVdecsw) - structure layouts follow shadPS4's `vdecsw.h` and were checked against PPSA05684's movie decoder (`movie-decoder-vdecsw.cpp`), which asserts `pictureCount == 1` at offset 0xb of `OutputInfo` and reads `InputResult` offset 0x10 as the number of frames an access unit produced. The compute, decoder memory and decode input structures and the first 0x28 bytes of the 0x50-byte decoder config are those of libSceVideodec2, and decoding goes through it. The 0x78-byte AVC picture info is filled with Videodec2's leading fields and the cropping fields only. OUTPUT_PENDING (0x81510115), INPUT_QUEUE_EMPTY (0x81510116) and DECODE_PENDING (0x81510117) follow from the codes each call's assert accepts. At most four decoded pictures are queued and the last one is held until `sceVdecswFinalizeDecodeSequence`; the console's queue depth is unknown. `sceVdecswSyncDecodeInput` / `sceVdecswSyncDecodeOutput` retry the TrySync variants until they stop returning DECODE_PENDING / OUTPUT_PENDING; they do not sleep beyond a yield, so another thread must drain outputs when the picture queue is full. `sceVdecswGetHevcPictureInfo` still throws: the 0xb8-byte HEVC picture layout is in shadPS4's `vdecsw.h`, but this wrapper only keeps AVC picture info from Videodec2 +- [libSceVdecsw](../../../core/libs/prx/libSceVdecsw/Export.cpp) (libSceVdecsw) - structure layouts follow shadPS4's `vdecsw.h` and were checked against PPSA05684's movie decoder (`movie-decoder-vdecsw.cpp`), which asserts `pictureCount == 1` at offset 0xb of `OutputInfo` and reads `InputResult` offset 0x10 as the number of frames an access unit produced. The compute, decoder memory and decode input structures and the first 0x28 bytes of the 0x50-byte decoder config are those of libSceVideodec2, and decoding goes through it. The 0x78-byte AVC picture info is filled with Videodec2's leading fields and the cropping fields only. OUTPUT_PENDING (0x81510115), INPUT_QUEUE_EMPTY (0x81510116) and DECODE_PENDING (0x81510117) follow from the codes each call's assert accepts. At most four decoded pictures are queued and the last one is held until `sceVdecswFinalizeDecodeSequence`; the console's queue depth is unknown. `sceVdecswSyncDecodeInput` / `sceVdecswSyncDecodeOutput` retry the TrySync variants until they stop returning DECODE_PENDING / OUTPUT_PENDING; they do not sleep beyond a yield, so another thread must drain outputs when the picture queue is full. The 0xb8-byte HEVC picture info (shadPS4's `vdecsw.h`) carries only the timestamps, coded size, bit depth, IDR and IRAP flags, profile, level and conformance window (in chroma samples, repeated in the frame cropping fields, whose console unit is unknown) - [sceVdecswGetVp9PictureInfo](../../../core/libs/prx/libSceVdecsw/Export.cpp) (libSceVdecsw) - no console behaviour is recorded; the signature and 0x58-byte layout come from [SharpProspero's Vdecsw.cs](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Video/Vdecsw.cs#L892-L952), and shadPS4 has no VP9 getter. It copies the leading `thisSize - 8` bytes of the picture's info, as [shadPS4's `sceVdecswGetPictureInfo`](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/videodec/vdecsw.cpp#L303-L334) does for AVC and HEVC, from Videodec2's VP9 info (`level` is 0 there). An output with no picture returns 0 and writes nothing. Null pointers, `thisSize` below 8, two pictures and a frame buffer no VP9 decoder returned (an H.264 picture too) throw; console error codes are unknown. VP9 decoding goes through Videodec2 and rejects pictures deeper than 10 bits - -### Functional - -- [sceVdecswGetHevcPictureInfo](../../../core/libs/prx/libSceVdecsw/Export.cpp) (libSceVdecsw) - throws until HEVC picture info is filled from the decoder diff --git a/docs/dev/debt/libSceVideoOut.md b/docs/dev/debt/libSceVideoOut.md index 56dce0b93f..5fdf3bd52f 100644 --- a/docs/dev/debt/libSceVideoOut.md +++ b/docs/dev/debt/libSceVideoOut.md @@ -12,16 +12,17 @@ - [sceVideoOutColorSpaceConversionSettingsSetDefaultSdrToHdrConversion_, sceVideoOutColorSpaceConversionSettingsSetSdrToHdrConversion_, sceVideoOutAdjustColorSpaceConversion_, sceVideoOutSetGlobalBlendSpace](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - PPSA30630 imports these to configure color space conversion and global blend space. Color space conversion settings are accepted with size validation (32 bytes) but conversion blocks and blend space parameters are not applied to the presentation swapchain - [sceVideoOutDeleteVrrActiveStatusEvent](../../../core/libs/prx/libSceVideoOut/src/Event.cpp) (libSceVideoOut, NID `-q-vbO859Tw`) - the name hashes to the NID PPSA02530 imports and is listed in [sce_symbols](https://github.com/zecoxao/sce_symbols) `aerolib.csv` at `fe2cc2948e`. The signature `(eq, handle)` and the behaviour, removing the handle's VRR status registration from the queue as the other `Delete*Event` calls do, follow sceVideoOutAddVrrActiveStatusEvent; no title has been observed calling it - [sceVideoOutCursorEnable, sceVideoOutCursorDisable, sceVideoOutCursorSetImageAddress, sceVideoOutCursorSetPosition](../../../core/libs/prx/libSceVideoOut/Unimplemented.cpp) (libSceVideoOut, NIDs `UPZWCH8qTM4`, `fEPEwROdCwM`, `u-9bhWQApq8`, `N17AK88NdD0`) - the names hash to the NIDs Prospero Win (PPSA99995) imports, and the parameter lists `(handle, index, address)`, `(handle, index)`, `(handle, index, address)` and `(handle, index, x, y)` follow its [declarations](https://github.com/mpereiraesaa/prospero-win/blob/644e32e400c555e340d507383398c5d21b39b0b0/wine/ps5/pw_vulkan_radv.c#L65-L68). The cursor image's format and size and the console's return values are not known; the functions report that they are not implemented. -- [sceVideoOutGetOutputStatusWithVrr](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut, NID `gWT7X8H0bYs`) - `(int handle, void* status)`, the 80-byte layout and the return codes follow the clean-room description in #2746. Bytes 0-47 are `sceVideoOutGetOutputStatus`'s; bytes 48-53, which the console copies from an internal query whose fields are not established, are written as zero, as are the rest, so the 16-bit value at byte 52 never adjusts `refreshRate`. A null `status` returns `0x80290002` and an unknown handle `0x8029000B`. The console's earlier global-flag check (`0x8029001C`) and its object-field check (`0x8029001B`) test states that are not modelled and are never reported. +- [sceVideoOutGetOutputStatusWithVrr](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut, NID `gWT7X8H0bYs`) - bytes 48-53 are written as zero, where a PS5 (firmware 13.40, measured 2026-10-19 inside a retail title) with a 4K TV that takes 120 Hz returned the 16-bit values 48, 60 and 60 (0x30, 0x3C, 0x3C); their meaning is not established, so the value at byte 52 never adjusts `refreshRate`. `(int handle, void* status)`, the 80-byte layout and the return codes follow the clean-room description in #2746: bytes 0-47 are `sceVideoOutGetOutputStatus`'s and the rest 0, a null `status` returns `0x80290002` and an unknown handle `0x8029000B`, all as that console answered. The console's earlier global-flag check (`0x8029001C`) and its object-field check (`0x8029001B`) test states that are not modelled and are never reported. - [sceVideoOutRegisterBuffers2](../../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut) - buffer option 8 (STRICT_COLORIMETRY) is presented as option 0, with the stored values unchanged; what the console changes for it is unknown. Only options 0 and 8 are accepted - [sceVideoOutAddBufferAttributeOption](../../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut, NID `MBlv8YWoyOk`) - the signature `(VideoOutBufferAttribute2*, uint64_t option)` comes from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/VideoOut/VideoOut.cs#L174-L176) and the call site in PPSA25380, which passes 0x20 and ignores the result. The option is ORed into `attribute->option`; whether the console validates the bits, keeps them elsewhere or fails on an unknown option is unknown. A null attribute throws. Option 0x20 makes `sceVideoOutRegisterBuffers2` throw, as every option other than 0 and 8; what it selects for an overlay buffer is unknown - [sceVideoOutRegisterBuffers2](../../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut) - DCC buffers (category 1): `dcc_control` is taken as the CB_DCC_CONTROL block layout, only bits 0x10026c are accepted; `dcc_cb_register_clear_color` as the register-clear texel, only 32-bit values are presented -- [sceVideoOutIsOutputSupported](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - mode 1 is supported; mode 15 is supported when the current host display runs at 120 Hz or above. Configuring mode 15 requires attribute3 bit 0x40; this single-bit mask is inferred from the measured full value 0x80040, not isolated by a console measurement. Vblanks run at 119.88 Hz until mode 1 restores 59.94 Hz. Display modes are read through SDL; a move to a slower display falls back the configured output mode to mode 1, fires the output-mode event and continues at 59.94 Hz. The console's exact fallback behaviour when a 120 Hz display is lost has not been measured; this fallback avoids the abort in #7877 and matches the HDMI output mode renegotiation that a display change requires. Selecting another host display mode and VRR are not implemented. The console answers per display and per title: with a 120 Hz TV it also answers 1 for mode 15 (119.88 Hz), and [PS5_Vulkan](https://github.com/mihawk-99/PS5_Vulkan/blob/5b5e4fc2d80fdb67a4f61ba9e6a8a424026bb8a5/docs/HARDWARE_FINDINGS.md) measured that `sceVideoOutConfigureOutput` accepts 15 only from a title whose param.json declares high-frame-rate output (attribute3 0x80040). Modes 4, 7, 8, 12-14, 16-17 and 19 return `VIDEO_OUT_ERROR_UNSUPPORTED_OUTPUT_MODE` here, as they did on the measured display. Mode 18 throws, because one console answered 0x80290016 and another 0x8029001E. The name `VIDEO_OUT_ERROR_UNKNOWN_OUTPUT_MODE` for 0x8029001E is ours. Word 3 of the output options is accepted with any value, as on the console, and its meaning is unknown. PPSA12544 probes `0xd000000a`, which the console answers with 0x8029001E, so the title selects 3840 pixels; PPSA26344 at `0xCD0946` passes `(handle, 0xf, nullptr, nullptr, 0)` and branches on the result equalling 1 -- [sceVideoOutGetResolutionStatus](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - the 48-byte layout follows the declarations in ps5-opengl and EVO-PLAYER-PS5 (#1578), which agree; not confirmed on a console. `screenSizeInInch` is 0, the value EDID uses for an undefined size, since the host display's size is not read; `flags` is 0, its bits are unknown. The pane equals the full size because `sceVideoOutSetWindowModeMargins` applies no margins +- [sceVideoOutIsOutputSupported](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - mode 1 is supported; mode 15 is supported when the current host display runs at 120 Hz or above. Configuring mode 15 requires attribute3 bit 0x40; this single-bit mask is inferred from the measured full value 0x80040, not isolated by a console measurement. Vblanks run at 119.88 Hz until mode 1 restores 59.94 Hz. Display modes are read through SDL; a move to a slower display falls back the configured output mode to mode 1, fires the output-mode event and continues at 59.94 Hz. The console's exact fallback behaviour when a 120 Hz display is lost has not been measured; this fallback avoids the abort in #7877 and matches the HDMI output mode renegotiation that a display change requires. Selecting another host display mode and VRR are not implemented. The console answers per display and per title: with a 120 Hz TV it also answers 1 for mode 15 (119.88 Hz), and [PS5_Vulkan](https://github.com/mihawk-99/PS5_Vulkan/blob/5b5e4fc2d80fdb67a4f61ba9e6a8a424026bb8a5/docs/HARDWARE_FINDINGS.md) measured that `sceVideoOutConfigureOutput` accepts 15 only from a title whose param.json declares high-frame-rate output (attribute3 0x80040). Modes 4, 7, 8, 12-14, 16-17 and 19 return `VIDEO_OUT_ERROR_UNSUPPORTED_OUTPUT_MODE` here, as they did on the measured display. Mode 18 throws, because one console answered 0x80290016 and two others 0x8029001E, the last a PS5 on 13.40 with a 120 Hz TV (measured 2026-10-19 inside a retail title), whose answers for every other mode match the ones here. The name `VIDEO_OUT_ERROR_UNKNOWN_OUTPUT_MODE` for 0x8029001E is ours. Word 3 of the output options is accepted with any value, as on the console, and its meaning is unknown. PPSA12544 probes `0xd000000a`, which the console answers with 0x8029001E, so the title selects 3840 pixels; PPSA26344 at `0xCD0946` passes `(handle, 0xf, nullptr, nullptr, 0)` and branches on the result equalling 1 +- [sceVideoOutGetResolutionStatus](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - `screenSizeInInch` is 0, the value EDID uses for an undefined size, since the host display's size is not read; a PS5 (firmware 13.40, measured 2026-10-19 inside a retail title) reported its TV's 60.0f. The 48-byte layout, from the declarations in ps5-opengl and EVO-PLAYER-PS5 (#1578), is confirmed there: full size and pane 3840 x 2160, `refreshRate` 3, `flags` 0 and the rest 0 on the title's handle, 0x8029000B for handles 0 to 8. `flags` is 0 here too; its bits are unknown. The pane equals the full size because `sceVideoOutSetWindowModeMargins` applies no margins - [sceVideoOutOpen](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - the open param's first word is unknown (PPSA21564 passes 16; it is not the byte size, since the affinity mask is at offset 16); only 16 is accepted. PPSA26344's only call at `0xCCF5EA` passes `(0xFF, 0, 0, nullptr)`. Whether a non-null param continues past offset 24 is also unknown - [sceVideoOutGetBufferLabelAddress](../../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) exposes a port-owned contiguous array of 16 aligned 64-bit labels and returns its count. Astro Bot PPSA21564's bundled AGC driver passes a pointer output and addresses each label at `base + index * 8`. The count, zero initialization on registration, and release of the previous displayed buffer when its replacement completes follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/23c41333822fe8eca4c609b43119180af63ae4ac/src/core/libraries/videoout/video_out.cpp) and its driver; PS5 console equivalence has not been measured. Labels live until the port closes, and reopening creates a new array. Error precedence follows the reference for null output and the existing open-port policy for handles. Exact scanout release timing, repeated same-buffer flips and concurrent guest label stores remain unverified. EOP-flip registration and interrupt tokens remain unimplemented; exposing these labels does not implement that separate API. - [GPU flips](../../../core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp) (libSceVideoOut) - a command-buffer flip of a buffer index with no registered buffer when the GPU reaches it presents black and completes like any flip (flip event, count, `flipPendingNum`). The console's behaviour is unknown; PPSA05684 requires that it completes: its renderer submits a flip of buffer 0 while another thread waits for `sceVideoOutIsFlipPending` to reach 0 before registering the buffers, and `flipPendingNum` counts that flip from its submission as in shadPS4 - [sceVideoOutSubmitFlip](../../../core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp) (libSceVideoOut, libSceAgcDriver) - in the [AGC driver](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp), a CPU flip waits for the graphics-queue submissions ahead of it when the calling thread has submitted graphics-queue work itself: a title that submits a frame and flips it without waiting relies on that order, since emulated GPU work finishes later than the console's. A flip from a thread that has never submitted graphics-queue work is presented without waiting for queued submissions, unless a command-buffer or EOP flip ahead of it has not been reached. Such a thread can only know its frame is finished by waiting for it, as the PS5 RADV WSI's flip thread waits on the frame's fence. Whether the console ever orders a CPU flip behind the graphics queue is not measured; `APS5_CPU_FLIP_IN_ORDER=1` orders every CPU flip +- [sceVideoOutGetVblankStatus](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - the third 64-bit word of the PS5 status (`reserved` in [SceTypes.hpp](../../../core/libs/SceTypes.hpp)) is written 0. On a PS5 (firmware 13.40, measured 2026-10-19) a retail title's own call, caught inside it, got count 0xb16b, `processTime` 0x381a8790 (941 s), third word 0x1c485ffaadc, `processTimeCounter` 0x15dd63c364b, flags 0x04 and phase 0: the third word is a running counter, about 970 s at ~2 GHz, consistent with a TSC, not zero. Its clock and what it counts from are not established. Closes when: the third word's source is measured and written - [sceVideoOutAddPreVblankStartEvent](../../../core/libs/prx/libSceVideoOut/src/Event.cpp) (libSceVideoOut) - how long before the vblank the console signals pre-vblank start is unknown. The event is triggered on each emulated vblank tick immediately before the vblank event, with its own count as the event data - [sceVideoOutRegisterBuffers, sceVideoOutGetFlipStatus, sceVideoOutGetVblankStatus](../../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut) - PS4 callers. A handle is treated as PS4 once buffers are registered through the PS4 `sceVideoOutRegisterBuffers`; from then on its flip and vblank status are written in the PS4 64- and 32-byte layouts, with `tsc` and `submitTsc` 0. A PS4 title that reads a status before it registers buffers gets the larger PS5 layout. The PS4 function accepts only linear A8R8G8B8_SRGB (0x80000000) and A8B8G8R8_SRGB (0x80002200) buffers without options; tiled PS4 buffers throw, as their layout differs from the PS5 tiling. Linear PS4 buffers may be 4 KiB aligned: the OpenOrbis graphics sample registers its second 1920x1080 buffer 8,294,400 bytes after the first. The PS4 alignment rule is not known diff --git a/docs/dev/debt/libSceVideodec.md b/docs/dev/debt/libSceVideodec.md index c24087ab78..8abac004cf 100644 --- a/docs/dev/debt/libSceVideodec.md +++ b/docs/dev/debt/libSceVideodec.md @@ -2,14 +2,14 @@ ### Unknown function info -- [sceVideodecCreateDecoder, sceVideodecDecode, sceVideodecDeleteDecoder, sceVideodecFlush and sceVideodecReset](../../../core/libs/prx/libSceVideodec/Export.cpp) (libSceVideodec) - the structure layouts and the error codes follow [shadPS4's PS4 library](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/videodec/videodec.h) and its [implementation](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/videodec/videodec.cpp), not measured on a PS5: the console's error for a rejected argument and its own version of the structures are unknown. The decoder runs through libSceVideodec2, which decodes H.264 with FFmpeg, and the picture is copied into the caller's frame buffer with the pitch aligned to 64 and the coded height, as the reference writes it - -- [libSceVideodec](../../../core/libs/prx/libSceVideodec/Export.cpp) (libSceVideodec) - the module's other two exports, `sceVideodecMapMemory` and `sceVideodecQueryResourceInfo`, are not provided, so a title that imports them fails to start +- [sceVideodecCreateDecoder, sceVideodecDecode, sceVideodecDeleteDecoder, sceVideodecFlush, sceVideodecQueryResourceInfo and sceVideodecReset](../../../core/libs/prx/libSceVideodec/Export.cpp) (libSceVideodec) - the structure layouts and the error codes follow [shadPS4's PS4 library](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/videodec/videodec.h) and its [implementation](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/videodec/videodec.cpp), not measured on a PS5: the console's error for a rejected argument and its own version of the structures are unknown. The decoder runs through libSceVideodec2, which decodes H.264 with FFmpeg, and the picture is copied into the caller's frame buffer with the pitch aligned to 64 and the coded height, as the reference writes it ### Functional - [sceVideodecCreateDecoder](../../../core/libs/prx/libSceVideodec/Export.cpp) (libSceVideodec) - the codec type is not checked and the decoder always decodes H.264, as the PS4 library does; the console's codec ids are not documented. A configuration without a maximum frame size is refused with the configuration error, and the resource information the caller passes is not used +- [sceVideodecQueryResourceInfo](../../../core/libs/prx/libSceVideodec/Export.cpp) (libSceVideodec) - the sizes it reports are libSceVideodec2's, not the console's: the CPU memory, CPU-GPU memory and frame buffer sizes and the frame buffer alignment that `sceVideodec2QueryDecoderMemoryInfo` gives for the configuration mapped as `sceVideodecCreateDecoder` maps it. The GPU memory size that query also gives has no field in this structure and is dropped. The memory pointers and `thisSize` are left as the caller passed them, where [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/c2814041e546bf75c12756dbec9da911c721ce1c/src/core/libraries/videodec/videodec.cpp#L137-L187) clears the pointers; what the console writes there is not known. A larger structure is accepted, as by the module's other exports, where shadPS4 requires the exact size, and a configuration without a maximum frame size is refused with the configuration error, where shadPS4 assumes 1920x1080 or 3840x2160 + - [sceVideodecDecode and sceVideodecFlush](../../../core/libs/prx/libSceVideodec/Export.cpp) (libSceVideodec) - the picture information's video usability information (the tick rate, the sample aspect ratio and the colour description) stays zero, as the decoder does not report it, and the crop offsets are the decoded picture's crop halved, the unit the H.264 offsets use. The frame buffer holds the coded picture the decoder reports, so the rows the crop covers are in it and the caller crops them -- [sceVideodecMapMemory and sceVideodecQueryResourceInfo](../../../core/libs/prx/libSceVideodec/Export.cpp) (libSceVideodec) - their names are listed in the PS5 3.20 export table and cross-checked against PS5 13.60. Their signatures and behaviour are unverified; these no-parameter declarations only resolve imports and throw through NotImplemented_nid_no_patch. +- [sceVideodecMapMemory](../../../core/libs/prx/libSceVideodec/Export.cpp) (libSceVideodec) - its name is listed in the PS5 3.20 export table and cross-checked against PS5 13.60. Its signature and behaviour are unverified; this no-parameter declaration only resolves the import and throws through NotImplemented_nid_no_patch. diff --git a/docs/dev/debt/libSceVideodec2.md b/docs/dev/debt/libSceVideodec2.md index 3706171e00..74a8f81c0f 100644 --- a/docs/dev/debt/libSceVideodec2.md +++ b/docs/dev/debt/libSceVideodec2.md @@ -2,6 +2,7 @@ ### 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 @@ -13,5 +14,6 @@ - [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 - [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. What the console returns for them, and for a stream that continues across the flush without a new IDR picture, is not measured. +- [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. diff --git a/docs/dev/debt/libSceVoice.md b/docs/dev/debt/libSceVoice.md index 9e8d8fc46e..978eee2cf9 100644 --- a/docs/dev/debt/libSceVoice.md +++ b/docs/dev/debt/libSceVoice.md @@ -16,11 +16,9 @@ - [sceVoiceVADAdjustment](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature - [sceVoiceVADPVTAdjustment](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature - [sceVoiceVADSetVersion](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature -- [sceVoiceSetMuteFlagAll](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature -- [sceVoiceSetBitRate](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature -- [sceVoiceUpdatePort](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature +- [sceVoiceSetMuteFlagAll, sceVoiceSetBitRate, sceVoiceUpdatePort](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - not measured on a PS5; signatures follow [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json), behaviour the Vita's [VitaSDK](https://github.com/vitasdk/vita-headers/blob/ae5808f3105c42fa3373ba4ab64ab54e2e3ca848/include/psp2/voice.h#L250-L557). They return the not-initialized, port-range and missing-port codes the other port calls return. The mute-all flag makes `sceVoiceGetMuteFlag` report every port muted without changing port flags. A non-voice port, a bitrate `sceVoiceCreatePort` rejects, a null param or a different port type throw, since the console's codes for them are unknown - [sceVoiceGetPortAttr](../../../core/libs/prx/libSceVoice/Export.cpp): native initialization, port-range, pointer, attribute-size and port-type validation is implemented. Supported device attributes throw because the host has no device state or voice-activity processing. The former constant-zero voice-activity fallback is removed; failed calls leave output untouched. -- [sceVoiceSetThreadsParams](../../../core/libs/prx/libSceVoice/Export.cpp): the 48-byte mode/priority/affinity layout and initialization, null, started-state and mode validation are recovered. Mode 1 throws because worker scheduling is not implemented; mode 0 is the native no-op. The 32-byte event callback union is recovered, but callback delivery and device-state byte semantics remain unimplemented. +- [sceVoiceSetThreadsParams](../../../core/libs/prx/libSceVoice/Export.cpp): the priorities and affinity masks that mode 1 sets reach no host scheduling, because no voice worker threads run (see sceVoiceStart below). Mode 1 returns 0 for priorities 256-767 with non-zero masks within 0x1fff, the values a PS5 thread takes; the native result for other values is unverified, so they throw. The 32-byte event callback union is recovered, but callback delivery and device-state byte semantics remain unimplemented. - [sceVoiceStart, sceVoiceStop and sceVoiceEnd](../../../core/libs/prx/libSceVoice/Export.cpp) validate initialization, container presence and native mode-dependent memory sizes, but do not partition guest working memory or run the audio workers. Start/stop currently change software state only; backend failures and active-stream shutdown are not reproduced. diff --git a/docs/dev/debt/libSceVorbisDec.md b/docs/dev/debt/libSceVorbisDec.md index f0c2ddc877..45ad9e2d0e 100644 --- a/docs/dev/debt/libSceVorbisDec.md +++ b/docs/dev/debt/libSceVorbisDec.md @@ -2,37 +2,13 @@ ### Unknown function info -- [sceVorbisDecBitrate](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database -- [sceVorbisDecBitrateInstant](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database -- [sceVorbisDecClear](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database -- [sceVorbisDecComment](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database -- [sceVorbisDecCrossLap](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database +- [sceVorbisDecOpenCallbacks, sceVorbisDecRead and the other libvorbisfile exports](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - the 31 exports named after libvorbisfile functions are taken to be the libvorbisfile API with a `sceVorbisDec` prefix (`Read` is `ov_read`, `CrossLap` is `ov_crosslap`, `HalfrateP` is `ov_halfrate_p`). `ReadFilter`, `ReadFloat` and `Halfrate` exist in libvorbis (`ov_read_filter` since 1.2.2, with the same `vorbisfile.h` signatures through 1.3.7 apart from `const`) but not in Tremor, so times are `double` and `ov_read` takes `bigendianp`, `word` and `sgned`. Guest `long` arguments and results are 64-bit. They run the bundled libvorbis 1.3.7 and libogg 1.3.6; the console's libvorbis version, any Sony changes to its behaviour or error codes, and the signatures themselves are unverified on a PS5. - [sceVorbisDecGetDecodePriority](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database - [sceVorbisDecGetLastDecodeError](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database -- [sceVorbisDecHalfrate](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database -- [sceVorbisDecHalfrateP](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database -- [sceVorbisDecInfo](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database -- [sceVorbisDecOpenCallbacks](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database -- [sceVorbisDecPcmSeek](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database -- [sceVorbisDecPcmSeekLap](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database -- [sceVorbisDecPcmSeekPage](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database -- [sceVorbisDecPcmSeekPageLap](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database -- [sceVorbisDecPcmTell](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database -- [sceVorbisDecPcmTotal](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database -- [sceVorbisDecRawSeek](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database -- [sceVorbisDecRawSeekLap](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database -- [sceVorbisDecRawTell](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database -- [sceVorbisDecRawTotal](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database -- [sceVorbisDecRead](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database -- [sceVorbisDecReadFilter](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database -- [sceVorbisDecReadFloat](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database -- [sceVorbisDecSeekable](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database -- [sceVorbisDecSerialNumber](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database - [sceVorbisDecSetDecodePriority](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database -- [sceVorbisDecStreams](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database -- [sceVorbisDecTimeSeek](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database -- [sceVorbisDecTimeSeekLap](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database -- [sceVorbisDecTimeSeekPage](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database -- [sceVorbisDecTimeSeekPageLap](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database -- [sceVorbisDecTimeTell](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database -- [sceVorbisDecTimeTotal](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database + +### Functional + +- [libSceVorbisDec](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - the guest `OggVorbis_File` is never written: the decoder state lives on the host, keyed by the struct's address, so fields a title reads directly keep what it stored, and a struct never opened, cleared or failed to open behaves as libvorbisfile's zeroed struct. `sceVorbisDecInfo` returns a copy of each link's `vorbis_info` with 64-bit `long` fields, refreshed after every call on the struct and valid until `sceVorbisDecClear`. Reopening a struct that is still open frees its old state without calling its close callback; libvorbisfile leaks it. +- [sceVorbisDecClear and sceVorbisDecCrossLap](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - a null `vf` throws, as libvorbisfile dereferences it, except that `sceVorbisDecClear` and `sceVorbisDecCrossLap` with the same pointer twice return 0, and `sceVorbisDecCrossLap` with a null second pointer after a first that is not open returns `OV_EINVAL`, as libvorbisfile does. `sceVorbisDecClear` frees the state before it calls the close callback, as `ov_clear` does, so when that callback throws the struct is already cleared, where libvorbisfile leaves it freed but not zeroed. +- [sceVorbisDecOpenCallbacks and sceVorbisDecBitrate](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - on Windows the host `long` is 32-bit: a tell result outside it, or an initial byte count outside it with a non-null `initial`, throws; a sample rate of 2^31 Hz or more is rejected as a bad header; and `sceVorbisDecBitrate` rounds halves up instead of to even. diff --git a/docs/dev/debt/libSceWebBrowserDialog.md b/docs/dev/debt/libSceWebBrowserDialog.md index f86ba5ae8f..c057cc3710 100644 --- a/docs/dev/debt/libSceWebBrowserDialog.md +++ b/docs/dev/debt/libSceWebBrowserDialog.md @@ -3,6 +3,7 @@ ### Silent stubs - [libSceWebBrowserDialog](../../../core/libs/prx/libSceWebBrowserDialog/Export.cpp) - `sceWebBrowserDialogOpen` and `sceWebBrowserDialogOpenForPredeterminedContent` run the common-dialog state machine without displaying an interactive browser, and `sceWebBrowserDialogGetResult` reports that the user cancelled. Minecraft Bedrock (PPSA17221) imports `sceWebBrowserDialogOpenForPredeterminedContent` (#5744). The 424-byte allow-list parameter and its twenty optional address pointers are validated, but address filtering is not modelled without an interactive browser. Main open-parameter validation remains incomplete. +- [sceWebBrowserDialogSetCookie, sceWebBrowserDialogResetCookie](../../../core/libs/prx/libSceWebBrowserDialog/Export.cpp) (libSceWebBrowserDialog) - there is no browser cookie store. Both calls check their parameters in the same order and with the same codes as the PS5 12.00 module, then return 0 without storing or clearing anything. On the console they then forward the request to the browser service. While an opened dialog has not been updated to finished, closed or terminated, they return `0x80B80006` (the console's open-dialog flag). ### Unknown function info - [sceWebBrowserDialogGetEvent, sceWebBrowserDialogNavigate, sceWebBrowserDialogSetZoom](../../../core/libs/prx/libSceWebBrowserDialog/Export.cpp) (libSceWebBrowserDialog) - their names and NIDs are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceWebBrowserDialog.c); the no-parameter prototypes follow [shadPS4 `webbrowserdialog.cpp`](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/web_browser_dialog/webbrowserdialog.cpp#L23-L85) and are not measured on a console. They are declared without parameters solely to resolve known imports; this is not an ABI claim, and every call throws. diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index ef9b20680c..c9cda8fa5a 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -52,8 +52,11 @@ - [__umodti3](../../../core/libs/prx/libc/src/RuntimeSupport.cpp) (libc) - the hardware traps on a zero divisor; this throws instead - [__modti3](../../../core/libs/prx/libc/src/RuntimeSupport.cpp) (libc) - the hardware traps on a zero divisor; this throws instead +- [snprintf and vsnprintf](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) reject sizes above `INT_MAX + 1` with `EOVERFLOW` (84), a terminating NUL and return value -1, following FreeBSD 11.4 [snprintf.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdio/snprintf.c) and [vsnprintf.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdio/vsnprintf.c). This limit is unverified on the console. A null buffer at an oversized nonzero size returns the error without writing, where FreeBSD dereferences it. + ### Functional +- [_Cosh](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows the two-argument Dinkumware CRT helper used by `std::complex`, including a scaled large-finite-input path; the signature, exact result and floating-point exception behavior are not verified against the console. - [_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. - [sscanf_s and vsscanf_s](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) return `EOF` when a reached conversion has a null destination, stopping before further assignments, following C11 K.3.5.3.7 and K.3.5.3.14. Earlier assignments are retained. The constraint handler is not invoked, and console behaviour is unverified. @@ -75,6 +78,7 @@ - [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. - [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 - [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](../../../core/libs/prx/libc/src/DinkumwareInternals.cpp) (libSceLibcInternal, implemented in libc) throws 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 - [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 - [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. `rmdir("dir/.")`, `unlink("dir/.")` and the same calls with `..` follow the same lookup rule in FreeBSD but are not changed here: the guest still resolves the dot component away and acts on `dir` or its parent. 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) stops on nanf at start-up after strsep. @@ -95,13 +99,13 @@ - 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 fail to open a directory in a read mode with `EACCES`, where FreeBSD opens it; directory streams are not emulated +- [`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 - [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` - [`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 -- The Windows libc [exception unwinder](../../../core/libs/prx/libc/src/exception/Unwind.cpp) steps through frames of modules without the project's `.ehmeta` and `.ehfram` tables (system DLLs, and WinLibs GCC 15.2's `libstdc++-6.dll` and `libwinpthread-1.dll`, whose `.eh_frame` sections are empty) with their PE unwind data through `RtlVirtualUnwind`, but does not run Windows exception or termination handlers: it stops at a frame whose unwind data has one, such as a C++ cleanup through `__gxx_personality_seh0` in `libstdc++-6.dll` or a system DLL function like `qsort` in `ucrtbase.dll` (Windows 10 22H2), and the exception is then reported as unhandled +- The Windows libc [exception unwinder](../../../core/libs/prx/libc/src/exception/Unwind.cpp) cannot run arbitrary Windows exception or termination handlers. It accepts GCC C and C++ handlers identified by a project's `.sehgcc` section and interprets their LSDA with the guest personalities. A handler in an unmarked module, such as a C++ cleanup through `__gxx_personality_seh0` in `libstdc++-6.dll` or a system DLL function like `qsort` in `ucrtbase.dll` (Windows 10 22H2), still stops unwinding and the exception is reported as unhandled. SjLj exceptions are unsupported; Scoop GCC 15.2.0 selects them when SEH is disabled and lacks the System V ABI SEH compiler fix carried by MSYS2. - The libc [SSE4a trap emulation](../../../core/libs/prx/libc/src/specifics/windows/Sse4aEmulation.hpp) on Windows is superseded by `--to-intel` and remains only until the relinked title has been verified without it. - [getifaddrs](../../../core/libs/prx/libc/src/Interfaces.cpp) (libc) lists host interfaces and IPv4/IPv6 addresses in FreeBSD's layouts. Windows omits per-interface AF_LINK entries and interfaces without an address; its flags are only `IFF_UP`, `IFF_RUNNING`, `IFF_LOOPBACK`, `IFF_BROADCAST` and `IFF_MULTICAST` (PPP/tunnel adapters omit broadcast and point-to-point, and an unspecified prefix length gets a host mask). On Linux, `AF_LINK` `if_data` maps non-Ethernet/loopback/PPP types to `IFT_OTHER` (1) and leaves `ifi_omcasts`, `ifi_noproto`, `ifi_hwassist`, `ifi_metric`, `ifi_epoch`, `ifi_lastchange`, unknown baud rates and address-entry `ifa_data` as 0/null; a point-to-point address without a peer loses `IFF_POINTOPOINT`, and an omitted IPv4 broadcast is derived from the netmask. Interface names are the host's (`if` on Windows above 15 bytes). - [gethostname](../../../core/libs/prx/libc/src/Hostname.cpp) (libc) reports the fixed guest name `AnyPS5` without querying the host machine name. Buffer copying, null queries and `ENAMETOOLONG` follow [FreeBSD gethostname](https://github.com/freebsd/freebsd-src/blob/main/lib/libc/gen/gethostname.c); the console's configurable hostname and its integration with `sysctl` and name resolution are unverified and not implemented. @@ -125,7 +129,7 @@ - [syslog](../../../core/libs/prx/libc/src/Syslog.cpp) (libc) writes each message to `stderr` as `[syslog:] `, with a newline added when missing, instead of sending it to the console's system log. Priority and facility handling follow FreeBSD's `vsyslog`: `%m` expands to the guest errno's message, unknown bits are reported (as priority 0x23, since FreeBSD passes `LOG_ERR|LOG_CONS|LOG_PERROR|LOG_PID` as the priority) and masked, a missing facility becomes `LOG_USER` or the facility set by `openlog`, and errno is preserved. `openlog` applies its identity and `LOG_PID`; a null identity retains the previous one, and a valid nonzero facility replaces the default. `closelog` clears the identity but retains the options and facility, as in FreeBSD 11; when no identity is set, FreeBSD falls back to the program name, which this backend does not emit. `LOG_PERROR` has no extra effect because stderr is the emulated log sink; `LOG_NDELAY`/`LOG_ODELAY`, `LOG_NOWAIT` and `LOG_CONS` have no daemon, child or console backend to control. `setlogmask` follows [FreeBSD 11](https://github.com/freebsd/freebsd-src/blob/8d2708de5d6112a492744d367cfdfb8fd219227a/lib/libc/gen/syslog.c): the process-wide mask defaults to 0xff, zero queries it, and nonzero values replace it; masked messages are skipped before formatting, including the unknown-priority diagnostic. The console's mask behaviour is unverified - [fwscanf, vfwscanf, wscanf, vwscanf and their `_s` forms](../../../core/libs/prx/libc/src/ScanningWide.cpp) (libc) run the `swscanf` scanner on characters read with `fgetwc`, so everything listed for `swscanf` in this file applies to them. As in the console's `libc.prx`, a field takes characters while they can still begin a valid item, so a failed field such as `0x` for `%x` stays consumed, and only the character that ended the scan goes back to the stream. That character is pushed back as its UTF-8 bytes with `ungetc`, because `ungetwc` takes single bytes only; the Windows C runtime guarantees one byte of push-back, so a non-ASCII lookahead can be lost there. A null stream or format returns `EOF`; the console checks them only in the `_s` forms. - [daemon](../../../core/libs/prx/libc/src/Process.cpp) (libc) throws: forking into the background is not supported. PPSA85300 imports it and fails to load without it. VLC 3.0 calls it only when its `daemon` option is set ([libvlc.c](https://github.com/videolan/vlc/blob/6de05adcbaf2e8b85fe86aad4169393098628119/src/libvlc.c#L190-L197)), and the title starts libVLC with `--ignore-config` and without `--daemon` ([player.cc](https://github.com/ZiZc3/VLC/blob/f3253020a64368153233ede29183014c0b6c8979/src/player.cc#L960-L981)) -- [_WStoul](../../../core/libs/prx/libc/src/Strings.cpp), [_Iswctype](../../../core/libs/prx/libc/src/RuneLocale.cpp), [swscanf_s and swscanf](../../../core/libs/prx/libc/src/ScanningWide.cpp) (libc) - `_WStoul` is `wcstoul` and throws for a base other than 0 or 2 to 36. `_Iswctype` takes the class numbers of Dinkumware's [xwcc.h](https://github.com/superna9999/ti-cgt6x/blob/cgt6x_7_3_4/include/xwcc.h) (1 `alnum` to 11 `xdigit`, 12 `blank`) and throws for any other value, including the FreeBSD masks `wctype` returns; the numbering is not confirmed on the console's `libc.prx`. `swscanf_s` follows C11 7.29.2.2 and K.3.9.1.5, but reads the size argument as `unsigned int` like `sscanf_s` where K.3.9.1.5 specifies `rsize_t`; the console's width is unverified. A runtime-constraint violation (null buffer, format or receiving pointer) only returns `EOF`, and a too small array gets a null first element as in `sscanf_s`. A literal mismatch is a matching failure and a field that is only a prefix, such as `0x` or `1e`, does not convert, as C11 requires; FreeBSD's `vfwscanf` reports an input failure for the first and converts the second. Integers out of range are truncated from `strtoll`/`strtoull`. `%p` follows FreeBSD 11's [vfwscanf.c](https://github.com/freebsd/freebsd-src/blob/4ab2e064d7950be84256d671a7ae93f87cc6aa36/lib/libc/stdio/vfwscanf.c#L610-L615): a hexadecimal integer with an optional `0x`, converted as unsigned and stored as a pointer; a length modifier on it throws, where FreeBSD ignores it. It throws for `%S`, `%C`, unknown length modifiers, scanset ranges, `%n` with suppression or width, and narrow `%s`, `%c` or `%[` input that the C-locale `wcrtomb` cannot convert. `swscanf` runs the same scanner without the size arguments of `%s`, `%c` and `%[`, as FreeBSD's [swscanf.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libc/stdio/swscanf.c) passes its arguments to `vswscanf`, so everything above except the sizes applies to it; it also keeps the null buffer, format and receiving pointer checks returning `EOF`, where C leaves those undefined. Neither variant is confirmed on a PS5 title +- [_WStoul](../../../core/libs/prx/libc/src/Strings.cpp), [_Iswctype](../../../core/libs/prx/libc/src/RuneLocale.cpp), [swscanf_s and swscanf](../../../core/libs/prx/libc/src/ScanningWide.cpp) (libc) - `_WStoul` is `wcstoul` and throws for a base other than 0 or 2 to 36. `_Iswctype` takes the class numbers of Dinkumware's [xwcc.h](https://github.com/superna9999/ti-cgt6x/blob/cgt6x_7_3_4/include/xwcc.h) (1 `alnum` to 11 `xdigit`, 12 `blank`) and throws for any other value, including the FreeBSD masks `wctype` returns; the numbering is not confirmed on the console's `libc.prx`. `swscanf_s` follows C11 7.29.2.2 and K.3.9.1.5, but reads the size argument as `unsigned int` like `sscanf_s` where K.3.9.1.5 specifies `rsize_t`; the console's width is unverified. A runtime-constraint violation (null buffer, format or receiving pointer) only returns `EOF`, and a too small array gets a null first element as in `sscanf_s`. A literal at end of input returns the number of assignments, including zero, rather than `EOF` as FreeBSD does, as reported from the console scanner in #8479; `%%` at end of input still returns `EOF`. A literal mismatch is a matching failure, where FreeBSD's `vfwscanf` reports an input failure. An incomplete floating exponent such as `1e+` converts its mantissa, as reported from the console scanner in #8479; other incomplete exponent forms are not separately verified. A short `%c` field is currently a matching failure without storing its available prefix, while #9321 reports that the console stores a short `%3lc` field; its return count, partial-output extent and precedence are unverified. An integer field that is only a prefix, such as `0x`, does not convert; FreeBSD's `vfwscanf` pushes back the `x` and converts the `0`. Integers out of range are truncated from `strtoll`/`strtoull`. `%p` follows FreeBSD 11's [vfwscanf.c](https://github.com/freebsd/freebsd-src/blob/4ab2e064d7950be84256d671a7ae93f87cc6aa36/lib/libc/stdio/vfwscanf.c#L610-L615): a hexadecimal integer with an optional `0x`, converted as unsigned and stored as a pointer; a length modifier on it throws, where FreeBSD ignores it. It throws for `%S`, `%C`, unknown length modifiers, scanset ranges, `%n` with suppression or width, and narrow `%s`, `%c` or `%[` input that the C-locale `wcrtomb` cannot convert. `swscanf` runs the same scanner without the size arguments of `%s`, `%c` and `%[`, as FreeBSD's [swscanf.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libc/stdio/swscanf.c) passes its arguments to `vswscanf`, so everything above except the sizes applies to it; it also keeps the null buffer, format and receiving pointer checks returning `EOF`, where C leaves those undefined. Neither variant is confirmed on a PS5 title - [flock](../../../core/libs/prx/libc/src/FileLock.cpp) (libkernel) on Windows shares one lock state between duplicated descriptors, following FreeBSD 11.4's [flock(2)](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libc/sys/flock.2). Independent opens have separate states; closing a descriptor or reopening a stream drops only its reference. Lock transitions on one open file description are serialized. The lock is a host [LockFileEx](https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-lockfileex) lock on one byte at offset 2^63 - 2, which no file reaches, so it excludes other `flock` calls and leaves reads, writes and mapped views alone, like FreeBSD's advisory locks. Console behavior, signal interruption and concurrent closure of a descriptor while its lock call is pending are unverified. - [std::_Throw_Cpp_error](../../../core/libs/prx/libc/src/exception/StandardExceptions.cpp) (libc) follows Dinkumware `thread0.cpp` ([VC12](https://github.com/ganboing/crt.vc12/blob/f1d47126/src/thread0.cpp), [STL](https://github.com/microsoft/STL/blob/da52dc0f/stl/src/thread0.cpp)), without console verification. Codes outside 0..6 throw `std::out_of_range`, the deleting destructor throws, and STREAMS errnos absent from FreeBSD 11.4 [`errno.h`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/sys/errno.h) (`ENODATA`, `ENOSR`, `ENOSTR`, `ETIME`) are omitted. Codes 0..6 throw a 32-byte [VC12 `system_error`](https://github.com/ganboing/crt.vc12/blob/f1d47126/src/system_error) with FreeBSD errnos (16, 22, 3, 12, 1, 11, 35), a static `generic_category` (not `_Generic_object`), and unverified 40-byte [GuestLocale::String](../../../core/libs/prx/libc/include/GuestLocale.hpp) messages from [`syserror.cpp`](https://github.com/ganboing/crt.vc12/blob/f1d47126/src/syserror.cpp) - [Call tracing](../../../core/libs/prx/libc/src/specifics/x86_64/CallTrace.cpp) (`--trace-imports`, `APS5_TRACE_CALLS`) is missing on macOS and records only the entry of a call, not its arguments or result. One process-wide ring keeps the last 65536 calls, so a busy thread pushes the others out; the reporting thread's own calls are listed only while they are still in that ring. Calls are counted per traced target; an import bound to a target already traced under another name is counted under the first name. The thunk saves the integer argument registers and `xmm0` to `xmm7`; wider vector arguments survive only while the recording code avoids AVX, which is untested. Linux has no crash report, so it prints the trace only on `SIGABRT` (guest or host `abort()`), not on `SIGSEGV`; a caller in a module loaded after the last traced import prints without a module name, and a thread aborting while another writes the report waits for it at most 2 seconds @@ -134,3 +138,4 @@ - [fopen_s](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) - returns the errno of the failed open, as C11 K.3.5.2.1 leaves the nonzero value open and Microsoft's CRT returns errno; the console's Dinkumware `fopen_s` is not verified - [ios_base standard streams](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) `_Addstd` queues the stream in the standard stream table of Dinkumware [ios.cpp](https://github.com/ganboing/crt.vc12/blob/master/src/ios.cpp) (`stdstr`, `stdopens`, `_Addstd`, `_Ios_base_dtor`): it takes the first of slots 1 to 7 that is free or already holds the stream, and slot 8 (`NSTDSTR`) when none is, so slot 8 is shared by every stream past the first seven and registration never fails. It stores the 1-based slot index in the guest `IosBase::standardStream` (Dinkumware's `_Stdstr`, where 0 means not a standard stream) and counts the stream objects registered per slot; registering the same stream again reuses its slot and raises its count. Measured on a PS5 (firmware 13.60) from a homebrew payload calling the system `libSceLibcInternal` export: in a process whose own standard streams already hold slots 1 to 7, twelve distinct streams and a repeated one all get 8, written as a 64-bit value at offset 0x8 with no other byte of the object touched; the assignment of slots 1 to 7 to fresh streams is not observable there and follows VC12. The counts are `int`, not VC12's `char`, and the registration and the destructor lock the table where VC12 locks only `_Addstd`; a slot index past the table throws in the destructor instead of indexing out of bounds. The destructor decrements its slot's count and suppresses the rest of the destruction while the count stays positive, as `_Ios_base_dtor` does, and the slots of destroyed streams keep their pointer and stay reserved for that stream, as `stdstr` does. The streams themselves are destroyed by the title's own code as it registers them through `__cxa_atexit`; when the last reference closes, the unsupported stream storage and callback cleanup is still the throw it was and the locale is released as for a non-standard stream. - [std::_Pad, _Thrd_join, _Thrd_detach, _Thrd_id](../../../core/libs/prx/libc/src/Threads.cpp) (libc) follow Dinkumware V6.00 `thr/xthread` and `thr/threadcall.cpp` ([crt.vc12 `f1d47126`](https://github.com/ganboing/crt.vc12/tree/f1d47126/src/thr)), without console verification. `_Thrd_id` throws because whether the console returns `pthread_t` or a kernel thread ID and at what width is unknown; `_Launch` omits `_Cnd_do_broadcast_at_thread_exit` and calls `pthread_create` registered by libkernel with default attributes. The 32-byte pad layout (`_Go()` vtable at 0x0, `_Cnd_t` at 0x8, `_Mtx_t` at 0x10, `bool _Started` at 0x18) is assumed from VC12, unverified. Failing primitives or thread start throw the guest `std::system_error`; null arguments throw. `_Thrd_join` returns `_Thrd_error` (4) on `pthread_join` failure and truncates the exit code to `int`; `_Thrd_detach` returns `_Thrd_error` (4) on `pthread_detach` failure and throws when none is registered +- [num_get::id, time_get::id and the num_get vtable](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) are data objects, as the public SDK's libSceLibcInternal table declares them and as the other facet ids and the `num_put` and `codecvt` vtables are kept: the ids start at zero and the vtable is 17 zeroed slots, the 136-byte size of `KfcTPbeaOqg` in the console's libSceLibcInternal, whose relocations fill the offset-to-top and typeinfo entries, the two destructors, `locale::facet::_Incref` and `_Decref`, and the eleven `do_get` of Dinkumware's crt.vc12 `xlocnum`, so a guest resolves the symbols and reads data, and a facet call through the vtable faults. libSceAmpr exported the three as throwing functions, which a data consumer reads as code; its qualified imports fall back to libc. diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index d34f5f2425..52c5621330 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -5,6 +5,7 @@ - [sceKernelGetSocPowerConsumption](../../../core/libs/prx/libkernel/System/src/ThermalSensors.cpp) (libkernel) - returns `EINVAL` and writes nothing, as `sceKernelGetSocSensorTemperature` does when the host has no sensor: the host's package power is not read (Linux RAPL `energy_uj` is readable by root only, and Windows has no general source). The prototype is not documented. PS5LM Plus (PPSA99581) passes a zeroed 128-byte buffer and, on success, reads its first 32-bit word as milliwatts, accepting 1000 to 350000; on failure it shows 0 W - [scePthreadMutexattrSetprotocol](../../../core/libs/prx/libkernel/Pthread/src/Mutex.cpp) (libkernel) - accepts 0 to 2 as a PS5 (firmware 13.60) does and returns `EINVAL` otherwise, but the protocol is not stored or applied: priority inheritance and protection are not modelled. On the console a null or uninitialized mutex attribute returns `EINVAL` from the SCE attribute calls, `scePthreadMutexInit` takes an uninitialized attribute as the default one, and a destroyed mutex returns `EINVAL` from `scePthreadMutexDestroy`; a null mutex pointer crashed the payload and still throws - [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 null, uninitialized or destroyed attribute returns `EINVAL` before the other arguments are checked; this error behavior is unverified on the console. 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 12.00 `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 - [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 - [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. @@ -28,7 +29,7 @@ - [sceKernelGetProsperoSystemSwVersion](../../../core/libs/prx/libkernel/System/src/SoftwareVersion.cpp) (libkernel) reports system software version `0x01000000` ("01.000.000"); the console's firmware version is not modeled. The 0x28-byte block (a 64-bit size at 0, the version string at 8 and the packed version at 0x24) follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Kernel/KernelSystem.cs), which Prospero Multi Tools (PPSA99110) and Prospero Explorer (PPSA99109) are built with and which sets the size to 40, and the same fields in [etaHEN](https://github.com/etaHEN/etaHEN/blob/main/Source%20Code/daemon/include/ipc.hpp) and [shsrv](https://github.com/ps5-payload-dev/shsrv/blob/master/sh.c). A size other than 0x28 returns `SCE_KERNEL_ERROR_EINVAL` without writing; whether the console checks the size is unverified, since etaHEN and shsrv declare a 0x30-byte block and do not set it - [sceKernelGetAllowedSdkVersionOnSystem](../../../core/libs/prx/libkernel/System/src/SoftwareVersion.cpp) (libkernel) reports the system software version with the 12 patch bits set, `0x01000fff`, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/98fd0689/src/core/libraries/kernel/kernel.cpp) does on the PS4 and as [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Kernel/KernelSystem.cs) describes it, which Prospero Explorer (PPSA99109) reads it through; a null pointer returns `SCE_KERNEL_ERROR_EINVAL`. The console's value and its errors are unverified - [sceKernelConvertUtcToLocaltime, sceKernelConvertLocaltimeToUtc](../../../core/libs/prx/libkernel/Time/Time.cpp) (libkernel) convert with the host time zone at the given instant, as libc `localtime` does, instead of the console's time zone setting. `west_sec` holds the standard-time offset in seconds east of UTC, despite its name, and `dst_sec` the daylight saving seconds in effect. This follows PPSA99203 ([PS5SX2](https://github.com/Swordpdf/PS5SX2/blob/b4cf55e87c34e8a2a1cd9302df4373e5a94d28f4/ps5/frontend/fe_ps5.cpp#L54-L63)), whose author saw a console write 7200 and 3600 into the 16-byte block; which zone that console was set to is not recorded, so whether `west_sec` includes daylight saving is not settled. [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/23c41333/src/core/libraries/kernel/time.cpp) also stores seconds east. PPSA99203 declares the last argument of `sceKernelConvertUtcToLocaltime` as `uint64_t*`; 32 bits are written. On Windows the standard offset and the daylight saving amount are the C runtime's current `_get_timezone` and `_get_dstbias` for every instant, as libc `localtime_s` uses them, so instants under a zone's earlier rules get today's offsets; a `TZ` variable does not set `_dstbias`, which keeps the value an earlier `_tzset` read from the system (0 when the system zone has no daylight saving), and summer time then adds nothing. In a repeated or skipped local hour, `sceKernelConvertLocaltimeToUtc` takes the offset in effect one standard offset earlier. Its last argument receives the zone's daylight saving amount whether or not it is in effect, found by sampling the host zone over the year around the result, while `dst_sec` is the amount in effect: the libc `mktime` that PPSA99109 bundles (text 0x70ebb-0x70ee5) subtracts that amount when `tm_isdst` is set outside summer time. That `mktime` passes `tm_isdst` as the second argument, which is ignored, so it does not choose the offset in a repeated hour. `sceKernelGettimezone` and the `gettimeofday` time zone still report no offset and no daylight saving. A host time the C runtime cannot convert throws -- [getuid, geteuid, getgid, getegid and issetugid](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) report the root identity (0) and a process that never changed credentials (`issetugid` 0), as a jailbroken process has; the host user and group are not exposed to the guest. The values a title sees on a console are unverified +- [getuid, geteuid, getgid, getegid, getresuid, getresgid and issetugid](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) report the root identity (0) and a process that never changed credentials (`issetugid` 0), as a jailbroken process has; the host user and group are not exposed to the guest. `getresuid` and `getresgid` follow FreeBSD 11.4's `sys_getresuid` and `sys_getresgid`, writing 0 to non-null pointers and leaving errno untouched on success. The values a title sees on a console are unverified - [sceKernelGetAppInfo](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - only `app_id` and the title ID (offset 0x10, up to 9 characters plus a terminator) are written; callers declare the structure with different sizes, so the rest is left alone. `app_id` is 1, the running title's id in libSceSystemService, which is a guess, not something measured. The error codes follow shadPS4: `EPERM` for another process, and 0 without writing anything for a null pointer. - [AMPR object layout and compact gathers](../../../core/libs/prx/libkernel/Apr/include/AprCommandBuffer.hpp) - the 24-byte object and offsets (type 0, cursor 4, count 8, capacity 12, pointer 16) follow [ampr_emu assertions](https://github.com/drakmor/ampr_emu/blob/cfa85df379f6eeeb165d7badf9b648e266fe77b7/include/ampr_emu_command_buffer_types.h#L138-L143) and its [C declaration](https://github.com/drakmor/ampr_emu/blob/cfa85df379f6eeeb165d7badf9b648e266fe77b7/include/ampr.h#L41-L47). AnyPS5 represents the 32-bit type word as low 16-bit type and high 16-bit recording flags. Command encoding is AnyPS5 internal: opcode 29 and three 32-bit words encode the high offset byte, length minus one and low offset word. The 12-byte size reduces HFW's 1 MiB buffer pressure; it is not a claim about console opcode encoding. Existing 40-bit offsets and lengths 1 through 2^32 are retained; console behavior outside those limits is unverified. - [APR resolve ForEach result](../../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libkernel) - `sceKernelAprResolveFilepathsToIdsAndFileSizesForEach` returns the number of paths that resolved, with the per-path status in `results` and the failed ids/sizes set to the invalid id and 0. The [ampr_emu](https://github.com/drakmor/ampr_emu) bridge passes the count of its resolver through and does not show what is counted; counting successes is not confirmed on a title. The guest errno of the failing non-`ForEach` resolves is set through `__error_nid_postfix` @@ -82,7 +83,7 @@ - [select](../../../core/libs/prx/libkernel/Socket/src/Socket.cpp) (libkernel) - a descriptor the socket poller reports as unknown that is below `GuestSockets::FirstDescriptor` is polled on the host instead of throwing (on Windows only a pipe, as above); an invalid host descriptor fails with `EBADF`, and mixed host and socket sets are polled in short alternating slices so neither side waits out the full slice alone. A descriptor that stops being a socket, a kqueue or a pipe during the wait fails the call with `EBADF` when it is closed and is selected again with the rest of the timeout when its number was reused, as FreeBSD 11.4's [`kern_select`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/sys_generic.c#L1129-L1140) looks every descriptor up again in each pass. A close that has already removed the descriptor from the guest's kqueue, pipe or file tables but not yet closed its host descriptor is seen by that new look-up as a descriptor of unknown kind, which throws on Windows and is polled through the host on Linux. Wobbly Life (PPSA29413) selects on file descriptors while streaming its bundles - [__progname](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - unknown data export - [environ](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - data export pointing to a fixed empty environment so titles that import it load; it is not the environment libc's `getenv` reads, so it lists none of those variables and `setenv`/`putenv` changes do not appear in it -- [seteuid, setegid, setuid, setgid, setreuid, setregid, setgroups, getgroups](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - the guest process is assumed to run as root, with real, effective and saved user and group ids 0. As in FreeBSD's `sys_seteuid`, `sys_setegid`, `sys_setuid`, `sys_setgid`, `sys_setreuid` and `sys_setregid`, id 0 (or -1 for an unchanged `setreuid`/`setregid` id) is then accepted without a privilege check and changes no credential, so 0 is returned; any other id would change an id, which is not modelled, and throws. `setgroups` keeps the supplementary group list as `kern_setgroups` does (`NGROUPS_MAX` 1023, an empty list leaves the effective group as the only entry, a first entry other than 0 would change the effective group and throws) and `getgroups` reports it. The ids and privileges a title has on a console are unverified +- [seteuid, setegid, setuid, setgid, setreuid, setregid, setresuid, setresgid, setgroups, getgroups](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - the guest process is assumed to run as root, with real, effective and saved user and group ids 0. As in FreeBSD's `sys_seteuid`, `sys_setegid`, `sys_setuid`, `sys_setgid`, `sys_setreuid`, `sys_setregid`, `sys_setresuid` and `sys_setresgid`, id 0 (or -1 for an unchanged `setreuid`/`setregid`/`setresuid`/`setresgid` id) is then accepted without a privilege check and changes no credential, so 0 is returned; any other id would change an id, which is not modelled, and throws. `setgroups` keeps the supplementary group list as `kern_setgroups` does (`NGROUPS_MAX` 1023, an empty list leaves the effective group as the only entry, a first entry other than 0 would change the effective group and throws) and `getgroups` reports it. The ids and privileges a title has on a console are unverified - [pthread_getname_np](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - a thread that has already been joined returns `ESRCH` (3) and its output buffer is left untouched, as measured on a PS5 (firmware 13.60) by the author of this entry; since [#4481](https://github.com/boykopovar/AnyPS5/pull/4481) the joined thread's object is retained, so the posix wrapper answers `ESRCH` for it instead of reading the retained name. A null name buffer is answered with `EFAULT` before that, an order the console table does not measure. The SCE sibling `scePthreadGetname` has no measured joined-thread behaviour on the console and still reads the retained object's name, so it is unchanged, as is the pre-existing dangling case of a thread that ended detached (its object is released at exit; see the scePthreadJoin entry) - [sceKernelGetOperationMode](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - the signature `(int* mode, int* submode)` comes from PPSA12544, which logs both values after the call and ignores the result. A homebrew payload on a PS5 (firmware 13.60) reads mode 2 and submode 0, nothing else is written, and the result register holds no status, so the function is taken to return nothing. Whether a title process reads the same values is unverified. Null outputs throw - [sceCoredumpGetStopInfoCpu](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) has no active coredump context. Native null/size validation is recovered, including both 8-byte and 16-byte records; valid requests return `NOT_IN_COREDUMP_HANDLER` without accessing the output. Active-handler data retrieval remains unimplemented. @@ -129,12 +130,14 @@ - [Windows file timestamps](../../../core/libs/prx/libkernel/File/include/WindowsFileTime.hpp) use the host filesystem's timestamp range and resolution. `utimes`, `futimes`, and `lutimes` reject timestamps outside nonnegative signed 64-bit Windows file-time ticks with `EINVAL`, including exactly 1601-01-01 00:00:00 UTC, whose zero value means "leave unchanged" to `SetFileTime`. Directory timestamp updates resolve the current path of the opened handle. Filesystems with coarser timestamp resolution cannot retain every guest microsecond - [EVFILT_VNODE on macOS](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) (libkernel) registers `EVFILT_VNODE` on a duplicate of the descriptor in a host kqueue, whose notes have FreeBSD's values, and drops the `NOTE_LINK` that XNU adds to `NOTE_DELETE` when the watched file or directory is removed, since FreeBSD's `vop_remove_post` and `vop_rmdir_post` report `NOTE_DELETE` alone. As on Windows, a directory registered with `NOTE_EXTEND` is accepted but never receives it: XNU reports a move between directories as `NOTE_WRITE` alone, with `NOTE_LINK` for a subdirectory - [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 reported the same way when the name left after that prefix is a drive path (`X:\...`): the print name when that name is a drive path, otherwise the substitute name. A mounted volume, whose substitute name is `\??\Volume{GUID}\`, and any other reparse point throw, as for `readlink`. `open` with `O_NOFOLLOW` returns `EMLINK` for that junction and does not open the target +- [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 reported the same way when the name left after that prefix is a drive path (`X:\...`): the print name when that name is a drive path, otherwise the substitute name. `open` with `O_NOFOLLOW` returns `EMLINK` for that junction and does not open the target +- [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 - [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 - [sceKernelWriteThrottlingStatus](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) reports no write throttling: the host SSD has no write budget, so the first qword of the 32-byte status is set to `UINT32_MAX` and the rest is zeroed; returns `EFAULT` (`0x8002000e`) when passed a null status buffer pointer. PPSA17221 reads only that qword: it multiplies it by 64 KiB and compares the product with byte thresholds up to 0x4AF00000 to pick one of five tiers. When the call fails the title assumes 0x4B000001 bytes, which selects the same top tier as `UINT32_MAX`. What the value measures on hardware (a remaining budget or a rate) and the meaning of the other three qwords are unknown - [Windows directory descriptors](../../../core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp) (libkernel) take one snapshot of the directory, on the first `getdents`, `getdirentries` or non-zero `lseek` (`SEEK_SET` or `SEEK_CUR`), and keep it until it is discarded; `lseek` to offset 0 only discards the snapshot, and the next read lists the directory again, so every rewind costs O(entries) on the next read. `lseek` takes the offset as an entry index and `SEEK_CUR` with offset 0 only reports the index. `SEEK_END` throws because FreeBSD defines it relative to the directory size, which is not modelled. A negative result returns `EINVAL` and a `SEEK_CUR` offset that overflows returns `EOVERFLOW`, as FreeBSD `vn_seek` reports them. Names whose UTF-8 encoding exceeds the one-byte `d_namlen` limit are not representable in this guest directory-record layout: `getdents` returns `ENAMETOOLONG` without consuming the entry. Repeated calls therefore report the same error and entries after that name cannot be reached through this descriptor; FreeBSD's `getdents` behavior for host filenames that exceed its `MAXNAMLEN` is not verified. `getdirentries` still reports 0 through `basep` instead of the position of the block it read, so only 0 and values returned by `lseek` are valid offsets; FreeBSD offsets are filesystem cookies. Descriptors retain the opened directory's native handle across renames. Enumeration reloads and `*at` operations resolve its current path, so a concurrent rename between resolution and the operation is not atomic. Enumeration of a removed directory and network-share path queries have not been verified +- [Linux directory entry types](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) - `sceKernelGetdirentries` reports a host symlink inside the app0 tree as the type of its target (directory or regular file), since a console app0 has no links and links there come from how the title was unpacked; a link elsewhere, or a dangling one, stays a link. A directory opened through an app0 link resolves to its target's host path, so links nested inside that target keep the link type. - [ftruncate, truncate, sceKernelFtruncate](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) extend a file on Windows with `SetFileInformationByHandle` (`FileEndOfFileInfo`), which reserves the clusters but writes no zero bytes, and report `EFBIG` for a length the volume cannot hold (`ERROR_INVALID_PARAMETER` on a disk file; seen with a length of `INT64_MAX`, the size from which NTFS refuses it was not measured) and `ENOSPC` when the free space is smaller. FreeBSD 11.4 makes the extension a hole on a file system that has them ([`ffs_truncate`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/ufs/ffs/ffs_inode.c) writes only the last block), so a length above the free space succeeds there. A write behind the end of the file still makes NTFS write zero bytes over the gap, as the file is not sparse. None of this was measured on a console. - [readv, writev, preadv, pwritev and their sceKernel forms](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) - unreadable iovec-table errors follow FreeBSD 11.4's [`copyinuio`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/subr_uio.c#L401-L434), not console measurements. The table is copied before inspecting its entries; payload-buffer validation is unchanged. A Linux `process_vm_readv` failure other than `EFAULT` throws, including when the host denies that API. - [/dev/random and /dev/urandom](../../../core/libs/prx/libkernel/File/src/RandomDevice.cpp) (libkernel) opened for reading return a descriptor backed by the host null device whose reads are filled with host OS entropy (`getentropy`, `rand_s` on Windows); both paths behave the same, never block, and positioned reads ignore the offset (FreeBSD `random(4)`). Only the exact paths are recognised, `fstat`, `lseek` and `mmap` see the null device, and opening them for writing throws @@ -184,7 +187,7 @@ - [setrlimit](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) validates the resource and input in FreeBSD order, maps negative limits to infinity, and lowers a soft limit above the hard limit. A request other than `RLIMIT_NPROC` succeeds when its normalized values match `getrlimit`, or, for `RLIMIT_NOFILE`, when `rlim_max` does not exceed the current hard limit and `rlim_cur` does not exceed `rlim_max`, storing the updated limits for subsequent `getrlimit` queries; `RLIMIT_NPROC` always throws; descriptor allocation enforcement of the lowered soft limit is not implemented, and changing other limits throws because guest limit state is not stored. In a payload process on a PS5 (firmware 12.70), `setrlimit(RLIMIT_NOFILE)` lowers a requested 13952 to 12555, as the hard and as the soft limit; here a request within the current hard limit is stored as given and one above it throws; setting `RLIMIT_STACK`, `RLIMIT_RSS` and `RLIMIT_MEMLOCK` to their current values succeeds and changes nothing, as here; setting `RLIMIT_NPROC` to its current 1604 also succeeds but leaves 1443, as FreeBSD 11 would if the console's `maxproc` were 1604: process 0's limit starts at `maxproc` ([`init_main.c`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/init_main.c#L550-L551)), `maxprocperuid` starts at nine tenths of it ([`subr_param.c`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/subr_param.c#L258)), and `setrlimit` lowers an `RLIMIT_NPROC` limit above `maxprocperuid` to it ([`kern_resource.c`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/kern_resource.c#L714-L718)); 1604 × 9 / 10 gives 1443. Neither value is measured: `kern.maxproc` fails with `ENOENT` in the payload process. Here that call throws, because the limit is not stored. Java 25 LAN / DNS (PPSA99869) sets `rlim_cur = MIN(0x100000, rlim_max)` during VM initialization - [execvp](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) throws: replacing the process image is not supported. PPSA85300 imports it and fails to load without it; its `fork` and `vfork` fail with `ENOSYS` ([libc_missing.c](https://github.com/ZiZc3/VLC/blob/f3253020a64368153233ede29183014c0b6c8979/ps5/compat/libc_missing.c#L290-L292)), so the child process that would call it is never created. - [nmount](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) throws: mounting file systems is not supported. Prospero Themes (PPSA90210) imports it and fails to load without it; it opens its theme library without calling it, and its strings (`/dev/ssd0.system_ex`, `/system_ex`, `exfatfs`, `fstype`, `fspath`, `from`, `ignoreacl`) point to remounting the console's `/system_ex` partition to write theme files there. -- [socketpair](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - the pair is the host's `AF_UNIX` pair on Linux and macOS and, on Windows, a loopback TCP connection for `SOCK_STREAM` or two connected UDP sockets for `SOCK_DGRAM`; `SOCK_SEQPACKET` throws on Windows and macOS, which has no such `AF_UNIX` pair. A datagram sent to a full receiver gets `ENOBUFS` on the console, waits or gets `EAGAIN` on Linux and is dropped on Windows, and on Windows a datagram `recv` after the peer closed reports `ECONNRESET` instead of the console's `ENOTCONN`, and a stream `send` after the peer closed reports `ECONNABORTED` instead of the console's `EPIPE`. `read` and `write` on a pair end go to the host like on every guest socket. `getpeername` on a pair end succeeds after the peer closed, where the console returns `ENOTCONN`, and `connect`, `bind` and `sendto` with an address on a pair end report an address-family error instead of the console's `EISCONN`. On Windows `poll` on a stream end reports `POLLHUP` without `POLLIN` after the peer's `shutdown(SHUT_WR)` or `close`, where the console reports `POLLIN`, with `POLLHUP` only after `close` +- [socketpair](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - the pair is the host's `AF_UNIX` pair on Linux and macOS and, on Windows, a loopback TCP connection for `SOCK_STREAM` or two connected UDP sockets for `SOCK_DGRAM`; `SOCK_SEQPACKET` throws on Windows and macOS, which has no such `AF_UNIX` pair. A datagram sent to a full receiver gets `ENOBUFS` on the console, waits or gets `EAGAIN` on Linux and is dropped on Windows, and on Windows a datagram `recv` after the peer closed reports `ECONNRESET` instead of the console's `ENOTCONN`, and the first stream `send` after the peer closed succeeds on the loopback connection, where the console returns `EPIPE`; the sends after it report `EPIPE`. `read` and `write` on a pair end go to the host like on every guest socket. `getpeername` on a pair end succeeds after the peer closed, where the console returns `ENOTCONN`, and `connect`, `bind` and `sendto` with an address on a pair end report an address-family error instead of the console's `EISCONN`. On Windows `poll` on a stream end reports `POLLIN` after the peer's `shutdown(SHUT_WR)` or `close`, as the console does after a `shutdown(SHUT_WR)`; after the peer's `close` the console reports `POLLIN` with `POLLHUP`, because `unp_drop` disconnects the other end, and the loopback connection cannot tell that from a shutdown, so no `POLLHUP` is reported - Guest sockets on macOS ([the equeue wait, kevent, fstat and ioctl](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp), [gethostbyname](../../../core/libs/prx/libScePosixForWebKit/Resolver.cpp)): macOS has no `ppoll`, so the wait uses `poll` with the timeout rounded up to a millisecond; it has no `POLLRDHUP`, so a stream that the peer closed is found by peeking one byte when it is readable; `FIONREAD` on a datagram socket, and the byte count that `fstat` and `kevent` report for it, is `SO_NREAD`, the size of the next datagram, because macOS adds the source address to `FIONREAD`; and `gethostbyname` replaces `gethostbyname_r`, which macOS lacks. - [UDP recv/recvfrom](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) and [sceNetRecv/sceNetRecvfrom](../../../core/libs/prx/libSceNet/Export.cpp) translate Windows WSAEMSGSIZE into the copied byte count for datagram sockets, including MSG_PEEK, and retain the sender address. This follows FreeBSD 11.4 recv(2) and Winsock's documented truncated-datagram behavior; console behavior and existing title impact are unverified. - [recvmsg](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) loses received stream data on an output copy fault: outputs are checked after the host receive. FreeBSD 11.4 leaves unread stream data queued, though it also drops a datagram on a copy fault ([`soreceive_dgram`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L2308-L2312), [`soreceive_generic`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L1758-L1774)). A Linux `process_vm_readv` or `process_vm_writev` failure other than `EFAULT` throws @@ -197,13 +200,14 @@ - [Socket descriptor numbers](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel, libSceNet) - `socket`, `accept`, `socketpair`, `dup`, `sceNetSocket` and `sceNetAccept` take the lowest free number of the host descriptor table that files use, as FreeBSD 11.4 `fdalloc` takes the lowest free entry of the one process table (`kern_descrip.c`), so every number stays below `FD_SETSIZE` (1024) while fewer than 1024 descriptors are open. The number holds a zero-access `NUL` handle on Windows and a duplicate of the host socket on Linux, and the socket tables map it to the socket. The host table also holds the runtime's own files, so the numbers differ from the console's; the first number `socket` and `sceNetSocket` return on a console and its `getdtablesize` are not measured. Paths that bypass the socket tables see the placeholder: libc `fdopen` on a socket opens it, and libkernel `close` of a libSceNet socket releases the number but leaves the libSceNet socket open. Palworld (PPSA23126), The Mound (PPSA26223) and Dying Light 2 (PPSA02262) set socket bits in a 1024-bit `fd_set` - [fstat on a guest socket](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) fills the fields FreeBSD 11's soo_stat and pru_sense_null fill and zeroes the rest: S_IFSOCK, the read bits while the receive side is open or data is queued, the write bits while the send side is open, st_size and st_blksize. Only this process's shutdown closes a side here; a peer's shutdown, close or reset does not clear the bits as FreeBSD's socantrcvmore and socantsendmore do. st_size is the host's FIONREAD, which for a Linux datagram socket is the size of the next datagram where FreeBSD counts every queued byte, and st_blksize is the host's SO_SNDBUF, which Linux reports as twice the size that was set. A socketpair end reports st_dev and st_ino 0, where FreeBSD's uipc_sense reports NODEV and a per-socket number - [poll](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) waits on guest sockets and pipes, through `WSAPoll` on Windows and `poll` on Linux; the `pollfd` layout and flag values are FreeBSD's. A libSceNet socket joins the same host wait through the native socket libSceNet lends to libkernel. `WSAPoll` rejects `POLLPRI`, so on Windows a socket's `POLLPRI` is asked as `POLLRDBAND` and `POLLIN` as `POLLRDNORM`; Winsock reports urgent data until it is read with `MSG_OOB`, where FreeBSD 11.4's `sopoll_generic` stops once the normal stream passes the mark. Guest `send` and `recv` refuse `MSG_OOB` with `EOPNOTSUPP`, so urgent data can only come from an outside peer and Windows keeps reporting `POLLPRI` after the stream passes the mark. A pipe follows FreeBSD's `pipe_poll`: the read end is readable when data is buffered, either end reports `POLLHUP` once the other end is closed, with the requested `POLLIN` and `POLLRDNORM` bits and without `POLLOUT`, which `pollscan` clears, and the write end is writable when `PIPE_BUF` (512) bytes are free on Windows, or when the host kernel says so on Linux. `POLLOUT` on a read end throws: FreeBSD opens both ends for reading and writing and reports it writable, and host pipes are one-way. Windows pipes cannot be waited on, so while a pipe is polled `poll` checks it with `NtQueryInformationFile` and waits on the sockets in slices of 1 ms. A regular file, a directory or a file read from app0.zar is always ready for `POLLIN`, `POLLRDNORM` and `POLLOUT` and never for `POLLPRI`, `POLLRDBAND` or `POLLWRBAND`, as FreeBSD 11.4's [`vop_stdpoll`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_default.c#L563-L574) and `vop_nopoll` answer every event `poll` accepts; which of the two the console's filesystems use is not known, and they agree on these events. `/dev/random` and `/dev/urandom` follow FreeBSD 11.4's [`randomdev_poll`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/dev/random/randomdev.c#L274-L285) for a seeded device: a request with `POLLIN` or `POLLRDNORM` gets only those bits back, and any other request gets every requested bit. Open descriptors 0 to 2 follow the console model `select` uses: descriptor 0 never reports an event and descriptors 1 and 2 report `POLLOUT`, whatever the host attaches to them. A closed descriptor reports `POLLNVAL`, and an open descriptor that is neither a guest socket, a libSceNet socket, a pipe, a file, a directory, the random device nor descriptor 0 to 2 (other devices, a kqueue) throws, as do event bits other than `POLLIN`, `POLLPRI`, `POLLOUT`, `POLLRDNORM`, `POLLRDBAND` and `POLLWRBAND` (the `POLLERR`, `POLLHUP` and `POLLNVAL` bits are ignored in `events`), an infinite timeout with no socket or pipe to wait on. A closed socket descriptor reports `POLLNVAL`; the console's limit on the descriptor count is unknown and not checked +- [poll](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) on Windows reports a guest stream socket whose peer closed as readable and adds `POLLHUP` only once the socket has shut its own sending side, as FreeBSD 11.4's [`sopoll_generic`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L3080-L3086) does; `WSAPoll` reports `POLLHUP` alone in both cases. A reset or refused connection (`POLLHUP` with `POLLERR`) keeps `WSAPoll`'s flags, a libSceNet socket keeps them too, and a call that asks for no event still returns early on the peer's close with nothing reported. Not measured on a console. - [kevent EV_CLEAR on a socket or a pipe](../../../core/libs/prx/libkernel/Equeue/src/EdgeWatch.cpp) (libkernel) - FreeBSD 11 activates an `EV_CLEAR` knote when `kqueue_register` finds it ready, which every `EV_ADD` or `EV_ENABLE` of an enabled knote does again, and on each `KNOTE` the socket or pipe raises (`sorwakeup`, `sowwakeup`, `pipeselwakeup`); [`kqueue_scan`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/kern_event.c) checks the filter again before reporting, drops a knote that is no longer ready and deactivates a reported one until the next activation. On Linux each such knote registers the host socket or pipe with its own epoll instance in edge-triggered mode (`EPOLLIN | EPOLLRDHUP` for `EVFILT_READ`, `EPOLLOUT` for `EVFILT_WRITE`), and a wakeup of the host descriptor with that mask stands for a `KNOTE`: the knote is then checked with the same readiness test as a level-triggered one, `NOTE_LOWAT` mark included, and a wait blocks on the epoll descriptor instead of the socket, so a socket that stays readable does not wake it. The host's wakeups are not FreeBSD's: FreeBSD raises `sowwakeup` whenever send buffer space is freed, while Linux TCP wakes `EPOLLOUT` waiters only once the socket is writable again after a write found it full (`SOCK_NOSPACE`, [`tcp_check_space`](https://github.com/torvalds/linux/blob/v6.8/net/ipv4/tcp_input.c#L5639)), so a TCP writer that never fills its buffer sees fewer `EVFILT_WRITE` events than on FreeBSD; a host stack may also coalesce data wakeups, so the number of times a busy socket fires can differ. Windows and macOS throw, as the host there has no edge-triggered wait over these descriptors that is wired up; `EV_CLEAR` on the timer, user and vnode filters does not use this and works everywhere. - [kevent on a pipe](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) - host pipes are one-way, where FreeBSD's [`kern_pipe`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/sys_pipe.c#L422) opens both ends for reading and writing, so `EVFILT_READ` on the write end and `EVFILT_WRITE` on the read end throw; on macOS `EVFILT_WRITE` on a pipe throws, as the host has no call for a pipe's capacity. `EVFILT_WRITE` reports the free space of the host buffer, not of FreeBSD's 16 KiB one, and on Linux fires when the host says the pipe is writable rather than at `PIPE_BUF` (512) free bytes. Windows pipes cannot be waited on, so a kqueue waiting on one checks it every 1 ms. Otherwise [`filt_piperead`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/sys_pipe.c#L1787-L1828) and `filt_pipewrite` are followed: `EV_EOF` once the other end closes, `EPIPE` for a write event added after the read end closed, and `NOTE_LOWAT` ignored. On Windows `fcntl` cannot make a pipe write end non-blocking, so the full-pipe and drain case of `EVFILT_WRITE` is tested on Linux only and the `NtQueryInformationFile` write quota is checked there only for the free space, `EV_EOF` and `EPIPE`. - [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`. A nonblocking `connect` in progress reports `EINPROGRESS` on Linux but `EAGAIN` on Windows (`WSAEWOULDBLOCK`), where FreeBSD reports `EINPROGRESS`. On Windows a refused nonblocking `connect` is never reported (`SO_ERROR` is unsupported), so a repeated one keeps reporting `EALREADY` where FreeBSD reports `ECONNREFUSED`. A receive after `shutdown(SHUT_RD)` reports `EPIPE` on Windows where FreeBSD returns 0 - [getsockopt, setsockopt](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) on a guest socket handle the SOL_SOCKET level, `TCP_NODELAY` and `IPV6_V6ONLY`, after FreeBSD 11.4's [`sosetopt` and `sogetopt`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c): `SO_REUSEADDR`, `SO_KEEPALIVE`, `SO_DONTROUTE`, `SO_BROADCAST`, `SO_OOBINLINE` and `SO_LINGER` act on the host socket, and `SO_ERROR`, `SO_TYPE`, `SO_ACCEPTCONN`, `SO_SNDBUF` and `SO_RCVBUF` are read. A boolean option reads back as its own bit, as FreeBSD returns `so_options & option`. Winsock does not clear a socket's pending error when it is read, so on Windows the library reports a pending error once and shows it again only after a new `connect`; a second failure with the same code on the same socket without a `connect` in between would be hidden. `SO_SNDBUF` and `SO_RCVBUF` read the host's value, which is not the size FreeBSD reports (Linux doubles it). `SO_NOSIGPIPE` is kept per socket and reads 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. 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. `SO_NBIO` (`0x1200`) is the console's own option, not FreeBSD's. It switches the socket between blocking and non-blocking as `FIONBIO` does and reads back as 0 or 1, as `sceNetSetsockopt` and `sceNetGetsockopt` treat it here; what the console reads back for it is not measured. `TCP_NODELAY` (`IPPROTO_TCP`, 1) is set on the host socket and reads back as `TF_NODELAY` (4), as [`tcp_default_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet/tcp_usrreq.c#L1860-L1861) returns it. The `IPPROTO_TCP` level on a datagram or `AF_UNIX` socket fails with `EINVAL`, as [`ip_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet/ip_output.c#L943-L944) and [`uipc_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_usrreq.c#L1228-L1229) answer it, and an accepted socket takes `TCP_NODELAY` through the host. `SO_SNDTIMEO` and `SO_RCVTIMEO` follow [`sosetopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L2657-L2689) and [`sogetopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L2846-L2850). A 16-byte `timeval` with a negative field or 1000000 microseconds or more fails with `EDOM`, and more than `INT32_MAX` seconds becomes the longest wait. The value is kept and read back through FreeBSD's 32.32 fixed-point conversion ([`tvtosbt`, `sbttotv`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/sys/time.h#L178-L286)), so half a second reads back as 499999 microseconds and one microsecond as 0. A `recv`, `recvfrom`, `recvmsg`, `read` or `readv`, and a `send`, `sendto`, `sendmsg`, `write` or `writev`, on a blocking socket then waits that long for the host to report the socket readable or writable, and fails with `EWOULDBLOCK`; a call with `MSG_DONTWAIT` does not wait. An accepted socket takes both from its listener ([`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L629-L630)). The wait covers only the first transfer: once the socket is writable the host send runs without a limit, where FreeBSD stops a send that cannot finish and returns the bytes sent so far, and a receive whose data another thread takes between the wait and the host call blocks again. `sendfile` ignores the send timeout. The host wait has millisecond steps, so a shorter timeout lasts up to a millisecond longer. A null value fails with `EINVAL`, where FreeBSD's [`kern_setsockopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_syscalls.c#L1414-L1415) returns `EFAULT`. `IPV6_V6ONLY` (`IPPROTO_IPV6`, 27) on an `AF_INET6` socket 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` ([L1572](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet6/ip6_output.c#L1572)), as does setting it on a bound socket, which the host refuses too. `socket` turns it on for every `AF_INET6` socket, as FreeBSD's default `net.inet6.ip6.v6only` does, 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 `IPPROTO_IPV6` level on an `AF_INET` or `AF_UNIX` socket fails with `EINVAL`, as `ip_ctloutput` and `uipc_ctloutput` answer it. Every other SOL_SOCKET option (`SO_DEBUG`, `SO_USELOOPBACK`, `SO_REUSEPORT` and `SO_TIMESTAMP`, which FreeBSD stores, the low-water marks among them; PPSA99871 sets `SO_REUSEPORT` and carries on when it fails), every other `IPPROTO_TCP` option (`TCP_MAXSEG`, `TCP_KEEPIDLE`, `TCP_KEEPINTVL` and `TCP_KEEPCNT` among them), every other `IPPROTO_IPV6` option ([L1928-L1929](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet6/ip6_output.c#L1928-L1929), [L2158-L2159](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet6/ip6_output.c#L2158-L2159)) and the `IPPROTO_IP` level fail with `ENOPROTOOPT`. None of this was measured on a console. - [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 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; one bound to port 0 is bound again to the wildcard address with the port it had -- [connect](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - `connect` to port 0 on an unconnected `AF_INET` or `AF_INET6` stream or datagram socket fails with `EADDRNOTAVAIL` (49), as FreeBSD 11.4's `in_pcbconnect_setup()` and `in6_pcbconnect()` do. A connected stream socket keeps its host `EISCONN` answer. Known gaps: on an already connected datagram socket `connect` to port 0 keeps the old peer, where FreeBSD's `soconnect()` disconnects first and then fails with 49; `sendto` and `sendmsg` to port 0 are sent, where `udp_output()` fails with 49 on the unbound path but FreeBSD can send to port 0 from a socket bound to a specific address and port. The answers are taken from the FreeBSD source and are not measured on a console. [sceNetConnect](../../../core/libs/prx/libSceNet/Export.cpp) is not changed +- [connect](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - `connect` to port 0 on an unconnected `AF_INET` or `AF_INET6` stream or datagram socket fails with `EADDRNOTAVAIL` (49), as FreeBSD 11.4's `in_pcbconnect_setup()` and `in6_pcbconnect()` do. A connected stream socket keeps its host `EISCONN` answer. Known gaps: on an already connected datagram socket `connect` to port 0 keeps the old peer, where FreeBSD's `soconnect()` disconnects first and then fails with 49; `sendto` and `sendmsg` to port 0 on an `AF_INET` datagram socket fail with 49 unless the socket is bound to a specific address and port and the destination is neither `INADDR_ANY` nor `INADDR_BROADCAST`, which is the path where `udp_output()` skips `in_pcbconnect_setup()` and FreeBSD sends to port 0. There nothing can receive on port 0, so the call returns the length without the host, because Linux's `udp_sendmsg` refuses port 0 with `EINVAL`; an `AF_INET6` socket still goes to the host. The answers are taken from the FreeBSD source and are not measured on a console. [sceNetConnect](../../../core/libs/prx/libSceNet/Export.cpp) is not changed - [msync](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) follows FreeBSD 11's `kern_msync` and `vm_map_sync`, not measured on the console: the address is rounded down to the page, a range that wraps is `EINVAL`, as is `MS_ASYNC` with `MS_INVALIDATE`, and a range whose start or any page is not mapped (as `sceKernelVirtualQuery` reports it, reservations included) is `ENOMEM`; a page unmapped from inside a larger mapping still counts as mapped there, since `sceKernelVirtualQuery` keeps reporting the whole mapping. The guest has no file mappings, so a successful call writes nothing back. `MS_INVALIDATE` on a mapped range throws: FreeBSD returns `EBUSY` when the range holds `mlock`ed pages, and locked pages are not tracked - [madvise](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) accepts `MADV_PROTECT` (10) as a no-op hint without modifying process protection. FreeBSD 11's `kern_madvise` (`sys/vm/vm_mmap.c`) handles it before the address check, ignores the address and length, and calls `kern_procctl(..., PROC_SPROTECT, PPROT_SET)`, which requires `PRIV_VM_MADV_PROTECT` and fails with `EPERM` without it. Returning 0 unconditionally assumes the calling process is privileged. Console behaviour is unverified - [mincore](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) follows FreeBSD 11's `sys_mincore` (`sys/vm/vm_mmap.c`), not measured on the console: the address is rounded down to the page, length 0 returns 0, a null vector over a mapped range returns `EFAULT`, an overflowing range or any unmapped page in the range (as reported by `sceKernelVirtualQuery`) returns `ENOMEM`. For valid ranges, a page reports `MINCORE_INCORE` (0x1) when `sceKernelVirtualQuery` reports it committed and 0 when it is only reserved; actual residency, referenced and modified bits are not modelled. Length 0 returns 0 when the start page is mapped and `ENOMEM` otherwise, and the range and mapping checks come before the null vector `EFAULT`, as in `kern_mincore` Java 25 LAN / DNS (PPSA99869) imports it @@ -213,6 +217,7 @@ - [pause, sigsuspend, sigwait, sigwaitinfo](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) take the pending set of `sigtimedwait` instead of throwing. `pause` waits for a pending signal that is not blocked and whose disposition can be delivered, runs its handler on the calling thread and returns -1 with `EINTR`; an ignored signal is discarded and the wait continues, and it never returns for a signal whose handler cannot run. A signal whose registered handler has `SA_SIGINFO` still throws, and one whose disposition is the default action and that has no host equivalent (every signal except 2, 4, 6, 8, 11 and 15) is neither delivered nor discarded: it stays pending until a wait takes it, where FreeBSD runs the default action, which terminates the process for most of them. A pending `SIGALRM` with the default disposition exits the process with status 142 once it is unblocked, as FreeBSD's default action does. PPSA68091, PPSA99169, PPSA99420 and PPSA97358 import them and fail to load without them. PPSA68091 calls `pause` at one site (0x8F7266), in a loop that waits while a global holds another thread's `errno` address. PPSA99169 only stores the address of `pause` in a table of symbol addresses (0x30DB1). PPSA99420 calls `pause` at 0x3BD80F6 and 0x3C9E220, `sigwait` at 0x3C9E4C7 and `sigwaitinfo` at 0x3C9E588. PPSA97358 calls `sigwait` from QEMU's `sigwait_compat` thread ([compatfd.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/compatfd.c#L28-L53)), which `qemu_signal_init` ([main-loop.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/main-loop.c#L110-L147)) starts for `SIGIO`, `SIGALRM` and `SIGBUS` when the main loop is created, so it reaches the wait at start-up, and from the vCPU thread of the dummy accelerator ([dummy-cpus.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/accel/dummy-cpus.c#L48-L56)). `sigsuspend` swaps the process-wide mask of `_sigprocmask` for its argument, waits as `pause` does through the same helper, then restores the mask through `_sigprocmask`, and returns -1 with `EINTR`; a null set throws, as the console faults in libthr instead of returning `EFAULT`. All four words of the argument replace the mask and are restored afterwards, as in `_sigprocmask`. Java 25 LAN / DNS (PPSA99869, #5501) imports it and fails to load without it, after `sysctlnametomib`, `fegetenv` and `mincore`. None of the five waits is a cancellation point - [getsid](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) returns the calling process ID for pid 0 and the calling process, and `ESRCH` for other process IDs, assuming the single guest process is its own session leader. PPSA74126 imports it and fails to load without it - [sigaction](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) records the handler, flags and mask of every signal from 1 to 128 and reports them back, following FreeBSD's `sigaction` (`EINVAL` for other signals, and for a handler on `SIGKILL` or `SIGSTOP`); `signal` records its handler with `SA_RESTART`, as FreeBSD's libc does. Only handlers without `SA_SIGINFO` on the signals `signal` already delivers (`SIGINT`, `SIGILL`, `SIGABRT`, `SIGFPE`, `SIGSEGV`, `SIGTERM`) run, the same way `signal` handlers do; their mask and flags are not applied. `SA_SIGINFO` handlers are recorded but never called, since that needs FreeBSD's `siginfo_t` and `ucontext_t`, and neither are the handlers of other signals (`SIGBUS`, `SIGHUP`, ...), whether they are set by `sigaction` or by `signal`, which records them too (every number from 1 to 128, with `SA_RESTART` and an empty mask replacing what `sigaction` stored; `SIGKILL` and `SIGSTOP` accept only the default handler in both calls, as in FreeBSD's `kern_sigaction`). `raise` and `kill` accept every number from 1 to 128 now: a blocked one stays pending on the set of `sigtimedwait`, an unblocked one whose handler is registered runs it on the calling thread, and an unblocked one that has no host equivalent and no handler still fails with `EINVAL`, where FreeBSD runs its default action. `raise` of an `SA_SIGINFO` handler still throws instead of terminating +- [signal](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) records its handler with `SA_RESTART` unless the signal is in the exported `_sigintr` set, as FreeBSD's libc does (`lib/libc/gen/signal.c`). `siginterrupt`, which fills that set, is a libSceLibcInternal export and is not provided: libc does not link libkernel and cannot reach the dispositions, so only a guest that writes the set itself clears `SA_RESTART` this way. - [kill, killpg](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) follow FreeBSD's `kern_kill` and `killpg1` for the only guest process: `EINVAL` for signals outside 0 to 128 and for negative `killpg` process groups, signal 0 only checks that the target exists, and its own pid, 0 and minus its own pid (the guest is taken to lead its own process group) signal it through `raise`, so they share `raise`'s limits above. Any other pid, and -1, which skips the caller, report `ESRCH`, so `EPERM` is never reported - [sceKernelFcntl](../../../core/libs/prx/libkernel/File/src/Open.cpp) (libkernel) runs the POSIX `fcntl` below and returns its guest errno as `SCE_KERNEL_ERROR_*`, the form the other `sceKernel` file exports use. The return values and error codes are unverified on a console - [fcntl](../../../core/libs/prx/libkernel/File/src/DescriptorControl.cpp) (libkernel) follows FreeBSD 11.4's [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L479-L799) and [`kern_dup`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L812) on file, directory, pipe and character device descriptors; a socket keeps the libSceNet path (`F_DUPFD`, `F_DUP2FD`, `F_DUPFD_CLOEXEC`, `F_DUP2FD_CLOEXEC`, `F_GETFL`, `F_SETFL` with `O_NONBLOCK`, `F_GETFD` and `F_SETFD`, `EINVAL` for every other command, see the next entry). On Linux and macOS a host socket that is not a guest socket, such as a standard descriptor a service manager connected to its journal, takes the flag handling of a pipe end and `FIONREAD` asks the host for its queued bytes (for a datagram socket Linux counts only the next datagram, where FreeBSD's [`soo_ioctl`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/sys_socket.c#L202-L205) counts every buffered byte); its record locks and read-ahead hints fail with `EBADF`, as FreeBSD fails them on a descriptor without a vnode, and `O_ASYNC` on it throws. The close-on-exec flag and the status flags live in a guest table, the status flags shared by `dup`, `dup2` and `F_DUPFD` as in an open file description; a descriptor the table has not seen, such as 0 to 2, starts from the host's flags, since the console's are not known. `F_GETFL` takes the access mode from the host (the granted access of the handle on Windows), so pipe ends report `O_RDONLY` and `O_WRONLY` where FreeBSD's bidirectional pipes report `O_RDWR`. `F_DUPFD` and `F_DUPFD_CLOEXEC` return the lowest free host descriptor at or above the start, so the bound is the host table (`EMFILE` on Windows, `EINVAL` past `RLIMIT_NOFILE` on Linux), not the console's unknown `RLIMIT_NOFILE` and `kern.maxfilesperproc`. `O_NONBLOCK` switches a host pipe (`PIPE_NOWAIT` on Windows, where an empty read and a full write then report `EAGAIN`) and is only recorded on files, directories and the random device, as their reads never block; on a Windows console or other character device it throws. Changing `O_APPEND` on a Windows file, changing `O_FSYNC` on a file, `O_ASYNC` on a pipe or character device (`SIGIO` is not delivered), `F_GETOWN`, `F_SETOWN` and `F_SETLK_REMOTE` throw; `O_DIRECT`, `F_RDAHEAD` and `F_READAHEAD` are recorded as the hints they are. Record locks (`F_GETLK`, `F_SETLK`, `F_SETLKW` and their forms without `l_sysid`) use the host's POSIX locks on Linux and macOS. `F_GETLK` with `F_UNLCK` asks the host with `F_RDLCK`: as in [`lf_blocks`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L866) only another owner's overlapping write lock conflicts with either request. On Windows they follow [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L607-L752) and [`lf_advlockasync`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L433-L487) for the one guest process, which owns every lock: `EOVERFLOW` for an `SEEK_CUR` start past `OFF_MAX`, then `EBADF` for `F_RDLCK` without read access or `F_WRLCK` without write access and `EINVAL` for another type, then `EINVAL` and `EOVERFLOW` for the range; as [`lf_blocks`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L866) never lets locks of one owner conflict, whichever descriptor or thread asks, `F_SETLK` and `F_SETLKW` then succeed without waiting and `F_GETLK` reports `F_UNLCK`, so no lock list is kept and merging, splitting and the release on close have nothing to change. Nothing is held on the host, since Windows byte-range locks are mandatory and would fail the guest's own reads and writes through other descriptors, so other host processes neither see these locks nor are seen; a lock from the end of a directory throws. The return values and error codes are unverified on a console @@ -228,7 +233,7 @@ - [pthread_spin_lock](../../../core/libs/prx/libkernel/Pthread/Posix/Spinlock.cpp) (libkernel) - a lock taken again by the thread that holds it throws here. On a PS5 (firmware 12.70) that call had not returned after 2 s, as FreeBSD 11.4 libthr [`thr_pspinlock.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_pspinlock.c) spins forever - [pthread_mutex_getspinloops_np, pthread_mutex_setspinloops_np, pthread_mutex_getyieldloops_np, pthread_mutex_setyieldloops_np and their scePthreadMutex counterparts](../../../core/libs/prx/libkernel/Pthread/src/Mutex.cpp) (libkernel) store and report the counts, but host mutexes do not spin or yield by them. The defaults follow FreeBSD 11.4's [`mutex_init_body`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_mutex.c#L236-L272) without `LIBPTHREAD_SPINLOOPS` and `LIBPTHREAD_YIELDLOOPS`: 2000 spins for an adaptive mutex, 0 otherwise; the console's defaults are unverified. Setting the yield count of a destroyed mutex returns 0 as FreeBSD's [`__pthread_mutex_setyieldloops_np`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_mutex.c#L1125-L1135) does; the console's result is unverified - [pthread_rwlockattr_getpshared, pthread_rwlockattr_setpshared](../../../core/libs/prx/libkernel/Pthread/Posix/Rwlock.cpp) (libkernel) - `pthread_rwlockattr_destroy` clears the attribute here, so a later get, or a set of 0, throws. On a PS5 (firmware 12.70) destroy leaves the attribute set; a get afterwards returned 0 with a stale value and a set of 0 returned 0 -- [sceKernelGetEventError](../../../core/libs/prx/libkernel/Equeue/src/EventAccessors.cpp) (libkernel) returns the event's `data` as an `int` when `EV_ERROR` (0x4000) is set in `flags` and 0 otherwise, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/947627d10d4ef4b68ba3c0189aab6c368e70a8d2/src/kernel/eventQueue.cpp#L663) does and as FreeBSD 11's [kevent](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/kern_event.c#L1005) reports a failed change (`flags = EV_ERROR`, errno in `data`). [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/kernel/ps4_queue.pas#L643) instead returns `fflags` plus `0x80020000` when `fflags` is nonzero. Whether the console reads `data` or `fflags` and returns the raw errno or an SCE error code is unverified. No event delivered by the equeue here carries `EV_ERROR`, so the call returns 0 for every event `sceKernelWaitEqueue` reports +- [sceKernelGetEventError](../../../core/libs/prx/libkernel/Equeue/src/EventAccessors.cpp) (libkernel) throws for `fflags` above 0xFFFF, where OR and addition of 0x80020000 differ and the console was not measured. Otherwise it returns 0 for `fflags` 0 and `0x80020000 | fflags`, whatever `flags` and `data` hold, as [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/kernel/ps4_queue.pas#L643) does and a PS5 (firmware 13.40, measured 2026-10-19 in a payload process) answered: `{flags 0x4000, fflags 0, data 22}`, `{0x4000, 0, -1}`, `{0x4000, 0, 0x80020016}` and `{0, 0, 0}` gave 0, `{0x4000, 5, 0}` and `{0x4000, 5, 22}` 0x80020005, `{0, 7, 22}` 0x80020007, `{0x8000, 3, 9}` 0x80020003. Video-out events carry their count in `fflags`, so they get 0x80020001 and up; only events built by the caller were measured. Closes when: `fflags` above 0xFFFF is measured - [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_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. @@ -246,3 +251,4 @@ - [param.sfo](../../../core/libs/prx/libkernel/AppMetadata/src/ParamSfoParser.cpp) (libkernel) is read when `param.json` is absent, for `TITLE`, `TITLE_ID`, `DOWNLOAD_DATA_SIZE` and `USER_DEFINED_PARAM_1` to `_4`, and mounts `/data` and `/download0` like `param.json`; the localized `TITLE_00` to `TITLE_30` are not selected by the console language. - [sysctl MIBs for kern.smp.maxcpus, kern.stackprot, kern.sched.name and kern.timecounter.hardware](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - on a PS5 (firmware 12.70), a payload process resolves these names to {1, 2147482816, 2147482814}, {1, 2147483151}, {1, 2147483182, 2147483181} and {1, 2147482838, 2147482834}; `sysctl` returns -1 with `ENOENT` for reads and size queries by those exact MIBs, leaving the buffer and length unchanged, including when either or both output pointers are null. Writes were not measured and throw. `sysctlnametomib` still throws for these four names; #7541 implements the mappings for nine of the other names. - [sceKernelGetProcessAbiType](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel) throws: no measured value, prototype or documented consumer of the process ABI type was found, and PS5_MihawkOS (`mihawk-99/PS5_MihawkOS`, PPSA99170) is the only title known to import it. Returning a guessed constant would let a title branch on a value the console never answered with, so the declaration only resolves the import until the console value is measured +- [flock on a directory descriptor](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) returns 0 on Windows without taking a lock, as FreeBSD 11.4's `sys_flock` accepts any vnode descriptor and so succeeds on a directory; Windows `LockFileEx` rejects a directory handle, which used to abort the process. Two descriptors of the same directory therefore never conflict: a second `LOCK_EX | LOCK_NB` succeeds where FreeBSD fails with `EWOULDBLOCK`. Linux and macOS use the host `flock`. Not measured on a console diff --git a/docs/dev/debt/libkernel_sys.md b/docs/dev/debt/libkernel_sys.md index 25499ae3d1..ab4b58c378 100644 --- a/docs/dev/debt/libkernel_sys.md +++ b/docs/dev/debt/libkernel_sys.md @@ -5,16 +5,19 @@ ### Functional -- [fchdir](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) changes the guest working directory to the descriptor's directory: on Windows through the path recorded when the descriptor was opened, on Linux through the `/proc/self/fd` link target. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. Outside Windows it checks search permission on the directory with `faccessat`, which fails with `EACCES` as FreeBSD `change_dir` does; on Windows that check is not modelled. A directory renamed after it was opened is still reached through the recorded path. Console behaviour is unverified +- [fchdir](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) changes the guest working directory to the descriptor's directory: on Windows through the path recorded when the descriptor was opened, on Linux through the `/proc/self/fd` link target. It returns `EBADF` for a descriptor that is not open, `EINVAL` for a pipe or socket, as FreeBSD `getvnode` does for a descriptor without a vnode, and `ENOTDIR` for any other descriptor that is not a directory. Outside Windows it checks search permission on the directory with `faccessat`, which fails with `EACCES` as FreeBSD `change_dir` does; on Windows that check is not modelled. A directory renamed after it was opened is still reached through the recorded path. Console behaviour is unverified - [openat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `open`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. As with `open`, `O_NOFOLLOW` and `O_NONBLOCK` are ignored. PPSA99003 opens directory entries this way. Console behaviour is unverified - [fchmodat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `chmod`; any other relative path is joined to the descriptor's host directory the same way as for `openat`, so guest path aliases and case-insensitive name matching do not apply to it and a `..` component is not kept inside the guest root. A flag other than `0` or `AT_SYMLINK_NOFOLLOW` (0x200) returns `EINVAL`; the mode is masked to `07777`. With `AT_SYMLINK_NOFOLLOW` a symbolic link returns success silently on Windows and Linux, since the host cannot change a link's own mode, while a regular file is changed either way. The mode maps to the host file mode, which on Windows is only the CRT's read-only bit. PPSA99621 calls it through `std::filesystem::permissions`. Console behaviour is unverified +- [lchmod](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) throws for a symbolic link on Windows and Linux, whose hosts cannot change a link's own mode; on macOS the link's mode is changed. Anything else goes to `fchmodat(AT_FDCWD, path, mode, AT_SYMLINK_NOFOLLOW)`, as FreeBSD 11.4's [`sys_lchmod`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_syscalls.c#L2490-L2496) does, so a file or directory is changed as by `chmod`, with `fchmodat`'s path lookup and Windows mode mapping. On Windows any reparse point counts as a link. Console behaviour is unverified - [pathconf](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) looks the path up as FreeBSD 11's `kern_pathconf` does (links followed, `ENAMETOOLONG` for a path of 1024 bytes or more or a component over 255) and returns the constants of FreeBSD 11's UFS `ufs_pathconf` for files and directories alike: `_PC_LINK_MAX` 32767, `_PC_NAME_MAX` 255, `_PC_PATH_MAX` and `_PC_SYMLINK_MAX` 1024, `_PC_PIPE_BUF` 512, `_PC_CHOWN_RESTRICTED` and `_PC_NO_TRUNC` 1, `_PC_FILESIZEBITS` 64, `_PC_REC_MAX_XFER_SIZE` -1, `_PC_PRIO_IO` and `_PC_SYNC_IO` 0. Whether the console's filesystems report the same is not verified. It throws for the names whose UFS value depends on the mount (`_PC_ALLOC_SIZE_MIN`, `_PC_REC_INCR_XFER_SIZE`, `_PC_REC_MIN_XFER_SIZE`, `_PC_MIN_HOLE_SIZE`), the kernel options (`_PC_ACL_EXTENDED`, `_PC_ACL_PATH_MAX`, `_PC_ACL_NFS4`, `_PC_MAC_PRESENT`) or the page size (`_PC_REC_XFER_ALIGN`), and on which UFS and `vop_stdpathconf` disagree (`_PC_MAX_CANON`, `_PC_MAX_INPUT`, `_PC_VDISABLE`, `_PC_ASYNC_IO`); other names fail with `EINVAL` - [mkdirat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `mkdir`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. On Windows, `mode` is ignored. Console behaviour is unverified -- [readlink](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) on Windows reads a symbolic link's target from its reparse data and returns it with `/` separators, only when the target is relative, as `symlink` creates them. A symbolic link with an absolute target returns its print name, or its substitute name when the print name is empty, without a `\??\` or `\\?\` prefix. A directory junction returns the same kind of name when it is a drive path (`X:\...`), using the print name when that is a drive path and the substitute name otherwise. A mounted volume, whose substitute name is `\??\Volume{GUID}\`, stays a throw, because the name left after the prefix is not a drive path. Any other reparse point throws too, where FreeBSD reports `EINVAL` for a file that is not a link +- [readlink](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) on Windows reads a symbolic link's target from its reparse data and returns it with `/` separators, only when the target is relative, as `symlink` creates them. A symbolic link with an absolute target returns its print name, or its substitute name when the print name is empty, without a `\??\` or `\\?\` prefix. A directory junction returns the same kind of name when it is a drive path (`X:\...`), using the print name when that is a drive path and the substitute name otherwise. A mounted volume (`\??\Volume{GUID}\`), any other name surrogate and every reparse point without the name surrogate bit (WOF compression, deduplication, cloud files) return `EINVAL`, as FreeBSD does for a file that is not a link and as CPython's `os.readlink`, which reads only symbolic links and junctions, does - [readlinkat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `readlink`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. Console behaviour is unverified +- [link](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) on Windows resolves a symbolic link source through the host, which follows longer chains than FreeBSD: a chain of 40 links is linked, where FreeBSD 11.4 returns `ELOOP` after 32 (`MAXSYMLINKS`). A loop returns `ELOOP` as on FreeBSD - [fstatat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `stat`, or to `lstat` with `AT_SYMLINK_NOFOLLOW` (0x200). Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory, and flags other than `AT_SYMLINK_NOFOLLOW` return `EINVAL`. PPSA74126 imports it. Console behaviour is unverified - [unlinkat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `rmdir` (with `AT_REMOVEDIR`, 0x800) or `unlink`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. Removing a directory without `AT_REMOVEDIR` fails with `EPERM`, removing `.` or `..` with `AT_REMOVEDIR` fails with `EINVAL`, and flags other than `AT_REMOVEDIR` fail with `EINVAL`. Console behaviour is unverified - [renameat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `rename`. Any other relative path is joined to its descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to relative descriptors, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. Console behaviour is unverified - [fork](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) throws: creating a child process is not supported. PPSA19111 imports it and fails to load without it. Its engine calls `fork` in `Posix_Daemonize`, only with `-daemonize`, and in `Platform_ShellExecute` before `execvp("xdg-open")`, which the main menu's Previews button reaches, so pressing Previews ends in this throw. [setsid](../../../core/libs/prx/libkernel_sys/Export.cpp) fails with `EPERM`, as FreeBSD's does for a process group leader, since the guest is taken to lead its own process group (see `kill`). [umask](../../../core/libs/prx/libkernel_sys/Export.cpp) keeps the guest's mask (initially 022, masked to 07777) and returns the previous one, Linux named shared-memory creation applies it, but ordinary files are still created with the host process's mask - [chroot](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) follows FreeBSD 11.4's [`sys_chroot`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_syscalls.c#L776-L807) and [`pwd_chroot`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L3059-L3085) for a root caller: `EFAULT` for a null path, `ENOENT` for an empty or missing one, `ENAMETOOLONG` for a path of 1024 bytes or more or a component over 255, and `ENOTDIR` for a path that is not a directory or passes through a file. A directory that is the guest root (`/`, `/..`, or `.` after `fchdir` to a descriptor of `/`) succeeds and changes nothing, as the root stays the same directory; the guest is taken never to have changed its root, so open directory descriptors do not make it fail with `EPERM` (`kern.chroot_allow_open_directories` 1). Any other directory throws, naming the path: the guest path resolver has a fixed root and mount aliases (`/app0`, `/savedata0`) that do not move with it. On the console a title starts in a sandbox root, where `pwd_chroot` refuses with `EPERM` while a directory is open; whether that holds for titles, and whether a title holds `PRIV_VFS_CHROOT`, are unverified. Porpoise (PPSA99764) weak-imports it and calls it only as `fchdir` to its saved descriptor of `/` then `chroot(".")`, to put its sandbox root back after a jailbreak daemon replaced it ([`put_root_back`](https://github.com/elripalda/Porpoise-Dolphin-Emulator-for-PS5/blob/2ed1ab5a06995aa198f7bccd4da0f90c3fc29d39/src/porpoise_jailbreak.cpp#L303-L331)) - [fchown](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - ownership is not modelled: the guest's own owner and group (0) and the unchanged value (-1) are accepted, a descriptor that is not open fails with `EBADF`, and any other owner or group throws, naming it. The check reads the descriptor with `fstat`, ignoring the CRT's invalid-parameter handler on Windows as `DescriptorControl` does. The console's ownership rules are unverified. Morrowind (`mshivam019/morrowind-ps5`) imports it +- [pthread_set_defaultstacksize_np and scePthreadSetDefaultstacksize](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) set the stack size that `scePthreadAttrInit` and a thread created without attributes use, as [fpPS4's `pthread_set_defaultstacksize_np`](https://github.com/red-prig/fpPS4/blob/04cefd4/kernel/ps4_pthread_attr.pas#L89-L102) does: a size of 16384 bytes or less fails with `EINVAL` (`0x80020016` from the sce form) and there is no upper limit, so a default the host cannot allocate fails only when a thread is created. Attributes initialised earlier keep their size. Not checked on a PS5 diff --git a/docs/dev/debt/relinker.md b/docs/dev/debt/relinker.md index 43ff7062d0..f4e849cbda 100644 --- a/docs/dev/debt/relinker.md +++ b/docs/dev/debt/relinker.md @@ -11,8 +11,10 @@ - Converted Windows main executables [omit `DYNAMIC_BASE`](../../../core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp) by default, not only with `--windows-fixed-base`: on the console the executable lies inside the 40-bit window that the title's own `sceLibcMspaceCreate` checks (#3043). Relocations are kept, so if `0x140000000` is taken Windows relocates the image, possibly above that window. Verified only on Windows 10. - [SDK memory-management syscall traps](../../../core/relinker/relinker/src/analysis/SyscallScanner.cpp) under `--trap-sdk-syscall-stubs` recognize three exact instruction sequences from [ps5-payload-dev SDK libc/mman.c](https://github.com/ps5-payload-dev/sdk/blob/12cf3dda40/libc/mman.c), observed in Nuvio Maxd 0.5.15 (PPSA99288): syscall 74 and the two syscall 477 paths, with their argument preparation and carry-flag error handling. Matching requires a decoded instruction boundary and the complete sequence inside the scanned code range. Other SDK/compiler output remains rejected. The matched syscall becomes `UD2`; this does not implement mmap or mprotect and a call still faults. - The [instruction decoder](../../../core/relinker/codegen/src/x86/X64InstructionDecoder.cpp) reads a 16-bit displacement for `66 E8`, `66 E9` and `66 0F 80`-`8F` without REX.W, as AMD processors, the console included, do (binutils `objdump -M amd64`; REX.W selects the 32-bit form). AMD also truncates the target of every near relative branch with that prefix, rel8, LOOP and JRCXZ forms included, to 16 bits. Instruction metadata marks these branches as truncated and reports no branch target for them, and stubs refuse to move them. Compilers do not emit these forms. +- Converted Linux main executables are [placed at `0x400000`](../../../core/relinker/relinker/src/pipeline/Conversion.cpp) by default, as `ET_EXEC` without ASLR, not only with `--linux-fixed-base`: the console executable lies inside the 40-bit window that the title's own `sceLibcMspaceCreate` checks (#3043). Where the console loads an executable that does not require an address is not measured; `0x400000` is the base that executables reading their GOT through `app0/imports.txt` assume (#4013). The relinker refuses an image that spans 2 GiB or more, so the executable ends below the guest arena at `0x200000000`. With `--lazy-binding` the executable stays position independent. - `--to-intel` lowers MCOMMIT to STC plus NOP padding and RDPRU through a runtime-checked stub in the [matcher](../../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp): AMD APM Vol. 3 defines RDPRU ECX 0 as MPERF and ECX 1 as APERF, neither readable on Intel hosts, so both selectors return the host TSC as a disclosed monotonic-counter approximation (their ratio always reads 1.0) while other selectors yield zero EDX:EAX. STC asserts CF=1 because under x86-TSO prior stores are already ordered and guest memory is host-DRAM-backed, leaving the commit-success case as the only observable one. Non-zero RDPRU selectors, the TSC-for-MPERF/APERF substitution, and any console path reporting MCOMMIT CF=0 are unverified. -- [Unscoped imports on Windows](../../../core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp) - an import without a module that no searched library exports under its name is searched again under the NID of that name, because homebrew built with the [ps5-payload-dev SDK](https://github.com/ps5-payload-dev/sdk) imports by symbol name (Quake, from the websrv homebrew launcher; Mari0, #6239) while the libraries in `libs/` export NIDs. A dependency named with `.sprx`, `.suprx` or `.debug_prx` loads the `.prx` library of the same stem from `libs/`, and `libkernel_web` also loads `libkernel.prx`, as `libSceLibcInternal` already loads `libc.prx`: AnyPS5's `libkernel_web.prx` exports only two functions, where the console's `libkernel_web.sprx` is a libkernel variant. A name that matches a library's export as written still binds to that export first. Linux and macOS resolve these imports through the host loader by name and are unchanged. +- [Unscoped imports on Windows](../../../core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp) - an import without a module that no searched library exports under its name is searched again under the NID of that name, because homebrew built with the [ps5-payload-dev SDK](https://github.com/ps5-payload-dev/sdk) imports by symbol name (Quake, from the websrv homebrew launcher; Mari0, #6239) while the libraries in `libs/` export NIDs. A dependency named with `.sprx`, `.suprx` or `.debug_prx` loads the `.prx` library of the same stem from `libs/`, and `libkernel_web` also loads `libkernel.prx`, as `libSceLibcInternal` already loads `libc.prx`: AnyPS5's `libkernel_web.prx` exports only two functions, where the console's `libkernel_web.sprx` is a libkernel variant. A name that matches a library's export as written still binds to that export first. macOS resolves these imports through the host loader by name and is unchanged. +- [Unscoped imports on Linux](../../../core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp) - in an executable that names a dependency with `.sprx`, `.suprx` or `.debug_prx`, as homebrew built with the [ps5-payload-dev SDK](https://github.com/ps5-payload-dev/sdk) does, an import without a module that is named by symbol is bound to the NID of that name only. Unlike Windows, the name as written is not tried first, because the host loader also searches the host C library, which exports `malloc` or `pthread_create` under their names. A name that a bundled module exports still binds to that module, and such a dependency loads the `.prx` library of the same stem. Unqualified imports of other executables, and imports of bundled modules, are still bound by name. - The length-changing path of the [instruction rewriter](../../../core/relinker/codegen/src/x86/X64InstructionRewriter.cpp) is not used by the [converter](../../../core/relinker/codegen/src/Amd64OnlyConverter.cpp): it does not adjust data-to-code references (relocations, FDEs, jump tables) or segment sizes, so every substitution keeps the instruction length. - The [converter](../../../core/relinker/codegen/src/Amd64OnlyConverter.cpp) takes the branch targets that keep an instruction in place from a linear scan of the code segment, so data in that segment decodes as branches too: OpenSSL's x86-64 assembly keeps strings such as "SHA1 block transform for x86_64, CRYPTOGAMS by " in its code, and in SuperTuxKart (PPSA69421) and Mortalfin (PPSA99141) the `jae` read from "sf" points into the VEX prefix of a 256-bit store. A direct branch target inside a scanned instruction counts only where every byte before it is a legacy prefix or REX, which a real branch can skip (a `lock` prefix, for example); a target in a VEX prefix, opcode or operand is ignored, as a jump-table entry that starts no instruction already is. A real branch to such a target, overlapping instructions as hand-written code can, is not seen, and the instruction it enters can be moved. - A `PT_SCE_RELRO` segment that no `PT_LOAD` covers, the PS4 layout, is [loaded](../../../core/relinker/relinker/src/parsing/ElfReader.cpp) as a read-write `PT_LOAD` and stays writable after relocation; FreeBSD rtld makes `PT_GNU_RELRO` read-only once relocation is done, and what the console does with `PT_SCE_RELRO` is not checked. diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 551838eb6d..2b6ace99db 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -22,6 +22,9 @@ - [s_barrier in vertex-path programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp) (recompiler) - emits no instruction when the program has no LDS or GDS access and no memory write or atomic, so no wave of the subgroup can observe another, or when the barrier runs before every memory access of the program (it is on the entry's straight-line path with no load, store, atomic, image or LDS access ahead of it), so it orders nothing; the `GS_ALLOC_REQ` such a barrier orders on NGG passthrough programs is already dropped on the vertex path. The Vertex execution model has no workgroup barrier. Other programs with LDS, GDS or memory writes throw. Not measured on hardware - [Vertex clip and cull distances](../../../core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp) (libSceAgcDriver) - `PA_CL_VS_OUT_CNTL` enable bit `s` of `CLIP_DIST_ENA`/`CULL_DIST_ENA` is taken to select component `s` of the `pos1`/`pos2` exports, with the Vulkan array index counted by the enabled bits below it, and `VS_OUT_CCDIST0_VEC_ENA` as bit 22; this follows the recompiler's existing position-export mapping, not hardware or documentation. The guest clip distances share one `gl_ClipDistance` array with the all-zero clip position distance, which takes the index after them. A vertex program whose own code has no `pos1`..`pos3` export is compiled and prepared without these bits (`PositionExportLayout`), so registration and draws agree on its artifact; an export in a subroutine it calls is not seen by that scan - [32_AR color exports](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - with `SPI_SHADER_COL_FORMAT` 3 the red value is export channel 0 and the alpha is export channel 1, the GFX10 layout of Mesa's [`emit_ps_color_export`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/3e32b0e1f132231b7a972c1b0b5b06c7e16cf8b9/src/amd/common/nir/ac_nir_lower_ps_late.c#L400-L411), ACO and radeonsi in Mesa 22.0 ([aco_instruction_selection.cpp](https://gitlab.freedesktop.org/mesa/mesa/-/blob/a82920af6efa4a7a66919885c6477fbd8fdc7b9d/src/amd/compiler/aco_instruction_selection.cpp#L11087-L11096), [si_shader_llvm_ps.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/a82920af6efa4a7a66919885c6477fbd8fdc7b9d/src/gallium/drivers/radeonsi/si_shader_llvm_ps.c#L323-L333)) and AMD's [LLPC](https://github.com/GPUOpen-Drivers/llpc/blob/40cb8d95ad8d6f7f1652e3fd47d39667594cce08/lgc/lowering/FragmentColorExport.cpp#L157-L168); before GFX10 the alpha was channel 3. Channels 2 and 3, which none of these compilers enables, are ignored; green and blue are 0 and the alpha of an export without channel 1 is 1.0, the defaults of the other formats, which no source gives. Not measured on hardware, neither on gfx1013 nor on a PC RDNA 2 GPU; pinned in [Pixel32ArExport.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/Pixel32ArExport.cpp) +- [GCN instructions](../../../core/shader/recompiler/RdnaDecoder/src/GcnInstructionDecoder.cpp) (shader, PS4) - PS4 (Sea Islands) instructions are re-encoded to their RDNA encodings with opcode tables built from shadPS4's `opcodes.h` and decoded by the RDNA decoder; an encoding without an RDNA counterpart throws. MIMG has no DIM field on GCN: the dimension is taken as 2D, or 2D array when DA is set, and the [materializer](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) rejects a T# whose type needs another address count. MIMG word 1 bits [31:26] are reserved on GCN (A16 and D16 on RDNA). +- [GCN buffer and image descriptors](../../../core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp) (shader, PS4) - a GCN V# word 3 (DATA_FORMAT [18:15], NUM_FORMAT [14:12], names read from the most significant field) is rewritten to the unified format of the PS5 layout with OOB_SELECT 1 for a strided buffer and 3 otherwise, following the GFX6-7 NUM_RECORDS rules in Mesa's `ac_descriptors`; ATC, HASH, HEAP and MTYPE are dropped. The V# base is 44 bits, as in shadPS4's `AmdGpu::Buffer`: Limbo leaves 0x7 in word 1 bits [15:12]. A GCN T# is rewritten to the PS5 layout with SW_MODE linear, keeping TILING_INDEX [4:0], POW2_PAD [5] and PITCH - 1 [19:6] in word 7. None of it is checked on a console. +- [PS4 legacy vertex shaders](../../../core/shader/recompiler/Recompiler.cpp) (shader, PS4) - a PS4 vertex program at the VS slot takes its user data from s0, its vertex index in v0 and its instance index in v1-v3, and a fetch shader it calls through the user pair (`s_swappc_b64 s[0:1], s[0:1]`) returns through the same pair. These rules apply only while GCN shaders are decoded. ### Functional @@ -66,7 +69,7 @@ - [Unnormalized texel offsets](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) are added to the f32 coordinates: exact on RDNA2 (gfx1035), which applies the offset before the clamp ([measured by adriadam10](https://github.com/boykopovar/AnyPS5/pull/822#issuecomment-6033192725)). - [Cube image gradients](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (shader recompiler) - `image_sample_d` and `image_sample_cd` on a cube pass their gradients to the host sample without the -1.0 that moves the face coordinates from [1, 2] to [0, 1], as Mesa's AMD lowering builds them (`src/amd/common/nir/ac_nir_lower_tex_coords.c`, `prepare_cube_coords`: face-space derivatives with no offset, coordinates offset by 1.5). Measured on RDNA2 (gfx1035) with the [hardware oracle](../HW_ORACLE.md): on a 16x16 32_FLOAT cube with five levels, point mip filter, `image_sample_d` reads the level of `round(log2(16 * max(|ddx|, |ddy|)))` for gradients of 2^(level ± 0.3) texels on each of the four components, both signs, mixed components, every face and two coordinates (1032 rows). `image_sample_cd` takes one level per quad of lanes, from the gradients of its first lane; the host sample computes it per lane. - [Image atomics](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) on 32_SINT and 32_FLOAT surfaces act on the raw dword through an R32_UINT storage view. On gfx1100 (RDNA3, [hardware oracle](../HW_ORACLE.md), wave32 and wave64), all 13 integer image atomics, `swap` to `dec`, return the old dword and store the raw-dword result identically on 32_UINT, 32_SINT and 32_FLOAT T#s, over 121 old/data pairs per op, among them 0x80000000, 0xffffffff, 1.0, -1.0 and NaN: `add` adds the bits and `umin` is unsigned on every format. A 16_UINT control differs, so the format is read. gfx1031 (RDNA2) gives the same rows, and its `fmin`, `fmax` and `fcmpswap`, which gfx1100 lacks, also match on the three formats ([measured by thuanquang](https://github.com/boykopovar/AnyPS5/pull/7939#issuecomment-6275099482)). The console's gfx1013 is unmeasured. Other formats throw. -- [`s_memtime`](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) on RADV with a GFX10.3 to GFX11.7 GPU (RDNA 2, RDNA 3, RDNA 3.5) reads the Vulkan device clock instead of the subgroup clock, which [ACO](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/compiler/instruction_selection/aco_select_nir_intrinsics.cpp) reads from the 20-bit `SHADER_CYCLES` register there and which wraps every 2^20 cycles. The device clock is a monotonic 64-bit counter, but it counts the 100 MHz reference clock (the same one as `s_memrealtime`), not shader cycles, so `s_memtime` deltas are about 20 times smaller than on hardware. The families are matched by the name RADV reports ([`NarrowSubgroupClock`](../../../core/libs/prx/libSceAgcDriver/Execution/src/SubgroupClock.cpp)); a family added to Mesa later reads the subgroup clock until it is listed. AMD's proprietary Windows driver also exposes a 20-bit subgroup clock on device 0x1114 (Radeon 860M, measured in #1615), device 0x744c (Radeon RX 7900 XT, measured in #1632), and device 0x747e (Radeon RX 7800 XT, measured in #1632); other AMD proprietary devices remain unverified. +- [`s_memtime`](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) on RADV with a GFX10.3 to GFX11.7 GPU (RDNA 2, RDNA 3, RDNA 3.5) reads the Vulkan device clock instead of the subgroup clock, which [ACO](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/compiler/instruction_selection/aco_select_nir_intrinsics.cpp) reads from the 20-bit `SHADER_CYCLES` register there and which wraps every 2^20 cycles. The device clock is a monotonic 64-bit counter, but it counts the 100 MHz reference clock (the same one as `s_memrealtime`), not shader cycles, so `s_memtime` deltas are 22.35 times smaller than on a PS5 (13.40, measured 2026-10-19 by a gfx1013 kernel run from inside a retail title), where it counts the ~2.23 GHz core clock (44,041,632 against 1,970,792 `s_memrealtime` ticks). The families are matched by the name RADV reports ([`NarrowSubgroupClock`](../../../core/libs/prx/libSceAgcDriver/Execution/src/SubgroupClock.cpp)); a family added to Mesa later reads the subgroup clock until it is listed. AMD's proprietary Windows driver also exposes a 20-bit subgroup clock on device 0x1114 (Radeon 860M, measured in #1615), device 0x744c (Radeon RX 7900 XT, measured in #1632), and device 0x747e (Radeon RX 7800 XT, measured in #1632); other AMD proprietary devices remain unverified. - [`s_memtime` and `s_memrealtime`](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) on llvmpipe read only the low 32 bits of the host's nanosecond clock at either scope: Mesa's [`emit_clock`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/gallium/auxiliary/gallivm/lp_bld_nir_soa.c) builds the high dword with a left shift by 32, so it is always 0 and both counters wrap every 2^32 ns (about 4.3 s). The [shader clock test](../../../core/libs/prx/libSceAgcDriver/tests/execution/ShaderClock.cpp) compares them modulo 2^32 there - [`ds_wrxchg2_rtn_b32`, `ds_wrxchg2st64_rtn_b32`, `ds_wrxchg2_rtn_b64` and `ds_wrxchg2st64_rtn_b64`](../../../core/shader/recompiler/Translation/src/MemoryInstructions/SharedMemoryInstructions.cpp) are translated as two separate 32-bit or 64-bit atomic swaps per invocation. RDNA2 hardware (measured on gfx1035) performs every lane's first exchange before any lane's second one, so when lanes of the same wave or of different waves overlap addresses, the result can differ from hardware. `offset0 == offset1` throws, although on hardware it is two exchanges in order. - [`ds_condxchg32_rtn_b64`](../../../core/shader/recompiler/Translation/src/MemoryInstructions/SharedMemoryInstructions.cpp) uses two 32-bit atomic conditional exchanges. RDNA2 (gfx1035) measurements confirmed aligned return values, conditional bit clearing and 16-bit offsets; out-of-range accesses return zero without modifying LDS. Misaligned accesses fault the wave on gfx1035; the recompiler does not model this fault. Radeon 780M (gfx1103, RDNA3) measurements confirmed aligned return values, conditional bit clearing, EXEC masking and operand overlap in wave32 and wave64; contended cases passed per-dword conservation checks. Contended lane ordering and gfx1013 remain unmeasured; pair-wide indivisibility is not assumed, and concurrent 64-bit LDS atomics use a separate lock. GDS throws. VGPR pairs extending beyond v255 throw; allocated-VGPR EXEC nullification is not emulated. @@ -84,7 +87,7 @@ - [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 -- [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: `src_shared_base` 0x00010000_00000000, `src_shared_limit` 0x00010000_ffffffff). The PS5's base values, high dwords 0x80000000 (shared, `SharedApertureHigh`) and 0x70000000 (private, `PrivateApertureHigh`), are inferred from PPSA04263's traversal addresses, not measured on a console or read from SH_MEM_BASES. The scalar aperture sources read the same two values +- [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. - [EXEC read as a value](../../../core/shader/recompiler/Translation/src/TranslationContext/ExecMask.cpp) (`s_mov_b64 sN, exec`, the `s_*_saveexec` copy, ALU sources) is limited to host lanes. Pixel reads retain helper bits in quads with an active fragment, preserving whole-quad masks across copies. Quad expansion follows the [RDNA 2 ISA, section 12.3, S_WQM_B32/B64, page 111](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf#page=119); save/restore followed by implicit-LOD sampling is checked on an RX 6700 XT, not on a console. The console also holds lanes the host lacks, e.g. 32-63 of a wave64 on 32 host lanes. @@ -96,7 +99,7 @@ - [Mesh layer output](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - a mesh shader writes each primitive's layer through a one-member `Block` struct array whose member is decorated `BuiltIn Layer` and `PerPrimitiveEXT`, as glslang declares `gl_Layer` inside `gl_MeshPerPrimitiveEXT`, not through a standalone `PerPrimitiveEXT` `Layer` array. Both forms pass `spirv-val`, but AMD's Windows driver (32.0.21045.5002, RX 6700 XT) crashes in `amdvlk64.dll` creating a pipeline whose mesh shader writes the standalone form, whether the layer is signed or unsigned and whatever the target, while the block form draws, also with a pixel shader that reads the layer. - [Shader recompilation](../../../core/shader/recompiler/Recompiler.cpp) currently occurs right before it was transferred to Vulkan with caching, but should be moved to the [relinker](../../../core/relinker/main.cpp) stage. For this purpose, [shader/recompiler](../../../core/shader/recompiler) was written completely independently from [libs/prx](../../../core/libs/prx). - [v_interp_p1ll_f16, v_interp_p1lv_f16 and v_interp_p2_f16](../../../core/shader/recompiler/Translation/src/AttributeInstructions.cpp) read the 16-bit halves of each vertex's parameter through `VK_KHR_fragment_shader_barycentric`, as fixed-function interpolation cannot split a 32-bit attribute. Without the extension the pixel shader takes the [barycentric emulation](../../../core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp), whose generated geometry stage passes each vertex's parameter to it as a flat input, so the halves arrive bit for bit; a device without geometry shaders takes the tessellation stages of the same emulation instead, which no 16-bit interpolation test has run through yet. P10 and P20 are the vertex differences rounded to f16 to nearest even, which is not confirmed on hardware. v_interp_p1ll_f16 and v_interp_p1lv_f16 round the exact `P10 * I + P0` once to f32 and v_interp_p2_f16 rounds the exact `P20 * J + S2` once to f16, as measured on RDNA2 (#697); the exact sums are computed in f64 or, on a device without the Float64 capability, in 64-bit integers: the exact product is added with a sticky bit and rounded once. They follow the shader's float mode, measured on RDNA2 (gfx1035) in all eight combinations of `IEEE_MODE` and kept/flushed f32 and f16 denormals over 768 special and 1,024 random rows. With IEEE=1 a NaN operand is returned quieted; with IEEE=0 it is returned unquieted, except that v_interp_p2_f16's f16 result is always quiet. Flushed f32 denormals flush I/J, v_interp_p2_f16's f32 S2 and the f32 result; flushed f16 denormals flush P0, P10/P20, v_interp_p1lv_f16's f16 S2 and the f16 result. Denormal modes 1 and 2 (flush only inputs or only outputs) and rounding other than to nearest even are not measured and throw. An unknown mode follows IEEE=0 with denormals kept. Lavapipe has no barycentrics, so the draw test is skipped in CI; `agc_driver_interpolation_modes` checks the integer arithmetic on every device, and the f64 arithmetic on devices with Float64. It checks both against the hardware rows in every mode, and against a CPU reference on random rows in every mode and f16 rounding. -- Without `VK_KHR_fragment_shader_barycentric`, a pixel shader whose I/J VGPRs [only feed `v_interp_p1_f32`/`v_interp_p2_f32` of their own pair](../../../core/shader/recompiler/Optimization/src/HostInterpolationChecker.cpp) reads the attribute Vulkan interpolated. Any other read of them or of a raw vertex parameter takes the weights and the three vertex values from a [generated geometry stage](../../../core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp), which needs `geometryShader` and a vertex shader before it. On a device with `tessellationShader` but no `geometryShader` (MoltenVK), a triangle list without primitive restart takes them from [generated tessellation stages](../../../core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp) instead: each triangle is one three-point patch at tessellation level 1, the evaluation stage writes the corner `gl_TessCoord` points at as the weights, and the vertex clip distances are forwarded: the control stage reads them as the separate `ClipDistance` array the vertex shader writes, so the vertex-to-control interfaces match, and passes them on inside its per-vertex block, which the evaluation stage reads and writes. MoltenVK 1.4.2 (SPIRV-Cross) cannot translate a separate `ClipDistance` array in a tessellation evaluation stage (`no member named 'gl_ClipDistance' in 'main0_out'`), so the two generated stages keep it in their blocks. A flat parameter takes the provoking vertex's value there, the first or with `PROVOKING_VTX_LAST` the last, and every corner of the patch writes it, so the host's own provoking vertex does not matter and `VK_EXT_provoking_vertex` is not needed; a flat per-vertex parameter with the last provoking vertex still throws. The patch keeps the triangle's facing with `VertexOrderCw`, checked with face culling on MoltenVK 1.4.2 (Apple M2) only. Tessellation, mesh and rect-list pipelines, primitives other than triangles, triangle strips and fans without `geometryShader`, more than 32 fragment input locations and interfaces beyond the device's geometry or tessellation limits throw. +- Without `VK_KHR_fragment_shader_barycentric`, a pixel shader whose I/J VGPRs [only feed `v_interp_p1_f32`/`v_interp_p2_f32` of their own pair](../../../core/shader/recompiler/Optimization/src/HostInterpolationChecker.cpp) reads the attribute Vulkan interpolated. Any other read of them or of a raw vertex parameter takes the weights and the three vertex values from a [generated geometry stage](../../../core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp), which needs `geometryShader` and a vertex shader before it. On a device with `tessellationShader` but no `geometryShader` (MoltenVK), a triangle list without primitive restart takes them from [generated tessellation stages](../../../core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp) instead: each triangle is one three-point patch at tessellation level 1, the evaluation stage writes the corner `gl_TessCoord` points at as the weights, and the vertex clip distances are forwarded: the control stage reads them as the separate `ClipDistance` array the vertex shader writes, so the vertex-to-control interfaces match, and passes them on inside its per-vertex block, which the evaluation stage reads and writes. MoltenVK 1.4.2 (SPIRV-Cross) cannot translate a separate `ClipDistance` array in a tessellation evaluation stage (`no member named 'gl_ClipDistance' in 'main0_out'`), so the two generated stages keep it in their blocks. A flat parameter takes the provoking vertex's value there, the first or with `PROVOKING_VTX_LAST` the last, and every corner of the patch writes it, so the host's own provoking vertex does not matter and `VK_EXT_provoking_vertex` is not needed; a flat per-vertex parameter with the last provoking vertex still throws. The patch keeps the triangle's facing with `VertexOrderCw`, checked with face culling on MoltenVK 1.4.2 (Apple M2) only. Tessellation, mesh and rect-list pipelines, primitives other than triangles, triangle fans and primitive restart without `geometryShader`, more than 32 fragment input locations and interfaces beyond the device's geometry or tessellation limits throw. - [v_fma_f64 and v_div_fmas_f64](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp) with a subnormal operand or result round once, as IEEE 754 fusedMultiplyAdd does with f64 denormals kept, measured on RDNA2 (gfx1035) at IEEE=0 and IEEE=1, `v_div_fmas_f64` with VCC clear and set. The f16/f64 denormal field of `FLOAT_MODE` (bits 7:6) is not read: when it flushes, the hardware flushes the operands and the result of `v_fma_f64` (tininess before rounding) and only the result of `v_div_fmas_f64`, while these keep them. Shaders with f64 instructions declare `DenormPreserve` for 64-bit floats only when `shaderDenormPreserveFloat64` is true and Vulkan reports `denormBehaviorIndependence` as `ALL`; other independence modes are not handled by the current backend because they require coupled execution modes. See [VkShaderFloatControlsIndependence](https://docs.vulkan.org/refpages/latest/refpages/source/VkShaderFloatControlsIndependence.html). Elsewhere (including lavapipe), the driver may flush f64 subnormals, and `agc_driver_f64_fma_subnormals` is skipped. - [v_floor_f64, v_ceil_f64, v_fract_f64, v_frexp_mant_f64 and v_frexp_exp_i32_f64](../../../core/shader/recompiler/Translation/src/Float64Instructions.cpp) follow the `FLOAT_MODE` f16/f64 denormal field: a denormal input is flushed to a zero of its sign when bit 0 is clear (0 and 2), and `v_fract_f64` flushes a denormal result, the fraction of a positive denormal input, to +0 when bit 1 is clear (0 and 1). `v_trunc_f64` and `v_rndne_f64` give the same results either way and are unchanged. 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 over 23 f64 values (zeros, denormals, minimum normals, ±0.5, ±1, values next to 1, the largest finite value, infinities and quiet and signaling NaNs) and 16 of them with neg and abs modifiers. Shaders without a known float mode keep the previous lowering. The PS5's gfx1013 is not measured. - [v_add_f64 and v_mul_f64 denormals](../../../core/shader/recompiler/Translation/src/Float64Instructions.cpp) follow the `FLOAT_MODE` f16/f64 denormal field; the PS5's gfx1013 is not measured. Inputs flush to a zero of their sign when bit 0 is clear (0 and 2). When bit 1 is clear (0 and 1), a sum below 2^-1022, or a product whose exact value rounded to 53 significant bits with an unbounded exponent is below 2^-1022, flushes to a zero of its sign, so `0x2000000000000001 * 0x1ffffffffffffffe` (exactly 2^-1022 - 2^-1126) rounds to 2^-1022 and is kept. Measured on RDNA2 (gfx1031, RX 6700 XT, AMD Windows driver 32.0.21045.5002, HIP) at IEEE=0 and 1, denorm16 0..3, over 576 pairs of 24 f64 values (zeros, denormals, minimum normals, values next to 1 and to 2^-1022, products just below 2^-1022, the largest finite value, infinities), with neg, abs and clamp. `v_fma_f64`, `v_div_fmas_f64` and shaders without a known float mode keep the previous lowering. @@ -146,8 +149,8 @@ - [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. 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, a misaligned 64-bit atomic records the `Unaligned` shader fault with the LDS byte address instead of updating, 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. On that path a 64-bit atomic at an address that is 4-byte but not 8-byte aligned is skipped and returns 0. - [LDS float atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_min/max/cmpst_f32`, `ds_min/max/cmpst_f64` and their `_rtn` forms) follow the hardware measured on RDNA2 (gfx1036) over 14 x 14 operand sets (zeros, ones, infinities, denormals, quiet and signalling NaNs of both signs and with payloads) in IEEE 0 and 1 with every value 0-3 of the type's denormal field: both operands are flushed to a signed zero when bit 0 of the field is clear (0 and 2; 1 and 3 keep them; the f32 field for the f32 forms, the f16/f64 field for the f64 forms; a shader without a known float mode flushes f32 and keeps f64), then a signalling-NaN data operand is stored quieted, else a signalling-NaN old value is stored quieted, else a quiet-NaN old value is replaced by the data, else a quiet-NaN data operand loses, else the ordered compare with -0 below +0 decides, and the flushed value is what is written, also when the old value wins. `cmpst` compares the flushed bits (both zeros equal, NaN never equal) and leaves memory unchanged when they differ. The returned value is always the old value as it was in memory. `IEEE_MODE` has no effect. The same bit rule serves the buffer float atomics (`AtomicFloatBits`). Not measured: a denormal or signalling-NaN `data1` of `cmpst` (stored as given). `ds_add_f32` and `ds_add_rtn_f32` follow RDNA1 (gfx1010, RX 5700 XT), measured with the [hardware oracle](../HW_ORACLE.md) template run through the Windows driver's comgr and HIP runtime over 1574 operand pairs (signed zeros, denormals, the smallest normals, infinities, quiet and signalling NaNs with payloads, the largest normals, random pairs near the denormal range and pairs that nearly cancel) in all 16 combinations of the f32 denormal field and the f32 round mode, and at IEEE 1 and DX10 clamp 0: both operands are flushed to a signed zero when bit 0 of the field is clear and the result when bit 1 is clear, the sum is rounded to nearest even whatever the round mode, a NaN data operand is stored quieted, else a NaN old value is stored quieted, `+inf + -inf` stores 0xffc00000, and `IEEE_MODE` and `DX10_CLAMP` have no effect. The sum is the host's `OpFAdd` between those flushes, so where the field keeps f32 denormals the result also depends on the host preserving them, which the module does not request. thuanquang measured the same rule on RDNA2 (gfx1031, RX 6700 XT) in #5492, with the same operand classes and 18 modes in wave32 and wave64; the PS5's gfx1013 is not measured. - [LDS in pixel, vertex-path and fetch programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp) (shader recompiler) - is an array private to each host invocation, in `Function` storage, as only compute and mesh programs get workgroup memory; on the console one LDS region is shared by the wave (RDNA 2 ISA sections 3.6.6 and 10.4), so a lane does not see another lane's LDS writes. Sub-dword writes (`ds_write_b8`, `ds_write_b16` and their `_d16_hi` forms), the 32-bit LDS atomics, `ds_append` and `ds_consume` load, update and store the invocation's own dword without an atomic, as Vulkan allows atomics only on `Uniform`, `Workgroup`, `Image`, `StorageBuffer`, `PhysicalStorageBuffer` and `TaskPayloadWorkgroupEXT` pointers (VUID-StandaloneSpirv-None-04686). `ds_append` and `ds_consume` update only the first active lane's copy and return its old value to every lane, so a second append after the first active lane changes, or a read of the counter by another lane, differs from the hardware. Not measured on hardware. -- [V# bases off a DWORD boundary](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - raw accesses use the exact bytes and `s_buffer_load` reads `(base & ~3) + (offset & ~3)`, as measured on gfx1035 (Linux, alignment mode UNALIGNED; the console's mode is unknown). Formatted accesses off their element's alignment throw. Atomics use the byte address `base + offset`, base's low two bits included: measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) with `buffer_atomic_add`, `buffer_atomic_swap` and `buffer_atomic_cmpswap` and bases 0 to 3 bytes past a DWORD boundary with offsets 0 to 7, an address on a DWORD boundary updates that DWORD and returns its old value, and any other address stops the dispatch, which the recompiler reports as an unaligned access fault. -- [Unaligned 32-bit buffer atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp) (shader recompiler) act on the dword that holds their address, as section 8.1.7 of the [RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) describes for alignment mode DWORD ("the two LSBs of the byte-address are ignored, thus forcing Dword alignment"). The mode comes from `SH_MEM_CONFIG.alignment_mode`, and the console's mode is unknown. MXGP 26 (PPSA30839) sets bits with `buffer_atomic_or` at odd byte offsets, so the console is not in a strict mode, where section 3.11 reports a memory violation for a misaligned atomic. In mode UNALIGNED, which Linux KFD sets, a misaligned atomic stops the dispatch (measured on gfx1031 and gfx1036), so the console is not in that mode either. A 64-bit buffer atomic off an 8-byte boundary still records an unaligned BDA fault: mode DWORD would align it only to 4 bytes, which a host 64-bit atomic cannot address. When the V# base itself is off a DWORD boundary and the containing dword lies before the first aligned dword the buffer view binds to, that atomic is out of bounds and returns 0 without writing. Covered by [BufferAtomicUnaligned.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/BufferAtomicUnaligned.cpp). +- [V# bases off a DWORD boundary](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - raw accesses use the exact bytes, as measured on gfx1035 (Linux, alignment mode UNALIGNED), which differs from the console's mode DWORD: on a PS5 (13.40, measured 2026-10-19 by a gfx1013 kernel run from inside a retail title) a raw `buffer_load_dword` with the V# base 1 byte past a dword, or an aligned base and `voffset` 1, returned the aligned dword (0x44332211, not 0x55443322). `s_buffer_load` reads `(base & ~3) + (offset & ~3)`, as on the console. Formatted accesses off their element's alignment throw. Atomics use the byte address `base + offset`, base's low two bits included: measured on RDNA2 (gfx1031, AMD Windows driver 32.0.21045.5002, HIP) with `buffer_atomic_add`, `_swap` and `_cmpswap`, bases 0 to 3 bytes past a DWORD boundary and offsets 0 to 7, an aligned address updates that DWORD and any other stops the dispatch, which the recompiler reports as an unaligned access fault. Closes when: raw buffer accesses drop the two low address bits as on the console +- [Unaligned 32-bit buffer atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp) (shader recompiler) act on the dword that holds their address, as section 8.1.7 of the [RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) describes for alignment mode DWORD, the mode `SH_MEM_CONFIG.alignment_mode` selects on the console: on a PS5 (13.40, measured 2026-10-19 by a gfx1013 kernel run from inside a retail title) a `buffer_atomic_add` of 1 at byte offset 0x0D incremented the dword at 0x0C and left its neighbours unchanged. A 64-bit buffer atomic off an 8-byte boundary still records an unaligned BDA fault: mode DWORD would align it only to 4 bytes, which a host 64-bit atomic cannot address. When the V# base itself is off a DWORD boundary and the containing dword lies before the first aligned dword the buffer view binds to, that atomic is out of bounds and returns 0 without writing. Covered by [BufferAtomicUnaligned.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/BufferAtomicUnaligned.cpp). - [32-bit bitfield masks](../../../core/shader/recompiler/Translation/src/IntegerInstructions.cpp) (`S_BFM_B32`, `V_BFM_B32`) use the truncating 32-bit shift specified by the [RDNA 1.0 ISA](https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/instruction-set-architectures/rdna-shader-instruction-set-architecture.pdf), sections 12.1 (opcode 36) and 12.12 (opcode 867). Width and offset use their low five bits. This matches gfx1035 (Linux) for every width/offset pair, with and without upper operand bits set, and both leave SCC unchanged; not measured on the console. - [Vector buffer offsets that are not a multiple of 4](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - dword accesses count whole dwords from the V# base, where the hardware reads the exact bytes at base + offset. - [Raw `buffer_load_dwordx2`](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) is one 64-bit access on the hardware (with or without glc), so it never sees half of a `buffer_atomic_swap_x2`. Only a glc load that is 8-byte aligned and in bounds, in a program that has the 64-bit storage view (it uses 64-bit buffer atomics), is read with one 64-bit atomic load; every other dwordx2 load is two dword loads and can tear against a concurrent 64-bit write: loads without glc, and loads in a program without 64-bit buffer atomics while another shader writes the pair with `swap_x2`. @@ -158,7 +161,7 @@ - [2D biased sampling through a one-slice, single-level 3D T#](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) supplies a zero third coordinate and retains the bias operation, with normalized point minification and magnification and no linear mip filtering. Measured on RDNA2 (AMD integrated gfx1036, driver 32.0.21036.18, Windows 11) by running an OpenCL-loaded native kernel with `image_sample_b` DIM 2D against 16x16x1 R32_UINT and RGBA8_UNORM 3D images: biases -16, -0.5, 0, 0.5 and 16 read the (u,v) texel in slice zero, including wrapped coordinates, point min/max reduction and the title's sampler with Z border addressing. Wave32, IEEE off, DX10 clamp on, denormals preserved, round to nearest even, FP16 overflow off; wave64 and the PS5's gfx1013 are not measured. Pixel-stage, direct, non-depth, unpacked samples whose only use is the plain 32-bit bias form are accepted; extra slices, any nonzero mip range, linear or anisotropic filtering and unnormalized coordinates throw. Vulkan readback is checked on NVIDIA RTX 5070 and AMD integrated graphics for positive and negative bias and point weighted-average/min/max reduction. - [FORCE_DEGAMMA on RGBA8_UINT](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) materializes a direct image sampled only by plain biased pixel accesses as RGBA8_SRGB when every paired sampler forces degamma. Measured on AMD integrated RDNA2 gfx1036 (driver 32.0.21036.18, Windows 11, wave32, IEEE off, DX10 clamp on, denormals preserved, round to nearest even, FP16 overflow off): a native `image_sample_b` DIM 2D on a 16x16x1 RGBA8_UINT 3D T# returns integer channels without the bit, but the same floating-point decoded values as RGBA8_UNORM with it, including point min/max reduction and the title's exact sampler. The metadata and image view must both become floating-point sRGB; ignoring the sampler bit or retaining a UINT heap is incorrect. The accepted case requires one slice, mip zero, normalized point sampling and uniform FORCE_DEGAMMA across its samplers. Other image accesses, mixed sampler states, indirect descriptors and other integer formats remain refused. Wave64, other filtering modes and the PS5's gfx1013 are not measured. - The [s_swappc_b64 fetch-shader call at vertex/local/tessellation/mesh entry](../../../core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp) assumes the called fetch-shader blob ends in `s_setpc_b64` to the link pair (the GNM fetch-shader shape shadPS4's `EmitFetch` copies); the blob is not part of the shader program, so its return closes the pairing by assumption and any in-program reader of the link pair throws. Attribute fetches are inlined from the request's parsed fetch-shader plan through the same `GetAttribute` path as the embedded fetch analyzer, honoring the buffer resource `dst_sel`; an attribute writing more than four registers, or an entry call without a parsed plan, throws. The vertex/instance offset accumulator inside the fetch blob is not replayed for external blobs. -- The scalar aperture sources [src_shared_base, src_shared_limit, src_private_base, src_private_limit](../../../core/shader/recompiler/RdnaDecoder/src/RdnaInstructionDecoder.cpp) (core/shader/recompiler, scalar source codes 235 to 238) read as the 64-bit apertures the flat routing uses: a base is the aperture's high dword (`SharedApertureHigh`, `PrivateApertureHigh`) over a low dword of 0, a limit is the same high dword over 0xffffffff. A 64-bit read (`s_mov_b64`, a 64-bit VALU source, a wave64 lane mask) gets the whole value and a 32-bit read gets the low dword. This follows LLVM's `SIRegisterInfo.td` (aperture registers are 64-bit, their low 32 bits are always 0 for a base and -1 for a limit) and `getSegmentAperture` (`AMDGPULegalizerInfo.cpp`, `SIISelLowering.cpp`: a 64-bit read whose high half is the aperture), the KFD's `MAKE_LDS_APP_LIMIT` (limit = base | 0xffffffff) and the gfx1030 and gfx1035 readouts on [#1190](https://github.com/boykopovar/AnyPS5/pull/1190) and [#3803](https://github.com/boykopovar/AnyPS5/issues/3803). Not measured on a console: the PS5's high dwords are the inferred values of the flat aperture bases entry, and the wave is taken to be in 64-bit address mode (PTR32 clear) +- The scalar aperture sources [src_shared_base, src_shared_limit, src_private_base, src_private_limit](../../../core/shader/recompiler/RdnaDecoder/src/RdnaInstructionDecoder.cpp) (core/shader/recompiler, scalar source codes 235 to 238) read as the 64-bit apertures the flat routing uses: a base is the aperture's high dword (`SharedApertureHigh`, `PrivateApertureHigh`) over a low dword of 0, a limit is the same high dword over 0xffffffff, as a PS5 (13.40, measured 2026-10-19 by a gfx1013 kernel run from inside a retail title) read them (0x80000000_00000000 / 0x80000000_FFFFFFFF shared, 0x70000000_00000000 / 0x70000000_FFFFFFFF private). A 64-bit read (`s_mov_b64`, a 64-bit VALU source, a wave64 lane mask) gets the whole value and a 32-bit read the low dword, after LLVM's `SIRegisterInfo.td` and `getSegmentAperture` and the gfx1030 and gfx1035 readouts on [#1190](https://github.com/boykopovar/AnyPS5/pull/1190) and [#3803](https://github.com/boykopovar/AnyPS5/issues/3803). The 32-bit read and the wave's address mode (taken to be 64-bit, PTR32 clear) were not measured on the console - [EmitSetAttribute](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - a primitive with a vertex whose clip position is (0,0,0,0) is clipped away through an extra `gl_ClipDistance` element on vertex and tessellation evaluation shaders, matching what a Radeon 610M (RDNA2, RADV) and llvmpipe rasterise; NVIDIA 615.78.08 without it draws the vertex at the viewport centre. Measured for triangles (strip and list) and for a point at (0,0,0,0), which the Radeon 610M does not draw, NVIDIA draws at the viewport centre and llvmpipe at the top-left corner; lines with such a vertex are clipped by the same element and were not measured. Mesh, geometry and the generated barycentric geometry stage do not write it, the generated rect-list and barycentric tessellation stages forward the vertex shader's array, and a vertex or tessellation evaluation shader whose guest clip and cull distances already take all eight does not write it either, so NVIDIA 615.78.08 draws such a vertex at the viewport centre; a device without `shaderClipDistance` cannot compile vertex shaders that write it (the compile throws). The PS5 rasteriser was not measured. - [PERSP_SAMPLE and LINEAR_SAMPLE](../../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) (recompiler) are the barycentrics at the iterated sample, which in a pixel shader the guest does not iterate is taken to be sample 0, inferred from the ancillary's iterated sample number and not measured. They are interpolated with `InterpolateAtOffset`, which [GLSL_EXT_fragment_shader_barycentric](https://github.com/KhronosGroup/GLSL/blob/0099bf83b0288469917fc2e2972cb0a2b74d6711/extensions/ext/GLSL_EXT_fragment_shader_barycentric.txt#L286-L299) names for barycentrics away from the default position, at the sample's offset from the pixel centre, read per pixel of the 2x2 quad from `PA_SC_AA_SAMPLE_LOCS` (the standard pattern unless the draw sets its own): sample 0's in a shader the guest does not iterate, and in an iterated shader the offset of the host's `SampleId` from a table of the guest's locations, because Vulkan leaves the position of plain barycentrics under sample shading implementation-defined (lavapipe uses the pixel centre). That needs the device's `sampleRateShading`; without it, and when every location is the pixel centre (a single-sample draw), they keep the host's default position, the pixel centre once per pixel and an implementation-defined position under sample shading. PERSP_CENTER and LINEAR_CENTER are interpolated at offset 0 in an iterated shader, so they stay at the pixel centre as on the console; the centroid pairs keep the host's default position. With barycentric emulation the offsets apply to the emulated barycentrics. Devices that interpolate attributes on the host instead (no usable fragment-shader barycentrics, as on MoltenVK) keep the default position for all four pairs. - [Pixel positions](../../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) (recompiler) - POS_X_FLOAT and POS_Y_FLOAT follow `SPI_BARYC_CNTL.POS_FLOAT_LOCATION` (bits 17:16) and `POS_FLOAT_ULC` (bit 20), whose positions come from [PAL's GFX9+ register header](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/hw/gfxip/gfx9/chip/gfx9_plus_merged_registers.h) and whose meanings from [LLPC](https://github.com/GPUOpen-Drivers/llpc/blob/40cb8d95ad8d6f7f1652e3fd47d39667594cce08/lgc/lowering/RegisterMetadataBuilder.cpp#L767-L779): the pixel centre (0), the iterated sample (2), or the upper-left corner (ULC). Under host sample shading the centre is `floor(FragCoord) + 0.5`, and a non-iterated shader's sample is the centre plus sample 0's offset. The centroid (1) keeps `FragCoord`, the pixel centre or under sample shading the sample, not the centroid of the covered samples. A missing `SPI_BARYC_CNTL` reads as the pixel centre and the reserved location 3 throws. POS_Z_FLOAT and POS_W_FLOAT keep `FragCoord`'s depth and 1/w at the host's position. `PERSP_CENTER_CNTL`, `PERSP_CENTROID_CNTL`, `LINEAR_CENTER_CNTL` and `LINEAR_CENTROID_CNTL` (bits 0, 4, 8 and 12) are not read: PAL and LLPC never set them. diff --git a/docs/user/INPUT_MAPPING.md b/docs/user/INPUT_MAPPING.md index 7ab80c3d4f..7fd7a8d16d 100644 --- a/docs/user/INPUT_MAPPING.md +++ b/docs/user/INPUT_MAPPING.md @@ -42,6 +42,7 @@ Without an `anyps5-input.ini`, or for actions the file does not mention, these b | `Shake` | X (reports a swinging acceleration and angular velocity while held) | | `ToggleMouse` | Middle mouse button, F7 | | `ToggleFullscreen` | F11 | +| `TogglePerformanceOverlay` | F10 | | `Screenshot` | F12 | For example, this changes Cross to F or Space, moves the left stick to IJKL, uses the mouse buttons for Square and R2, and keeps all other built-in bindings: diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 672e35bbbf..eadd1e54ec 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -12,6 +12,8 @@ Select or drop multiple app ZIP archives to convert them into one ZIP bundle. Th ## Input and conversion +The SELF unwrapper refuses containers requiring more than 64 MiB of total alignment padding before allocating the output buffer. This limit counts padding across file-backed and empty segments, not segment data; a refusal preserves the input and any existing output file. + Use a clean ELF executable. Place its bundled ELF modules in `sce_module/`, `sce_modules/`, or `prx/` beside the input executable, or under the parent directory selected with `--sce-module-path `. The option accepts absolute paths and paths relative to the current working directory; do not include the `sce_module` directory itself. Only files directly inside the module directories are processed. SELF/fSELF containers are skipped with a warning; unwrap them before relinking if they need to be included. `tools/unwrap_self.py ` does that for a decrypted container: it places the segments at the offsets of the SELF segment records and restores `p_offset % p_align == p_vaddr % p_align`, and it refuses a container whose load segments all look encrypted. The command-line relinker skips an ELF module as a backup copy, with a message, when its file name extends the name of another ELF module in the same directory and both have the same SONAME, such as a dumper's `libX.prx.esbak` or `libX.prxbak` beside an ELF `libX.prx`; a single backup beside a SELF container is still relinked. `prx/` can coexist with either `sce_module/` or `sce_modules/`. Both `sce_module/` and `sce_modules/` present is an error. All three absent is an error unless an explicit `--module-dir` is supplied. ```text @@ -64,7 +66,7 @@ All switches are disabled by default. `unused-filter` defaults to `0`; `--rpath` | `--windows-gui` | Select the Windows GUI subsystem instead of the console subsystem. Requires `--windows`. | | `--windows-fixed-base
` | Map the lowest ELF load page at this fixed Windows address, given in decimal or hexadecimal (`0x`). Requires `--windows`, a 64 KiB-aligned address from `0x20000` through `0x7fffffff0000`, and a complete image fitting in the user address space. The main executable has no ASLR or base relocations; an occupied address fails to load. Bundled modules keep their normal placement. | | `--trace-imports` | Pass each function import through the libc.prx call tracer, see [tracing system library calls](#tracing-system-library-calls). Windows and Linux output only; conflicts with `--lazy-binding`. | -| `--linux-fixed-base
` | Load the Linux executable at this fixed address, given in decimal or hexadecimal (`0x`): every virtual address of the image is offset by it, so virtual address 0 is placed at the address, as the console places an executable. Requires a 64 KiB-aligned address from `0x10000` through `0x7fffffff0000` that is a multiple of every `PT_LOAD` alignment, and an image that ends in the user address space. The executable is written as `ET_EXEC`, without ASLR. Bundled modules keep their normal placement. Conflicts with `--windows`, `--macos` and `--lazy-binding`. | +| `--linux-fixed-base
` | Load the Linux executable at this fixed address instead of the default `0x400000`, given in decimal or hexadecimal (`0x`): every virtual address of the image is offset by it, so virtual address 0 is placed at the address, as the console places an executable. Requires a 64 KiB-aligned address from `0x10000` through `0x7fffffff0000` that is a multiple of every `PT_LOAD` alignment, and an image that ends in the user address space. The executable is written as `ET_EXEC`, without ASLR, as it is at the default address. Bundled modules keep their normal placement. Conflicts with `--windows`, `--macos` and `--lazy-binding`. | | `--macos-fixed-base
` | Map the lowest ELF load page of the macOS executable at this fixed address, given in decimal or hexadecimal (`0x`). Requires `--macos`, a 64 KiB-aligned address from `0x20000` through `0x7fffffff0000`, and an image that ends in the macOS user address space. `__PAGEZERO` ends 64 KiB below the address and the Mach-O header occupies those 64 KiB. The executable has no ASLR; an address the process cannot map fails to load. Bundled modules keep their normal placement. | | `--to-intel` | Convert supported AMD-only instructions in the executable and bundled modules. Unsupported instructions or unreachable conversion stubs cause an error. | | `--to-rosetta` | Convert as `--to-intel` does and also lower what Rosetta lacks: ANDN becomes scalar NOT/AND, RDSEED becomes RDRAND, CLWB a NOP, and RDPID and the ECX of RDTSCP return processor 0. Implies `--to-intel`. Independent of `--macos`: `--macos` alone leaves these instructions as they are. Conflicts with `--windows`. | @@ -85,7 +87,7 @@ All switches are disabled by default. `unused-filter` defaults to `0`; `--rpath` | `--exclude-sce-module ` | Deprecated. Exclude a bundled module by exact filename, not path. Repeat for multiple files; a missing filename is an error. Conflicts with `--skip-sce-module`. | | `--skip-syscall-check` | Deprecated. Disable syscall scanning in the executable and bundled modules. | | `--trap-sdk-syscall-stubs` | Replace the exact ps5-payload-dev generic and numbered syscall wrapper stubs and the recognized inline mmap/mprotect sequences from SDK libc/mman.c with `UD2`, so unused wrappers do not block relinking. Other syscall instructions still cause an error. If a trapped wrapper runs, it raises an illegal-instruction exception. For debugging only. | -| `--lazy-binding` | Deprecated. Enable lazy symbol binding instead of eager binding. Incompatible with bundled ELF modules. | +| `--lazy-binding` | Deprecated. Enable lazy symbol binding instead of eager binding. Incompatible with bundled ELF modules. The Linux executable stays position independent (`ET_DYN`) instead of being placed at `0x400000`. | Specify `unused-filter=0|1|2` without `--`, at most once. Unknown options and extra positional arguments are errors. Use `relinker --help` to print the usage syntax to stdout and exit with code `0` without converting files. Invoking `relinker` without arguments prints the usage syntax to stderr and exits with code `1`. @@ -120,11 +122,11 @@ align = 0x1000 description = "pool size" ``` -`format = 1` is required. The top-level keys `relinker_version`, `relinker_commit`, `config`, `config_sha256`, `main_target`, `settings` and `settings_sha256` are accepted in any file; only `main_target` is used, by `--verify-output`. Any other top-level key is an error. +`format = 1` is required. The top-level keys `relinker_version`, `relinker_commit`, `config`, `config_sha256`, `main_target`, `linux_base`, `settings` and `settings_sha256` are accepted in any file; only `main_target` and `linux_base` are used, by `--verify-output`. Any other top-level key is an error. | Entry key | Meaning | |-----------|---------| -| `name` | Optional. A setting name of `[a-z0-9_]`, unique within the file and not one of the reserved top-level key names (`format` and the seven keys above), used by a later `--set` option. Shown in the progress line, the manifest and the verify report. | +| `name` | Optional. A setting name of `[a-z0-9_]`, unique within the file and not one of the reserved top-level key names (`format` and the eight keys above), used by a later `--set` option. Shown in the progress line, the manifest and the verify report. | | `target` | Required. The main executable's file name, or for `--executable` files and bundled modules the path relative to the module parent directory, for example `sce_module/libc.prx`. | | `va` | Required. The virtual address of the ELF as a disassembler shows it. | | `type` | Optional: `u8 u16 u32 u64 i8 i16 i32 i64`. Then `expected`, `value`, `min`, `max` and `align` are integers, range-checked and written little-endian. Without it, `expected` and `value` are hexadecimal byte strings of the same length. | @@ -137,11 +139,11 @@ x86 `mov r/m64, imm32` and `add`/`sub`/`cmp r/m64, imm32` sign-extend their imme Entries apply in file order. A patch range must lie in the file bytes of one `PT_LOAD` and outside the ELF header and program header table. The conversion fails before writing any output if bytes differ from `expected`, a range is not valid, two entries overlap, a target matches no file or more than one file, or a range overlaps bytes the relinker itself rewrites (`--to-intel` replacements and trampolines, relinker patches, syscall traps) or the 8-byte target of a relocation of the file. -When at least one patch is applied, `.patches.toml` is written beside the output. It lists every applied entry for every converted file, with `main_target` and `config` (the patch file path in forward-slash form, reduced to its file name when the path holds a `"`, backslash or control character; the conversion fails before writing any output when even that cannot be held); typed entries keep their `type`, integers and `min`, `max` and `align`, and `--patches` accepts it to reproduce the conversion. The manifest must not replace the patch file it was read from. +When at least one patch is applied, `.patches.toml` is written beside the output. It lists every applied entry for every converted file, with `main_target`, `linux_base` (the address a Linux `ET_EXEC` output is placed at) and `config` (the patch file path in forward-slash form, reduced to its file name when the path holds a `"`, backslash or control character; the conversion fails before writing any output when even that cannot be held); typed entries keep their `type`, integers and `min`, `max` and `align`, and `--patches` accepts it to reproduce the conversion. The manifest must not replace the patch file it was read from. `--verify-patches ` writes nothing and exits `0` when no entry is a mismatch. Without `--verify-output`, `` is the input executable of any type, modules are found as in a conversion (module options and `--executable` apply), and each entry is `applicable` (the input holds `expected`) or `mismatch`. A target that matches nothing is an error that suggests `--verify-output`. -With `--verify-output`, `` is a converted Linux ELF (`ET_DYN`); `ET_EXEC` output from `--linux-fixed-base`, PE and Mach-O outputs are refused. The file must have `main_target`, so verify the generated manifest, not a hand-written file. The entry whose target is `main_target` is checked against ``, and entries for the `--executable` strings given are checked against the output file derived as in a conversion; an entry holding `value` is `applied`. Every other entry is `not-checked (module, or executable not named with --executable)`, including a mistyped module target. Two targets with the same name, such as an `--executable` equal to `main_target`, are an `ambiguous patch target` error. Module discovery options are rejected with `--verify-output`. +With `--verify-output`, `` is a converted Linux ELF; PE and Mach-O outputs are refused. The file must have `main_target`, so verify the generated manifest, not a hand-written file. Entries are checked at their `va` plus the manifest's `linux_base` in an `ET_EXEC` output, which requires `linux_base`; an `ET_DYN` output, from `--lazy-binding`, requires a manifest without it. The entry whose target is `main_target` is checked against ``, and entries for the `--executable` strings given are checked against the output file derived as in a conversion; an entry holding `value` is `applied`. Every other entry is `not-checked (module, or executable not named with --executable)`, including a mistyped module target. Two targets with the same name, such as an `--executable` equal to `main_target`, are an `ambiguous patch target` error. Module discovery options are rejected with `--verify-output`. Verify mode rejects the conversion options (`--windows`, `--macos`, `--to-intel`, `--rpath`, `--patches`, `--no-patches` and the like) and a second path. @@ -149,7 +151,7 @@ Verify mode rejects the conversion options (`--windows`, `--macos`, `--to-intel` For an executable that assumes its image starts at `0x400000`, use `relinker --windows --windows-fixed-base 0x400000 source/input.elf app.exe`. This maps its lowest ELF load page there; the PE headers occupy the preceding 64 KiB. Use this option only when the executable requires an exact address. Without it, the Windows executable has no `DYNAMIC_BASE` and loads at its preferred base `0x140000000`; relocations are kept, so Windows relocates it if that range is taken. Modules keep `DYNAMIC_BASE`. This option does not change macOS output. -For an executable that assumes its image starts at `0x400000`, as Ruffle (PPSA68091), VLC (PPSA85300) and XPSemu (PPSA97358) do when they read their `app0/imports.txt`, use `relinker --linux-fixed-base 0x400000 source/input.elf app.elf` on Linux, or `relinker --macos --macos-fixed-base 0x400000 source/input.elf app` on macOS. Use these options only when the executable requires a fixed address. Without them, Linux output is a position-independent executable as before, and macOS output places the lowest ELF load page at `0x140010000`. +For an executable that assumes its image starts at `0x400000`, as Ruffle (PPSA68091), VLC (PPSA85300) and XPSemu (PPSA97358) do when they read their `app0/imports.txt`, use `relinker --macos --macos-fixed-base 0x400000 source/input.elf app` on macOS. Use this option only when the executable requires a fixed address; without it, macOS output places the lowest ELF load page at `0x140010000`. Linux output places virtual address 0 at `0x400000` without an option, inside the 40-bit window that a title's own `sceLibcMspaceCreate` checks; `--linux-fixed-base` selects another address, and with `--lazy-binding` the executable stays position independent. The same base is applied to executables selected with `--executable`; bundled PRX modules are not rebased. @@ -166,7 +168,7 @@ app0/ ``` -Use `sce_modules/` or `prx/` instead of `sce_module/` if that is the input directory name. Relinker preserves each module's directory under `app0/` and prints its exact path. Place app resources in `app0/` separately, including the title's original `eboot.bin`: titles test `/app0/eboot.bin` to find their own directory, as it is always present on the console (ProsperoTV, ProsperoRadio, ProsperoLight and ProsperoLichess fall back to an empty `/data/homebrew/` and fail to load their fonts without it). Only the converted executable runs. On Linux, `app0/` must not contain symbolic links: a directory listing gives the title the link's own type, not the type of the file or directory it points to, so a title that walks its directories skips a linked directory. Use copies or hard links. Copy the built system libraries from `build/core/libs/libs/*.prx` into `libs/`; use libraries built for the target OS. A custom `--rpath` changes the system library location. +Use `sce_modules/` or `prx/` instead of `sce_module/` if that is the input directory name. Relinker preserves each module's directory under `app0/` and prints its exact path. Place app resources in `app0/` separately, including the title's original `eboot.bin`: titles test `/app0/eboot.bin` to find their own directory, as it is always present on the console (ProsperoTV, ProsperoRadio, ProsperoLight and ProsperoLichess fall back to an empty `/data/homebrew/<title id>` and fail to load their fonts without it). Only the converted executable runs. On Linux, a symbolic link inside the `app0/` tree is listed with the type of the file or directory it points to, so a linked directory is walked like a real one. Links inside a linked directory whose target is outside `app0/` keep the link type; for nested trees, link only the top-level entries or use copies or hard links. Copy the built system libraries from `build/core/libs/libs/*.prx` into `libs/`; use libraries built for the target OS. A custom `--rpath` changes the system library location. Some applications also need their original `app0/sce_sys/param.json` at runtime. AnyPS5 reads this file for the title ID and display name; it must contain a non-empty `titleId` string and a non-empty `localizedParameters` object with a language entry that has a non-empty `titleName`. It selects the entry named by `defaultLanguage` (inside `localizedParameters`, or at the top level); if that entry is absent, it tries `en-US`, then the first object-valued language entry. Preserve the values from the title's own metadata and place the file under the same `app0/` directory as the other app resources. At startup, its presence in the working directory creates `data/`; a non-zero `downloadDataSize` also creates `download0/`. Without the file, neither directory is created. If it is missing or invalid, metadata calls such as title/window setup can fail with a `param.json` error. @@ -216,11 +218,12 @@ python3 tools/pad_input.py /tmp/anyps5-pad release python3 tools/pad_input.py /tmp/anyps5-pad release --left-stick 128 0 --hold-ms 2000 python3 tools/pad_input.py /tmp/anyps5-pad cross --left-stick 255 128 --right-stick 0 128 python3 tools/pad_input.py /tmp/anyps5-pad touch-right +python3 tools/pad_input.py /tmp/anyps5-pad l1+r1 ``` For timed button sequences, see [Controller timelines](INPUT_MAPPING.md#controller-timelines). -Run the helper in a second terminal. Commands are polled at most every 100 ms during controller reads, and their buttons add to physical controller input. A press lasts 300 ms by default, starting when the application reads it, and releases automatically; `--hold-ms` accepts 1–5000 ms. `--left-stick X Y` and `--right-stick X Y` hold the sticks for the same time: each axis is 0-255, 128 is the centre, X grows to the right and Y grows downward. While such a command is active both sticks come from it instead of the physical controller, and a stick that was not named stays centred. `touch-left` and `touch-right` press the touchpad with a finger on its left or right half, as the `TouchLeft` and `TouchRight` keys do; in the command file they are bits `0x200000` and `0x400000` of the button field. Wait for the application to observe a release before sending another press. The file holds only the latest command; rapid commands can replace one another. On POSIX, replacing an existing command file retains its permission mode; a privileged helper also retains its owner and group so the game user can still read it. Commands left over when the application first reads the controller are ignored. Without the environment variable, file input is disabled. +Run the helper in a second terminal. Commands are polled at most every 100 ms during controller reads, and their buttons add to physical controller input. A press lasts 300 ms by default, starting when the application reads it, and releases automatically; `--hold-ms` accepts 1–5000 ms. `--left-stick X Y` and `--right-stick X Y` hold the sticks for the same time: each axis is 0-255, 128 is the centre, X grows to the right and Y grows downward. While such a command is active both sticks come from it instead of the physical controller, and a stick that was not named stays centred. `touch-left` and `touch-right` press the touchpad with a finger on its left or right half, as the `TouchLeft` and `TouchRight` keys do; in the command file they are bits `0x200000` and `0x400000` of the button field. Button names joined by `+`, such as `l1+r1`, are pressed together in one command; `release` cannot be joined. Wait for the application to observe a release before sending another press. The file holds only the latest command; rapid commands can replace one another. On POSIX, replacing an existing command file retains its permission mode; a privileged helper also retains its owner and group so the game user can still read it. Commands left over when the application first reads the controller are ignored. Without the environment variable, file input is disabled. ### Video-out and pad telemetry diff --git a/tools/build_web.py b/tools/build_web.py index 66798820d2..ed871bb1d4 100644 --- a/tools/build_web.py +++ b/tools/build_web.py @@ -4,6 +4,7 @@ import io import json import os from pathlib import Path +import sys import re import shutil import tempfile @@ -141,7 +142,7 @@ def publish_release(output): target.parent.mkdir(parents=True, exist_ok=True) target.write_bytes(archive.read(file)) publish_libraries(staging, release) - manifest = json.loads((staging / "release.json").read_text(encoding="utf-8")) + manifest = json.loads((staging / "release.json").read_text(encoding="utf-8-sig")) manifest["revision"] = metadata["revision"] (staging / "release.json").write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8") (staging / ".nojekyll").touch() @@ -163,23 +164,30 @@ def publish_release(output): print(f"Published web converter and libraries from {tag} ({metadata['revision']})") -def main(): +def main(argv=None): + for stream in (sys.stdout, sys.stderr): + if hasattr(stream, "reconfigure"): + stream.reconfigure(encoding="utf-8") parser = argparse.ArgumentParser() parser.add_argument("--build", type=Path, required=True) parser.add_argument("--release", "--libraries", dest="release", action="store_true", help="Download the matching converter and libraries from the latest release") - args = parser.parse_args() + args = parser.parse_args(argv) output = args.build.resolve() / "web" - if args.release: - publish_release(output) + try: + if args.release: + publish_release(output) + print(output) + return 0 + for name in ("relinker_wasm.mjs", "relinker_wasm.wasm"): + if not (output / "native" / name).is_file(): + parser.error(f"Build relinker_wasm first: missing {name}") + copy_sources(output) print(output) - return - for name in ("relinker_wasm.mjs", "relinker_wasm.wasm"): - if not (output / "native" / name).is_file(): - raise ValueError(f"Build relinker_wasm first: missing {name}") - copy_sources(output) - print(output) + return 0 + except (OSError, ValueError) as error: + parser.error(str(error)) if __name__ == "__main__": - main() + sys.exit(main()) diff --git a/tools/check_compile.py b/tools/check_compile.py index a251673abf..9b66a1cd16 100644 --- a/tools/check_compile.py +++ b/tools/check_compile.py @@ -45,7 +45,7 @@ def entries(build): raise ValueError(f"{build} is not a directory; configure the build first") manifest = build / "compile_commands.json" if manifest.is_file(): - return json.loads(manifest.read_text(encoding="utf-8")) + return json.loads(manifest.read_text(encoding="utf-8-sig")) ninja = shutil.which("ninja") if ninja is None: raise ValueError(f"no {manifest} and ninja is not on PATH") diff --git a/tools/check_conventions.py b/tools/check_conventions.py index bc985e1480..675981e57b 100644 --- a/tools/check_conventions.py +++ b/tools/check_conventions.py @@ -171,6 +171,7 @@ class Check: core = path.startswith("core/") and "tests" not in PurePosixPath(path).parts source = self.show(path).split("\n") masked = cpp_text("\n".join(source), preserve_comments=True).split("\n") + assembly = asm_exports("\n".join(source)) if core and path.startswith("core/libs/prx/") else {} for number, text in lines: bare = masked[number - 1] if ("//" in bare or "/*" in bare) and not ALLOWED_COMMENT.match(bare): @@ -182,6 +183,7 @@ class Check: if core and path.startswith("core/libs/prx/"): unknowns.extend((nid, name, path, number) for nid, name in source_aliases(text) if "unknown" in name.lower()) symbols.extend((symbol, path, number) for symbol in nid_symbols(text)) + exports.update((name, (path, number)) for name in assembly.get(number, ()) if not name.endswith(("_nid_no_patch", "_nid_no_patch_cut"))) match = EXPORT.search(bare) if match and core and path.startswith("core/libs/prx/") and not match.group(1).endswith(("_nid_no_patch", "_nid_no_patch_cut")): source = source or self.show(path).splitlines() @@ -212,17 +214,18 @@ class Check: self.read(paths) for path in paths: text = self.show(path) - if "APS5_VABI" not in text: + if "APS5_VABI" not in text and "ASM_FUNCTION" not in text: continue source = cpp_text(text).split("\n") + assembly = asm_exports(text) for number, line in enumerate(source): - for match in EXPORT.finditer(line): - name = match.group(1) - if name not in exports: - continue + names = [match.group(1) for match in EXPORT.finditer(line) + if match.group(1) in exports and library(path) != library(exports[match.group(1)][0]) + and function_body(source, number) is not None] + names += [name for name in assembly.get(number + 1, ()) if name in exports and library(path) != library(exports[name][0])] + for name in names: own, own_number = exports[name] - if library(path) != library(own) and function_body(source, number) is not None: - self.report("duplicate-export", own, own_number, f"{name} is also in {library(path)}") + self.report("duplicate-export", own, own_number, f"{name} is also in {library(path)}") def unknown_nids(self, unknowns, symbols): if not unknowns and not symbols: @@ -517,6 +520,16 @@ def symbol_nid(name): return compute_nid(base) if base else None +def asm_exports(text): + tokens = [match for match in CPP_TOKEN.finditer(text) if match.group("comment") is None] + exports = defaultdict(list) + for index, match in enumerate(tokens[:-2]): + name = tokens[index + 2].group()[1:-1] + if match.group() in ("APS5_ASM_FUNCTION", "APS5_SIGNAL_ASM_FUNCTION") and tokens[index + 1].group() == "(" and tokens[index + 2].group("string") is not None and IDENTIFIER.fullmatch(name): + exports[text.count("\n", 0, tokens[index + 2].start()) + 1].append(name) + return exports + + def nid_symbols(text): tokens = [match for match in CPP_TOKEN.finditer(text) if match.group("comment") is None] names = [] diff --git a/tools/check_main_sync.py b/tools/check_main_sync.py index e1f41677d7..63d952ee12 100644 --- a/tools/check_main_sync.py +++ b/tools/check_main_sync.py @@ -88,14 +88,17 @@ def render(data): return "\n".join(out) -if __name__ == "__main__": +def main(argv=None): + for stream in (sys.stdout, sys.stderr): + if hasattr(stream, "reconfigure"): + stream.reconfigure(errors="backslashreplace") parser = argparse.ArgumentParser(description="report whether a branch is in step with the upstream main before you push or test") parser.add_argument("--base", help="the main ref to compare against (default: upstream/main, then origin/main, then main)") parser.add_argument("--head", default="HEAD", help="the branch or commit to check (default: HEAD)") parser.add_argument("--repo", type=Path, default=Path("."), help="the repository to check (default: the current directory)") parser.add_argument("--fetch", action="store_true", help="fetch the base's remote first") parser.add_argument("--strict", action="store_true", help="also fail when the branch is behind the base") - args = parser.parse_args() + args = parser.parse_args(argv) try: def fetch(ref): remote, _, branch = ref.partition("/") @@ -117,9 +120,11 @@ if __name__ == "__main__": data = report(args.repo, base, args.head) except RuntimeError as error: parser.error(str(error)) - sys.stdout.reconfigure(errors="backslashreplace") print(render(data)) - if data["conflicts"]: - raise SystemExit(1) - if args.strict and data["behind"]: - raise SystemExit(1) + if data["conflicts"] or (args.strict and data["behind"]): + return 1 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/check_skips.py b/tools/check_skips.py index 99628018e3..c837fb1096 100644 --- a/tools/check_skips.py +++ b/tools/check_skips.py @@ -1,3 +1,4 @@ +import sys import argparse import re import xml.etree.ElementTree as ET @@ -40,12 +41,15 @@ def skips(path): yield name, "\n".join(output) -if __name__ == "__main__": +def main(argv=None): + for stream in (sys.stdout, sys.stderr): + if hasattr(stream, "reconfigure"): + stream.reconfigure(encoding="utf-8") parser = argparse.ArgumentParser(description="fail when a test is skipped that the job's device is not expected to skip") parser.add_argument("junit", type=Path, help="the file written by ctest --output-junit") parser.add_argument("--job", choices=("linux", "windows"), default="linux") parser.add_argument("--list", action="store_true", help="print the skipped tests and why the file says they skipped") - args = parser.parse_args() + args = parser.parse_args(argv) try: found = sorted(skips(args.junit)) except (OSError, ET.ParseError, ValueError) as error: @@ -54,7 +58,7 @@ if __name__ == "__main__": for name, output in found: reason = next((line.strip() for line in output.splitlines() if line.strip().lower().startswith("skipped")), "") print(f"{name}: {reason}") - raise SystemExit + return 0 if args.job == "windows": unexpected = [name for name, output in found if not name.startswith(WINDOWS_PREFIX) and not NO_DISPLAY.search(output) and not NO_VULKAN_DEVICE.search(output) and not high_refresh_limitation(output)] expected = f"any {WINDOWS_PREFIX}* test, which the runner has no Vulkan driver for, or a test that reports no display or Vulkan device or a display without a 120 Hz mode" @@ -65,5 +69,10 @@ if __name__ == "__main__": print(f"error: {name} skipped") if unexpected: print(f"{len(unexpected)} test(s) skipped for a reason that is not expected: the job lost that coverage and ctest still reports every test as passed") - raise SystemExit(1) + return 1 print(f"{len(found)} test(s) skipped, all expected ({expected})") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/check_test_registration.py b/tools/check_test_registration.py index fcce86cbd1..bbc80508d8 100644 --- a/tools/check_test_registration.py +++ b/tools/check_test_registration.py @@ -16,7 +16,7 @@ SCRIPT = re.compile(r"(?:[\w.-]+/)*test_\w+\.py") def built(build): program, generator = None, None - for line in (Path(build) / "CMakeCache.txt").read_text(encoding="utf-8").splitlines(): + for line in (Path(build) / "CMakeCache.txt").read_text(encoding="utf-8-sig").splitlines(): if line.startswith("CMAKE_MAKE_PROGRAM:"): program = line.partition("=")[2] elif line.startswith("CMAKE_GENERATOR:"): @@ -157,21 +157,24 @@ def unrun_scripts(root): return scripts, [name for name in scripts if not any(name == path or name.endswith("/" + path) for path in run)] -if __name__ == "__main__": +def main(argv=None): + for stream in (sys.stdout, sys.stderr): + if hasattr(stream, "reconfigure"): + stream.reconfigure(encoding="utf-8") parser = argparse.ArgumentParser(description="fail when a test executable is built that no ctest test runs") parser.add_argument("build", type=Path, nargs="?", default=Path("build"), help="configured build directory") parser.add_argument("--list", action="store_true", help="print every test executable and whether ctest runs it") parser.add_argument("--scripts", action="store_true", help="instead check that an add_test or a workflow runs every test_*.py of the source tree") - args = parser.parse_args() + args = parser.parse_args(argv) if args.scripts: scripts, unrun = unrun_scripts(Path(__file__).resolve().parents[1]) for name in unrun: print(f"error: {name} is a test script that no add_test or workflow runs") if unrun: print(f"{len(unrun)} test script(s) would never run: register each with add_test, or run it from a workflow") - raise SystemExit(1) + return 1 print(f"{len(scripts)} test script(s), each run by an add_test or a workflow") - raise SystemExit + return 0 try: every = sorted(built(args.build)) run = registered(args.build) @@ -180,11 +183,11 @@ if __name__ == "__main__": if args.list: for name in every: print(f"{'run' if name in run else 'NOT RUN'} {name}") - raise SystemExit + return 0 unrun = sorted(set(every) - run) if not every: print(f"no test executables found under {args.build}; is it configured with -DBUILD_TESTING=ON?") - raise SystemExit(1) + return 1 for name in sorted(set(unrun) & KNOWN_UNRUN): print(f"note: {name} is built and not run, as recorded") stale = sorted(KNOWN_UNRUN & run) @@ -196,5 +199,10 @@ if __name__ == "__main__": if fresh: print(f"{len(fresh)} test executable(s) would never run: ctest reports a full pass over what it knows, so a test nobody registered is invisible in CI") if stale or fresh: - raise SystemExit(1) + return 1 print(f"{len(every)} test executable(s), {len(unrun)} built and not run as recorded, none new") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/configure_seed.py b/tools/configure_seed.py index d1936ae21a..8e042f9722 100644 --- a/tools/configure_seed.py +++ b/tools/configure_seed.py @@ -25,7 +25,10 @@ def quote(value): return '"' + value.replace("\\", "\\\\").replace('"', '\\"').replace("$", "\\$") + '"' -def main(): +def main(argv=None): + for stream in (sys.stdout, sys.stderr): + if hasattr(stream, "reconfigure"): + stream.reconfigure(encoding="utf-8") parser = argparse.ArgumentParser(description="Write the configure check results of a CMake cache as an initial cache file") parser.add_argument("cache", help="CMakeCache.txt of a configured tree") parser.add_argument("seed", help="file to pass to cmake -C") @@ -33,7 +36,7 @@ def main(): help="seed matching check names only; repeat to include several patterns") parser.add_argument("--exclude", action="append", default=[], metavar="GLOB", help="omit matching check names after inclusion; patterns are case-sensitive") - args = parser.parse_args() + args = parser.parse_args(argv) cache, seed = Path(args.cache), Path(args.seed) if cache.resolve() == seed.resolve() or (cache.exists() and seed.exists() and cache.samefile(seed)): parser.error("seed output would overwrite the input cache") @@ -44,9 +47,12 @@ def main(): found = {name: entry for name, entry in results.items() if (not args.include or any(fnmatch.fnmatchcase(name, pattern) for pattern in args.include)) and not any(fnmatch.fnmatchcase(name, pattern) for pattern in args.exclude)} - seed.parent.mkdir(parents=True, exist_ok=True) - seed.write_text("".join(f"set({quote(name)} {quote(value)} CACHE INTERNAL {quote(doc)})\n" - for name, (value, doc) in sorted(found.items())), encoding="utf-8") + try: + seed.parent.mkdir(parents=True, exist_ok=True) + seed.write_text("".join(f"set({quote(name)} {quote(value)} CACHE INTERNAL {quote(doc)})\n" + for name, (value, doc) in sorted(found.items())), encoding="utf-8") + except OSError as error: + parser.error(f"{seed}: {error}") print(f"{len(found)} check results") return 0 diff --git a/tools/package_release.py b/tools/package_release.py index 88558908a6..effd4c1cac 100644 --- a/tools/package_release.py +++ b/tools/package_release.py @@ -4,6 +4,7 @@ import os import re import shutil import stat +import sys import tarfile import tempfile import zipfile @@ -15,12 +16,15 @@ WEB_FILES = ("index.html", "app.css", "app.mjs", "archive.mjs", "batch.mjs", "ou def windows_runtime(build): - for line in (build / "CMakeCache.txt").read_text(encoding="utf-8-sig").splitlines(): + cache = build / "CMakeCache.txt" + if not cache.is_file(): + raise RuntimeError(f"Missing CMakeCache.txt in build directory: {build}") + for line in cache.read_text(encoding="utf-8-sig").splitlines(): if line.startswith("CMAKE_CXX_COMPILER:"): compiler = line.partition("=")[2] if compiler and not compiler.endswith("-NOTFOUND"): return Path(compiler).parent - raise RuntimeError(f"CMAKE_CXX_COMPILER is not configured in {build / 'CMakeCache.txt'}") + raise RuntimeError(f"CMAKE_CXX_COMPILER is not configured in {cache}") def tracy_runtime(platform, build): @@ -52,6 +56,9 @@ def package(platform, build, output, version): licenses = [] if "libSceAjm.native.prx" in expected: licenses.append((ROOT / "3rdparty/vgmstream/COPYING", "libs/licenses/vgmstream.txt")) + if "libSceVorbisDec.prx" in expected: + licenses.append((ROOT / "3rdparty/ogg/COPYING", "libs/licenses/ogg.txt")) + licenses.append((ROOT / "3rdparty/vorbis/COPYING", "libs/licenses/vorbis.txt")) if platform == "windows": runtime = windows_runtime(build) files.extend(runtime / name for name in ("libgcc_s_seh-1.dll", "libstdc++-6.dll", "libwinpthread-1.dll")) @@ -92,6 +99,8 @@ def package(platform, build, output, version): def collect_docs(source, output): + if not source.is_dir(): + raise RuntimeError(f"Documentation source is not a directory: {source}") documents = sorted(document for document in source.rglob("*.md") if document.is_file()) if not documents: raise RuntimeError(f"No Markdown documents found in {source}") @@ -129,7 +138,11 @@ def package_web(build, output, version, revision): asset.replace(destination) -def main(): +def main(argv=None): + if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8", errors="backslashreplace") + if hasattr(sys.stderr, "reconfigure"): + sys.stderr.reconfigure(encoding="utf-8", errors="backslashreplace") parser = argparse.ArgumentParser() parser.add_argument("--platform", choices=("linux", "windows", "web")) parser.add_argument("--build", type=Path) @@ -137,23 +150,27 @@ def main(): parser.add_argument("--revision") parser.add_argument("--docs", type=Path) parser.add_argument("--output", required=True, type=Path) - args = parser.parse_args() - if args.docs is not None: - if args.platform is not None or args.build is not None or args.version is not None or args.revision is not None: - parser.error("--docs cannot be combined with --platform, --build, --version or --revision") - collect_docs(args.docs, args.output) - else: - if args.platform is None or args.build is None or args.version is None: - parser.error("--platform, --build and --version are required when --docs is not specified") - if args.platform == "web": - if args.revision is None: - parser.error("--revision is required for --platform web") - package_web(args.build, args.output, args.version, args.revision) + args = parser.parse_args(argv) + try: + if args.docs is not None: + if args.platform is not None or args.build is not None or args.version is not None or args.revision is not None: + parser.error("--docs cannot be combined with --platform, --build, --version or --revision") + collect_docs(args.docs, args.output) else: - if args.revision is not None: - parser.error("--revision requires --platform web") - package(args.platform, args.build, args.output, args.version) + if args.platform is None or args.build is None or args.version is None: + parser.error("--platform, --build and --version are required when --docs is not specified") + if args.platform == "web": + if args.revision is None: + parser.error("--revision is required for --platform web") + package_web(args.build, args.output, args.version, args.revision) + else: + if args.revision is not None: + parser.error("--revision requires --platform web") + package(args.platform, args.build, args.output, args.version) + except (RuntimeError, ValueError, OSError) as error: + parser.error(str(error)) + return 0 if __name__ == "__main__": - main() + sys.exit(main()) diff --git a/tools/pad_input.py b/tools/pad_input.py index ab93227072..69b1d64579 100644 --- a/tools/pad_input.py +++ b/tools/pad_input.py @@ -52,14 +52,28 @@ def next_sequence(path): return sequence +def button_mask(value): + names = value.split("+") + if any(name not in BUTTONS for name in names) or ("release" in names and len(names) > 1) or len(set(names)) != len(names): + raise argparse.ArgumentTypeError(f"invalid button {value!r}: use one of {', '.join(BUTTONS)}, or several joined by '+'") + mask = 0 + for name in names: + mask |= BUTTONS[name] + return mask + + def main(): parser = argparse.ArgumentParser(description="Queue a controller pulse for APS5_PAD_INPUT_FILE.") parser.add_argument("path", type=Path) - parser.add_argument("button", choices=BUTTONS) + parser.add_argument("button", help=f"a button name ({', '.join(BUTTONS)}), or several joined by '+' to press them together (for example l1+r1); release must be used alone") parser.add_argument("--hold-ms", type=int, default=300) parser.add_argument("--left-stick", type=int, nargs=2, metavar=("X", "Y")) parser.add_argument("--right-stick", type=int, nargs=2, metavar=("X", "Y")) args = parser.parse_args() + try: + mask = button_mask(args.button) + except argparse.ArgumentTypeError as error: + parser.error(str(error)) if not 1 <= args.hold_ms <= 5000: parser.error("--hold-ms must be between 1 and 5000") sticks = "" @@ -86,7 +100,7 @@ def main(): if os.geteuid() == 0: os.fchown(output.fileno(), metadata.st_uid, metadata.st_gid) os.fchmod(output.fileno(), stat.S_IMODE(metadata.st_mode)) - output.write(f"{sequence} {BUTTONS[args.button]} {args.hold_ms}{sticks}\n") + output.write(f"{sequence} {mask} {args.hold_ms}{sticks}\n") os.replace(temporary, args.path) finally: if temporary is not None: diff --git a/tools/performance_compare.py b/tools/performance_compare.py index 4642a983ff..6a6c599351 100644 --- a/tools/performance_compare.py +++ b/tools/performance_compare.py @@ -166,7 +166,7 @@ def markdown(report): def main(argv=None): for stream in (sys.stdout, sys.stderr): if hasattr(stream, "reconfigure"): - stream.reconfigure(encoding="utf-8") + stream.reconfigure(encoding="utf-8", errors="backslashreplace") parser = argparse.ArgumentParser(description="Compare two aps5-summary.jsonl captures. " "Reports numerical differences, not statistical significance or an automatic pass/fail verdict.") parser.add_argument("baseline", type=Path, help="Baseline summary JSONL file") diff --git a/tools/pr_labels.py b/tools/pr_labels.py index 4ca5c70a9b..f6f0039090 100644 --- a/tools/pr_labels.py +++ b/tools/pr_labels.py @@ -121,18 +121,25 @@ def process(repo, pr, dry_run, comment=True): return add, remove, post -def main(): +def main(argv=None): + if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8", errors="backslashreplace") + if hasattr(sys.stderr, "reconfigure"): + sys.stderr.reconfigure(encoding="utf-8", errors="backslashreplace") parser = argparse.ArgumentParser(description="Label pull requests from their title and template fields.") which = parser.add_mutually_exclusive_group(required=True) which.add_argument("--pr", type=int, help="label one pull request") which.add_argument("--all", action="store_true", help="label every open pull request") parser.add_argument("--dry-run", action="store_true", help="print the planned changes and write nothing") - args = parser.parse_args() - repo = repository() - if args.pr is not None: - prs = [pull(json.loads(gh("api", f"repos/{repo}/pulls/{args.pr}")))] - else: - prs = [pull(data) for data in decode_pages(gh("api", "--paginate", f"repos/{repo}/pulls?state=open&base=main&per_page=100"))] + args = parser.parse_args(argv) + try: + repo = repository() + if args.pr is not None: + prs = [pull(json.loads(gh("api", f"repos/{repo}/pulls/{args.pr}")))] + else: + prs = [pull(data) for data in decode_pages(gh("api", "--paginate", f"repos/{repo}/pulls?state=open&base=main&per_page=100"))] + except subprocess.CalledProcessError as error: + parser.error((error.stderr or str(error)).strip()) failed = [] for pr in prs: try: @@ -148,7 +155,8 @@ def main(): time.sleep(WRITE_PAUSE) if failed: sys.exit(f"{len(failed)} pull requests not labeled: {', '.join(f'#{number}' for number in failed)}") + return 0 if __name__ == "__main__": - main() + sys.exit(main()) diff --git a/tools/pr_overlap.py b/tools/pr_overlap.py index ca1bc134e5..29008a4d9d 100644 --- a/tools/pr_overlap.py +++ b/tools/pr_overlap.py @@ -3,11 +3,14 @@ import json import os import re import subprocess +import sys from concurrent.futures import ThreadPoolExecutor from itertools import combinations from pathlib import Path from urllib.parse import quote, urlencode +from nid_names import compute_nid, source_aliases + PARENT = 'if .parent then "\\(.parent.owner.login)/\\(.parent.name)" else .nameWithOwner end' EXPORT = re.compile(r"^\+.*\bAPS5_VABI\s+(\w+)\s*\(") NID_DISAMBIG = re.compile(r"_nid_disambig[0-9]+$") @@ -41,10 +44,15 @@ def upstream(): def depends_on(body): body = re.sub(r"<!--.*?-->", "", body or "", flags=re.S) + repo = re.escape(globals().get("REPO", "boykopovar/AnyPS5")) + repo_pattern = rf"(?:{repo}|boykopovar/AnyPS5)" + body = re.sub(rf"\[([^\]]*)\]\(https?://github\.com/{repo_pattern}/pull/(\d+)(?:[#/?][^)\s]*)?\)", r"#\2", body, flags=re.IGNORECASE) + body = re.sub(rf"\[([^\]]*)\]\(https?://github\.com/(?!{repo_pattern}/)[^/\s]+/[^/\s]+/pull/\d+[^)\s]*\)", "", body, flags=re.IGNORECASE) + body = re.sub(r"\[([^\]]+)\]\([^)]+\)", r"\1", body) + body = re.sub(rf"https?://github\.com/{repo_pattern}/pull/(\d+)\b(?:/[^\s]*)?", r"#\1", body, flags=re.IGNORECASE) match = next((match for match in DEPENDS.finditer(body) if match.group(1).strip()), None) or PLAIN_DEPENDS.search(body) return {int(n) for n in re.findall(r"#(\d+)", match.group(1))} if match else set() - def open_prs(base): query = urlencode({"state": "open", "base": base, "per_page": 100}) out = gh("api", "--paginate", f"repos/{REPO}/pulls?{query}") @@ -116,6 +124,21 @@ def exports(diff): return found +def aliases(diff): + added = "\n".join(line[1:] for line in diff.splitlines() if line.startswith("+") and not line.startswith("+++")) + return {nid for nid, _ in source_aliases(added)} + + +def export_ids(pr): + return {compute_nid(name) for name in pr["exports"]} | pr.get("aliases", set()) + + +def same_exports(pa, pb, shared): + common = (export_ids(pa) & export_ids(pb)).difference(*(export_ids(pr) for pr in shared)) + named = {name for name in pa["exports"] | pb["exports"] if compute_nid(name) in common} + return named | common.difference(compute_nid(name) for name in named) + + def scan(pr): tree, conflicted = merge("refs/pr/base", pr["ref"]) pr["tree"] = None if conflicted else commit(tree) @@ -128,6 +151,7 @@ def scan(pr): pr["added"].add(path) diff = git("diff", "-U0", "--no-color", old, new, "--", "core/libs/prx/*.cpp") pr["exports"] = exports(diff) + pr["aliases"] = aliases(diff) def scan_all(prs, jobs): @@ -182,7 +206,7 @@ def overlaps(prs, a, b): found = {"added": (pa["added"] & pb["added"]).difference(*(pr["added"] for pr in shared))} if pa["author"] != pb["author"]: found["conflicts"] = conflicts(pa, pb) - found["exports"] = (pa["exports"] & pb["exports"]).difference(*(pr["exports"] for pr in shared)) + found["exports"] = same_exports(pa, pb, shared) if not (pa["tree"] and pb["tree"]): uncompared = pa.get("files", set()) & pb.get("files", set()) if uncompared: @@ -247,19 +271,27 @@ def describe_failure(error): return f"pr_overlap: {command} failed with exit code {error.returncode}: {message}" -if __name__ == "__main__": +def main(argv=None): + if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8", errors="backslashreplace") + if hasattr(sys.stderr, "reconfigure"): + sys.stderr.reconfigure(encoding="utf-8", errors="backslashreplace") parser = argparse.ArgumentParser(description="report open pull requests that overlap each other") parser.add_argument("--base", default="main") parser.add_argument("--out", type=Path, help="write <number>.md for every open pull request (empty when clean)") parser.add_argument("--branch", help="check a local branch against the open pull requests instead") parser.add_argument("--repo", help="owner/name; defaults to the upstream of the current checkout") parser.add_argument("--jobs", type=int, default=4, help="number of pull request comparisons to run at once") - args = parser.parse_args() + args = parser.parse_args(argv) if bool(args.out) == bool(args.branch): parser.error("pass exactly one of --out or --branch") if args.jobs < 1: parser.error("--jobs must be at least 1") + global REPO + if args.out: + args.out = args.out.resolve() try: + os.chdir(git("rev-parse", "--show-toplevel").strip()) REPO = args.repo or upstream() prs = open_prs(args.base) fetch(args.base, prs) @@ -278,5 +310,10 @@ if __name__ == "__main__": raise SystemExit(describe_failure(error)) if args.branch: print(report(rows[0], blob) or "No overlap with open pull requests.", end="") - raise SystemExit + return 0 write_reports(args.out, rows, blob) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/progress_history.py b/tools/progress_history.py index ca8460652a..b223ab7c58 100644 --- a/tools/progress_history.py +++ b/tools/progress_history.py @@ -26,7 +26,7 @@ def git(*args): def daily_commits(ref): seen = {} - for line in git("log", "--first-parent", "--format=%H %ct", ref).decode().splitlines(): + for line in git("log", "--first-parent", "--format=%H %ct", ref).decode("utf-8").splitlines(): sha, timestamp = line.split() seen.setdefault(datetime.fromtimestamp(int(timestamp), timezone.utc).date().isoformat(), sha) return sorted(seen.items()) @@ -221,12 +221,12 @@ def page(points): ]) + "\n" -if __name__ == "__main__": +def main(argv=None): parser = argparse.ArgumentParser() parser.add_argument("output", type=Path) parser.add_argument("--ref", default="HEAD", help="branch or commit whose first-parent history is measured") parser.add_argument("--cache", type=Path, help="reuse per-commit counts while the scanner, ISA list and Python version match") - args = parser.parse_args() + args = parser.parse_args(argv) try: if args.cache: outputs = [args.output / name for name in ("history.json", "history.svg", "history.html")] @@ -239,7 +239,11 @@ if __name__ == "__main__": if not points: parser.error("no commit with a measurable tree") args.output.mkdir(parents=True, exist_ok=True) - (args.output / "history.json").write_text(json.dumps(points, indent=2)) - (args.output / "history.svg").write_text(chart(points) + "\n") - (args.output / "history.html").write_text(page(points)) + (args.output / "history.json").write_text(json.dumps(points, indent=2), encoding="utf-8") + (args.output / "history.svg").write_text(chart(points) + "\n", encoding="utf-8") + (args.output / "history.html").write_text(page(points), encoding="utf-8") print(f"{len(points)} days, {points[0]['date']} .. {points[-1]['date']}") + + +if __name__ == "__main__": + main() diff --git a/tools/select_tests.py b/tools/select_tests.py index efb972d0a1..a69c482e43 100644 --- a/tools/select_tests.py +++ b/tools/select_tests.py @@ -16,7 +16,7 @@ BATCH = 20000 class Build: def __init__(self, path): cache = {} - for line in (Path(path) / "CMakeCache.txt").read_text(encoding="utf-8").splitlines(): + for line in (Path(path) / "CMakeCache.txt").read_text(encoding="utf-8-sig").splitlines(): name, _, value = line.partition("=") cache[name.partition(":")[0]] = value self.path = os.path.normpath(cache["CMAKE_CACHEFILE_DIR"]) @@ -225,7 +225,10 @@ def escape(name): return name.replace("$", "$$").replace(":", "$:").replace(" ", "$ ") -def main(): +def main(argv=None): + for stream in (sys.stdout, sys.stderr): + if hasattr(stream, "reconfigure"): + stream.reconfigure(encoding="utf-8") parser = argparse.ArgumentParser(description="Select the tests whose build inputs a change touches") sub = parser.add_subparsers(dest="command", required=True) d = sub.add_parser("dump", help="write the test graph of a built tree") @@ -241,15 +244,15 @@ def main(): 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") - args = parser.parse_args() + args = parser.parse_args(argv) if args.command == "ctest": selected = Path(args.build, "select.tests") - extra = ["-I", selected.read_text(encoding="utf-8").strip()] if selected.exists() else [] + extra = ["-I", selected.read_text(encoding="utf-8-sig").strip()] if selected.exists() else [] return subprocess.run(["ctest", "--test-dir", args.build, *args.arguments, *extra]).returncode if args.command == "explain": try: - graph = json.loads(args.graph.read_text(encoding="utf-8")) - changed = [line.strip() for line in args.changed.read_text(encoding="utf-8").splitlines() if line.strip()] + graph = json.loads(args.graph.read_text(encoding="utf-8-sig")) + changed = [line.strip() for line in args.changed.read_text(encoding="utf-8-sig").splitlines() if line.strip()] reasons = {} picked, full = select(graph, changed, reasons) except (OSError, ValueError) as error: @@ -270,7 +273,7 @@ def main(): if not os.path.exists(args.graph): print("full run: no test graph") return 0 - graph = json.loads(Path(args.graph).read_text(encoding="utf-8")) + graph = json.loads(Path(args.graph).read_text(encoding="utf-8-sig")) changed = [line.strip() for line in Path(args.changed).read_text(encoding="utf-8-sig").splitlines() if line.strip()] picked, full = select(graph, changed) if picked is None: diff --git a/tools/stubgen.py b/tools/stubgen.py index a701cb5472..7426cd8aa2 100644 --- a/tools/stubgen.py +++ b/tools/stubgen.py @@ -211,6 +211,9 @@ def write_outputs(outputs): def main(argv=None): + for stream in (sys.stdout, sys.stderr): + if hasattr(stream, "reconfigure"): + stream.reconfigure(encoding="utf-8", errors="backslashreplace", newline="\n") ap = argparse.ArgumentParser(description="Generate AnyPS5 Export.cpp skeletons and TechnicalDebt lines") ap.add_argument("--module", required=True, help="target module name, e.g. libSceAmpr, used for UnknownNN prefix and default TechnicalDebt path core/libs/prx/MODULE/Export.cpp") ap.add_argument("--nids", required=True, help="path to missing-NIDs file, one 11-char NID per line") @@ -221,9 +224,6 @@ def main(argv=None): ap.add_argument("--td-out", required=False, default=None, help="output file for TechnicalDebt lines, which go in docs/dev/debt/MODULE.md, default stderr when code uses stdout, otherwise stdout") ap.add_argument("--td-only", action="store_true", help="print only TechnicalDebt lines, no code") args = ap.parse_args(argv) - for stream in (sys.stdout, sys.stderr): - if hasattr(stream, "reconfigure"): - stream.reconfigure(encoding="utf-8", newline="\n") try: if not args.td_only and args.out and args.td_out and same_file(args.out, args.td_out): ap.error("--out and --td-out must name different files") diff --git a/tools/test_web.py b/tools/test_web.py index 245e6b4eac..9da700335f 100644 --- a/tools/test_web.py +++ b/tools/test_web.py @@ -5,6 +5,7 @@ import re import subprocess import sys import tempfile +import urllib.error import urllib.request ROOT = Path(__file__).resolve().parent.parent @@ -13,25 +14,39 @@ from test_guest_intel_trampolines import main_fixture from test_guest_module_directories import module_with_symbol -def main(): +def main(argv=None): + if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8", errors="backslashreplace") + if hasattr(sys.stderr, "reconfigure"): + sys.stderr.reconfigure(encoding="utf-8", errors="backslashreplace") parser = argparse.ArgumentParser() parser.add_argument("--build", type=Path, required=True) - args = parser.parse_args() - subprocess.run(["node", "--test", str(ROOT / "demo/web/tests/output-directory.test.mjs")], check=True) - with tempfile.TemporaryDirectory(prefix="anyps5-web-") as directory: - fixture = Path(directory) - (fixture / "main.elf").write_bytes(main_fixture()) - (fixture / "module.elf").write_bytes(module_with_symbol(True)) - source = (args.build / "web/archive.mjs").read_text(encoding="utf-8") - url = re.search(r'from "(https://cdn.jsdelivr.net/[^"\n]+)"', source).group(1) - library = fixture / "zip.mjs" - with urllib.request.urlopen(url, timeout=30) as response: - library.write_bytes(response.read()) - environment = dict(os.environ, ANYPS5_TEST_ZIP_URL=url, ANYPS5_TEST_ZIP_PATH=library.as_uri()) - subprocess.run(["node", str(ROOT / "demo/web/tests/archive-root.test.mjs"), str(args.build.resolve() / "web")], env=environment, check=True) - subprocess.run(["node", str(ROOT / "demo/web/tests/archive-windows.test.mjs"), str(args.build.resolve() / "web")], env=environment, check=True) - subprocess.run(["node", str(ROOT / "demo/web/tests/archive.test.mjs"), str(args.build.resolve() / "web"), str(fixture)], env=environment, check=True) + args = parser.parse_args(argv) + archive = args.build / "web/archive.mjs" + if not archive.is_file(): + parser.error(f"archive module does not exist: {archive}") + try: + subprocess.run(["node", "--test", str(ROOT / "demo/web/tests/output-directory.test.mjs")], check=True) + with tempfile.TemporaryDirectory(prefix="anyps5-web-") as directory: + fixture = Path(directory) + (fixture / "main.elf").write_bytes(main_fixture()) + (fixture / "module.elf").write_bytes(module_with_symbol(True)) + source = archive.read_text(encoding="utf-8-sig") + match = re.search(r'from "(https://cdn.jsdelivr.net/[^"\n]+)"', source) + if not match: + parser.error(f"zip library URL not found in {archive}") + url = match.group(1) + library = fixture / "zip.mjs" + with urllib.request.urlopen(url, timeout=30) as response: + library.write_bytes(response.read()) + environment = dict(os.environ, ANYPS5_TEST_ZIP_URL=url, ANYPS5_TEST_ZIP_PATH=library.as_uri()) + subprocess.run(["node", str(ROOT / "demo/web/tests/archive-root.test.mjs"), str(args.build.resolve() / "web")], env=environment, check=True) + subprocess.run(["node", str(ROOT / "demo/web/tests/archive-windows.test.mjs"), str(args.build.resolve() / "web")], env=environment, check=True) + subprocess.run(["node", str(ROOT / "demo/web/tests/archive.test.mjs"), str(args.build.resolve() / "web"), str(fixture)], env=environment, check=True) + except (subprocess.CalledProcessError, urllib.error.URLError, OSError) as error: + parser.error(str(error)) + return 0 if __name__ == "__main__": - main() + sys.exit(main()) diff --git a/tools/tests/test_build_web.py b/tools/tests/test_build_web.py index 3a56e49429..91e093b70d 100644 --- a/tools/tests/test_build_web.py +++ b/tools/tests/test_build_web.py @@ -1,8 +1,10 @@ +import os import contextlib import hashlib import io import json from pathlib import Path +import subprocess import sys import tempfile import unittest @@ -125,5 +127,28 @@ class LibraryPublicationTests(unittest.TestCase): self.assertFalse(any(file.suffix == ".download" for file in self.libraries.iterdir())) +class BuildWebCliTests(unittest.TestCase): + def test_missing_relinker_wasm_reports_error_without_traceback(self): + with tempfile.TemporaryDirectory() as directory: + build = Path(directory) + result = subprocess.run([sys.executable, str(Path(build_web.__file__)), "--build", str(build)], + capture_output=True, text=True, + env={**os.environ, "PYTHONPATH": str(Path(__file__).resolve().parents[1])}) + self.assertEqual(result.returncode, 2) + self.assertIn("Build relinker_wasm first: missing", result.stderr) + self.assertNotIn("Traceback", result.stderr) + + def test_main_with_explicit_argv(self): + with tempfile.TemporaryDirectory() as directory: + build = Path(directory) + native = build / "web" / "native" + native.mkdir(parents=True) + (native / "relinker_wasm.mjs").touch() + (native / "relinker_wasm.wasm").touch() + with contextlib.redirect_stdout(io.StringIO()): + code = build_web.main(["--build", str(build)]) + self.assertEqual(code, 0) + + if __name__ == "__main__": unittest.main() diff --git a/tools/tests/test_check_compile.py b/tools/tests/test_check_compile.py index 3dcc4b1d43..ffd20390ab 100644 --- a/tools/tests/test_check_compile.py +++ b/tools/tests/test_check_compile.py @@ -216,6 +216,15 @@ class CheckCompileTests(unittest.TestCase): self.assertEqual(result.returncode, 0) self.assertEqual(json.loads(seen.read_text(encoding="utf-8")), ["-fsyntax-only", str(source)]) + def test_compile_commands_accepts_utf8_bom(self): + source = self.source("unit.cpp") + script = self.compiler("import sys\nsys.exit(0)\n") + entry = self.entry("unit.cpp", f'"{sys.executable}" "{script}" "{source}"') + (self.build / "compile_commands.json").write_text("\ufeff" + json.dumps([entry]), encoding="utf-8") + result = self.tool("unit.cpp") + self.assertEqual(result.returncode, 0) + self.assertIn("1 translation units compile", result.stdout) + def test_every_configuration_is_checked_and_each_source_is_counted_once(self): source = self.source("unit.cpp") script = self.compiler( diff --git a/tools/tests/test_check_conventions_comments.py b/tools/tests/test_check_conventions_comments.py index 95aa949321..6bc843e5d2 100644 --- a/tools/tests/test_check_conventions_comments.py +++ b/tools/tests/test_check_conventions_comments.py @@ -399,7 +399,7 @@ class SilentStubSectionTests(unittest.TestCase): class TestFixtureExportTests(unittest.TestCase): - def check(self, path, text=None): + def check(self, path, text=None, base=None): files = path if text is None else {path: text} checker = Path(__file__).resolve().parents[1] / "check_conventions.py" with tempfile.TemporaryDirectory() as directory: @@ -420,6 +420,8 @@ class TestFixtureExportTests(unittest.TestCase): git("init", "-q") write("core/libs/prx/libkernel/src/Time.cpp", "std::uint64_t APS5_VABI sceKernelGetProcessTime() { return now(); }\n") write("core/libs/prx/libSceNgs2/tests/Render.cpp", "static int APS5_VABI Process(int value) { return value; }\n") + for relative, content in (base or {}).items(): + write(relative, content) git("add", ".") git("commit", "-qm", "feat: add libraries") for relative, content in files.items(): @@ -451,6 +453,63 @@ class TestFixtureExportTests(unittest.TestCase): self.assertIn("core/libs/prx/libSceAudioOut/src/Clock.cpp:1: error: [duplicate-export]", result.stdout) self.assertIn("sceKernelGetProcessTime is also in libkernel", result.stdout) + def test_large_export_set_uses_a_file_backed_pattern(self): + names = [f"ConventionExport{index:08d}LongName" for index in range(1100)] + self.assertGreater(len("|".join(names)), 32767) + source = "\n".join(f"int APS5_VABI {name}(void) {{ return 0; }}" for name in names) + source += "\nstd::uint64_t APS5_VABI sceKernelGetProcessTime() { return now(); }\n" + result = self.check("core/libs/prx/libSceAudioOut/src/Exports.cpp", source) + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("core/libs/prx/libSceAudioOut/src/Exports.cpp:1101: error: [duplicate-export]", result.stdout) + self.assertIn("sceKernelGetProcessTime is also in libkernel", result.stdout) + + def test_export_shadowing_an_assembly_export_is_a_duplicate(self): + result = self.check("core/libs/prx/libSceLibcInternal/Export.cpp", + "int APS5_VABI setjmp_nid_postfix(void) {\n NotImplemented_nid_no_patch(__func__);\n return 0;\n}\n", + base={"core/libs/prx/libc/src/JumpBuffer.cpp": 'asm(".text\\n"\n APS5_ASM_FUNCTION("setjmp_nid_postfix")\n " ret\\n");\n'}) + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("core/libs/prx/libSceLibcInternal/Export.cpp:1: error: [duplicate-export]", result.stdout) + self.assertIn("setjmp_nid_postfix is also in libc", result.stdout) + + def test_export_shadowing_a_spaced_or_split_assembly_export_is_a_duplicate(self): + for macro, call in (("APS5_ASM_FUNCTION", ' ("setjmp_nid_postfix")'), + ("APS5_ASM_FUNCTION", '(\n "setjmp_nid_postfix")'), + ("APS5_SIGNAL_ASM_FUNCTION", ' (\n "setjmp_nid_postfix")')): + with self.subTest(macro=macro, call=call): + result = self.check("core/libs/prx/libSceLibcInternal/Export.cpp", + "int APS5_VABI setjmp_nid_postfix(void) {\n NotImplemented_nid_no_patch(__func__);\n return 0;\n}\n", + base={"core/libs/prx/libc/src/JumpBuffer.cpp": 'asm(".text\\n"\n ' + macro + call + '\n " ret\\n");\n'}) + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("core/libs/prx/libSceLibcInternal/Export.cpp:1: error: [duplicate-export]", result.stdout) + self.assertIn("setjmp_nid_postfix is also in libc", result.stdout) + + def test_assembly_export_in_another_library_is_a_duplicate(self): + result = self.check("core/libs/prx/libSceAudioOut/src/Clock.cpp", + 'asm(".text\\n"\n APS5_SIGNAL_ASM_FUNCTION("sceKernelGetProcessTime")\n " ret\\n");\n') + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("core/libs/prx/libSceAudioOut/src/Clock.cpp:2: error: [duplicate-export]", result.stdout) + self.assertIn("sceKernelGetProcessTime is also in libkernel", result.stdout) + + def test_assembly_export_after_a_raw_string_is_a_duplicate(self): + result = self.check("core/libs/prx/libSceAudioOut/src/Jump.cpp", + 'asm(R"(\n .text\n)" APS5_ASM_FUNCTION("setjmp_nid_postfix") R"(\n ret\n)");\n', + base={"core/libs/prx/libc/src/JumpBuffer.cpp": 'asm(".text\\n"\n APS5_ASM_FUNCTION("setjmp_nid_postfix")\n " ret\\n");\n'}) + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("core/libs/prx/libSceAudioOut/src/Jump.cpp:3: error: [duplicate-export]", result.stdout) + self.assertIn("setjmp_nid_postfix is also in libc", result.stdout) + + def test_assembly_export_in_a_block_comment_is_not_an_export(self): + result = self.check("core/libs/prx/libSceLibcInternal/Export.cpp", + "int APS5_VABI setjmp_nid_postfix(void) {\n NotImplemented_nid_no_patch(__func__);\n return 0;\n}\n", + base={"core/libs/prx/libc/src/JumpBuffer.cpp": '/*\n APS5_ASM_FUNCTION("setjmp_nid_postfix")\n*/\n'}) + self.assertNotIn("duplicate-export", result.stdout, result.stdout + result.stderr) + + def test_assembly_name_that_is_not_an_identifier_keeps_the_check(self): + result = self.check("core/libs/prx/libSceAudioOut/src/Clock.cpp", + 'asm(".text\\n"\n APS5_ASM_FUNCTION("x(y")\n " ret\\n");\nstd::uint64_t APS5_VABI sceKernelGetProcessTime() {\n return 1;\n}\n') + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("sceKernelGetProcessTime is also in libkernel", result.stdout) + def test_comment_in_a_shader_is_reported(self): shaders = {} for path in ("core/libs/prx/libSceAgcDriver/Graphics/shaders/Fill.comp", "core/libs/prx/libSceAgcDriver/tests/shaders/Fill.frag", "core/libs/prx/libSceAgcDriver/Graphics/shaders/Fill.glsl"): diff --git a/tools/tests/test_check_main_sync.py b/tools/tests/test_check_main_sync.py index a4590a60e5..22b06de59d 100644 --- a/tools/tests/test_check_main_sync.py +++ b/tools/tests/test_check_main_sync.py @@ -1,4 +1,6 @@ from pathlib import Path +import contextlib +import io import os import subprocess import sys @@ -65,6 +67,14 @@ class CheckMainSyncTests(unittest.TestCase): self.assertEqual((data["behind"], data["ahead"], data["conflicts"], data["shared"]), (0, 1, [], [])) self.assertEqual(run_tool(self.repo.path, "--head", "feature", "--strict").returncode, 0) + def test_main_with_explicit_argv(self): + self.branch("feature") + self.repo.write("other.txt", "y\n") + self.repo.commit("feature change") + with contextlib.redirect_stdout(io.StringIO()): + code = check_main_sync.main(["--repo", str(self.repo.path), "--base", "main", "--head", "feature"]) + self.assertEqual(code, 0) + def test_behind_main_is_reported_and_only_fails_when_strict(self): self.branch("feature") self.repo.write("other.txt", "y\n") diff --git a/tools/tests/test_check_skips.py b/tools/tests/test_check_skips.py index f4de797f05..ea82a476d1 100644 --- a/tools/tests/test_check_skips.py +++ b/tools/tests/test_check_skips.py @@ -1,4 +1,6 @@ from pathlib import Path +import contextlib +import io import subprocess import sys import tempfile @@ -192,6 +194,15 @@ class CheckSkipsTests(unittest.TestCase): self.assertIn("indented: skipped, host imports are unavailable", result.stdout) self.assertIn("capitalized: Skipped, no display", result.stdout) + def test_main_with_explicit_argv(self): + xml = '<testsuite><testcase name="normal"><skipped message="the device lacks geometryShader"/></testcase></testsuite>' + with tempfile.TemporaryDirectory() as directory: + junit = Path(directory) / "junit.xml" + junit.write_text(xml, encoding="utf-8") + with contextlib.redirect_stdout(io.StringIO()): + code = check_skips.main([str(junit), "--job", "linux"]) + self.assertEqual(code, 0) + diff --git a/tools/tests/test_configure_seed.py b/tools/tests/test_configure_seed.py index 2cff72cd94..785b724136 100644 --- a/tools/tests/test_configure_seed.py +++ b/tools/tests/test_configure_seed.py @@ -62,6 +62,14 @@ class ConfigureSeedPathTests(unittest.TestCase): self.assertEqual(result.returncode, 0, result.stderr) self.assertIn("HAVE_FEATURE", seed.read_text(encoding="utf-8")) + def test_unwritable_seed_is_reported_without_a_traceback(self): + seed_dir = self.root / "seed_dir" + seed_dir.mkdir() + result = self.run_seed(seed_dir) + self.assertEqual(result.returncode, 2, result.stderr) + self.assertIn(str(seed_dir), result.stderr) + self.assertNotIn("Traceback", result.stderr) + def test_same_path_preserves_cache(self): self.assert_rejected(self.cache) diff --git a/tools/tests/test_package_release.py b/tools/tests/test_package_release.py index 4abcb1f37b..cc485db136 100644 --- a/tools/tests/test_package_release.py +++ b/tools/tests/test_package_release.py @@ -61,6 +61,21 @@ class ReleasePackageTests(unittest.TestCase): with tarfile.open(self.output / "prx-linux-v1.tar.gz") as archive: self.assertEqual(archive.extractfile("libs/licenses/vgmstream.txt").read(), b"decoder license") + def test_vorbis_decoder_licenses_in_archives(self): + (self.source / "core/libs/prx/libSceVorbisDec").mkdir() + (self.build / "core/libs/libs/libSceVorbisDec.prx").write_bytes(b"vorbis") + for name in ("ogg", "vorbis"): + license_file = self.source / f"3rdparty/{name}/COPYING" + license_file.parent.mkdir(parents=True) + license_file.write_bytes(f"{name} license".encode()) + package_release.package("linux", self.build, self.output, "v1") + with zipfile.ZipFile(self.output / "prx-linux-v1.zip") as archive: + self.assertEqual(archive.read("libs/licenses/ogg.txt"), b"ogg license") + self.assertEqual(archive.read("libs/licenses/vorbis.txt"), b"vorbis license") + with tarfile.open(self.output / "prx-linux-v1.tar.gz") as archive: + self.assertEqual(archive.extractfile("libs/licenses/ogg.txt").read(), b"ogg license") + self.assertEqual(archive.extractfile("libs/licenses/vorbis.txt").read(), b"vorbis license") + def test_windows_runtime_follows_the_configured_compiler_directory(self): runtime = self.root / "custom toolchain" / "bin" runtime.mkdir(parents=True) @@ -350,9 +365,19 @@ class ReleasePackageTests(unittest.TestCase): package_release.package("linux", self.build, self.output, "../v1.0") self.assertFalse(self.output.exists()) + def test_windows_runtime_missing_cmake_cache_raises_runtime_error(self): + with tempfile.TemporaryDirectory() as tmp: + with self.assertRaisesRegex(RuntimeError, "Missing CMakeCache.txt"): + package_release.windows_runtime(Path(tmp)) + class DocumentationAssetsTests(unittest.TestCase): + def test_source_not_directory_raises_runtime_error(self): + with tempfile.TemporaryDirectory() as tmp: + with self.assertRaisesRegex(RuntimeError, "not a directory"): + package_release.collect_docs(Path(tmp) / "nonexistent", Path(tmp) / "assets") + def test_unique_nested_documents_are_flattened(self): with tempfile.TemporaryDirectory() as tmp: source = Path(tmp) / "source" @@ -601,5 +626,48 @@ class ReleaseNotesEdgeCaseTests(unittest.TestCase): self.assertIn("Full Changelog", result) +class ReleaseMainTests(unittest.TestCase): + def test_main_with_docs_arguments(self): + with tempfile.TemporaryDirectory() as tmp: + docs = Path(tmp) / "docs" + docs.mkdir() + (docs / "guide.md").write_text("# Guide", encoding="utf-8") + output = Path(tmp) / "out" + result = package_release.main(["--docs", str(docs), "--output", str(output)]) + self.assertEqual(result, 0) + self.assertEqual((output / "guide.md").read_text(encoding="utf-8"), "# Guide") + + def test_main_reports_packaging_error_via_parser_error(self): + with tempfile.TemporaryDirectory() as tmp: + output = Path(tmp) / "out" + stderr = io.StringIO() + with contextlib.redirect_stderr(stderr), self.assertRaises(SystemExit) as error: + package_release.main(["--docs", str(Path(tmp) / "nonexistent"), "--output", str(output)]) + self.assertEqual(error.exception.code, 2) + self.assertIn("not a directory", stderr.getvalue()) + + def test_main_reconfigures_streams(self): + class Stream: + def __init__(self): + self.encoding = None + self.errors = None + + def reconfigure(self, encoding=None, errors=None): + self.encoding = encoding + self.errors = errors + + out, err = Stream(), Stream() + with patch.object(package_release.sys, "stdout", out), patch.object(package_release.sys, "stderr", err): + with tempfile.TemporaryDirectory() as tmp: + docs = Path(tmp) / "docs" + docs.mkdir() + (docs / "doc.md").write_text("text", encoding="utf-8") + package_release.main(["--docs", str(docs), "--output", str(Path(tmp) / "out")]) + self.assertEqual(out.encoding, "utf-8") + self.assertEqual(out.errors, "backslashreplace") + self.assertEqual(err.encoding, "utf-8") + self.assertEqual(err.errors, "backslashreplace") + + if __name__ == "__main__": unittest.main() diff --git a/tools/tests/test_pad_input.py b/tools/tests/test_pad_input.py index 76e820c325..bb557866d4 100644 --- a/tools/tests/test_pad_input.py +++ b/tools/tests/test_pad_input.py @@ -85,6 +85,22 @@ class PadInputTests(unittest.TestCase): self.assertEqual(self.queue(1000, "touch-left"), [1000, 0x200000, 300]) self.assertEqual(self.queue(1000, "touch-right"), [1001, 0x400000, 300]) + def test_joined_buttons_are_pressed_together(self): + self.assertEqual(self.queue(1000, "l1+r1"), [1000, 0x400 | 0x800, 300]) + self.assertEqual(self.queue(1000, "cross+square+up", "--hold-ms", "40"), [1001, 0x4000 | 0x8000 | 0x10, 40]) + + def test_invalid_joined_buttons_report_error_and_preserve_command(self): + original = "2000 16384 300\n" + for button in ["l1+nothing", "l1+release", "l1+l1", "l1+", "+r1"]: + with self.subTest(button=button): + self.path.write_text(original, encoding="utf-8") + errors = io.StringIO() + with contextlib.redirect_stderr(errors), self.assertRaises(SystemExit) as error: + self.queue(1000, button) + self.assertEqual(error.exception.code, 2) + self.assertIn("invalid button", errors.getvalue()) + self.assertEqual(self.path.read_text(encoding="utf-8"), original) + def test_valid_stick_command_and_unsigned_tokens_preserve_sequence(self): self.path.write_text("+2000 +16384 +300 +0 +255 +128 +128\n", encoding="utf-8") self.assertEqual(self.queue(1000, "release"), [2001, 0, 300]) diff --git a/tools/tests/test_pad_timeline.py b/tools/tests/test_pad_timeline.py index cc40a5b7d8..b7a91f3d96 100644 --- a/tools/tests/test_pad_timeline.py +++ b/tools/tests/test_pad_timeline.py @@ -74,6 +74,26 @@ class TimelineTests(unittest.TestCase): with self.assertRaises(ValueError): pad_timeline.compile_timeline(self.document, 1, repeat, gap) + def test_repeated_event_count_at_capacity(self): + rows = pad_timeline.compile_timeline(self.document, 42, 2048, 0).splitlines() + self.assertEqual(rows[0], "APS5_PAD_TIMELINE 1 42 4096") + self.assertEqual(len(rows) - 1, 4096) + self.assertEqual(rows[1:3], ["200 100 18432", "500 200 64"]) + self.assertEqual(rows[2047:2051], [ + "716300 100 18432", "716600 200 64", "717000 100 18432", "717300 200 64", + ]) + self.assertEqual(rows[-2:], ["1433100 100 18432", "1433400 200 64"]) + with self.assertRaisesRegex(ValueError, "1 to 4096 events after repetition"): + pad_timeline.compile_timeline(self.document, 42, 2049, 0) + + def test_repeated_duration_includes_inter_iteration_gaps_only(self): + document = {"version": 1, "events": [{"at_ms": 0, "hold_ms": 5000, "buttons": ["cross"]}]} + self.assertEqual( + pad_timeline.compile_timeline(document, 42, 2, 3590000), + "APS5_PAD_TIMELINE 1 42 2\n0 5000 16384\n3595000 5000 16384\n") + with self.assertRaisesRegex(ValueError, "repeated timeline exceeds one hour"): + pad_timeline.compile_timeline(document, 42, 2, 3590001) + def test_sequence_bounds(self): for sequence in [0, -1, True, 2**64]: with self.assertRaises(ValueError): diff --git a/tools/tests/test_performance_compare.py b/tools/tests/test_performance_compare.py index 1cabac3fe2..4016f14439 100644 --- a/tools/tests/test_performance_compare.py +++ b/tools/tests/test_performance_compare.py @@ -5,6 +5,7 @@ import subprocess import sys import tempfile import unittest +from unittest.mock import patch from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parents[1])) @@ -317,5 +318,33 @@ class PerformanceCompareTests(unittest.TestCase): + def test_main_reconfigures_streams(self): + class Stream: + def __init__(self): + self.encoding = None + self.errors = None + + def reconfigure(self, encoding=None, errors=None): + self.encoding = encoding + self.errors = errors + + def write(self, text): + pass + + def flush(self): + pass + + out, err = Stream(), Stream() + with patch.object(performance_compare.sys, "stdout", out), patch.object(performance_compare.sys, "stderr", err): + try: + performance_compare.main([]) + except SystemExit: + pass + self.assertEqual(out.encoding, "utf-8") + self.assertEqual(out.errors, "backslashreplace") + self.assertEqual(err.encoding, "utf-8") + self.assertEqual(err.errors, "backslashreplace") + + if __name__ == "__main__": unittest.main() diff --git a/tools/tests/test_pr_labels.py b/tools/tests/test_pr_labels.py index eec489a51a..76298e7657 100644 --- a/tools/tests/test_pr_labels.py +++ b/tools/tests/test_pr_labels.py @@ -1,4 +1,6 @@ +import contextlib import copy +import io import json import sys import unittest @@ -174,12 +176,50 @@ class MainTests(unittest.TestCase): return "" with patch.object(pr_labels, "gh", side_effect=fake), patch.object(pr_labels, "WRITE_PAUSE", 0), \ - patch.object(sys, "argv", ["pr_labels.py", "--all"]), patch.dict(pr_labels.os.environ, {"GITHUB_REPOSITORY": "o/r"}): + patch.dict(pr_labels.os.environ, {"GITHUB_REPOSITORY": "o/r"}): with self.assertRaises(SystemExit) as raised: - pr_labels.main() + pr_labels.main(["--all"]) self.assertEqual(edited, ["1", "2"]) self.assertIn("#1", str(raised.exception.code)) + def test_main_handles_called_process_error_when_fetching_pr(self): + def fake(*args): + raise pr_labels.subprocess.CalledProcessError(1, "gh", stderr="PR not found") + + with patch.object(pr_labels, "gh", side_effect=fake), \ + patch.dict(pr_labels.os.environ, {"GITHUB_REPOSITORY": "o/r"}), \ + contextlib.redirect_stderr(io.StringIO()): + with self.assertRaises(SystemExit) as raised: + pr_labels.main(["--pr", "999"]) + self.assertEqual(raised.exception.code, 2) + + def test_main_reconfigures_streams(self): + class Stream: + def __init__(self): + self.encoding = None + self.errors = None + + def reconfigure(self, encoding=None, errors=None): + self.encoding = encoding + self.errors = errors + + def write(self, text): + pass + + def flush(self): + pass + + out, err = Stream(), Stream() + with patch.object(pr_labels.sys, "stdout", out), patch.object(pr_labels.sys, "stderr", err): + try: + pr_labels.main([]) + except SystemExit: + pass + self.assertEqual(out.encoding, "utf-8") + self.assertEqual(out.errors, "backslashreplace") + self.assertEqual(err.encoding, "utf-8") + self.assertEqual(err.errors, "backslashreplace") + if __name__ == "__main__": unittest.main() diff --git a/tools/tests/test_pr_overlap.py b/tools/tests/test_pr_overlap.py index 520620c8a3..4017ab3718 100644 --- a/tools/tests/test_pr_overlap.py +++ b/tools/tests/test_pr_overlap.py @@ -312,6 +312,62 @@ class UnicodePathTests(unittest.TestCase): os.chdir(previous) +class SubdirectoryTests(unittest.TestCase): + @unittest.skipIf(os.name == "nt", "fake gh is a shell script") + def test_branch_report_from_a_subdirectory_matches_the_root(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory).resolve() / "repo" + fake = Path(directory) / "bin" + fake.mkdir() + + def git(*args): + return subprocess.run( + ["git", "-c", "user.name=Overlap Test", "-c", "user.email=test@example.com", + "-c", "commit.gpgsign=false", *args], cwd=root, + capture_output=True, text=True, encoding="utf-8", check=True).stdout.strip() + + filename = "core/libs/prx/libSceExample/Export.cpp" + source = root / filename + source.parent.mkdir(parents=True) + source.write_text("int value = 0;\n", encoding="utf-8") + (root / "tools").mkdir() + (root / "tools" / "tool.py").write_text("print()\n", encoding="utf-8") + git("init", "-q", "-b", "main") + git("add", ".") + git("commit", "-qm", "test: create base") + heads = {} + for branch, value in (("theirs", 1), ("mine", 2)): + git("checkout", "-qb", branch, "main") + source.write_text(f"int value = {value};\nint APS5_VABI sceExample() {{ return {value}; }}\n", encoding="utf-8") + git("add", ".") + git("commit", "-qm", "fix: change value") + heads[branch] = git("rev-parse", "HEAD") + pulls = [{"number": 7, "user": {"login": "someone"}, "head": {"sha": heads["theirs"]}, "body": "Depends on: none"}] + (fake / "pulls.json").write_text(json.dumps(pulls), encoding="utf-8") + (fake / "gh").write_text(f"#!/bin/sh\ncat '{fake / 'pulls.json'}'\n", encoding="utf-8") + (fake / "gh").chmod(0o755) + environment = {**os.environ, "PATH": f"{fake}{os.pathsep}{os.environ['PATH']}", "GIT_CONFIG_COUNT": "1", + "GIT_CONFIG_KEY_0": f"url.{root.as_uri()}.insteadOf", "GIT_CONFIG_VALUE_0": "https://github.com/owner/project.git"} + environment.pop("GITHUB_REPOSITORY", None) + script = Path(pr_overlap.__file__).resolve() + outputs = {} + for subdirectory in (".", "tools", "core/libs/prx"): + result = subprocess.run([sys.executable, str(script), "--repo", "owner/project", "--branch", "mine"], + cwd=root / subdirectory, env=environment, capture_output=True, text=True, + encoding="utf-8", timeout=60) + self.assertEqual(result.returncode, 0, result.stderr) + outputs[subdirectory] = result.stdout + self.assertIn(f"`{filename}` [L1](", outputs["."]) + self.assertIn("| `sceExample` |", outputs["."]) + self.assertEqual(outputs["tools"], outputs["."]) + self.assertEqual(outputs["core/libs/prx"], outputs["."]) + result = subprocess.run([sys.executable, str(script), "--repo", "owner/project", "--out", "reports"], + cwd=root / "tools", env=environment, capture_output=True, text=True, + encoding="utf-8", timeout=60) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertTrue((root / "tools" / "reports" / "7.md").is_file()) + + class CommentStateTests(unittest.TestCase): def setUp(self): if os.name == "nt" or not all(shutil.which(tool) for tool in ("bash", "jq", "sed", "sha256sum")): diff --git a/tools/tests/test_pr_overlap_dependencies.py b/tools/tests/test_pr_overlap_dependencies.py index 11754c81b7..9af379f868 100644 --- a/tools/tests/test_pr_overlap_dependencies.py +++ b/tools/tests/test_pr_overlap_dependencies.py @@ -47,6 +47,24 @@ class DependencyTests(unittest.TestCase): with self.subTest(body=body): self.assertEqual(pull_requests(body)[1]["depends"], {2, 3}) + def test_markdown_links_and_full_urls(self): + for body in ( + "- [x] Depends on: [#2](https://github.com/owner/repo/pull/2)", + "- [x] Depends on: https://github.com/owner/repo/pull/2", + "- [x] Depends on: [PR #2](https://github.com/owner/repo/pull/2)", + "- [x] Depends on: https://github.com/owner/repo/pull/2/files", + "- [x] Depends on: https://github.com/owner/repo/pull/2#issuecomment-1", + "- [x] Depends on: https://github.com/BOYKOPOVAR/AnyPS5/pull/2", + "Depends on: [#2](https://github.com/owner/repo/pull/2), [#3](https://github.com/owner/repo/pull/3)", + "Depends on:\n- [#2](https://github.com/owner/repo/pull/2)\n- [#3](https://github.com/owner/repo/pull/3)", + "- [x] Depends on: [none](https://example.com)", + "- [x] Depends on: https://github.com/shadps4-emu/shadPS4/pull/1234", + "- [x] Depends on: [#1234](https://github.com/shadps4-emu/shadPS4/pull/1234)", + ): + with self.subTest(body=body): + expected = set() if "none" in body or "shadps4" in body else ({2, 3} if "3" in body else {2}) + self.assertEqual(pull_requests(body)[1]["depends"], expected) + def test_later_references_are_not_dependencies(self): for newline in ("\n", "\r\n"): for declaration in ("Depends on #2.", "Depends on: #2"): diff --git a/tools/tests/test_pr_overlap_errors.py b/tools/tests/test_pr_overlap_errors.py index c5bb5e0b8e..f85c7c3267 100644 --- a/tools/tests/test_pr_overlap_errors.py +++ b/tools/tests/test_pr_overlap_errors.py @@ -37,5 +37,33 @@ class ErrorReportTests(unittest.TestCase): self.assertIn(MISSING_REF, str(raised.exception.code)) + def test_main_reconfigures_streams(self): + class Stream: + def __init__(self): + self.encoding = None + self.errors = None + + def reconfigure(self, encoding=None, errors=None): + self.encoding = encoding + self.errors = errors + + def write(self, text): + pass + + def flush(self): + pass + + out, err = Stream(), Stream() + with patch.object(pr_overlap.sys, "stdout", out), patch.object(pr_overlap.sys, "stderr", err): + try: + pr_overlap.main([]) + except SystemExit: + pass + self.assertEqual(out.encoding, "utf-8") + self.assertEqual(out.errors, "backslashreplace") + self.assertEqual(err.encoding, "utf-8") + self.assertEqual(err.errors, "backslashreplace") + + if __name__ == "__main__": unittest.main() diff --git a/tools/tests/test_pr_overlap_exports.py b/tools/tests/test_pr_overlap_exports.py index d326656d8b..d109c06966 100644 --- a/tools/tests/test_pr_overlap_exports.py +++ b/tools/tests/test_pr_overlap_exports.py @@ -53,5 +53,23 @@ class ExportTests(unittest.TestCase): self.assertNotIn("Helper_nid_no_patch", found) + def test_explicit_export_aliases_are_collected(self): + diff = ('+int APS5_VABI placeholder_nid_no_patch() {\n' + '+ NotImplemented_nid_no_patch(__func__);\n' + '+}\n' + '+APS5_EXPORT("Pz262plj20w", placeholder_nid_no_patch);\n' + '-APS5_EXPORT("sKneJjUXWkQ", removed_nid_no_patch);\n') + self.assertEqual(pr_overlap.exports(diff), set()) + self.assertEqual(pr_overlap.aliases(diff), {"Pz262plj20w"}) + self.assertEqual(pr_overlap.aliases(diff.replace('+APS5_EXPORT("Pz262plj20w", placeholder_nid_no_patch);\n', "")), set()) + + def test_explicit_alias_overlaps_named_export_with_the_same_nid(self): + named = {"author": "a", "exports": {"FT_Outline_GetInsideBorder"}, "aliases": set()} + explicit = {"author": "b", "exports": set(), "aliases": {"Pz262plj20w"}} + self.assertEqual(pr_overlap.same_exports(named, explicit, []), {"FT_Outline_GetInsideBorder"}) + self.assertEqual(pr_overlap.same_exports(explicit, dict(explicit, author="c"), []), {"Pz262plj20w"}) + self.assertEqual(pr_overlap.same_exports(named, explicit, [explicit]), set()) + + if __name__ == "__main__": unittest.main() diff --git a/tools/tests/test_progress_history.py b/tools/tests/test_progress_history.py index 813b596990..38b9f5de86 100644 --- a/tools/tests/test_progress_history.py +++ b/tools/tests/test_progress_history.py @@ -1,3 +1,4 @@ +import json import os import subprocess import sys @@ -17,9 +18,9 @@ OPCODES = Path("core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaOpcod def commit(root, day, message, body, time="12:00:00Z"): library = root / "core/libs/prx/libSceExample" library.mkdir(parents=True, exist_ok=True) - (library / "Export.cpp").write_text(body) + (library / "Export.cpp").write_text(body, encoding="utf-8") (root / OPCODES).parent.mkdir(parents=True, exist_ok=True) - (root / OPCODES).write_text("enum class RdnaOpcode : int {\n SEndpgm,\n};\n") + (root / OPCODES).write_text("enum class RdnaOpcode : int {\n SEndpgm,\n};\n", encoding="utf-8") env = {**os.environ, "GIT_AUTHOR_DATE": f"{day}T{time}", "GIT_COMMITTER_DATE": f"{day}T{time}"} subprocess.run(["git", "-C", str(root), "add", "-A"], check=True) subprocess.run(["git", "-C", str(root), "-c", "user.name=t", "-c", "user.email=t@t", "commit", "-q", "-m", message], @@ -140,6 +141,21 @@ class ProgressHistoryTests(unittest.TestCase): for label in ("Aug 3", "Aug 10", "Aug 17", "Aug 24", "Aug 31"): self.assertIn(f">{label}<", svg) + def test_main_writes_utf8_output_files(self): + point = {"date": "2026-01-01", "commit": "abc1234", + "libraries": {"done": 1, "total": 2, "percent": 50.0}, + "shaders": {"done": 3, "total": 4, "percent": 75.0}} + with tempfile.TemporaryDirectory() as directory: + output = Path(directory) / "site" + with patch.object(progress_history, "collect", return_value=[point]): + progress_history.main([str(output)]) + svg = (output / "history.svg").read_text(encoding="utf-8") + self.assertIn("\u25cf", svg) + html = (output / "history.html").read_text(encoding="utf-8") + self.assertIn("<!DOCTYPE html>", html) + data = json.loads((output / "history.json").read_text(encoding="utf-8")) + self.assertEqual(data, [point]) + if __name__ == "__main__": diff --git a/tools/tests/test_select_tests.py b/tools/tests/test_select_tests.py index e86b16ccad..7dbeb88bee 100644 --- a/tools/tests/test_select_tests.py +++ b/tools/tests/test_select_tests.py @@ -230,6 +230,40 @@ class BuildErrorTests(unittest.TestCase): self.assertIn("CMAKE_CACHEFILE_DIR", result.stderr) self.assertNotIn("Traceback", result.stderr) + def test_cache_with_utf8_bom_is_read(self): + with tempfile.TemporaryDirectory() as directory: + cache = Path(directory) / "CMakeCache.txt" + cache.write_text("\ufeffCMAKE_CACHEFILE_DIR:INTERNAL=C:/work/build\nCMAKE_HOME_DIRECTORY:INTERNAL=C:/work/source\n", encoding="utf-8") + with mock.patch.object(select_tests.Build, "run", return_value="Build file encoding: UTF-8"): + build = select_tests.Build(directory) + self.assertEqual(build.path, os.path.normpath("C:/work/build")) + self.assertEqual(build.source, os.path.normpath("C:/work/source")) + + def test_explain_accepts_utf8_bom_in_graph_and_changed(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + graph_path = root / "graph.json" + graph = { + "tests": { + "unit": { + "target": "tests/unit", + "arguments": [], + "registered": [], + "sets": [], + "requires": [], + "depends": [], + } + }, + "closures": {"tests/unit": ["src/main.c"]}, + } + graph_path.write_text("\ufeff" + json.dumps(graph), encoding="utf-8") + changed_path = root / "changed.txt" + changed_path.write_text("\ufeffsrc/main.c\n", encoding="utf-8") + result = self.run_tool("explain", graph_path, changed_path) + self.assertEqual(result.returncode, 0, result.stderr) + data = json.loads(result.stdout) + self.assertEqual(data["tests"], ["unit"]) + class CaseInsensitivePathTests(unittest.TestCase): def build(self): diff --git a/tools/tests/test_shader_cache_runtime_sources.py b/tools/tests/test_shader_cache_runtime_sources.py index 0c462bb7ea..d4d43946e7 100644 --- a/tools/tests/test_shader_cache_runtime_sources.py +++ b/tools/tests/test_shader_cache_runtime_sources.py @@ -11,8 +11,13 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[1])) import select_tests -SOURCE = Path(sys.argv.pop(1)) -CMAKE = sys.argv.pop(1) +EXPLICIT_ARGS = len(sys.argv) >= 3 and Path(sys.argv[1]).is_dir() +if EXPLICIT_ARGS: + SOURCE = Path(sys.argv.pop(1)) + CMAKE = sys.argv.pop(1) +else: + SOURCE = Path(__file__).resolve().parents[2] / 'core/shader/recompiler' + CMAKE = shutil.which('cmake') or 'cmake' class Fixture: @@ -106,6 +111,7 @@ file(WRITE "${{CMAKE_CURRENT_BINARY_DIR}}/runtime-sources.txt" "${{runtimeSource return {Path(line.split(' ', 1)[1]) for line in self.listing.read_text(encoding='utf-8').splitlines() if line.startswith('sources ')} +@unittest.skipUnless(EXPLICIT_ARGS or (shutil.which('ninja') and shutil.which(CMAKE) and SOURCE.is_dir()), 'ninja, cmake, and shader recompiler source required') class ShaderCacheRuntimeSourceTests(unittest.TestCase): def fixture(self, spirv_tools=False): temporary = tempfile.TemporaryDirectory(prefix='shader_cache_sources_') @@ -213,4 +219,5 @@ class ShaderCacheRuntimeSourceTests(unittest.TestCase): self.assertIn('has not been reviewed', fixture.version(False)) -unittest.main() +if __name__ == '__main__': + unittest.main() diff --git a/tools/tests/test_stubgen.py b/tools/tests/test_stubgen.py index bc4bf37488..eedd1e3a09 100644 --- a/tools/tests/test_stubgen.py +++ b/tools/tests/test_stubgen.py @@ -6,6 +6,7 @@ import tempfile from pathlib import Path import sys import unittest +from unittest.mock import patch sys.path.insert(0, str(Path(__file__).resolve().parents[1])) import check_conventions @@ -200,5 +201,37 @@ class ReadOnlyOutputTests(unittest.TestCase): self.assertEqual(sorted(path.name for path in root.iterdir()), ["Export.cpp", "nids.txt"]) + def test_main_reconfigures_streams(self): + class Stream: + def __init__(self): + self.encoding = None + self.errors = None + self.newline = None + + def reconfigure(self, encoding=None, errors=None, newline=None): + self.encoding = encoding + self.errors = errors + self.newline = newline + + def write(self, text): + pass + + def flush(self): + pass + + out, err = Stream(), Stream() + with patch.object(stubgen.sys, "stdout", out), patch.object(stubgen.sys, "stderr", err): + try: + stubgen.main([]) + except SystemExit: + pass + self.assertEqual(out.encoding, "utf-8") + self.assertEqual(out.errors, "backslashreplace") + self.assertEqual(out.newline, "\n") + self.assertEqual(err.encoding, "utf-8") + self.assertEqual(err.errors, "backslashreplace") + self.assertEqual(err.newline, "\n") + + if __name__ == "__main__": unittest.main() diff --git a/tools/tests/test_test_registration.py b/tools/tests/test_test_registration.py index 106d3bc3ae..de64bbec73 100644 --- a/tools/tests/test_test_registration.py +++ b/tools/tests/test_test_registration.py @@ -1,4 +1,6 @@ from pathlib import Path +import contextlib +import io import os import shutil import subprocess @@ -343,5 +345,24 @@ class ScriptRegistrationTests(unittest.TestCase): "tools/tests/test_f.py"]) +class CheckRegistrationCliTests(unittest.TestCase): + def test_cache_with_utf8_bom_is_read(self): + with tempfile.TemporaryDirectory() as directory: + build = Path(directory) + cache = build / "CMakeCache.txt" + cache.write_text("\ufeffCMAKE_MAKE_PROGRAM:FILEPATH=ninja\nCMAKE_GENERATOR:INTERNAL=Ninja\n", encoding="utf-8") + output = "tests/dummy_tests: EXECUTABLE\n" + with patch.object(check_test_registration.subprocess, "run", side_effect=lambda command, **kwargs: SimpleNamespace( + stdout="Build file encoding: UTF-8\n" if command[-1] == "wincodepage" else output)): + names = check_test_registration.built(build) + self.assertEqual(names, {"dummy_tests"}) + + def test_main_scripts_with_explicit_argv(self): + with patch.object(check_test_registration, "unrun_scripts", return_value=(["test_a.py"], [])): + with contextlib.redirect_stdout(io.StringIO()): + code = check_test_registration.main(["--scripts"]) + self.assertEqual(code, 0) + + if __name__ == "__main__": unittest.main() diff --git a/tools/tests/test_unwrap_self.py b/tools/tests/test_unwrap_self.py index 6ee6d2095e..f30eb5a74f 100644 --- a/tools/tests/test_unwrap_self.py +++ b/tools/tests/test_unwrap_self.py @@ -106,6 +106,16 @@ class UnwrapTest(unittest.TestCase): self.assertEqual(source.read_bytes(), container) self.assertEqual(alias.read_bytes(), container) + def test_main_with_explicit_argv(self): + container = build_container(default_segments(), default_records(), default_payloads()) + source = Path(self.directory) / "input.self" + output = Path(self.directory) / "output.elf" + source.write_bytes(container) + with contextlib.redirect_stdout(io.StringIO()): + code = unwrap_self.main([str(source), str(output)]) + self.assertEqual(code, 0) + self.assertTrue(output.exists()) + def convert(self, container, name="input.self"): directory = Path(self.directory) source = directory / name @@ -122,6 +132,27 @@ class UnwrapTest(unittest.TestCase): def tearDown(self): self.temporary.cleanup() + def test_program_table_overlapping_elf_header_preserves_output(self): + for offset in (0, 8, 32, 63): + for previous in (None, b"previous output"): + with self.subTest(offset=offset, previous=previous): + container = bytearray(build_container(default_segments(), default_records(), default_payloads())) + struct.pack_into("<Q", container, ELF_HEADER_OFFSET + 0x20, offset) + output = Path(self.directory) / "output.elf" + if previous is not None: + output.write_bytes(previous) + else: + output.unlink(missing_ok=True) + result, output = self.convert(container) + self.assertEqual(result.returncode, 2, result.stderr) + self.assertIn("ELF program header table overlaps the ELF header", result.stderr) + self.assertNotIn("Traceback", result.stderr) + self.assertEqual((Path(self.directory) / "input.self").read_bytes(), container) + if previous is None: + self.assertFalse(output.exists()) + else: + self.assertEqual(output.read_bytes(), previous) + def test_record_table_places_the_segments_and_keeps_the_alignment(self): container = build_container(default_segments(), default_records(), default_payloads()) result, output = self.convert(container) @@ -473,7 +504,7 @@ class UnwrapTest(unittest.TestCase): def allocate(value): if isinstance(value, int): - self.assertLessEqual(value, len(container) + unwrap_self.MAX_EMPTY_LOAD_PADDING) + self.assertLessEqual(value, len(container) + unwrap_self.MAX_ALIGNMENT_PADDING) return bytearray(value) for previous in (None, b"previous output"): @@ -486,7 +517,7 @@ class UnwrapTest(unittest.TestCase): contextlib.redirect_stderr(io.StringIO()) as stderr: self.assertEqual(unwrap_self.main(), 2) self.assertEqual(stdout.getvalue(), "") - self.assertIn("empty load alignment requires more than", stderr.getvalue()) + self.assertIn("segment alignment requires more than", stderr.getvalue()) self.assertNotIn("Traceback", stderr.getvalue()) if previous is not None: self.assertEqual(output.read_bytes(), previous) @@ -494,20 +525,104 @@ class UnwrapTest(unittest.TestCase): self.assertFalse(output.exists()) self.assertEqual(source.read_bytes(), container) + def test_excessive_file_backed_padding_is_refused_before_allocation(self): + cases = [ + ([(PT_LOAD, 5, 0x1234, (1 << 40) - 1, 0x100, 1 << 40)], + [(0x2804, 0x800, 0x100)], {0x800: b"x" * 0x100}), + ([(PT_LOAD, 5, 0x1234, 0, 0x100, PAGE), + (SCE_COMMENT, 0, 0xB00, 0, 0x20, 1 << 40)], + [(0x2804, 0x800, 0x100)], {0x800: b"x" * 0x100, 0xB00: pattern(9, 0x20)}), + ] + source = Path(self.directory) / "input.self" + output = Path(self.directory) / "output.elf" + for segments, records, payloads in cases: + container = build_container(segments, records, payloads) + source.write_bytes(container) + + def allocate(value): + if isinstance(value, int): + self.assertLessEqual(value, len(container) + 64 * 1024 * 1024) + return bytearray(value) + + for previous in (None, b"previous output"): + with self.subTest(segments=segments, previous=previous): + output.unlink(missing_ok=True) + if previous is not None: + output.write_bytes(previous) + with mock.patch.object(unwrap_self, "bytearray", side_effect=allocate, create=True), \ + mock.patch.object(sys, "argv", ["unwrap_self.py", str(source), str(output)]), \ + contextlib.redirect_stdout(io.StringIO()) as stdout, \ + contextlib.redirect_stderr(io.StringIO()) as stderr: + self.assertEqual(unwrap_self.main(), 2) + self.assertEqual(stdout.getvalue(), "") + self.assertIn("alignment requires more than", stderr.getvalue()) + self.assertNotIn("Traceback", stderr.getvalue()) + self.assertEqual(source.read_bytes(), container) + if previous is None: + self.assertFalse(output.exists()) + else: + self.assertEqual(output.read_bytes(), previous) + + def test_alignment_padding_limit_is_cumulative(self): + for empty in (False, True): + with self.subTest(empty=empty): + segments = [(PT_LOAD, 5, 0x1234, 0x80, 0x20, 0x100), + (PT_LOAD, 6, 0x5678, 0x580, 0 if empty else 0x10, 0x1000)] + records = [(0x800, 0x800, 0x20)] + payloads = {0x800: pattern(1, 0x20)} + if not empty: + records.append((0x100800, 0xA00, 0x10)) + payloads[0xA00] = pattern(9, 0x10) + source = Path(self.directory) / "input.self" + output = Path(self.directory) / "output.elf" + container = build_container(segments, records, payloads) + source.write_bytes(container) + output.write_bytes(b"previous output") + with mock.patch.object(unwrap_self, "MAX_ALIGNMENT_PADDING", 0x15F): + with self.assertRaisesRegex(unwrap_self.Refused, "alignment requires more than"): + unwrap_self.unwrap(source, output) + self.assertEqual(output.read_bytes(), b"previous output") + self.assertEqual(source.read_bytes(), container) + with mock.patch.object(unwrap_self, "MAX_ALIGNMENT_PADDING", 0x160): + unwrap_self.unwrap(source, output) + data, headers = read_program_headers(output) + for header in headers: + self.assertEqual(header[2] % header[7], header[3] % header[7]) + self.assertEqual(data[headers[0][2]:headers[0][2] + 0x20], payloads[0x800]) + if not empty: + self.assertEqual(data[headers[1][2]:headers[1][2] + 0x10], payloads[0xA00]) + + def test_large_alignment_with_small_padding_remains_supported(self): + payload = b"x" * 0x100 + segments = [(PT_LOAD, 5, 0x1234, FIRST_REGION + 0x10, len(payload), 1 << 40)] + container = build_container(segments, [(0x2804, 0x800, len(payload))], {0x800: payload}) + source = Path(self.directory) / "input.self" + output = Path(self.directory) / "output.elf" + source.write_bytes(container) + with mock.patch.object(unwrap_self, "MAX_ALIGNMENT_PADDING", 0x10): + unwrap_self.unwrap(source, output) + data, headers = read_program_headers(output) + load = headers[0] + self.assertEqual(load[2] % load[7], load[3] % load[7]) + self.assertEqual(load[2], FIRST_REGION + 0x10) + self.assertEqual(data[load[2]:load[2] + load[5]], payload) + self.assertEqual(source.read_bytes(), container) + def test_empty_load_padding_limit_counts_only_added_bytes(self): - baseline, output = self.convert(build_container(default_segments(), default_records(), default_payloads())) + backed = [(*segment[:5], 1) for segment in default_segments()] + baseline, output = self.convert(build_container(backed, default_records(), default_payloads())) self.assertEqual(baseline.returncode, 0, baseline.stderr) baseline_size = output.stat().st_size limit = 0x100 for extra in (-1, limit, limit + 1): with self.subTest(extra=extra): address = 0x10000 + baseline_size + extra - segments = default_segments() + [(PT_LOAD, 6, 0, address, 0, 0x10000)] + segments = backed + [(PT_LOAD, 6, 0, address, 0, 0x10000)] container = build_container(segments, default_records(), default_payloads()) source = Path(self.directory) / "input.self" source.write_bytes(container) previous = output.read_bytes() - with mock.patch.object(unwrap_self, "MAX_EMPTY_LOAD_PADDING", limit): + with mock.patch.object(unwrap_self, "MAX_ALIGNMENT_PADDING", limit): if extra <= limit: unwrap_self.unwrap(source, output) data, headers = read_program_headers(output) @@ -515,7 +630,7 @@ class UnwrapTest(unittest.TestCase): self.assertEqual(headers[3][2], baseline_size + extra) self.assertEqual(headers[3][2] % headers[3][7], address % headers[3][7]) else: - with self.assertRaisesRegex(unwrap_self.Refused, "empty load alignment requires more than"): + with self.assertRaisesRegex(unwrap_self.Refused, "segment alignment requires more than"): unwrap_self.unwrap(source, output) self.assertEqual(output.read_bytes(), previous) diff --git a/tools/tests/test_web_release.py b/tools/tests/test_web_release.py index 5fbc00dc65..e48e3fd3c7 100644 --- a/tools/tests/test_web_release.py +++ b/tools/tests/test_web_release.py @@ -1,3 +1,4 @@ +import contextlib import hashlib import io import json @@ -12,6 +13,7 @@ from unittest.mock import patch sys.path.insert(0, str(Path(__file__).resolve().parents[1])) import build_web import package_release +import test_web FILES = ("index.html", "app.css", "app.mjs", "archive.mjs", "batch.mjs", "output-directory.mjs", "worker.mjs", "native/relinker_wasm.mjs", "native/relinker_wasm.wasm") @@ -375,5 +377,58 @@ class WebPackageTests(unittest.TestCase): self.assertEqual(self.asset.read_bytes(), b"previous asset") +class TestWebRunnerTests(unittest.TestCase): + def test_main_rejects_missing_archive_file_without_traceback(self): + with tempfile.TemporaryDirectory() as tmp: + build = Path(tmp) / "build" + build.mkdir() + with self.assertRaises(SystemExit) as error, contextlib.redirect_stderr(io.StringIO()) as err: + test_web.main(["--build", str(build)]) + self.assertEqual(error.exception.code, 2) + self.assertIn("archive module does not exist", err.getvalue()) + + def test_main_rejects_archive_missing_zip_url(self): + with tempfile.TemporaryDirectory() as tmp: + build = Path(tmp) / "build" + (build / "web").mkdir(parents=True) + (build / "web/archive.mjs").write_text("const x = 1;\n", encoding="utf-8") + with patch("subprocess.run"): + with self.assertRaises(SystemExit) as error, contextlib.redirect_stderr(io.StringIO()) as err: + test_web.main(["--build", str(build)]) + self.assertEqual(error.exception.code, 2) + self.assertIn("zip library URL not found", err.getvalue()) + + def test_main_decodes_archive_with_utf8_bom(self): + with tempfile.TemporaryDirectory() as tmp: + build = Path(tmp) / "build" + (build / "web").mkdir(parents=True) + (build / "web/archive.mjs").write_text('\ufefffrom "https://cdn.jsdelivr.net/example/zip.mjs";\n', encoding="utf-8") + with patch("subprocess.run"), \ + patch("urllib.request.urlopen") as mock_url: + mock_resp = mock_url.return_value.__enter__.return_value + mock_resp.read.return_value = b"zip-content" + result = test_web.main(["--build", str(build)]) + self.assertEqual(result, 0) + + def test_main_reconfigures_streams(self): + class Stream: + def __init__(self): + self.encoding = None + self.errors = None + + def reconfigure(self, encoding=None, errors=None): + self.encoding = encoding + self.errors = errors + + out, err = Stream(), Stream() + with patch.object(test_web.sys, "stdout", out), patch.object(test_web.sys, "stderr", err): + with self.assertRaises(SystemExit): + test_web.main([]) + self.assertEqual(out.encoding, "utf-8") + self.assertEqual(out.errors, "backslashreplace") + self.assertEqual(err.encoding, "utf-8") + self.assertEqual(err.errors, "backslashreplace") + + if __name__ == "__main__": unittest.main() diff --git a/tools/unwrap_self.py b/tools/unwrap_self.py index 1ce81f2e12..659f4eb07a 100644 --- a/tools/unwrap_self.py +++ b/tools/unwrap_self.py @@ -43,7 +43,7 @@ RECORD_SIZE = 0x20 RECORD_HAS_SEGMENT = 0x800 RECORD_SEGMENT_SHIFT = 20 FIRST_REGION = 0x400 -MAX_EMPTY_LOAD_PADDING = 64 * 1024 * 1024 +MAX_ALIGNMENT_PADDING = 64 * 1024 * 1024 PT_LOAD = 1 PT_NOTE = 4 ENTROPY_LIMIT = 7.5 @@ -109,6 +109,8 @@ def parse_elf(data): count = struct.unpack_from("<H", header, 0x38)[0] if entry_size != 56 or count == 0: raise Refused("program header table is %d entries of %d bytes" % (count, entry_size)) + if program_offset < len(header): + raise Refused("ELF program header table overlaps the ELF header") if inner + program_offset + count * entry_size > len(data): raise Refused("ELF program header table does not fit the file") headers = [] @@ -273,9 +275,11 @@ def unwrap(path, output, verbose=False): regions.sort(key=lambda region: region["old"]) program_offset = struct.unpack_from("<Q", elf_header, 0x20)[0] cursor = max(FIRST_REGION, program_offset + len(headers) * 56) + padding = 0 for region in regions: alignment = region["align"] if region["align"] and (region["align"] & (region["align"] - 1)) == 0 else 1 placed = cursor + (region["vaddr"] - cursor) % alignment + padding += placed - cursor region["new"] = placed cursor = placed + region["size"] @@ -283,8 +287,9 @@ def unwrap(path, output, verbose=False): for header in headers: if header["type"] == PT_LOAD and header["size"] == 0: empty_end = max(empty_end, header["vaddr"] % max(header["align"], 1)) - if empty_end - cursor > MAX_EMPTY_LOAD_PADDING: - raise Refused("empty load alignment requires more than %d bytes of output padding" % MAX_EMPTY_LOAD_PADDING) + padding += empty_end - cursor + if padding > MAX_ALIGNMENT_PADDING: + raise Refused("segment alignment requires more than %d bytes of output padding" % MAX_ALIGNMENT_PADDING) cursor = empty_end result = bytearray(cursor) @@ -328,12 +333,15 @@ def unwrap(path, output, verbose=False): return len(result) -def main(): +def main(argv=None): + for stream in (sys.stdout, sys.stderr): + if hasattr(stream, "reconfigure"): + stream.reconfigure(encoding="utf-8") parser = argparse.ArgumentParser(description="unwrap a decrypted SELF or fSELF container into a clean ELF") parser.add_argument("input", help="SELF container to read") parser.add_argument("output", help="ELF executable to write") parser.add_argument("--verbose", action="store_true", help="print the program header mapping") - arguments = parser.parse_args() + arguments = parser.parse_args(argv) try: size = unwrap(arguments.input, arguments.output, arguments.verbose) except Refused as error: @@ -347,6 +355,4 @@ def main(): if __name__ == "__main__": - if hasattr(sys.stdout, "reconfigure"): - sys.stdout.reconfigure(encoding="utf-8") sys.exit(main())