diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 514435a23a..41ee9800bc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -32,7 +32,7 @@ permissions: contents: read concurrency: - group: build-${{ github.event_name }}-${{ github.event.pull_request.number || inputs.pr || github.ref }} + group: build-${{ github.event_name }}-${{ github.event.pull_request.number || inputs.pr || (github.event_name == 'push' && github.sha) || github.ref }} cancel-in-progress: ${{ github.event_name != 'push' }} env: @@ -56,6 +56,7 @@ jobs: relinker: ${{ steps.gate.outputs.relinker }} register-context: ${{ steps.gate.outputs.register-context }} write-watch: ${{ steps.gate.outputs.write-watch }} + edge-watch: ${{ steps.gate.outputs.edge-watch }} mingw-host-tests: ${{ steps.gate.outputs.mingw-host-tests }} full: ${{ steps.gate.outputs.full }} relinker-hosts: ${{ steps.gate.outputs.relinker-hosts }} @@ -139,6 +140,8 @@ jobs: registerContext: edits('register-context') || touches('core/libs/tests/register_context/', 'core/libs/tests/RegisterContext.cpp', libc), writeWatch: edits('write-watch') || touches('core/libs/tests/write_watch/', 'core/libs/tests/WriteWatch.cpp', libc), + edgeWatch: edits('edge-watch') || touches('core/libs/tests/edge_watch/', 'core/libs/tests/EdgeWatch.cpp', + 'core/libs/prx/libkernel/Equeue/src/EdgeWatch.cpp', 'core/libs/prx/libkernel/Equeue/include/EdgeWatch.hpp', libc), mingwHostTests: edits('eligibility') || edits('windows') || touches( 'core/CMakeLists.txt', 'core/libs/', 'cmake/', '3rdparty/', '.gitmodules', 'tools/build_mingw_host_only_tests.py', 'tools/build_changed_guest_tests.py', 'tools/configure_seed.py', 'tools/select_tests.py', @@ -178,6 +181,7 @@ jobs: core.setOutput('relinker', run.relinker); core.setOutput('register-context', run.registerContext); core.setOutput('write-watch', run.writeWatch); + core.setOutput('edge-watch', run.edgeWatch); core.setOutput('mingw-host-tests', false); core.setOutput('full', true); core.setOutput('relinker-hosts', relinkerHosts(true, true)); @@ -268,6 +272,7 @@ jobs: core.setOutput('relinker', relinker); core.setOutput('register-context', registerContext); core.setOutput('write-watch', writeWatch); + core.setOutput('edge-watch', run.edgeWatch); core.setOutput('mingw-host-tests', !manual && run.mingwHostTests); core.setOutput('full', full); core.setOutput('relinker-hosts', relinkerHosts(full, linux)); @@ -328,10 +333,10 @@ jobs: - if: runner.os == 'Windows' run: echo 'C:\winlibs\mingw64\bin' >> "$GITHUB_PATH" - if: ${{ matrix.architectures == 'x86_64' }} - run: sudo softwareupdate --install-rosetta --agree-to-license + run: arch -x86_64 /usr/bin/true || sudo softwareupdate --install-rosetta --agree-to-license - run: cmake -S . -B build-relinker -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER=${{ matrix.compiler }} -DANYPS5_RELINKER_ONLY=ON -DBUILD_TESTING=ON -DCMAKE_OSX_ARCHITECTURES=${{ matrix.architectures }} - run: cmake --build build-relinker --parallel 3 - - run: ctest --test-dir build-relinker --output-on-failure --timeout 120 --parallel 4 + - run: ctest --test-dir build-relinker --output-on-failure --timeout 120 --parallel 4 --no-tests=error relinker-asan: needs: eligibility @@ -648,7 +653,7 @@ jobs: uses: actions/cache/restore@v6 with: path: build/externals - key: ffmpeg-${{ runner.os }}-${{ steps.submodules.outputs.ffmpeg }} + key: ffmpeg-${{ runner.os }}-winlibs-${{ env.WINLIBS_ZIP }}-${{ steps.submodules.outputs.ffmpeg }} - id: seed-cache uses: actions/cache/restore@v6 with: @@ -707,7 +712,7 @@ jobs: - name: Build and run changed guest core/libs tests if: github.event_name == 'pull_request' shell: cmd - run: python tools/build_changed_guest_tests.py build changed.txt + run: python tools/build_changed_guest_tests.py build changed.txt --output-log ctest.log - id: libs if: github.event_name != 'pull_request' shell: cmd @@ -736,3 +741,36 @@ jobs: 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"] }' ctest.log + edge-watch: + needs: eligibility + if: ${{ needs.eligibility.outputs.edge-watch == 'true' }} + strategy: + fail-fast: false + matrix: + os: [macos-15-intel, macos-15] + runs-on: ${{ matrix.os }} + timeout-minutes: 5 + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ needs.eligibility.outputs.sha }} + persist-credentials: false + - name: Merge the pull request into main + if: ${{ needs.eligibility.outputs.head != '' }} + shell: bash + env: + PR_NUMBER: ${{ needs.eligibility.outputs.pr }} + PR_HEAD: ${{ needs.eligibility.outputs.head }} + run: | + read -ra numbers <<< "$PR_NUMBER" + read -ra heads <<< "$PR_HEAD" + refspecs=() + for number in "${numbers[@]}"; do refspecs+=("+refs/pull/$number/head:refs/remotes/pull/$number"); done + git fetch --no-tags --unshallow origin "${refspecs[@]}" + for i in "${!numbers[@]}"; do + if [ "$(git rev-parse "refs/remotes/pull/${numbers[i]}")" != "${heads[i]}" ]; then echo "::error::Pull request #${numbers[i]} has newer commits; start a new build"; exit 1; fi + if ! git -c user.name=ci -c user.email=ci@localhost merge --no-ff --no-edit "refs/remotes/pull/${numbers[i]}"; then echo "::error::Pull request #${numbers[i]} does not merge cleanly into main and the pull requests before it; rebase it first"; exit 1; fi + done + - run: cmake -S core/libs/tests/edge_watch -B build/edge-watch -DCMAKE_BUILD_TYPE=Release + - run: cmake --build build/edge-watch --parallel + - run: ctest --test-dir build/edge-watch --output-on-failure --repeat until-fail:3 --no-tests=error diff --git a/.github/workflows/emscripten-libraries.yml b/.github/workflows/emscripten-libraries.yml index 790bf599a3..2c21a1db79 100644 --- a/.github/workflows/emscripten-libraries.yml +++ b/.github/workflows/emscripten-libraries.yml @@ -26,7 +26,7 @@ jobs: run: | cmake -S core/libs/tests/emscripten -B build-native -DCMAKE_BUILD_TYPE=Release cmake --build build-native --parallel 2 - ctest --test-dir build-native --output-on-failure + ctest --test-dir build-native --output-on-failure --no-tests=error - uses: actions/setup-node@v6 with: node-version: '26' @@ -40,4 +40,4 @@ jobs: source "$RUNNER_TEMP/emsdk/emsdk_env.sh" emcmake cmake -S core/libs/tests/emscripten -B build-portable -DCMAKE_BUILD_TYPE=Release -DANYPS5_NODE="$ANYPS5_TEST_NODE" cmake --build build-portable --parallel 2 - ctest --test-dir build-portable --output-on-failure + ctest --test-dir build-portable --output-on-failure --no-tests=error diff --git a/.github/workflows/stale-conflicts.yml b/.github/workflows/stale-conflicts.yml index 00150e1b70..c7fbbb1b04 100644 --- a/.github/workflows/stale-conflicts.yml +++ b/.github/workflows/stale-conflicts.yml @@ -16,6 +16,7 @@ jobs: steps: - env: GH_TOKEN: ${{ github.token }} + shell: bash run: | gh pr list -R "$GITHUB_REPOSITORY" --label stale -L 1000 --json number,labels \ --jq '.[] | select(all(.labels[]; .name != "needs rebase")) | .number' | diff --git a/.github/workflows/web-release-tools.yml b/.github/workflows/web-release-tools.yml new file mode 100644 index 0000000000..147868a660 --- /dev/null +++ b/.github/workflows/web-release-tools.yml @@ -0,0 +1,38 @@ +name: Web release tools + +on: + pull_request: + paths: + - .github/workflows/web-release-tools.yml + - demo/web/** + - tools/build_web.py + - tools/nid_names.py + - tools/package_headers.py + - tools/package_release.py + - tools/test_web.py + - tools/tests/test_build_web.py + - tools/tests/test_web_release.py + - core/relinker/relinker/tests/guest_import_fixtures.py + - core/relinker/relinker/tests/test_guest_intel_trampolines.py + - core/relinker/relinker/tests/test_guest_module_directories.py + - core/relinker/relinker/tests/test_linux_load_alignment.py + +permissions: + contents: read + +concurrency: + group: web-release-tools-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + release-tools: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + - name: Test web library assets + run: python3 tools/tests/test_build_web.py + - name: Test web release packaging + run: python3 tools/tests/test_web_release.py diff --git a/.github/workflows/web-tests.yml b/.github/workflows/web-tests.yml index bf158fd625..7a73e61110 100644 --- a/.github/workflows/web-tests.yml +++ b/.github/workflows/web-tests.yml @@ -35,6 +35,8 @@ jobs: run: node demo/web/tests/archive-windows.test.mjs demo/web - name: Test archive reserved names run: node demo/web/tests/archive-reserved.test.mjs demo/web + - name: Test library manifest validation + run: node --test demo/web/tests/worker-manifest.test.mjs - name: Check browser module syntax run: | node --check demo/web/app.mjs diff --git a/.github/workflows/windows-msys2.yml b/.github/workflows/windows-msys2.yml index def52370d2..554adb7ee9 100644 --- a/.github/workflows/windows-msys2.yml +++ b/.github/workflows/windows-msys2.yml @@ -81,7 +81,7 @@ jobs: uses: actions/cache/restore@v6 with: path: build/externals - key: ffmpeg-${{ runner.os }}-${{ steps.submodules.outputs.ffmpeg }} + key: ffmpeg-${{ runner.os }}-msys2-${{ 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' @@ -99,7 +99,7 @@ jobs: 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: ctest --output-on-failure --timeout 120 --parallel 4 --output-log ctest.log --output-junit ctest.xml --no-tests=error - run: python tools/check_skips.py build/ctest.xml --job windows - name: Report tests slower than 30 s if: ${{ !cancelled() }} diff --git a/CMakeLists.txt b/CMakeLists.txt index 1e039e57b8..f135ac95ff 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -258,6 +258,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_unwrap_self.py) add_test(NAME audit_dump COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_audit_dump.py) + add_test(NAME ffmpeg_cache_keys COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_ffmpeg_cache_keys.py) foreach(shard RANGE 0 11) if(shard EQUAL 0) set(conventions_test_name conventions_function_bodies) @@ -272,7 +274,18 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_performance_csv.py) set_tests_properties(performance_csv PROPERTIES TIMEOUT 20) add_test(NAME conventions_wrapped_declarations COMMAND ${Python3_EXECUTABLE} - ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_wrapped_declarations.py) + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_wrapped_declarations.py WrappedDeclarationTests) + set_tests_properties(conventions_wrapped_declarations PROPERTIES TIMEOUT 30) + set(conventions_wrapped_declarations_source "${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_wrapped_declarations.py") + set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${conventions_wrapped_declarations_source}") + file(STRINGS "${conventions_wrapped_declarations_source}" conventions_wrapped_declarations_classes REGEX "^class [A-Za-z0-9_]*Tests\\(") + foreach(test_class IN LISTS conventions_wrapped_declarations_classes) + string(REGEX REPLACE "^class ([A-Za-z0-9_]+).*" "\\1" test_class "${test_class}") + if(NOT test_class STREQUAL "WrappedDeclarationTests") + add_test(NAME conventions_wrapped_declarations_${test_class} COMMAND ${Python3_EXECUTABLE} "${conventions_wrapped_declarations_source}" ${test_class}) + set_tests_properties(conventions_wrapped_declarations_${test_class} PROPERTIES TIMEOUT 30) + endif() + endforeach() add_test(NAME conventions_ci_gate COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_ci_gate.py) add_test(NAME preview_pad_timeline COMMAND ${Python3_EXECUTABLE} @@ -284,6 +297,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_base_ref.py) add_test(NAME tools_configure_diff COMMAND ${Python3_EXECUTABLE} ${CMAKE_SOURCE_DIR}/tools/tests/test_compare_configure_checks.py) set_tests_properties(tools_configure_diff PROPERTIES TIMEOUT 20) + add_test(NAME ctest_no_tests_flag COMMAND ${Python3_EXECUTABLE} ${CMAKE_SOURCE_DIR}/tools/tests/test_ctest_no_tests_flag.py) + set_tests_properties(ctest_no_tests_flag PROPERTIES TIMEOUT 20) add_test(NAME conventions_doc_links COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_doc_links.py) add_test(NAME conventions_unknown_nids COMMAND ${Python3_EXECUTABLE} @@ -292,10 +307,18 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_debt_length.py) add_test(NAME test_registration COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_test_registration.py) + add_test(NAME unbuilt_exports COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_unbuilt_exports.py) add_test(NAME test_script_registration COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/check_test_registration.py --scripts) + add_test(NAME tools_sanitizer_json COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_sanitizer_json.py) + set_tests_properties(tools_sanitizer_json PROPERTIES TIMEOUT 20) add_test(NAME build_eligibility COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_build_eligibility.py) + add_test(NAME tools_release_notes_check COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_release_notes_check.py) + set_tests_properties(tools_release_notes_check PROPERTIES TIMEOUT 20) add_test(NAME check_skips_ctest COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_skips_ctest.py) set_tests_properties(check_skips_ctest PROPERTIES TIMEOUT 30) @@ -309,6 +332,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_relinker_fuzz_fixture_warnings.py) add_test(NAME select_tests COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_select_tests.py) + add_test(NAME relinker_test_step COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_relinker_test_step.py) add_test(NAME tools_self_inspect COMMAND ${Python3_EXECUTABLE} ${CMAKE_SOURCE_DIR}/tools/tests/test_inspect_self.py) set_tests_properties(tools_self_inspect PROPERTIES TIMEOUT 20) add_test(NAME check_main_sync COMMAND ${Python3_EXECUTABLE} @@ -372,6 +397,14 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_wrapped_exports.py) add_test(NAME pr_overlap_own_branch COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_own_branch.py) + add_test(NAME main_sync_renames COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_main_sync_renames.py RenamedPathsTests.test_feature_rename_keeps_the_original_path) + set_tests_properties(main_sync_renames PROPERTIES TIMEOUT 20) + foreach(test_case IN ITEMS test_feature_rename_with_detection_disabled test_main_rename_keeps_the_original_path test_main_rename_with_detection_disabled) + add_test(NAME main_sync_renames_${test_case} COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_main_sync_renames.py RenamedPathsTests.${test_case}) + set_tests_properties(main_sync_renames_${test_case} PROPERTIES TIMEOUT 20) + endforeach() add_test(NAME pr_overlap_errors COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_errors.py) add_test(NAME pr_overlap_branch_scan COMMAND ${Python3_EXECUTABLE} @@ -397,6 +430,8 @@ if(BUILD_TESTING) add_test(NAME pad_input COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pad_input.py) set_tests_properties(pad_input PROPERTIES TIMEOUT 20) + add_test(NAME stale_conflicts_workflow COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_stale_conflicts_workflow.py) add_test(NAME pad_timeline COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pad_timeline.py) set_tests_properties(pad_timeline PROPERTIES TIMEOUT 20) @@ -450,10 +485,16 @@ if(BUILD_TESTING) "${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_env_reference.py" "EnvReferenceDiffTests.${test_case}") set_tests_properties(conventions_env_reference_${test_case} PROPERTIES TIMEOUT 30) endforeach() + add_test(NAME tools_annotation_symbols_json COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_annotation_symbols_json.py) + set_tests_properties(tools_annotation_symbols_json PROPERTIES TIMEOUT 20) add_test(NAME conventions_body_bom COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_body_bom.py) add_test(NAME conventions_blob_reads COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_blob_reads.py) + add_test(NAME ab_split_csv COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_ab_split_csv.py) + set_tests_properties(ab_split_csv PROPERTIES TIMEOUT 20) add_test(NAME conventions_agent_files COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_agent_files.py) add_test(NAME conventions_alignment COMMAND ${Python3_EXECUTABLE} @@ -481,6 +522,26 @@ if(BUILD_TESTING) endif() endif() endforeach() + set(unwrap_self_test_source "${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_unwrap_self.py") + set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${unwrap_self_test_source}") + file(STRINGS "${unwrap_self_test_source}" unwrap_self_cases REGEX "^class |^ def test_") + set(unwrap_self_first TRUE) + foreach(test_case IN LISTS unwrap_self_cases) + if(test_case MATCHES "^class ([A-Za-z0-9_]+)") + set(unwrap_self_class "${CMAKE_MATCH_1}") + else() + string(REGEX REPLACE "^ def (test_[A-Za-z0-9_]+).*" "\\1" test_case "${test_case}") + if(unwrap_self_first) + set_tests_properties(unwrap_self PROPERTIES + ENVIRONMENT "UNWRAP_SELF_TEST=${unwrap_self_class}.${test_case}" TIMEOUT 30) + set(unwrap_self_first FALSE) + else() + add_test(NAME unwrap_self_${unwrap_self_class}_${test_case} COMMAND ${Python3_EXECUTABLE} + "${unwrap_self_test_source}" "${unwrap_self_class}.${test_case}") + set_tests_properties(unwrap_self_${unwrap_self_class}_${test_case} PROPERTIES TIMEOUT 30) + endif() + endif() + endforeach() add_test(NAME tools_headers_manifest COMMAND ${Python3_EXECUTABLE} ${CMAKE_SOURCE_DIR}/tools/tests/test_public_headers_manifest.py) set_tests_properties(tools_headers_manifest PROPERTIES TIMEOUT 20) add_test(NAME conventions_pr_template COMMAND ${Python3_EXECUTABLE} diff --git a/README.md b/README.md index 9b19686b40..5c7d958438 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -**English** | [Português (Brasil)](docs/user/README.pt-BR.md) | [Español](docs/user/README.es.md) | [简体中文](docs/user/README.zh-CN.md) | [Русский](docs/user/README.ru.md) +**English** | [Português (Brasil)](docs/user/README.pt-BR.md) | [Español](docs/user/README.es.md) | [Italiano](docs/user/README.it.md) | [简体中文](docs/user/README.zh-CN.md) | [Русский](docs/user/README.ru.md) # About diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index d132628e46..7230150c09 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -302,6 +302,8 @@ include(prx/libSceFreeType/BuildInfo.cmake) add_custom_target(libs DEPENDS ${PATCHED_OUTPUTS}) include(prx/libSceLibreSsl/ErrorQueue.cmake) +include(prx/libSceLibreSsl/MemoryBio.cmake) +include(prx/libSceLibreSsl/Cmac.cmake) include(prx/libSceLibreSsl/ProtocolSelection.cmake) @@ -361,6 +363,16 @@ if(BUILD_TESTING) set_tests_properties(guest_kernel_sys_neo_mode PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_kernel_sys_constant_unknowns_tests tests/GuestKernelSysConstantUnknowns.cpp) + target_include_directories(guest_kernel_sys_constant_unknowns_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_kernel_sys_constant_unknowns_tests PRIVATE libkernel_sys libkernel libc) + configure_windows_unwind(guest_kernel_sys_constant_unknowns_tests) + add_test(NAME guest_kernel_sys_constant_unknowns COMMAND guest_kernel_sys_constant_unknowns_tests) + set_tests_properties(guest_kernel_sys_constant_unknowns PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_kernel_sys_constant_unknowns PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(nid_buffer_validation_tests nid/tests/BufferValidation.cpp) target_include_directories(nid_buffer_validation_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/nid/include) target_compile_features(nid_buffer_validation_tests PRIVATE cxx_std_20) @@ -687,6 +699,13 @@ if(BUILD_TESTING) endif() if(WIN32) + add_test_executable(windows_crash_thread_name_tests tests/WindowsCrashThreadName.cpp) + add_dependencies(windows_crash_thread_name_tests libc) + configure_host_test_executable(windows_crash_thread_name_tests) + add_test(NAME windows_crash_thread_name COMMAND windows_crash_thread_name_tests $) + set_tests_properties(windows_crash_thread_name PROPERTIES TIMEOUT 20 + PASS_REGULAR_EXPRESSION "on thread [0-9]+ 'a+Z'[^~]*thread [0-9]+ 'c+Y':" + FAIL_REGULAR_EXPRESSION "Zb|Yd|did not fault") add_test_executable(windows_crash_text_tests tests/WindowsCrashText.cpp) add_dependencies(windows_crash_text_tests libc) if(MINGW) @@ -696,6 +715,13 @@ if(BUILD_TESTING) set_tests_properties(windows_crash_text PROPERTIES TIMEOUT 20 PASS_REGULAR_EXPRESSION "faulting[^~]*FATAL: unhandled exception 0xc0000005[^~]* \\[r12\\] \"allocation failed in the heap\"[\r\n]+ \\[r13\\] u\"LowLevelFatalError \\[File:Unknown\\] \\[Line: 42\\] Out of memory\"[\r\n]+ \\[r14\\] \"PAGE END\"[.][.][.]" FAIL_REGULAR_EXPRESSION "\\[rbx\\] u?\"|did not fault") + add_test_executable(windows_integer_overflow_report_tests tests/WindowsIntegerOverflowReport.cpp) + add_dependencies(windows_integer_overflow_report_tests libc) + configure_host_test_executable(windows_integer_overflow_report_tests) + add_test(NAME windows_integer_overflow_report COMMAND windows_integer_overflow_report_tests $) + set_tests_properties(windows_integer_overflow_report PROPERTIES TIMEOUT 20 + PASS_REGULAR_EXPRESSION "dividing[^~]*FATAL: unhandled exception 0xc0000095" + FAIL_REGULAR_EXPRESSION "did not fault") endif() add_test_executable(guest_voice_port_attr_tests tests/GuestVoicePortAttr.cpp) @@ -793,7 +819,7 @@ if(BUILD_TESTING) target_include_directories(guest_np_common_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_np_common_tests PRIVATE libSceNpCommon libkernel libc) configure_windows_unwind(guest_np_common_tests) - foreach(testCase IN ITEMS ids sync clock) + foreach(testCase IN ITEMS ids sync clock wake) add_test(NAME guest_np_common_${testCase} COMMAND guest_np_common_tests ${testCase}) set_tests_properties(guest_np_common_${testCase} PROPERTIES TIMEOUT 20) if(WIN32) @@ -1116,6 +1142,16 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_test_executable(guest_np_cpp_web_api_matches_tests tests/GuestNpCppWebApiMatches.cpp) + target_include_directories(guest_np_cpp_web_api_matches_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_np_cpp_web_api_matches_tests PRIVATE libSceNpCppWebApi libc) + configure_windows_unwind(guest_np_cpp_web_api_matches_tests) + add_test(NAME guest_np_cpp_web_api_matches COMMAND guest_np_cpp_web_api_matches_tests) + if(WIN32) + set_tests_properties(guest_np_cpp_web_api_matches PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(savedata_native_write_replacement_tests tests/SaveDataWriteReplacement.cpp) target_include_directories(savedata_native_write_replacement_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(savedata_native_write_replacement_tests PRIVATE libSceSaveData.native) @@ -1304,6 +1340,15 @@ if(BUILD_TESTING) set_tests_properties(guest_gnm_neo_default_state PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_division_prx_abi_tests tests/GuestDivisionPrxAbi.cpp nid/src/NidCompute.cpp nid/src/Sha1.cpp) + target_include_directories(guest_division_prx_abi_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/nid/include) + configure_host_test_executable(guest_division_prx_abi_tests) + target_link_libraries(guest_division_prx_abi_tests PRIVATE ${CMAKE_DL_LIBS}) + add_custom_target(guest_division_prx_library DEPENDS ${LIBC_PATCHED}) + add_dependencies(guest_division_prx_abi_tests guest_division_prx_library) + add_test(NAME guest_division_prx_abi COMMAND guest_division_prx_abi_tests $ ${PATCHED_DIR}/libc.prx) + set_tests_properties(guest_division_prx_abi PROPERTIES TIMEOUT 20) + endif() include(prx/libSceFreeType/CurveBounds.cmake) diff --git a/core/libs/SceTypes.hpp b/core/libs/SceTypes.hpp index 179c84a095..5c175ffbf3 100644 --- a/core/libs/SceTypes.hpp +++ b/core/libs/SceTypes.hpp @@ -1820,6 +1820,8 @@ struct SaveDataEvent { std::uint8_t reserved[40]; }; +using SaveDataEventCallback = void (APS5_VABI *)(const SaveDataEvent* event, void* userdata); + struct SaveDataBackup { std::int32_t user_id; std::int32_t pad; @@ -1829,6 +1831,40 @@ struct SaveDataBackup { std::uint8_t reserved[32]; }; +struct SaveDataCheckBackupData { + std::int32_t user_id; + std::int32_t pad; + const SceSaveDataTitleId* title_id; + const SceSaveDataDirName* dir_name; + SaveDataParam* param; + SaveDataIcon* icon; + std::uint8_t reserved[32]; +}; + +struct SaveDataRestoreBackupData { + std::int32_t user_id; + std::int32_t pad; + const SceSaveDataTitleId* title_id; + const SceSaveDataDirName* dir_name; + const void* fingerprint; + std::uint32_t unused; + std::uint8_t reserved[32]; + std::int32_t pad2; +}; +static_assert(offsetof(SaveDataCheckBackupData, title_id) == 0x08); +static_assert(offsetof(SaveDataCheckBackupData, dir_name) == 0x10); +static_assert(offsetof(SaveDataCheckBackupData, param) == 0x18); +static_assert(offsetof(SaveDataCheckBackupData, icon) == 0x20); +static_assert(offsetof(SaveDataCheckBackupData, reserved) == 0x28); +static_assert(sizeof(SaveDataCheckBackupData) == 0x48); +static_assert(offsetof(SaveDataRestoreBackupData, title_id) == 0x08); +static_assert(offsetof(SaveDataRestoreBackupData, dir_name) == 0x10); +static_assert(offsetof(SaveDataRestoreBackupData, fingerprint) == 0x18); +static_assert(offsetof(SaveDataRestoreBackupData, unused) == 0x20); +static_assert(offsetof(SaveDataRestoreBackupData, reserved) == 0x24); +static_assert(offsetof(SaveDataRestoreBackupData, pad2) == 0x44); +static_assert(sizeof(SaveDataRestoreBackupData) == 0x48); + struct SaveDataConvert { std::int32_t user_id; std::int32_t pad; diff --git a/core/libs/nid/src/MachONidPatcher.cpp b/core/libs/nid/src/MachONidPatcher.cpp index 385d6298b3..a2263a97ea 100644 --- a/core/libs/nid/src/MachONidPatcher.cpp +++ b/core/libs/nid/src/MachONidPatcher.cpp @@ -188,7 +188,10 @@ std::optional FindTrie(const std::vector& binary, co return found; } -void ParseTrieNode(const std::vector& data, std::size_t start, std::size_t end, std::size_t node, std::string& prefix, std::set& visited, std::vector& out) { +constexpr std::size_t kMaxExportTrieDepth = 1024; + +void ParseTrieNode(const std::vector& data, std::size_t start, std::size_t end, std::size_t node, std::string& prefix, std::set& visited, std::vector& out, std::size_t depth) { + if (depth > kMaxExportTrieDepth) throw std::runtime_error("export trie nesting too deep"); if (node >= end - start) throw std::runtime_error("export trie node out of bounds"); if (!visited.insert(node).second) throw std::runtime_error("export trie has a cycle"); std::size_t cursor = start + node; @@ -216,7 +219,7 @@ void ParseTrieNode(const std::vector& data, std::size_t start, std const auto child = ReadUleb(data, cursor, end); const auto length = prefix.size(); prefix += label; - ParseTrieNode(data, start, end, static_cast(child), prefix, visited, out); + ParseTrieNode(data, start, end, static_cast(child), prefix, visited, out, depth + 1); prefix.resize(length); } } @@ -227,7 +230,7 @@ std::vector ParseTrie(const std::vector& binary, std: if (size == 0) return exports; std::string prefix; std::set visited; - ParseTrieNode(binary, offset, std::size_t{offset} + size, 0, prefix, visited, exports); + ParseTrieNode(binary, offset, std::size_t{offset} + size, 0, prefix, visited, exports, 0); return exports; } diff --git a/core/libs/nid/src/PeNidPatcher.cpp b/core/libs/nid/src/PeNidPatcher.cpp index e88c64d8a3..1e04d02937 100644 --- a/core/libs/nid/src/PeNidPatcher.cpp +++ b/core/libs/nid/src/PeNidPatcher.cpp @@ -99,6 +99,9 @@ void PeNidPatcher::PatchNids(std::vector& pe, const std::string& l const std::size_t namesArrayOffset = RvaToOffset(pe, exportTable.AddressOfNames, peHeaderOffset, numberOfSections, sizeOfOptionalHeader); const std::size_t ordinalsArrayOffset = RvaToOffset(pe, exportTable.AddressOfNameOrdinals, peHeaderOffset, numberOfSections, sizeOfOptionalHeader); + if (std::uint64_t{namesArrayOffset} + std::uint64_t{exportTable.NumberOfNames} * 4u > pe.size() || + std::uint64_t{ordinalsArrayOffset} + std::uint64_t{exportTable.NumberOfNames} * 2u > pe.size()) + throw std::runtime_error("export name tables out of file bounds"); std::vector nameRvas(exportTable.NumberOfNames); std::vector names(exportTable.NumberOfNames); diff --git a/core/libs/nid/tests/test_macho_nids.py b/core/libs/nid/tests/test_macho_nids.py index 71853aefac..e3c05d0fb6 100644 --- a/core/libs/nid/tests/test_macho_nids.py +++ b/core/libs/nid/tests/test_macho_nids.py @@ -117,8 +117,8 @@ def dylib_command(path): return struct.pack("> (7 * index)) & 0x7F) | 0x80 for index in range(4)] + [(value >> 28) & 0x7F]) + + node_size = 9 + nodes = [b"\x00\x01a\x00" + fixed_uleb(node_size * (index + 1)) for index in range(depth - 1)] + nodes.append(b"\x02\x00\x00\x00") + return b"".join(nodes) + bytes(-sum(map(len, nodes)) % 8) + + +def check_deep_trie(patcher, directory): + image = build_dylib([("_puts_nid_postfix", 0, 0x100)], [], trie=chain_trie(200000)) + result, _ = patch(patcher, directory, "deep-trie.dylib", image) + assert result.returncode != 0 and "export trie nesting too deep" in result.stdout + result.stderr, (result.returncode, result.stdout, result.stderr) + + def check_short_imports(patcher, directory): imports = [("_sceXy", 1), ("_malloc", 2), ("_sceKernelFoo", 1), ("_sceNetRecv", 1)] result, path = patch(patcher, directory, "short.dylib", build_dylib([("_puts_nid_postfix", 0, 0x100)], imports)) @@ -297,6 +313,7 @@ def main(): check_preserved(patcher, directory) check_export_list(patcher, directory) check_refusals(patcher, directory) + check_deep_trie(patcher, directory) print("NID patcher Mach-O tests passed") diff --git a/core/libs/nid/tests/test_pe_references.py b/core/libs/nid/tests/test_pe_references.py index e7ec3ba572..2c04c4f489 100644 --- a/core/libs/nid/tests/test_pe_references.py +++ b/core/libs/nid/tests/test_pe_references.py @@ -2,6 +2,7 @@ import struct import subprocess import sys import tempfile +import time import unittest from pathlib import Path from types import SimpleNamespace @@ -564,6 +565,18 @@ class LibraryImportTests(unittest.TestCase): self.assertIn("delay-loaded", result.stdout + result.stderr) self.assertEqual(patched, image) + def test_huge_name_count_is_rejected_before_allocation(self): + for count in (0xFFFFFFFF, 0x40000000): + with self.subTest(count=count): + image = self.library(["puts"]) + struct.pack_into(" +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libMozaSDK/libMozaSDK.hpp b/core/libs/prx/libMozaSDK/libMozaSDK.hpp new file mode 100644 index 0000000000..6f70f09bee --- /dev/null +++ b/core/libs/prx/libMozaSDK/libMozaSDK.hpp @@ -0,0 +1 @@ +#pragma once diff --git a/core/libs/prx/libReactNative.Modules.Vsh.WebAppLauncher/Export.cpp b/core/libs/prx/libReactNative.Modules.Vsh.WebAppLauncher/Export.cpp new file mode 100644 index 0000000000..2186d0d301 --- /dev/null +++ b/core/libs/prx/libReactNative.Modules.Vsh.WebAppLauncher/Export.cpp @@ -0,0 +1,251 @@ +#include "prx/libc/include/general/ExportMacros.hpp" +#include "prx/libReactNative.Modules.Vsh.WebAppLauncher/include/InventoryExports.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI CheckSystemEvents() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI CommerceDialogBrowseCategory() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI CommerceDialogBrowseProduct() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI CommerceDialogInit() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI CommerceDialogRedeemPromotionCode() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI CommerceHidePsStoreIcon() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI CommerceSetPsStoreIconLayout() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI CommerceShowPsStoreIcon() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI DisableSystemMedia() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI ErrorDialogClose() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI ErrorDialogInit() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI ErrorDialogOpen() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI GetAccountAgeA() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI GetAccountCountryA() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI GetAccountIdA() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI GetAccountLanguage2() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI GetAuthorizationCode() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI GetClosedCaptionSettings() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI GetOnlineId() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI GetProcessTime() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI GetVideoOutputStatus() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI GetVideoOutputStatusInt() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI Hide() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI IsDebuggingSupported() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI NpWebAbortRequest() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI NpWebDeleteRequest() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI NpWebInitialize() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI NpWebReadData() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI NpWebTerminate() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI OpenClosedCaptionAdvancedSettings() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI OpenClosedCaptionSettings() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI SoundInitialize() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI SoundLoad() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI SoundPlay() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI SoundTerminate() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI SpeechCancel() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI SpeechClose() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI SpeechInitialize() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI SpeechSpeak() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI SpeechStatus() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI SpeechTerminate() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI SystemDialogInit() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI SystemDialogOpen() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI SystemServiceParamGetInt() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI SystemServiceParamGetString() { + NotImplemented_nid_no_patch(__func__); +} + +} + +extern "C" { + +int APS5_VABI libReactNativeWebAppLauncherInventory__plus_ucrAt9fI7c_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+ucrAt9fI7c", libReactNativeWebAppLauncherInventory__plus_ucrAt9fI7c_nid_no_patch); + +int APS5_VABI libReactNativeWebAppLauncherInventory_06N_minus_dAyE_plus_HY_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("06N-dAyE+HY", libReactNativeWebAppLauncherInventory_06N_minus_dAyE_plus_HY_nid_no_patch); + +int APS5_VABI libReactNativeWebAppLauncherInventory_41blU2JCqLQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("41blU2JCqLQ", libReactNativeWebAppLauncherInventory_41blU2JCqLQ_nid_no_patch); + +int APS5_VABI libReactNativeWebAppLauncherInventory_BGgSEXB1Now_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("BGgSEXB1Now", libReactNativeWebAppLauncherInventory_BGgSEXB1Now_nid_no_patch); + +int APS5_VABI libReactNativeWebAppLauncherInventory_CAK_minus_S2JVdsk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("CAK-S2JVdsk", libReactNativeWebAppLauncherInventory_CAK_minus_S2JVdsk_nid_no_patch); + +int APS5_VABI libReactNativeWebAppLauncherInventory_QxCdpkEsOgo_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("QxCdpkEsOgo", libReactNativeWebAppLauncherInventory_QxCdpkEsOgo_nid_no_patch); + +int APS5_VABI libReactNativeWebAppLauncherInventory_Tjj_plus__plus_S2qvOU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("Tjj++S2qvOU", libReactNativeWebAppLauncherInventory_Tjj_plus__plus_S2qvOU_nid_no_patch); + +int APS5_VABI libReactNativeWebAppLauncherInventory_f5NHqUziG_plus_4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("f5NHqUziG+4", libReactNativeWebAppLauncherInventory_f5NHqUziG_plus_4_nid_no_patch); + +int APS5_VABI libReactNativeWebAppLauncherInventory_mI_minus_ltMuVN_plus_Q_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("mI-ltMuVN+Q", libReactNativeWebAppLauncherInventory_mI_minus_ltMuVN_plus_Q_nid_no_patch); + +int APS5_VABI libReactNativeWebAppLauncherInventory_vDup_plus_0mWZf8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("vDup+0mWZf8", libReactNativeWebAppLauncherInventory_vDup_plus_0mWZf8_nid_no_patch); + +int APS5_VABI libReactNativeWebAppLauncherInventory_wR1ucKGDM9Q_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("wR1ucKGDM9Q", libReactNativeWebAppLauncherInventory_wR1ucKGDM9Q_nid_no_patch); + +int APS5_VABI libReactNativeWebAppLauncherInventory_xPA_minus_tXzPGpc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("xPA-tXzPGpc", libReactNativeWebAppLauncherInventory_xPA_minus_tXzPGpc_nid_no_patch); + +} diff --git a/core/libs/prx/libReactNative.Modules.Vsh.WebAppLauncher/include/InventoryExports.hpp b/core/libs/prx/libReactNative.Modules.Vsh.WebAppLauncher/include/InventoryExports.hpp new file mode 100644 index 0000000000..d84245abdb --- /dev/null +++ b/core/libs/prx/libReactNative.Modules.Vsh.WebAppLauncher/include/InventoryExports.hpp @@ -0,0 +1,67 @@ +#pragma once + +#include "SceTypes.hpp" + +extern "C" { + +int APS5_VABI CheckSystemEvents(); +int APS5_VABI CommerceDialogBrowseCategory(); +int APS5_VABI CommerceDialogBrowseProduct(); +int APS5_VABI CommerceDialogInit(); +int APS5_VABI CommerceDialogRedeemPromotionCode(); +int APS5_VABI CommerceHidePsStoreIcon(); +int APS5_VABI CommerceSetPsStoreIconLayout(); +int APS5_VABI CommerceShowPsStoreIcon(); +int APS5_VABI DisableSystemMedia(); +int APS5_VABI ErrorDialogClose(); +int APS5_VABI ErrorDialogInit(); +int APS5_VABI ErrorDialogOpen(); +int APS5_VABI GetAccountAgeA(); +int APS5_VABI GetAccountCountryA(); +int APS5_VABI GetAccountIdA(); +int APS5_VABI GetAccountLanguage2(); +int APS5_VABI GetAuthorizationCode(); +int APS5_VABI GetClosedCaptionSettings(); +int APS5_VABI GetOnlineId(); +int APS5_VABI GetProcessTime(); +int APS5_VABI GetVideoOutputStatus(); +int APS5_VABI GetVideoOutputStatusInt(); +int APS5_VABI Hide(); +int APS5_VABI IsDebuggingSupported(); +int APS5_VABI NpWebAbortRequest(); +int APS5_VABI NpWebDeleteRequest(); +int APS5_VABI NpWebInitialize(); +int APS5_VABI NpWebReadData(); +int APS5_VABI NpWebTerminate(); +int APS5_VABI OpenClosedCaptionAdvancedSettings(); +int APS5_VABI OpenClosedCaptionSettings(); +int APS5_VABI SoundInitialize(); +int APS5_VABI SoundLoad(); +int APS5_VABI SoundPlay(); +int APS5_VABI SoundTerminate(); +int APS5_VABI SpeechCancel(); +int APS5_VABI SpeechClose(); +int APS5_VABI SpeechInitialize(); +int APS5_VABI SpeechSpeak(); +int APS5_VABI SpeechStatus(); +int APS5_VABI SpeechTerminate(); +int APS5_VABI SystemDialogInit(); +int APS5_VABI SystemDialogOpen(); +int APS5_VABI SystemServiceParamGetInt(); +int APS5_VABI SystemServiceParamGetString(); + + +int APS5_VABI libReactNativeWebAppLauncherInventory__plus_ucrAt9fI7c_nid_no_patch(); +int APS5_VABI libReactNativeWebAppLauncherInventory_06N_minus_dAyE_plus_HY_nid_no_patch(); +int APS5_VABI libReactNativeWebAppLauncherInventory_41blU2JCqLQ_nid_no_patch(); +int APS5_VABI libReactNativeWebAppLauncherInventory_BGgSEXB1Now_nid_no_patch(); +int APS5_VABI libReactNativeWebAppLauncherInventory_CAK_minus_S2JVdsk_nid_no_patch(); +int APS5_VABI libReactNativeWebAppLauncherInventory_QxCdpkEsOgo_nid_no_patch(); +int APS5_VABI libReactNativeWebAppLauncherInventory_Tjj_plus__plus_S2qvOU_nid_no_patch(); +int APS5_VABI libReactNativeWebAppLauncherInventory_f5NHqUziG_plus_4_nid_no_patch(); +int APS5_VABI libReactNativeWebAppLauncherInventory_mI_minus_ltMuVN_plus_Q_nid_no_patch(); +int APS5_VABI libReactNativeWebAppLauncherInventory_vDup_plus_0mWZf8_nid_no_patch(); +int APS5_VABI libReactNativeWebAppLauncherInventory_wR1ucKGDM9Q_nid_no_patch(); +int APS5_VABI libReactNativeWebAppLauncherInventory_xPA_minus_tXzPGpc_nid_no_patch(); + +} diff --git a/core/libs/prx/libReactNative.Modules.Vsh.WebAppLauncher/libReactNative.Modules.Vsh.WebAppLauncher.hpp b/core/libs/prx/libReactNative.Modules.Vsh.WebAppLauncher/libReactNative.Modules.Vsh.WebAppLauncher.hpp new file mode 100644 index 0000000000..4f7d8e0467 --- /dev/null +++ b/core/libs/prx/libReactNative.Modules.Vsh.WebAppLauncher/libReactNative.Modules.Vsh.WebAppLauncher.hpp @@ -0,0 +1,3 @@ +#pragma once + +#include "prx/libReactNative.Modules.Vsh.WebAppLauncher/include/InventoryExports.hpp" diff --git a/core/libs/prx/libReactNative.Modules.Vsh/Export.cpp b/core/libs/prx/libReactNative.Modules.Vsh/Export.cpp new file mode 100644 index 0000000000..058f793c0f --- /dev/null +++ b/core/libs/prx/libReactNative.Modules.Vsh/Export.cpp @@ -0,0 +1,361 @@ +#include "prx/libc/include/general/ExportMacros.hpp" +#include "prx/libReactNative.Modules.Vsh/include/InventoryExports.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI CreateWebApiLibCtx() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI CryptographyDecryptAES128CBC() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI CryptographyDecryptAES128CBC_HS256() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI CryptographyDecryptAES128GCM() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI CryptographyEncryptAES128CBC() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI CryptographyEncryptAES128GCM() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI CryptographyGenerateHmacSha256() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI DestroyWebApiLibCtx() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI GetLibHttpCtxId() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI GetPs4AppCategoryForTitleId() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI InetStatGetResultNative() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI InetStatGetStateNative() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI InetStatStartTestNative() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI InitializeNetLib() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI LoginMgrCreateUser() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI LoginMgrDestroyUser() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI LoginMgrInitialize() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI LoginMgrSetLoginFlag() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI LoginMgrSetUserStatus() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI LoginMgrTerminate() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI NpStateManagerRegisterCallback() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI NpStateManagerRtcSetTick() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI NpStateManagerStart() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI NpStateManagerStop() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI NpStateManagerUnregisterCallback() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI PSNowGetStatus() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI PSNowGetStreamingStatus() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI PSNowInitialize() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI PSNowReset() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI PSNowSendCommand() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI PSNowSetAuthCodes() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI PSNowSetSettings() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI PSNowTerminate() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI RemotePlayClearConnectHistory() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI RemotePlayConfirmDeviceRegist() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI RemotePlayDisconnect() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI RemotePlayGeneratePinCode() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI RemotePlayGetConnectHistory() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI RemotePlayGetConnectUserId() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI RemotePlayGetConnectionStatus() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI RemotePlayGetRemoteplayStatus() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI RemotePlayGetRpMode() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI RemotePlayIsRemotePlaying() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI RemotePlayNotifyCanWakeup() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI RemotePlayNotifyPinCodeError() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI RemotePlayNotifyUserDelete() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI RemotePlayProhibitStreaming() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI RemotePlaySetProhibition() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI RemotePlaySetProhibitionForVsh() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI RemotePlaySetRpMode() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI RtcGetCurrentAdNetworkTickNative() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI RtcGetCurrentDebugNetworkTickNative() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI RtcGetCurrentNetworkTickNative() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI TerminateNetLib() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI WebBrowserGetEnvelopeData() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceRemotePlayClientAddEventListener() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceRemotePlayClientAttachVirtualMicrophone() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceRemotePlayClientDetachVirtualMicrophone() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceRemotePlayClientGetClientStatus() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceRemotePlayClientInitialize() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceRemotePlayClientRemoveController() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceRemotePlayClientShowHomeScreen() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceRemotePlayClientStartMicCapture() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceRemotePlayClientStopMicCapture() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceRemotePlayClientTerminateStreaming() { + NotImplemented_nid_no_patch(__func__); +} + +} + +extern "C" { + +int APS5_VABI libReactNativeModulesVshInventory__plus_I5sPs9FLXw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+I5sPs9FLXw", libReactNativeModulesVshInventory__plus_I5sPs9FLXw_nid_no_patch); + +int APS5_VABI libReactNativeModulesVshInventory__minus_9hYGDAsb4E_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-9hYGDAsb4E", libReactNativeModulesVshInventory__minus_9hYGDAsb4E_nid_no_patch); + +int APS5_VABI libReactNativeModulesVshInventory_BzJsgzD97LU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("BzJsgzD97LU", libReactNativeModulesVshInventory_BzJsgzD97LU_nid_no_patch); + +int APS5_VABI libReactNativeModulesVshInventory_IQNf2nnftNI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("IQNf2nnftNI", libReactNativeModulesVshInventory_IQNf2nnftNI_nid_no_patch); + +int APS5_VABI libReactNativeModulesVshInventory_N3mcm_minus_qDFR0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("N3mcm-qDFR0", libReactNativeModulesVshInventory_N3mcm_minus_qDFR0_nid_no_patch); + +int APS5_VABI libReactNativeModulesVshInventory_OME2Be_plus_FlwA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("OME2Be+FlwA", libReactNativeModulesVshInventory_OME2Be_plus_FlwA_nid_no_patch); + +int APS5_VABI libReactNativeModulesVshInventory_PLEe_minus_toB_minus_KE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("PLEe-toB-KE", libReactNativeModulesVshInventory_PLEe_minus_toB_minus_KE_nid_no_patch); + +int APS5_VABI libReactNativeModulesVshInventory_QGGsQrtLW9Y_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("QGGsQrtLW9Y", libReactNativeModulesVshInventory_QGGsQrtLW9Y_nid_no_patch); + +int APS5_VABI libReactNativeModulesVshInventory_Rm_plus_rgnBoPIE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("Rm+rgnBoPIE", libReactNativeModulesVshInventory_Rm_plus_rgnBoPIE_nid_no_patch); + +int APS5_VABI libReactNativeModulesVshInventory_SJU_plus_qK_plus_8yrM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("SJU+qK+8yrM", libReactNativeModulesVshInventory_SJU_plus_qK_plus_8yrM_nid_no_patch); + +int APS5_VABI libReactNativeModulesVshInventory_T7dL_minus_hEySvU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("T7dL-hEySvU", libReactNativeModulesVshInventory_T7dL_minus_hEySvU_nid_no_patch); + +int APS5_VABI libReactNativeModulesVshInventory_b_plus_MJDz3HB6s_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("b+MJDz3HB6s", libReactNativeModulesVshInventory_b_plus_MJDz3HB6s_nid_no_patch); + +int APS5_VABI libReactNativeModulesVshInventory_bbsP8_minus_e8GZA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("bbsP8-e8GZA", libReactNativeModulesVshInventory_bbsP8_minus_e8GZA_nid_no_patch); + +int APS5_VABI libReactNativeModulesVshInventory_fxEQAPDRhLw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("fxEQAPDRhLw", libReactNativeModulesVshInventory_fxEQAPDRhLw_nid_no_patch); + +int APS5_VABI libReactNativeModulesVshInventory_j917Pp4k_minus_CI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("j917Pp4k-CI", libReactNativeModulesVshInventory_j917Pp4k_minus_CI_nid_no_patch); + +int APS5_VABI libReactNativeModulesVshInventory_lZ_minus_JDEqB34k_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("lZ-JDEqB34k", libReactNativeModulesVshInventory_lZ_minus_JDEqB34k_nid_no_patch); + +int APS5_VABI libReactNativeModulesVshInventory_tKy_minus_Qdg_plus_FRc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("tKy-Qdg+FRc", libReactNativeModulesVshInventory_tKy_minus_Qdg_plus_FRc_nid_no_patch); + +int APS5_VABI libReactNativeModulesVshInventory_uQfQgwm36L4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("uQfQgwm36L4", libReactNativeModulesVshInventory_uQfQgwm36L4_nid_no_patch); + +} diff --git a/core/libs/prx/libReactNative.Modules.Vsh/include/InventoryExports.hpp b/core/libs/prx/libReactNative.Modules.Vsh/include/InventoryExports.hpp new file mode 100644 index 0000000000..5062f04693 --- /dev/null +++ b/core/libs/prx/libReactNative.Modules.Vsh/include/InventoryExports.hpp @@ -0,0 +1,93 @@ +#pragma once + +#include "SceTypes.hpp" + +extern "C" { + +int APS5_VABI CreateWebApiLibCtx(); +int APS5_VABI CryptographyDecryptAES128CBC(); +int APS5_VABI CryptographyDecryptAES128CBC_HS256(); +int APS5_VABI CryptographyDecryptAES128GCM(); +int APS5_VABI CryptographyEncryptAES128CBC(); +int APS5_VABI CryptographyEncryptAES128GCM(); +int APS5_VABI CryptographyGenerateHmacSha256(); +int APS5_VABI DestroyWebApiLibCtx(); +int APS5_VABI GetLibHttpCtxId(); +int APS5_VABI GetPs4AppCategoryForTitleId(); +int APS5_VABI InetStatGetResultNative(); +int APS5_VABI InetStatGetStateNative(); +int APS5_VABI InetStatStartTestNative(); +int APS5_VABI InitializeNetLib(); +int APS5_VABI LoginMgrCreateUser(); +int APS5_VABI LoginMgrDestroyUser(); +int APS5_VABI LoginMgrInitialize(); +int APS5_VABI LoginMgrSetLoginFlag(); +int APS5_VABI LoginMgrSetUserStatus(); +int APS5_VABI LoginMgrTerminate(); +int APS5_VABI NpStateManagerRegisterCallback(); +int APS5_VABI NpStateManagerRtcSetTick(); +int APS5_VABI NpStateManagerStart(); +int APS5_VABI NpStateManagerStop(); +int APS5_VABI NpStateManagerUnregisterCallback(); +int APS5_VABI PSNowGetStatus(); +int APS5_VABI PSNowGetStreamingStatus(); +int APS5_VABI PSNowInitialize(); +int APS5_VABI PSNowReset(); +int APS5_VABI PSNowSendCommand(); +int APS5_VABI PSNowSetAuthCodes(); +int APS5_VABI PSNowSetSettings(); +int APS5_VABI PSNowTerminate(); +int APS5_VABI RemotePlayClearConnectHistory(); +int APS5_VABI RemotePlayConfirmDeviceRegist(); +int APS5_VABI RemotePlayDisconnect(); +int APS5_VABI RemotePlayGeneratePinCode(); +int APS5_VABI RemotePlayGetConnectHistory(); +int APS5_VABI RemotePlayGetConnectUserId(); +int APS5_VABI RemotePlayGetConnectionStatus(); +int APS5_VABI RemotePlayGetRemoteplayStatus(); +int APS5_VABI RemotePlayGetRpMode(); +int APS5_VABI RemotePlayIsRemotePlaying(); +int APS5_VABI RemotePlayNotifyCanWakeup(); +int APS5_VABI RemotePlayNotifyPinCodeError(); +int APS5_VABI RemotePlayNotifyUserDelete(); +int APS5_VABI RemotePlayProhibitStreaming(); +int APS5_VABI RemotePlaySetProhibition(); +int APS5_VABI RemotePlaySetProhibitionForVsh(); +int APS5_VABI RemotePlaySetRpMode(); +int APS5_VABI RtcGetCurrentAdNetworkTickNative(); +int APS5_VABI RtcGetCurrentDebugNetworkTickNative(); +int APS5_VABI RtcGetCurrentNetworkTickNative(); +int APS5_VABI TerminateNetLib(); +int APS5_VABI WebBrowserGetEnvelopeData(); +int APS5_VABI sceRemotePlayClientAddEventListener(); +int APS5_VABI sceRemotePlayClientAttachVirtualMicrophone(); +int APS5_VABI sceRemotePlayClientDetachVirtualMicrophone(); +int APS5_VABI sceRemotePlayClientGetClientStatus(); +int APS5_VABI sceRemotePlayClientInitialize(); +int APS5_VABI sceRemotePlayClientRemoveController(); +int APS5_VABI sceRemotePlayClientShowHomeScreen(); +int APS5_VABI sceRemotePlayClientStartMicCapture(); +int APS5_VABI sceRemotePlayClientStopMicCapture(); +int APS5_VABI sceRemotePlayClientTerminateStreaming(); + + +int APS5_VABI libReactNativeModulesVshInventory__plus_I5sPs9FLXw_nid_no_patch(); +int APS5_VABI libReactNativeModulesVshInventory__minus_9hYGDAsb4E_nid_no_patch(); +int APS5_VABI libReactNativeModulesVshInventory_BzJsgzD97LU_nid_no_patch(); +int APS5_VABI libReactNativeModulesVshInventory_IQNf2nnftNI_nid_no_patch(); +int APS5_VABI libReactNativeModulesVshInventory_N3mcm_minus_qDFR0_nid_no_patch(); +int APS5_VABI libReactNativeModulesVshInventory_OME2Be_plus_FlwA_nid_no_patch(); +int APS5_VABI libReactNativeModulesVshInventory_PLEe_minus_toB_minus_KE_nid_no_patch(); +int APS5_VABI libReactNativeModulesVshInventory_QGGsQrtLW9Y_nid_no_patch(); +int APS5_VABI libReactNativeModulesVshInventory_Rm_plus_rgnBoPIE_nid_no_patch(); +int APS5_VABI libReactNativeModulesVshInventory_SJU_plus_qK_plus_8yrM_nid_no_patch(); +int APS5_VABI libReactNativeModulesVshInventory_T7dL_minus_hEySvU_nid_no_patch(); +int APS5_VABI libReactNativeModulesVshInventory_b_plus_MJDz3HB6s_nid_no_patch(); +int APS5_VABI libReactNativeModulesVshInventory_bbsP8_minus_e8GZA_nid_no_patch(); +int APS5_VABI libReactNativeModulesVshInventory_fxEQAPDRhLw_nid_no_patch(); +int APS5_VABI libReactNativeModulesVshInventory_j917Pp4k_minus_CI_nid_no_patch(); +int APS5_VABI libReactNativeModulesVshInventory_lZ_minus_JDEqB34k_nid_no_patch(); +int APS5_VABI libReactNativeModulesVshInventory_tKy_minus_Qdg_plus_FRc_nid_no_patch(); +int APS5_VABI libReactNativeModulesVshInventory_uQfQgwm36L4_nid_no_patch(); + +} diff --git a/core/libs/prx/libReactNative.Modules.Vsh/libReactNative.Modules.Vsh.hpp b/core/libs/prx/libReactNative.Modules.Vsh/libReactNative.Modules.Vsh.hpp new file mode 100644 index 0000000000..3fb0db9ae4 --- /dev/null +++ b/core/libs/prx/libReactNative.Modules.Vsh/libReactNative.Modules.Vsh.hpp @@ -0,0 +1,3 @@ +#pragma once + +#include "prx/libReactNative.Modules.Vsh/include/InventoryExports.hpp" diff --git a/core/libs/prx/libSceAbstractStorage/Export.cpp b/core/libs/prx/libSceAbstractStorage/Export.cpp index 061e032714..d7f544313d 100644 --- a/core/libs/prx/libSceAbstractStorage/Export.cpp +++ b/core/libs/prx/libSceAbstractStorage/Export.cpp @@ -1,3 +1,4 @@ +#include "prx/libSceAbstractStorage/include/Api.hpp" #include "prx/libSceAbstractStorage/include/LegacyExports.hpp" #include "prx/libc/include/general/ExportMacros.hpp" #include "prx/libSceAbstractStorage/include/InventoryExports.hpp" @@ -9,555 +10,445 @@ extern "C" { -int APS5_VABI sceAbstractStorageUnknown_7RnkdybfYiQ_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage12Serializable12readExternalESt10shared_ptrINS_7ContentEE() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("7RnkdybfYiQ", sceAbstractStorageUnknown_7RnkdybfYiQ_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_gINcKTbrqk4_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage12Serializable13writeExternalESt10shared_ptrINS_7ContentEE() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("gINcKTbrqk4", sceAbstractStorageUnknown_gINcKTbrqk4_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_F6tunp5_4ZM_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage12SerializableC1Ev() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("F6tunp5+4ZM", sceAbstractStorageUnknown_F6tunp5_4ZM_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_QRnz_VjPrkY_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage12SerializableC2Ev() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("QRnz-VjPrkY", sceAbstractStorageUnknown_QRnz_VjPrkY_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_DkAGX6EPi_g_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage12SerializableD1Ev() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("DkAGX6EPi+g", sceAbstractStorageUnknown_DkAGX6EPi_g_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_tDhVqVa_tvU_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage12SerializableD2Ev() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("tDhVqVa-tvU", sceAbstractStorageUnknown_tDhVqVa_tvU_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_o9RD998mN8M_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage14MemfileContent12readExternalESt10shared_ptrINS_7ContentEE() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("o9RD998mN8M", sceAbstractStorageUnknown_o9RD998mN8M_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_KAJD5gNtjzg_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage14MemfileContent13writeExternalESt10shared_ptrINS_7ContentEE() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("KAJD5gNtjzg", sceAbstractStorageUnknown_KAJD5gNtjzg_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_jekg6a2C_KE_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage14MemfileContent14CreateInstanceESbIcSt11char_traitsIcENS_8StlAllocIcEEEN3paf6common9SharedPtrINS6_6BufferEEE() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("jekg6a2C-KE", sceAbstractStorageUnknown_jekg6a2C_KE_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_SPPYTszg6LQ_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage14MemfileContent14CreateInstanceESbIcSt11char_traitsIcENS_8StlAllocIcEEEPvm() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("SPPYTszg6LQ", sceAbstractStorageUnknown_SPPYTszg6LQ_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_ppyod3XEauc_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage14MemfileContent4OpenEit() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("ppyod3XEauc", sceAbstractStorageUnknown_ppyod3XEauc_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_FC7E1riolAM_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage14MemfileContent4ReadEPvlPl() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("FC7E1riolAM", sceAbstractStorageUnknown_FC7E1riolAM_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_h64Ojqk4fbE_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage14MemfileContent5CloseEv() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("h64Ojqk4fbE", sceAbstractStorageUnknown_h64Ojqk4fbE_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_L9n8X5qE3gI_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage14MemfileContent5FlushEv() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("L9n8X5qE3gI", sceAbstractStorageUnknown_L9n8X5qE3gI_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_UqSO97fYNAs_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage14MemfileContent5WriteEPKvlPl() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("UqSO97fYNAs", sceAbstractStorageUnknown_UqSO97fYNAs_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_nxefs8JHV5U_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage14MemfileContent6RemoveEv() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("nxefs8JHV5U", sceAbstractStorageUnknown_nxefs8JHV5U_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_TI_djeSDh5s_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage14MemfileContent8SeekByteEliPl() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("TI+djeSDh5s", sceAbstractStorageUnknown_TI_djeSDh5s_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_23QZTBt3E60_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage14MemfileContent8SeekTimeElPl() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("23QZTBt3E60", sceAbstractStorageUnknown_23QZTBt3E60_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_CgSbdJjHTyc_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage14MemfileContentC1ESbIcSt11char_traitsIcENS_8StlAllocIcEEEN3paf6common9SharedPtrINS6_6BufferEEE() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("CgSbdJjHTyc", sceAbstractStorageUnknown_CgSbdJjHTyc_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_uhxDqVGiDTY_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage14MemfileContentC2ESbIcSt11char_traitsIcENS_8StlAllocIcEEEN3paf6common9SharedPtrINS6_6BufferEEE() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("uhxDqVGiDTY", sceAbstractStorageUnknown_uhxDqVGiDTY_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_93IuLXtj1sc_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage14MemfileContentD0Ev() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("93IuLXtj1sc", sceAbstractStorageUnknown_93IuLXtj1sc_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_6g_lsIudFQY_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage14MemfileContentD1Ev() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("6g-lsIudFQY", sceAbstractStorageUnknown_6g_lsIudFQY_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_r_kkC2OXw10_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage14MemfileContentD2Ev() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("r+kkC2OXw10", sceAbstractStorageUnknown_r_kkC2OXw10_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_xeunZETVUSk_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage14StorageManager10GetStorageERKSbIcSt11char_traitsIcENS_8StlAllocIcEEEPSt10shared_ptrINS_7StorageEE() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("xeunZETVUSk", sceAbstractStorageUnknown_xeunZETVUSk_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_k0BKafeYaUo_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage14StorageManager10InitializeEv() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("k0BKafeYaUo", sceAbstractStorageUnknown_k0BKafeYaUo_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_iAN_9pPHZSE_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage14StorageManager11GetInstanceERNS_12MemAllocatorEPv() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("iAN-9pPHZSE", sceAbstractStorageUnknown_iAN_9pPHZSE_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_QbKmf_2_cR0_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage14StorageManager11GetInstanceEv() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("QbKmf+2+cR0", sceAbstractStorageUnknown_QbKmf_2_cR0_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_JWT7NbPA9Ds_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage14StorageManager12NotifyCreateERKSbIcSt11char_traitsIcENS_8StlAllocIcEEEb() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("JWT7NbPA9Ds", sceAbstractStorageUnknown_JWT7NbPA9Ds_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown__fMkDXxU9E0_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage14StorageManager12NotifyRemoveERKSbIcSt11char_traitsIcENS_8StlAllocIcEEEb() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("+fMkDXxU9E0", sceAbstractStorageUnknown__fMkDXxU9E0_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_sFN0uAZmk04_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage14StorageManager15ReleaseInstanceEv() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("sFN0uAZmk04", sceAbstractStorageUnknown_sFN0uAZmk04_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_wvDKH1RiZhU_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage14StorageManager16NotifyWriteCloseERKSbIcSt11char_traitsIcENS_8StlAllocIcEEE() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("wvDKH1RiZhU", sceAbstractStorageUnknown_wvDKH1RiZhU_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_FxOHF0FBNfU_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage14StorageManager16RegisterListenerEPNS_17ItemEventListenerE() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("FxOHF0FBNfU", sceAbstractStorageUnknown_FxOHF0FBNfU_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_JvyLbYhVigM_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage14StorageManager16RegisterListenerEPNS_20StorageEventListenerE() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("JvyLbYhVigM", sceAbstractStorageUnknown_JvyLbYhVigM_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_bb_T5NMDPck_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage14StorageManager16get_storage_nameERKSbIcSt11char_traitsIcENS_8StlAllocIcEEE() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("bb-T5NMDPck", sceAbstractStorageUnknown_bb_T5NMDPck_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_LPRr40DzhbA_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage14StorageManager17NotifySetMetadataERKSbIcSt11char_traitsIcENS_8StlAllocIcEEE() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("LPRr40DzhbA", sceAbstractStorageUnknown_LPRr40DzhbA_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_oIR7L2c6df8_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage14StorageManager18GetStorageNameListEPSt6vectorISbIcSt11char_traitsIcENS_8StlAllocIcEEESaIS6_EE() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("oIR7L2c6df8", sceAbstractStorageUnknown_oIR7L2c6df8_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_mhLGIWhMLlk_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage14StorageManager18UnregisterListenerEPKNS_17ItemEventListenerE() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("mhLGIWhMLlk", sceAbstractStorageUnknown_mhLGIWhMLlk_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_0Ydc18_XsSw_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage14StorageManager18UnregisterListenerEPKNS_20StorageEventListenerE() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("0Ydc18+XsSw", sceAbstractStorageUnknown_0Ydc18_XsSw_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_piG6ktf2BTU_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage14StorageManager20get_service_instanceENS_11ServiceTypeE() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("piG6ktf2BTU", sceAbstractStorageUnknown_piG6ktf2BTU_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_iLakkVdPya4_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage14StorageManager21NotifyStorageAttachedERKSbIcSt11char_traitsIcENS_8StlAllocIcEEENS_11ServiceTypeE() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("iLakkVdPya4", sceAbstractStorageUnknown_iLakkVdPya4_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_1Ko_sKZgqqs_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage14StorageManager21NotifyStorageDetachedERKSbIcSt11char_traitsIcENS_8StlAllocIcEEE() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("1Ko+sKZgqqs", sceAbstractStorageUnknown_1Ko_sKZgqqs_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown__PYC3_W6yUE_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage14StorageManager4FreeEPv() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("-PYC3+W6yUE", sceAbstractStorageUnknown__PYC3_W6yUE_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_3iP9Xa6Z3z0_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage14StorageManager4LoadENS_11ServiceTypeE() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("3iP9Xa6Z3z0", sceAbstractStorageUnknown_3iP9Xa6Z3z0_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_JREmVpFIgEY_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage14StorageManager6MallocEm() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("JREmVpFIgEY", sceAbstractStorageUnknown_JREmVpFIgEY_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_kCX0k5t1v_w_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage14StorageManager6UnloadENS_11ServiceTypeE() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("kCX0k5t1v-w", sceAbstractStorageUnknown_kCX0k5t1v_w_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_3Cp_nOwl47U_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage14StorageManager6UpdateENS_11ServiceTypeE() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("3Cp-nOwl47U", sceAbstractStorageUnknown_3Cp_nOwl47U_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_v9CSqw6Ve0w_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage14StorageManager9TerminateEv() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("v9CSqw6Ve0w", sceAbstractStorageUnknown_v9CSqw6Ve0w_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_KxTVuhlCL7o_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage14StorageManagerC1Ev() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("KxTVuhlCL7o", sceAbstractStorageUnknown_KxTVuhlCL7o_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_YXXMdeJmxJw_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage14StorageManagerC2Ev() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("YXXMdeJmxJw", sceAbstractStorageUnknown_YXXMdeJmxJw_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_2_S0J1RW2vQ_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage14StorageManagerD1Ev() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("2-S0J1RW2vQ", sceAbstractStorageUnknown_2_S0J1RW2vQ_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_K3qnBgQ0nC4_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage14StorageManagerD2Ev() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("K3qnBgQ0nC4", sceAbstractStorageUnknown_K3qnBgQ0nC4_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_rHzXUC9OHh8_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage15HttpTransaction15SendHttpRequestERKSbIcSt11char_traitsIcENS_8StlAllocIcEEEiRKSt3mapIS5_S5_St4lessIS5_ESaISt4pairIS6_S5_EEEmb() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("rHzXUC9OHh8", sceAbstractStorageUnknown_rHzXUC9OHh8_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_Dlri33kcH6o_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage15HttpTransaction4InitEv() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("Dlri33kcH6o", sceAbstractStorageUnknown_Dlri33kcH6o_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_lH57wXDczHk_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage15HttpTransaction4TermEv() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("lH57wXDczHk", sceAbstractStorageUnknown_lH57wXDczHk_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_xJbY3QNYIzM_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage15HttpTransaction8SendDataEPKvm() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("xJbY3QNYIzM", sceAbstractStorageUnknown_xJbY3QNYIzM_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_tbeA_hlRlHc_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage15HttpTransactionC1Ev() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("tbeA-hlRlHc", sceAbstractStorageUnknown_tbeA_hlRlHc_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_01BHTyfcbBs_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage15HttpTransactionC2Ev() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("01BHTyfcbBs", sceAbstractStorageUnknown_01BHTyfcbBs_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_1PrEoSKt3_w_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage15HttpTransactionD1Ev() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("1PrEoSKt3-w", sceAbstractStorageUnknown_1PrEoSKt3_w_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_MoUJZiDoHMk_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage15HttpTransactionD2Ev() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("MoUJZiDoHMk", sceAbstractStorageUnknown_MoUJZiDoHMk_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_I69kRgYrWmY_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage4Item11GetPathNameEv() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("I69kRgYrWmY", sceAbstractStorageUnknown_I69kRgYrWmY_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_GCMRCWH3PU0_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage4Item11SetMetadataEPN23sceMetadataReaderWriter8MetadataE() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("GCMRCWH3PU0", sceAbstractStorageUnknown_GCMRCWH3PU0_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_SYb4Kmj0u88_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage4Item13ParseMetadataEPN23sceMetadataReaderWriter8MetadataES3_() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("SYb4Kmj0u88", sceAbstractStorageUnknown_SYb4Kmj0u88_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_k2VonUL2gF8_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage4Item14GetStorageNameEv() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("k2VonUL2gF8", sceAbstractStorageUnknown_k2VonUL2gF8_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_zkaNI1552m4_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage4Item6RenameERKSbIcSt11char_traitsIcENS_8StlAllocIcEEE() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("zkaNI1552m4", sceAbstractStorageUnknown_zkaNI1552m4_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_KAtGZCifCMo_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage4Item7GetNameERKSbIcSt11char_traitsIcENS_8StlAllocIcEEE() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("KAtGZCifCMo", sceAbstractStorageUnknown_KAtGZCifCMo_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown__sxz_ThMtGM_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage4Item7GetNameEv() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("-sxz-ThMtGM", sceAbstractStorageUnknown__sxz_ThMtGM_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_wyEuZhFhbOE_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage4Item8set_metaERN23sceMetadataReaderWriter8MetadataERKSbIcSt11char_traitsIcENS_8StlAllocIcEEESA_() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("wyEuZhFhbOE", sceAbstractStorageUnknown_wyEuZhFhbOE_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_F_Tz7UzYtJ4_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage4Item8set_metaERN23sceMetadataReaderWriter8MetadataERKSbIcSt11char_traitsIcENS_8StlAllocIcEEEm() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("F-Tz7UzYtJ4", sceAbstractStorageUnknown_F_Tz7UzYtJ4_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_zC_hnmcVEOw_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage4ItemC2ERKSbIcSt11char_traitsIcENS_8StlAllocIcEEE() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("zC+hnmcVEOw", sceAbstractStorageUnknown_zC_hnmcVEOw_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_scYjSEuq2ik_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage4ItemD0Ev() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("scYjSEuq2ik", sceAbstractStorageUnknown_scYjSEuq2ik_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_QPNELEk9zkI_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage4ItemD1Ev() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("QPNELEk9zkI", sceAbstractStorageUnknown_QPNELEk9zkI_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_WnE1cxQELbY_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage4ItemD2Ev() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("WnE1cxQELbY", sceAbstractStorageUnknown_WnE1cxQELbY_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_Hg0GBcA80dk_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage4ItemdlEPv() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("Hg0GBcA80dk", sceAbstractStorageUnknown_Hg0GBcA80dk_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_gid_GlhKgJM_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage4ItemnwEm() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("gid+GlhKgJM", sceAbstractStorageUnknown_gid_GlhKgJM_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_oJSXws_cIv8_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage6FolderC2ERKSbIcSt11char_traitsIcENS_8StlAllocIcEEE() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("oJSXws+cIv8", sceAbstractStorageUnknown_oJSXws_cIv8_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_GgNWVxe_YSU_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage6FolderD0Ev() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("GgNWVxe-YSU", sceAbstractStorageUnknown_GgNWVxe_YSU_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown__1PWHzoXWyU_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage6FolderD1Ev() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("-1PWHzoXWyU", sceAbstractStorageUnknown__1PWHzoXWyU_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_sLg6rUpiYSM_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage6FolderD2Ev() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("sLg6rUpiYSM", sceAbstractStorageUnknown_sLg6rUpiYSM_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_lYUGcgHYT7Y_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage7AbsFreeEPv() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("lYUGcgHYT7Y", sceAbstractStorageUnknown_lYUGcgHYT7Y_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_hQiPEotip7M_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage7Content12GetExtensionERKSbIcSt11char_traitsIcENS_8StlAllocIcEEE() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("hQiPEotip7M", sceAbstractStorageUnknown_hQiPEotip7M_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_pekT_IsAkQU_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage7Content12GetMimeTypesEv() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("pekT-IsAkQU", sceAbstractStorageUnknown_pekT_IsAkQU_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_xilbMn7q9qE_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage7Content6RenameERKSbIcSt11char_traitsIcENS_8StlAllocIcEEE() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("xilbMn7q9qE", sceAbstractStorageUnknown_xilbMn7q9qE_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_UZbuoykGKzE_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage7ContentC2ERKSbIcSt11char_traitsIcENS_8StlAllocIcEEE() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("UZbuoykGKzE", sceAbstractStorageUnknown_UZbuoykGKzE_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_RenWwlxG9Tw_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage7ContentD0Ev() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("RenWwlxG9Tw", sceAbstractStorageUnknown_RenWwlxG9Tw_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_JZ3wKDjl0yQ_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage7ContentD1Ev() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("JZ3wKDjl0yQ", sceAbstractStorageUnknown_JZ3wKDjl0yQ_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_m7OSrfA7K_s_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage7ContentD2Ev() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("m7OSrfA7K+s", sceAbstractStorageUnknown_m7OSrfA7K_s_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_spsjzV2pTSc_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage7Service11NextStorageERSbIcSt11char_traitsIcENS_8StlAllocIcEEE() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("spsjzV2pTSc", sceAbstractStorageUnknown_spsjzV2pTSc_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_6Fp8jJ7A7CY_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage7Service13CreateStorageERKSbIcSt11char_traitsIcENS_8StlAllocIcEEEPSt10shared_ptrINS_7StorageEE() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("6Fp8jJ7A7CY", sceAbstractStorageUnknown_6Fp8jJ7A7CY_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_wBaVZTLTjkg_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage7Service18clear_storage_listEv() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("wBaVZTLTjkg", sceAbstractStorageUnknown_wBaVZTLTjkg_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_DyerRilxWZ4_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage7Service4LoadENS_11ServiceTypeE() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("DyerRilxWZ4", sceAbstractStorageUnknown_DyerRilxWZ4_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_jCKhrVa9MEU_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage7Service4StopEv() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("jCKhrVa9MEU", sceAbstractStorageUnknown_jCKhrVa9MEU_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_tIIRbXa2ZOk_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage7Service5StartEv() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("tIIRbXa2ZOk", sceAbstractStorageUnknown_tIIRbXa2ZOk_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_vatp8UwS_nE_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage7Service6UnLoadEPS0_() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("vatp8UwS+nE", sceAbstractStorageUnknown_vatp8UwS_nE_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_CrZTHC5yd64_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage7ServiceC2Ev() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("CrZTHC5yd64", sceAbstractStorageUnknown_CrZTHC5yd64_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_IoNgvbVSw24_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage7ServiceD0Ev() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("IoNgvbVSw24", sceAbstractStorageUnknown_IoNgvbVSw24_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_PEuuRn6zhF4_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage7ServiceD1Ev() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("PEuuRn6zhF4", sceAbstractStorageUnknown_PEuuRn6zhF4_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_Ngfl7wTF7M0_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage7ServiceD2Ev() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("Ngfl7wTF7M0", sceAbstractStorageUnknown_Ngfl7wTF7M0_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_tLtIJWgGINI_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage7StorageC2ERKSbIcSt11char_traitsIcENS_8StlAllocIcEEE() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("tLtIJWgGINI", sceAbstractStorageUnknown_tLtIJWgGINI_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_4ns7J9OrJ5g_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage7StorageD0Ev() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("4ns7J9OrJ5g", sceAbstractStorageUnknown_4ns7J9OrJ5g_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_aKBqHZKouts_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage7StorageD1Ev() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("aKBqHZKouts", sceAbstractStorageUnknown_aKBqHZKouts_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_fydJOJjDxX4_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage7StorageD2Ev() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("fydJOJjDxX4", sceAbstractStorageUnknown_fydJOJjDxX4_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_qtgEboQL0YE_nid_no_patch() { +int APS5_VABI _ZN15AbstractStorage9AbsMallocEm() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("qtgEboQL0YE", sceAbstractStorageUnknown_qtgEboQL0YE_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_XBlKK5ijWI8_nid_no_patch() { +int APS5_VABI _ZNK15AbstractStorage14MemfileContent10GetServiceEv() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("XBlKK5ijWI8", sceAbstractStorageUnknown_XBlKK5ijWI8_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_xP3aCoP48XI_nid_no_patch() { +int APS5_VABI _ZNK15AbstractStorage14MemfileContent13GetCapabilityEv() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("xP3aCoP48XI", sceAbstractStorageUnknown_xP3aCoP48XI_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_FCSUVvY_0RM_nid_no_patch() { +int APS5_VABI _ZNK15AbstractStorage14MemfileContent7GetStatEPNS_4StatE() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("FCSUVvY-0RM", sceAbstractStorageUnknown_FCSUVvY_0RM_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_oc6n6hdUrEM_nid_no_patch() { +int APS5_VABI _ZNK15AbstractStorage4Item12GetThumbnailEPSt10shared_ptrINS_7ContentEE() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("oc6n6hdUrEM", sceAbstractStorageUnknown_oc6n6hdUrEM_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_Cb7xWd9_7tc_nid_no_patch() { +int APS5_VABI _ZNK15AbstractStorage4Item15CreateThumbnailEPvPSt10shared_ptrINS_7ContentEE() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("Cb7xWd9-7tc", sceAbstractStorageUnknown_Cb7xWd9_7tc_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_mv9rtQAknH4_nid_no_patch() { +int APS5_VABI _ZNK15AbstractStorage6Folder7GetTypeEv() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("mv9rtQAknH4", sceAbstractStorageUnknown_mv9rtQAknH4_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_ka5TVf37Akg_nid_no_patch() { +int APS5_VABI _ZNK15AbstractStorage7Content7GetTypeEv() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("ka5TVf37Akg", sceAbstractStorageUnknown_ka5TVf37Akg_nid_no_patch); -int APS5_VABI sceAbstractStorageUnknown_1uryzAWFUuA_nid_no_patch() { +int APS5_VABI _ZNK15AbstractStorage7Storage7GetTypeEv() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("1uryzAWFUuA", sceAbstractStorageUnknown_1uryzAWFUuA_nid_no_patch); } diff --git a/core/libs/prx/libSceAbstractStorage/include/Api.hpp b/core/libs/prx/libSceAbstractStorage/include/Api.hpp new file mode 100644 index 0000000000..1c4f950e36 --- /dev/null +++ b/core/libs/prx/libSceAbstractStorage/include/Api.hpp @@ -0,0 +1,118 @@ +#pragma once + +#include "SceTypes.hpp" + +extern "C" { + +int APS5_VABI _ZN15AbstractStorage12Serializable12readExternalESt10shared_ptrINS_7ContentEE(); +int APS5_VABI _ZN15AbstractStorage12Serializable13writeExternalESt10shared_ptrINS_7ContentEE(); +int APS5_VABI _ZN15AbstractStorage12SerializableC1Ev(); +int APS5_VABI _ZN15AbstractStorage12SerializableC2Ev(); +int APS5_VABI _ZN15AbstractStorage12SerializableD1Ev(); +int APS5_VABI _ZN15AbstractStorage12SerializableD2Ev(); +int APS5_VABI _ZN15AbstractStorage14MemfileContent12readExternalESt10shared_ptrINS_7ContentEE(); +int APS5_VABI _ZN15AbstractStorage14MemfileContent13writeExternalESt10shared_ptrINS_7ContentEE(); +int APS5_VABI _ZN15AbstractStorage14MemfileContent14CreateInstanceESbIcSt11char_traitsIcENS_8StlAllocIcEEEN3paf6common9SharedPtrINS6_6BufferEEE(); +int APS5_VABI _ZN15AbstractStorage14MemfileContent14CreateInstanceESbIcSt11char_traitsIcENS_8StlAllocIcEEEPvm(); +int APS5_VABI _ZN15AbstractStorage14MemfileContent4OpenEit(); +int APS5_VABI _ZN15AbstractStorage14MemfileContent4ReadEPvlPl(); +int APS5_VABI _ZN15AbstractStorage14MemfileContent5CloseEv(); +int APS5_VABI _ZN15AbstractStorage14MemfileContent5FlushEv(); +int APS5_VABI _ZN15AbstractStorage14MemfileContent5WriteEPKvlPl(); +int APS5_VABI _ZN15AbstractStorage14MemfileContent6RemoveEv(); +int APS5_VABI _ZN15AbstractStorage14MemfileContent8SeekByteEliPl(); +int APS5_VABI _ZN15AbstractStorage14MemfileContent8SeekTimeElPl(); +int APS5_VABI _ZN15AbstractStorage14MemfileContentC1ESbIcSt11char_traitsIcENS_8StlAllocIcEEEN3paf6common9SharedPtrINS6_6BufferEEE(); +int APS5_VABI _ZN15AbstractStorage14MemfileContentC2ESbIcSt11char_traitsIcENS_8StlAllocIcEEEN3paf6common9SharedPtrINS6_6BufferEEE(); +int APS5_VABI _ZN15AbstractStorage14MemfileContentD0Ev(); +int APS5_VABI _ZN15AbstractStorage14MemfileContentD1Ev(); +int APS5_VABI _ZN15AbstractStorage14MemfileContentD2Ev(); +int APS5_VABI _ZN15AbstractStorage14StorageManager10GetStorageERKSbIcSt11char_traitsIcENS_8StlAllocIcEEEPSt10shared_ptrINS_7StorageEE(); +int APS5_VABI _ZN15AbstractStorage14StorageManager10InitializeEv(); +int APS5_VABI _ZN15AbstractStorage14StorageManager11GetInstanceERNS_12MemAllocatorEPv(); +int APS5_VABI _ZN15AbstractStorage14StorageManager11GetInstanceEv(); +int APS5_VABI _ZN15AbstractStorage14StorageManager12NotifyCreateERKSbIcSt11char_traitsIcENS_8StlAllocIcEEEb(); +int APS5_VABI _ZN15AbstractStorage14StorageManager12NotifyRemoveERKSbIcSt11char_traitsIcENS_8StlAllocIcEEEb(); +int APS5_VABI _ZN15AbstractStorage14StorageManager15ReleaseInstanceEv(); +int APS5_VABI _ZN15AbstractStorage14StorageManager16NotifyWriteCloseERKSbIcSt11char_traitsIcENS_8StlAllocIcEEE(); +int APS5_VABI _ZN15AbstractStorage14StorageManager16RegisterListenerEPNS_17ItemEventListenerE(); +int APS5_VABI _ZN15AbstractStorage14StorageManager16RegisterListenerEPNS_20StorageEventListenerE(); +int APS5_VABI _ZN15AbstractStorage14StorageManager16get_storage_nameERKSbIcSt11char_traitsIcENS_8StlAllocIcEEE(); +int APS5_VABI _ZN15AbstractStorage14StorageManager17NotifySetMetadataERKSbIcSt11char_traitsIcENS_8StlAllocIcEEE(); +int APS5_VABI _ZN15AbstractStorage14StorageManager18GetStorageNameListEPSt6vectorISbIcSt11char_traitsIcENS_8StlAllocIcEEESaIS6_EE(); +int APS5_VABI _ZN15AbstractStorage14StorageManager18UnregisterListenerEPKNS_17ItemEventListenerE(); +int APS5_VABI _ZN15AbstractStorage14StorageManager18UnregisterListenerEPKNS_20StorageEventListenerE(); +int APS5_VABI _ZN15AbstractStorage14StorageManager20get_service_instanceENS_11ServiceTypeE(); +int APS5_VABI _ZN15AbstractStorage14StorageManager21NotifyStorageAttachedERKSbIcSt11char_traitsIcENS_8StlAllocIcEEENS_11ServiceTypeE(); +int APS5_VABI _ZN15AbstractStorage14StorageManager21NotifyStorageDetachedERKSbIcSt11char_traitsIcENS_8StlAllocIcEEE(); +int APS5_VABI _ZN15AbstractStorage14StorageManager4FreeEPv(); +int APS5_VABI _ZN15AbstractStorage14StorageManager4LoadENS_11ServiceTypeE(); +int APS5_VABI _ZN15AbstractStorage14StorageManager6MallocEm(); +int APS5_VABI _ZN15AbstractStorage14StorageManager6UnloadENS_11ServiceTypeE(); +int APS5_VABI _ZN15AbstractStorage14StorageManager6UpdateENS_11ServiceTypeE(); +int APS5_VABI _ZN15AbstractStorage14StorageManager9TerminateEv(); +int APS5_VABI _ZN15AbstractStorage14StorageManagerC1Ev(); +int APS5_VABI _ZN15AbstractStorage14StorageManagerC2Ev(); +int APS5_VABI _ZN15AbstractStorage14StorageManagerD1Ev(); +int APS5_VABI _ZN15AbstractStorage14StorageManagerD2Ev(); +int APS5_VABI _ZN15AbstractStorage15HttpTransaction15SendHttpRequestERKSbIcSt11char_traitsIcENS_8StlAllocIcEEEiRKSt3mapIS5_S5_St4lessIS5_ESaISt4pairIS6_S5_EEEmb(); +int APS5_VABI _ZN15AbstractStorage15HttpTransaction4InitEv(); +int APS5_VABI _ZN15AbstractStorage15HttpTransaction4TermEv(); +int APS5_VABI _ZN15AbstractStorage15HttpTransaction8SendDataEPKvm(); +int APS5_VABI _ZN15AbstractStorage15HttpTransactionC1Ev(); +int APS5_VABI _ZN15AbstractStorage15HttpTransactionC2Ev(); +int APS5_VABI _ZN15AbstractStorage15HttpTransactionD1Ev(); +int APS5_VABI _ZN15AbstractStorage15HttpTransactionD2Ev(); +int APS5_VABI _ZN15AbstractStorage4Item11GetPathNameEv(); +int APS5_VABI _ZN15AbstractStorage4Item11SetMetadataEPN23sceMetadataReaderWriter8MetadataE(); +int APS5_VABI _ZN15AbstractStorage4Item13ParseMetadataEPN23sceMetadataReaderWriter8MetadataES3_(); +int APS5_VABI _ZN15AbstractStorage4Item14GetStorageNameEv(); +int APS5_VABI _ZN15AbstractStorage4Item6RenameERKSbIcSt11char_traitsIcENS_8StlAllocIcEEE(); +int APS5_VABI _ZN15AbstractStorage4Item7GetNameERKSbIcSt11char_traitsIcENS_8StlAllocIcEEE(); +int APS5_VABI _ZN15AbstractStorage4Item7GetNameEv(); +int APS5_VABI _ZN15AbstractStorage4Item8set_metaERN23sceMetadataReaderWriter8MetadataERKSbIcSt11char_traitsIcENS_8StlAllocIcEEESA_(); +int APS5_VABI _ZN15AbstractStorage4Item8set_metaERN23sceMetadataReaderWriter8MetadataERKSbIcSt11char_traitsIcENS_8StlAllocIcEEEm(); +int APS5_VABI _ZN15AbstractStorage4ItemC2ERKSbIcSt11char_traitsIcENS_8StlAllocIcEEE(); +int APS5_VABI _ZN15AbstractStorage4ItemD0Ev(); +int APS5_VABI _ZN15AbstractStorage4ItemD1Ev(); +int APS5_VABI _ZN15AbstractStorage4ItemD2Ev(); +int APS5_VABI _ZN15AbstractStorage4ItemdlEPv(); +int APS5_VABI _ZN15AbstractStorage4ItemnwEm(); +int APS5_VABI _ZN15AbstractStorage6FolderC2ERKSbIcSt11char_traitsIcENS_8StlAllocIcEEE(); +int APS5_VABI _ZN15AbstractStorage6FolderD0Ev(); +int APS5_VABI _ZN15AbstractStorage6FolderD1Ev(); +int APS5_VABI _ZN15AbstractStorage6FolderD2Ev(); +int APS5_VABI _ZN15AbstractStorage7AbsFreeEPv(); +int APS5_VABI _ZN15AbstractStorage7Content12GetExtensionERKSbIcSt11char_traitsIcENS_8StlAllocIcEEE(); +int APS5_VABI _ZN15AbstractStorage7Content12GetMimeTypesEv(); +int APS5_VABI _ZN15AbstractStorage7Content6RenameERKSbIcSt11char_traitsIcENS_8StlAllocIcEEE(); +int APS5_VABI _ZN15AbstractStorage7ContentC2ERKSbIcSt11char_traitsIcENS_8StlAllocIcEEE(); +int APS5_VABI _ZN15AbstractStorage7ContentD0Ev(); +int APS5_VABI _ZN15AbstractStorage7ContentD1Ev(); +int APS5_VABI _ZN15AbstractStorage7ContentD2Ev(); +int APS5_VABI _ZN15AbstractStorage7Service11NextStorageERSbIcSt11char_traitsIcENS_8StlAllocIcEEE(); +int APS5_VABI _ZN15AbstractStorage7Service13CreateStorageERKSbIcSt11char_traitsIcENS_8StlAllocIcEEEPSt10shared_ptrINS_7StorageEE(); +int APS5_VABI _ZN15AbstractStorage7Service18clear_storage_listEv(); +int APS5_VABI _ZN15AbstractStorage7Service4LoadENS_11ServiceTypeE(); +int APS5_VABI _ZN15AbstractStorage7Service4StopEv(); +int APS5_VABI _ZN15AbstractStorage7Service5StartEv(); +int APS5_VABI _ZN15AbstractStorage7Service6UnLoadEPS0_(); +int APS5_VABI _ZN15AbstractStorage7ServiceC2Ev(); +int APS5_VABI _ZN15AbstractStorage7ServiceD0Ev(); +int APS5_VABI _ZN15AbstractStorage7ServiceD1Ev(); +int APS5_VABI _ZN15AbstractStorage7ServiceD2Ev(); +int APS5_VABI _ZN15AbstractStorage7StorageC2ERKSbIcSt11char_traitsIcENS_8StlAllocIcEEE(); +int APS5_VABI _ZN15AbstractStorage7StorageD0Ev(); +int APS5_VABI _ZN15AbstractStorage7StorageD1Ev(); +int APS5_VABI _ZN15AbstractStorage7StorageD2Ev(); +int APS5_VABI _ZN15AbstractStorage9AbsMallocEm(); +int APS5_VABI _ZNK15AbstractStorage14MemfileContent10GetServiceEv(); +int APS5_VABI _ZNK15AbstractStorage14MemfileContent13GetCapabilityEv(); +int APS5_VABI _ZNK15AbstractStorage14MemfileContent7GetStatEPNS_4StatE(); +int APS5_VABI _ZNK15AbstractStorage4Item12GetThumbnailEPSt10shared_ptrINS_7ContentEE(); +int APS5_VABI _ZNK15AbstractStorage4Item15CreateThumbnailEPvPSt10shared_ptrINS_7ContentEE(); +int APS5_VABI _ZNK15AbstractStorage6Folder7GetTypeEv(); +int APS5_VABI _ZNK15AbstractStorage7Content7GetTypeEv(); +int APS5_VABI _ZNK15AbstractStorage7Storage7GetTypeEv(); + +} diff --git a/core/libs/prx/libSceAbstractStorage/libSceAbstractStorage.hpp b/core/libs/prx/libSceAbstractStorage/libSceAbstractStorage.hpp index 02bce84cd0..476baa2df9 100644 --- a/core/libs/prx/libSceAbstractStorage/libSceAbstractStorage.hpp +++ b/core/libs/prx/libSceAbstractStorage/libSceAbstractStorage.hpp @@ -1,3 +1,4 @@ #pragma once +#include "prx/libSceAbstractStorage/include/Api.hpp" #include "prx/libSceAbstractStorage/include/InventoryExports.hpp" #include "prx/libSceAbstractStorage/include/LegacyExports.hpp" diff --git a/core/libs/prx/libSceAbstractYoutube/Export.cpp b/core/libs/prx/libSceAbstractYoutube/Export.cpp index 6479276d85..ea799bd6e8 100644 --- a/core/libs/prx/libSceAbstractYoutube/Export.cpp +++ b/core/libs/prx/libSceAbstractYoutube/Export.cpp @@ -1,3 +1,6 @@ +#include "prx/libSceAbstractYoutube/include/LegacyExports.hpp" +#include "prx/libc/include/general/ExportMacros.hpp" +#include "prx/libSceAbstractYoutube/include/InventoryExports.hpp" #include #include #include "SceTypes.hpp" @@ -92,3 +95,267 @@ int APS5_VABI sceAbstractYoutubeUnknown_vJGnqFj6ygE_nid_no_patch() { APS5_EXPORT("vJGnqFj6ygE", sceAbstractYoutubeUnknown_vJGnqFj6ygE_nid_no_patch); } + +extern "C" { + +int APS5_VABI _ZN15AbstractStorage13YoutubeFolder6RemoveEb() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZN15AbstractStorage13YoutubeFolderD0Ev() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZN15AbstractStorage13YoutubeFolderD1Ev() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZN15AbstractStorage13YoutubeFolderD2Ev() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZN15AbstractStorage14YoutubeContent11SetMetadataEPN23sceMetadataReaderWriter8MetadataE() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZN15AbstractStorage14YoutubeContent13ParseMetadataEPN23sceMetadataReaderWriter8MetadataES3_() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZN15AbstractStorage14YoutubeContent4OpenEit() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZN15AbstractStorage14YoutubeContent4ReadEPvlPl() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZN15AbstractStorage14YoutubeContent5CloseEv() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZN15AbstractStorage14YoutubeContent5FlushEv() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZN15AbstractStorage14YoutubeContent5WriteEPKvlPl() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZN15AbstractStorage14YoutubeContent6RemoveEv() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZN15AbstractStorage14YoutubeContent8SeekByteEliPl() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZN15AbstractStorage14YoutubeContent8SeekTimeElPl() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZN15AbstractStorage14YoutubeContentD0Ev() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZN15AbstractStorage14YoutubeContentD1Ev() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZN15AbstractStorage14YoutubeContentD2Ev() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZN15AbstractStorage14YoutubeService13SearchStorageEv() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZN15AbstractStorage14YoutubeService4StopEv() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZN15AbstractStorage14YoutubeService5StartEv() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZN15AbstractStorage14YoutubeServiceC1Ev() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZN15AbstractStorage14YoutubeServiceC2Ev() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZN15AbstractStorage14YoutubeServiceD0Ev() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZN15AbstractStorage14YoutubeServiceD1Ev() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZN15AbstractStorage14YoutubeServiceD2Ev() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZN15AbstractStorage14YoutubeStorage14GetStorageSizeEPlS1_S1_() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZN15AbstractStorage14YoutubeStorageD0Ev() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZN15AbstractStorage14YoutubeStorageD1Ev() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZN15AbstractStorage14YoutubeStorageD2Ev() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZNK15AbstractStorage13YoutubeFolder10GetServiceEv() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZNK15AbstractStorage13YoutubeFolder13GetCapabilityEv() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZNK15AbstractStorage13YoutubeFolder7GetStatEPNS_4StatE() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZNK15AbstractStorage14YoutubeContent10GetServiceEv() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZNK15AbstractStorage14YoutubeContent13GetCapabilityEv() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZNK15AbstractStorage14YoutubeContent7GetStatEPNS_4StatE() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZNK15AbstractStorage14YoutubeService14GetServiceTypeEv() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZNK15AbstractStorage14YoutubeStorage10GetServiceEv() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZNK15AbstractStorage14YoutubeStorage13GetCapabilityEv() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZNK15AbstractStorage14YoutubeStorage7GetStatEPNS_4StatE() { + NotImplemented_nid_no_patch(__func__); +} + +} + +extern "C" { + +int APS5_VABI abstractYoutubeLegacyUnknown__plus_XiOtdILEHI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+XiOtdILEHI", abstractYoutubeLegacyUnknown__plus_XiOtdILEHI_nid_no_patch); + +int APS5_VABI abstractYoutubeLegacyUnknown_4orcGYZG2cE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("4orcGYZG2cE", abstractYoutubeLegacyUnknown_4orcGYZG2cE_nid_no_patch); + +int APS5_VABI abstractYoutubeLegacyUnknown_6svBaAI4Ggw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("6svBaAI4Ggw", abstractYoutubeLegacyUnknown_6svBaAI4Ggw_nid_no_patch); + +int APS5_VABI abstractYoutubeLegacyUnknown_CCv9CYanMNI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("CCv9CYanMNI", abstractYoutubeLegacyUnknown_CCv9CYanMNI_nid_no_patch); + +int APS5_VABI abstractYoutubeLegacyUnknown_EEx55k0zSLo_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("EEx55k0zSLo", abstractYoutubeLegacyUnknown_EEx55k0zSLo_nid_no_patch); + +int APS5_VABI abstractYoutubeLegacyUnknown_HYDApQYDgcI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("HYDApQYDgcI", abstractYoutubeLegacyUnknown_HYDApQYDgcI_nid_no_patch); + +int APS5_VABI abstractYoutubeLegacyUnknown_IGnFlGNW_plus_Vo_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("IGnFlGNW+Vo", abstractYoutubeLegacyUnknown_IGnFlGNW_plus_Vo_nid_no_patch); + +int APS5_VABI abstractYoutubeLegacyUnknown_JzBDUIC5xok_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("JzBDUIC5xok", abstractYoutubeLegacyUnknown_JzBDUIC5xok_nid_no_patch); + +int APS5_VABI abstractYoutubeLegacyUnknown_NPYN89qO2Ks_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("NPYN89qO2Ks", abstractYoutubeLegacyUnknown_NPYN89qO2Ks_nid_no_patch); + +int APS5_VABI abstractYoutubeLegacyUnknown_P_plus_ZjKSLi5lg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("P+ZjKSLi5lg", abstractYoutubeLegacyUnknown_P_plus_ZjKSLi5lg_nid_no_patch); + +int APS5_VABI abstractYoutubeLegacyUnknown_RkaqMMDQJp4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("RkaqMMDQJp4", abstractYoutubeLegacyUnknown_RkaqMMDQJp4_nid_no_patch); + +int APS5_VABI abstractYoutubeLegacyUnknown_ZpVRh3D86UY_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("ZpVRh3D86UY", abstractYoutubeLegacyUnknown_ZpVRh3D86UY_nid_no_patch); + +int APS5_VABI abstractYoutubeLegacyUnknown_ZwkYXHwQ7Mc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("ZwkYXHwQ7Mc", abstractYoutubeLegacyUnknown_ZwkYXHwQ7Mc_nid_no_patch); + +int APS5_VABI abstractYoutubeLegacyUnknown_cktbTmca48I_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("cktbTmca48I", abstractYoutubeLegacyUnknown_cktbTmca48I_nid_no_patch); + +int APS5_VABI abstractYoutubeLegacyUnknown_fUXj4GBxyHs_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("fUXj4GBxyHs", abstractYoutubeLegacyUnknown_fUXj4GBxyHs_nid_no_patch); + +int APS5_VABI abstractYoutubeLegacyUnknown_g58APdyJ_minus_5Y_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("g58APdyJ-5Y", abstractYoutubeLegacyUnknown_g58APdyJ_minus_5Y_nid_no_patch); + +int APS5_VABI abstractYoutubeLegacyUnknown_hXCJIJZZ5MY_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("hXCJIJZZ5MY", abstractYoutubeLegacyUnknown_hXCJIJZZ5MY_nid_no_patch); + +int APS5_VABI abstractYoutubeLegacyUnknown_kgeuOe7oln8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("kgeuOe7oln8", abstractYoutubeLegacyUnknown_kgeuOe7oln8_nid_no_patch); + +int APS5_VABI abstractYoutubeLegacyUnknown_pR9ULw2VrSs_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("pR9ULw2VrSs", abstractYoutubeLegacyUnknown_pR9ULw2VrSs_nid_no_patch); + +int APS5_VABI abstractYoutubeLegacyUnknown_tjbj7QysZwk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("tjbj7QysZwk", abstractYoutubeLegacyUnknown_tjbj7QysZwk_nid_no_patch); + +} diff --git a/core/libs/prx/libSceAbstractYoutube/include/InventoryExports.hpp b/core/libs/prx/libSceAbstractYoutube/include/InventoryExports.hpp new file mode 100644 index 0000000000..7fb585ade6 --- /dev/null +++ b/core/libs/prx/libSceAbstractYoutube/include/InventoryExports.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include "SceTypes.hpp" + +extern "C" { + +int APS5_VABI _ZN15AbstractStorage13YoutubeFolder6RemoveEb(); +int APS5_VABI _ZN15AbstractStorage13YoutubeFolderD0Ev(); +int APS5_VABI _ZN15AbstractStorage13YoutubeFolderD1Ev(); +int APS5_VABI _ZN15AbstractStorage13YoutubeFolderD2Ev(); +int APS5_VABI _ZN15AbstractStorage14YoutubeContent11SetMetadataEPN23sceMetadataReaderWriter8MetadataE(); +int APS5_VABI _ZN15AbstractStorage14YoutubeContent13ParseMetadataEPN23sceMetadataReaderWriter8MetadataES3_(); +int APS5_VABI _ZN15AbstractStorage14YoutubeContent4OpenEit(); +int APS5_VABI _ZN15AbstractStorage14YoutubeContent4ReadEPvlPl(); +int APS5_VABI _ZN15AbstractStorage14YoutubeContent5CloseEv(); +int APS5_VABI _ZN15AbstractStorage14YoutubeContent5FlushEv(); +int APS5_VABI _ZN15AbstractStorage14YoutubeContent5WriteEPKvlPl(); +int APS5_VABI _ZN15AbstractStorage14YoutubeContent6RemoveEv(); +int APS5_VABI _ZN15AbstractStorage14YoutubeContent8SeekByteEliPl(); +int APS5_VABI _ZN15AbstractStorage14YoutubeContent8SeekTimeElPl(); +int APS5_VABI _ZN15AbstractStorage14YoutubeContentD0Ev(); +int APS5_VABI _ZN15AbstractStorage14YoutubeContentD1Ev(); +int APS5_VABI _ZN15AbstractStorage14YoutubeContentD2Ev(); +int APS5_VABI _ZN15AbstractStorage14YoutubeService13SearchStorageEv(); +int APS5_VABI _ZN15AbstractStorage14YoutubeService4StopEv(); +int APS5_VABI _ZN15AbstractStorage14YoutubeService5StartEv(); +int APS5_VABI _ZN15AbstractStorage14YoutubeServiceC1Ev(); +int APS5_VABI _ZN15AbstractStorage14YoutubeServiceC2Ev(); +int APS5_VABI _ZN15AbstractStorage14YoutubeServiceD0Ev(); +int APS5_VABI _ZN15AbstractStorage14YoutubeServiceD1Ev(); +int APS5_VABI _ZN15AbstractStorage14YoutubeServiceD2Ev(); +int APS5_VABI _ZN15AbstractStorage14YoutubeStorage14GetStorageSizeEPlS1_S1_(); +int APS5_VABI _ZN15AbstractStorage14YoutubeStorageD0Ev(); +int APS5_VABI _ZN15AbstractStorage14YoutubeStorageD1Ev(); +int APS5_VABI _ZN15AbstractStorage14YoutubeStorageD2Ev(); +int APS5_VABI _ZNK15AbstractStorage13YoutubeFolder10GetServiceEv(); +int APS5_VABI _ZNK15AbstractStorage13YoutubeFolder13GetCapabilityEv(); +int APS5_VABI _ZNK15AbstractStorage13YoutubeFolder7GetStatEPNS_4StatE(); +int APS5_VABI _ZNK15AbstractStorage14YoutubeContent10GetServiceEv(); +int APS5_VABI _ZNK15AbstractStorage14YoutubeContent13GetCapabilityEv(); +int APS5_VABI _ZNK15AbstractStorage14YoutubeContent7GetStatEPNS_4StatE(); +int APS5_VABI _ZNK15AbstractStorage14YoutubeService14GetServiceTypeEv(); +int APS5_VABI _ZNK15AbstractStorage14YoutubeStorage10GetServiceEv(); +int APS5_VABI _ZNK15AbstractStorage14YoutubeStorage13GetCapabilityEv(); +int APS5_VABI _ZNK15AbstractStorage14YoutubeStorage7GetStatEPNS_4StatE(); + +} diff --git a/core/libs/prx/libSceAbstractYoutube/include/LegacyExports.hpp b/core/libs/prx/libSceAbstractYoutube/include/LegacyExports.hpp new file mode 100644 index 0000000000..bdf94b0e20 --- /dev/null +++ b/core/libs/prx/libSceAbstractYoutube/include/LegacyExports.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include "SceTypes.hpp" + +extern "C" { + +int APS5_VABI abstractYoutubeLegacyUnknown__plus_XiOtdILEHI_nid_no_patch(); +int APS5_VABI abstractYoutubeLegacyUnknown_4orcGYZG2cE_nid_no_patch(); +int APS5_VABI abstractYoutubeLegacyUnknown_6svBaAI4Ggw_nid_no_patch(); +int APS5_VABI abstractYoutubeLegacyUnknown_CCv9CYanMNI_nid_no_patch(); +int APS5_VABI abstractYoutubeLegacyUnknown_EEx55k0zSLo_nid_no_patch(); +int APS5_VABI abstractYoutubeLegacyUnknown_HYDApQYDgcI_nid_no_patch(); +int APS5_VABI abstractYoutubeLegacyUnknown_IGnFlGNW_plus_Vo_nid_no_patch(); +int APS5_VABI abstractYoutubeLegacyUnknown_JzBDUIC5xok_nid_no_patch(); +int APS5_VABI abstractYoutubeLegacyUnknown_NPYN89qO2Ks_nid_no_patch(); +int APS5_VABI abstractYoutubeLegacyUnknown_P_plus_ZjKSLi5lg_nid_no_patch(); +int APS5_VABI abstractYoutubeLegacyUnknown_RkaqMMDQJp4_nid_no_patch(); +int APS5_VABI abstractYoutubeLegacyUnknown_ZpVRh3D86UY_nid_no_patch(); +int APS5_VABI abstractYoutubeLegacyUnknown_ZwkYXHwQ7Mc_nid_no_patch(); +int APS5_VABI abstractYoutubeLegacyUnknown_cktbTmca48I_nid_no_patch(); +int APS5_VABI abstractYoutubeLegacyUnknown_fUXj4GBxyHs_nid_no_patch(); +int APS5_VABI abstractYoutubeLegacyUnknown_g58APdyJ_minus_5Y_nid_no_patch(); +int APS5_VABI abstractYoutubeLegacyUnknown_hXCJIJZZ5MY_nid_no_patch(); +int APS5_VABI abstractYoutubeLegacyUnknown_kgeuOe7oln8_nid_no_patch(); +int APS5_VABI abstractYoutubeLegacyUnknown_pR9ULw2VrSs_nid_no_patch(); +int APS5_VABI abstractYoutubeLegacyUnknown_tjbj7QysZwk_nid_no_patch(); + +} diff --git a/core/libs/prx/libSceAbstractYoutube/libSceAbstractYoutube.hpp b/core/libs/prx/libSceAbstractYoutube/libSceAbstractYoutube.hpp index 6f70f09bee..76c0ec1754 100644 --- a/core/libs/prx/libSceAbstractYoutube/libSceAbstractYoutube.hpp +++ b/core/libs/prx/libSceAbstractYoutube/libSceAbstractYoutube.hpp @@ -1 +1,3 @@ #pragma once +#include "prx/libSceAbstractYoutube/include/InventoryExports.hpp" +#include "prx/libSceAbstractYoutube/include/LegacyExports.hpp" diff --git a/core/libs/prx/libSceAgc/CMakeLists.txt b/core/libs/prx/libSceAgc/CMakeLists.txt index b6af650fca..12ee608c54 100644 --- a/core/libs/prx/libSceAgc/CMakeLists.txt +++ b/core/libs/prx/libSceAgc/CMakeLists.txt @@ -91,6 +91,19 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(agc_draw_setup_tests EXCLUDE_FROM_ALL tests/DrawSetup.cpp) +target_include_directories(agc_draw_setup_tests PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(agc_draw_setup_tests PRIVATE libSceAgc libc) +configure_windows_unwind(agc_draw_setup_tests) +if(BUILD_TESTING) + set_target_properties(agc_draw_setup_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_draw_setup COMMAND agc_draw_setup_tests) + if(WIN32) + set_tests_properties(agc_draw_setup PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(agc_rewind_patch_tests EXCLUDE_FROM_ALL tests/RewindPatch.cpp) target_include_directories(agc_rewind_patch_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(agc_rewind_patch_tests PRIVATE libSceAgc libc) diff --git a/core/libs/prx/libSceAgc/tests/Commands.cpp b/core/libs/prx/libSceAgc/tests/Commands.cpp index 018e41bed2..72f3e4a785 100644 --- a/core/libs/prx/libSceAgc/tests/Commands.cpp +++ b/core/libs/prx/libSceAgc/tests/Commands.cpp @@ -30,6 +30,8 @@ #include "prx/libSceAgc/Acb/include/Sync.hpp" #include "prx/libSceAgc/Command/include/Packet.hpp" #include "prx/libSceAgc/Command/include/Memory.hpp" +#include "prx/libSceAgc/Acb/include/Memory.hpp" +#include "prx/libSceAgc/DcbFlow/include/Memory.hpp" #include "prx/libSceAgc/Command/include/RegisterDefaults.hpp" #include "prx/libc/include/Shutdown.hpp" @@ -835,6 +837,45 @@ void testAtomicGds() { } } +void testCopyDataAndComputeEvents() { + Storage graphics; + const auto* copy = sceAgcDcbCopyData(&graphics.buffer, 4, 1, 0x1122334450ull, 0xb, 2, 0x12345678u, 0, 1); + const std::array graphicsExpected{0xc0044000u, 0x42104205u, 0x12345678u, 0, 0x22334450u, 0x11u}; + check(copy == graphics.words.data() && std::equal(graphicsExpected.begin(), graphicsExpected.end(), graphics.words.begin()), "graphics COPY_DATA packet mismatch"); + Storage wide; + sceAgcDcbCopyData(&wide.buffer, 4, 0, 0x1000, 0xa, 0, 0x100000000ull, 1, 0); + check(wide.words[1] == 0x00010205u && wide.words[2] == 0 && wide.words[3] == 1, "a 64-bit immediate COPY_DATA lost its high word"); + Storage compute; + sceAgcAcbCopyData(&compute.buffer, 2, 0, 0x4000, 5, 0, 7, 0, 0); + const std::array computeExpected{0xc0044000u, 0x205u, 7, 0, 0x4000u, 0}; + check(std::equal(computeExpected.begin(), computeExpected.end(), compute.words.begin()), "compute COPY_DATA packet mismatch"); + Storage rejected; + rejected.words.fill(0xa5a5a5a5u); + const auto rejectedWords = rejected.words; + const auto* rejectedCursor = rejected.buffer.cursor_up; + expectFailure([&] { sceAgcDcbCopyData(&rejected.buffer, 4, 4, 0x1000, 0xa, 0, 1, 0, 0); }); + check(rejected.words == rejectedWords && rejected.buffer.cursor_up == rejectedCursor, "a refused packet changed the command buffer"); + expectFailure([&] { sceAgcDcbCopyData(&rejected.buffer, 4, 0, 0x1000, 0xa, 4, 1, 0, 0); }); + check(rejected.words == rejectedWords && rejected.buffer.cursor_up == rejectedCursor, "a refused packet changed the command buffer"); + expectFailure([&] { sceAgcAcbCopyData(&rejected.buffer, 2, 0, 0x4000, 5, 0, 1, 2, 0); }); + check(rejected.words == rejectedWords && rejected.buffer.cursor_up == rejectedCursor, "a refused packet changed the command buffer"); + expectFailure([&] { sceAgcAcbCopyData(&rejected.buffer, 2, 0, 0x4000, 5, 0, 1, 0, 2); }); + check(rejected.words == rejectedWords && rejected.buffer.cursor_up == rejectedCursor, "a refused packet changed the command buffer"); + check(sceAgcDcbCopyDataGetSize() == 24 && sceAgcAcbCopyDataGetSize() == 24, "COPY_DATA packet sizes"); + Storage events; + sceAgcAcbEventWrite(&events.buffer, 7, nullptr); + sceAgcAcbEventWrite(&events.buffer, 0x16, nullptr); + const std::array eventsExpected{0xc0004600u, 0x407u, 0xc0004600u, 0x16u}; + check(std::equal(eventsExpected.begin(), eventsExpected.end(), events.words.begin()), "compute EVENT_WRITE packet mismatch"); + Storage refused; + refused.words.fill(0xa5a5a5a5u); + const auto refusedWords = refused.words; + const auto* refusedCursor = refused.buffer.cursor_up; + expectFailure([&] { sceAgcAcbEventWrite(&refused.buffer, 0x40, nullptr); }); + check(refused.words == refusedWords && refused.buffer.cursor_up == refusedCursor, "a refused packet changed the command buffer"); + check(sceAgcAcbEventWriteGetSize(7) == 8, "compute EVENT_WRITE packet size"); +} + void testDebugRaiseException() { int result = -1; try { @@ -1080,6 +1121,7 @@ int main(int argc, char** argv) { testEndOfPipePatches(); testAtomicGds(); testDebugRaiseException(); + testCopyDataAndComputeEvents(); testDefaults(); testDefaultsTypes(); LibcRunShutdown_nid_postfix(); diff --git a/core/libs/prx/libSceAgc/tests/DrawSetup.cpp b/core/libs/prx/libSceAgc/tests/DrawSetup.cpp new file mode 100644 index 0000000000..a97371bae9 --- /dev/null +++ b/core/libs/prx/libSceAgc/tests/DrawSetup.cpp @@ -0,0 +1,88 @@ +#include "prx/libSceAgc/Command/include/Packet.hpp" +#include "prx/libSceAgc/DcbDraw/include/DrawIndexed.hpp" +#include "prx/libSceAgc/DcbDraw/include/DrawNonIndexed.hpp" +#include "prx/libSceAgc/DcbDraw/include/IndexBuffer.hpp" +#include "prx/libSceAgc/DcbDraw/include/Instancing.hpp" + +#include +#include +#include +#include +#include + +namespace { + +struct Storage { + std::array words{}; + CommandBuffer buffer{words.data(), words.data() + words.size(), words.data(), words.data() + words.size(), nullptr, nullptr, 0}; +}; + +void check(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +template +void expectFailure(Storage& storage, TAction action, const char* message) { + const auto before = storage.words; + const auto* cursor = storage.buffer.cursor_up; + try { + action(); + } catch (const std::runtime_error&) { + check(storage.words == before && storage.buffer.cursor_up == cursor, message); + return; + } + throw std::runtime_error(message); +} + +template +void expectPacket(const Storage& storage, const std::uint32_t* packet, const std::uint32_t* start, const std::array& expected, std::uint32_t size, const char* message) { + check(packet == start && std::equal(expected.begin(), expected.end(), packet), message); + check(storage.buffer.cursor_up == start + N, message); + check(size == N * sizeof(std::uint32_t), message); +} + +void testNumInstances() { + Storage storage; + const auto* packet = sceAgcDcbSetNumInstances(&storage.buffer, 7); + expectPacket(storage, packet, storage.words.data(), std::array{0xc0002f00u, 7}, sceAgcDcbSetNumInstancesGetSize(), "NUM_INSTANCES packet mismatch"); +} + +void testIndexSize() { + Storage storage; + const auto* packet = sceAgcDcbSetIndexSize(&storage.buffer, 1, 2); + expectPacket(storage, packet, storage.words.data(), std::array{0xc0017a00u, 0x20000243u, 0x481u}, sceAgcDcbSetIndexSizeGetSize(), "index size packet mismatch"); + const auto* objectId = sceAgcDcbSetIndexSize_0600(&storage.buffer, 2, 3, 1); + check(objectId == storage.words.data() + 3 && objectId[0] == 0xc0017a00u && objectId[1] == 0x20000243u && objectId[2] == (0x4c2u | 0x4000u), "index size packet with a per-instance object id mismatch"); + expectFailure(storage, [&] { sceAgcDcbSetIndexSize(&storage.buffer, 3, 0); }, "an index size above 2 was accepted"); + expectFailure(storage, [&] { sceAgcDcbSetIndexSize(&storage.buffer, 0, 4); }, "a cache policy above 3 was accepted"); + expectFailure(storage, [&] { sceAgcDcbSetIndexSize_0600(&storage.buffer, 0, 0, 2); }, "a per-instance object id above 1 was accepted"); +} + +void testDrawIndexOffset() { + Storage storage; + const auto* packet = sceAgcDcbDrawIndexOffset(&storage.buffer, 12, 36, 0); + expectPacket(storage, packet, storage.words.data(), std::array{0xc0033500u, 36, 12, 36, 0}, sceAgcDcbDrawIndexOffsetGetSize(), "DRAW_INDEX_OFFSET_2 packet mismatch"); + const auto* empty = sceAgcDcbDrawIndexOffset(&storage.buffer, 0, 0, 0); + check(empty == storage.words.data() + 5 && empty[1] == 1 && empty[3] == 0, "an empty indexed offset draw did not keep a maximum size of one"); +} + +void testDrawIndirect() { + Storage storage; + const auto* packet = sceAgcDcbDrawIndirect(&storage.buffer, 0x40, 0); + expectPacket(storage, packet, storage.words.data(), std::array{0xc0032400u, 0x40, 0x280, 0x280, 2}, sceAgcDcbDrawIndirectGetSize(), "DRAW_INDIRECT packet mismatch"); + expectFailure(storage, [&] { sceAgcDcbDrawIndirect(&storage.buffer, 2, 0); }, "a misaligned indirect argument offset was accepted"); +} + +} + +int main() { + try { + testNumInstances(); + testIndexSize(); + testDrawIndexOffset(); + testDrawIndirect(); + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index ddb3b58570..9a645f33d0 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1,3 +1,24 @@ +if(BUILD_TESTING) + add_test_executable(agc_preparation_inputs_tests + tests/PreparationInputs.cpp + Execution/src/Driver/Shaders/ShaderDiagnostics.cpp + ${CMAKE_SOURCE_DIR}/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp) + target_include_directories(agc_preparation_inputs_tests PRIVATE + ${LIBS_INCLUDE_DIR} + $ + ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) + target_link_libraries(agc_preparation_inputs_tests PRIVATE libc) + configure_windows_unwind(agc_preparation_inputs_tests) + foreach(mode enabled disabled selected failure concurrent write-failure capture-write-failure) + add_test(NAME agc_preparation_inputs_${mode} COMMAND agc_preparation_inputs_tests ${mode}) + set_tests_properties(agc_preparation_inputs_${mode} PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(agc_preparation_inputs_${mode} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() +endif() + function(ensure_agc_glslang) if(TARGET glslang AND TARGET SPIRV AND TARGET glslang-standalone) return() @@ -31,6 +52,16 @@ endfunction() ensure_agc_glslang() +if(BUILD_TESTING AND WIN32) + add_test_executable(agc_process_sampler_stack_tests tests/ProcessSamplerStack.cpp Execution/src/WorkerSampler.cpp) + target_include_directories(agc_process_sampler_stack_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(agc_process_sampler_stack_tests PRIVATE libc) + configure_windows_unwind(agc_process_sampler_stack_tests) + add_test(NAME agc_process_sampler_stack COMMAND agc_process_sampler_stack_tests) + set_tests_properties(agc_process_sampler_stack PROPERTIES TIMEOUT 20 ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") +endif() + set(agcTextureDetileShaderSource ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/shaders/TextureDetile.comp) set(agcTextureDetileGeneratedRoot ${CMAKE_CURRENT_BINARY_DIR}/generated) set(agcTextureDetileGeneratedDir ${agcTextureDetileGeneratedRoot}/prx/libSceAgcDriver/Graphics/shaders) @@ -56,6 +87,28 @@ add_custom_command( DEPENDS "${agcTextureDetileShaderSource}" glslang-standalone VERBATIM ) +set(agcTextureDetileWordsShaderSpv ${agcTextureDetileGeneratedDir}/TextureDetileWords.spv) +set(agcTextureDetileWordsShaderHeader ${agcTextureDetileGeneratedDir}/TextureDetileWords_spv.h) +add_custom_command( + OUTPUT "${agcTextureDetileWordsShaderHeader}" + COMMAND ${CMAKE_COMMAND} -E make_directory "${agcTextureDetileGeneratedDir}" + COMMAND "$" -V --target-env vulkan1.1 -DWORD_GROUPS=1 -o "${agcTextureDetileWordsShaderSpv}" "${agcTextureDetileShaderSource}" + COMMAND ${CMAKE_COMMAND} -DINPUT=${agcTextureDetileWordsShaderSpv} -DOUTPUT=${agcTextureDetileWordsShaderHeader} + -DSYMBOL=TEXTURE_DETILE_WORDS_SPV -P "${CMAKE_CURRENT_SOURCE_DIR}/embed_spirv.cmake" + DEPENDS "${agcTextureDetileShaderSource}" glslang-standalone + VERBATIM +) +set(agcTextureDetileImageWordsShaderSpv ${agcTextureDetileGeneratedDir}/TextureDetileImageWords.spv) +set(agcTextureDetileImageWordsShaderHeader ${agcTextureDetileGeneratedDir}/TextureDetileImageWords_spv.h) +add_custom_command( + OUTPUT "${agcTextureDetileImageWordsShaderHeader}" + COMMAND ${CMAKE_COMMAND} -E make_directory "${agcTextureDetileGeneratedDir}" + COMMAND "$" -V --target-env vulkan1.1 -DLINEAR_IMAGE=1 -DWORD_GROUPS=1 -o "${agcTextureDetileImageWordsShaderSpv}" "${agcTextureDetileShaderSource}" + COMMAND ${CMAKE_COMMAND} -DINPUT=${agcTextureDetileImageWordsShaderSpv} -DOUTPUT=${agcTextureDetileImageWordsShaderHeader} + -DSYMBOL=TEXTURE_DETILE_IMAGE_WORDS_SPV -P "${CMAKE_CURRENT_SOURCE_DIR}/embed_spirv.cmake" + DEPENDS "${agcTextureDetileShaderSource}" glslang-standalone + VERBATIM +) set(agcMeshArgumentsShaderSource ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/shaders/MeshArguments.comp) foreach(agcReadbackKind IN ITEMS Uint Depth) @@ -103,6 +156,19 @@ add_custom_command( VERBATIM ) +set(agcStagingCopyBackShaderSource ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/shaders/StagingCopyBack.comp) +set(agcStagingCopyBackShaderSpv ${agcTextureDetileGeneratedDir}/StagingCopyBack.spv) +set(agcStagingCopyBackShaderHeader ${agcTextureDetileGeneratedDir}/StagingCopyBack_spv.h) +add_custom_command( + OUTPUT "${agcStagingCopyBackShaderHeader}" + COMMAND ${CMAKE_COMMAND} -E make_directory "${agcTextureDetileGeneratedDir}" + COMMAND "$" -V --target-env vulkan1.1 -o "${agcStagingCopyBackShaderSpv}" "${agcStagingCopyBackShaderSource}" + COMMAND ${CMAKE_COMMAND} -DINPUT=${agcStagingCopyBackShaderSpv} -DOUTPUT=${agcStagingCopyBackShaderHeader} + -DSYMBOL=STAGING_COPY_BACK_SPV -P "${CMAKE_CURRENT_SOURCE_DIR}/embed_spirv.cmake" + DEPENDS "${agcStagingCopyBackShaderSource}" glslang-standalone + VERBATIM +) + set(agcCmaskClearShaderSource ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/shaders/CmaskClear.comp) set(agcCmaskClearShaderSpv ${agcTextureDetileGeneratedDir}/CmaskClear.spv) set(agcCmaskClearShaderHeader ${agcTextureDetileGeneratedDir}/CmaskClear_spv.h) @@ -167,6 +233,23 @@ add_custom_command( DEPENDS "${agcSampleDumpsShaderSource}" glslang-standalone VERBATIM ) +function(agc_embedded_shader SOURCE STEM SYMBOL OUT_LIST) + set(source "${CMAKE_CURRENT_SOURCE_DIR}/Graphics/shaders/${SOURCE}") + set(spv "${agcTextureDetileGeneratedDir}/${STEM}.spv") + set(header "${agcTextureDetileGeneratedDir}/${STEM}_spv.h") + add_custom_command( + OUTPUT "${header}" + COMMAND ${CMAKE_COMMAND} -E make_directory "${agcTextureDetileGeneratedDir}" + COMMAND "$" -V --target-env vulkan1.1 -o "${spv}" "${source}" + COMMAND ${CMAKE_COMMAND} -DINPUT=${spv} -DOUTPUT=${header} + -DSYMBOL=${SYMBOL} -P "${CMAKE_CURRENT_SOURCE_DIR}/embed_spirv.cmake" + DEPENDS "${source}" glslang-standalone + VERBATIM + ) + set(${OUT_LIST} ${${OUT_LIST}} "${header}" PARENT_SCOPE) +endfunction() +set(agcDepthTransferShaderHeaders) +agc_embedded_shader(DepthToStorage.comp DepthToStorage DEPTH_TO_STORAGE_SPV agcDepthTransferShaderHeaders) foreach(agcPresentationStage IN ITEMS Vert Frag) string(TOLOWER "${agcPresentationStage}" agcPresentationExtension) string(TOUPPER "${agcPresentationStage}" agcPresentationSymbol) @@ -372,12 +455,16 @@ set(agcDriverSources ${agcMultisampleUploadHeader} ${agcCmaskClearShaderHeader} ${agcMeshArgumentsShaderHeader} + ${agcStagingCopyBackShaderHeader} ${agcTextureDetileImageShaderHeader} ${agcTextureRepackShaderHeader} ${agcDispatchArgumentsShaderHeader} + ${agcTextureDetileWordsShaderHeader} + ${agcTextureDetileImageWordsShaderHeader} ${agcSampleCounterShaderHeader} ${agcSampleDumpsShaderHeader} ${agcPresentationShaderHeaders} + ${agcDepthTransferShaderHeaders} ${CMAKE_CURRENT_SOURCE_DIR}/Eq/src/Event.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Eq/src/Query.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Resource/src/Lifecycle.cpp @@ -426,6 +513,10 @@ add_shader_recompiler(shader_recompiler_agc_driver libc ${agcShaderCacheGuardSources} ) +target_include_directories(libSceAgcDriver INTERFACE + $ +) + target_link_libraries(libSceAgcDriver PRIVATE shader_recompiler_agc_driver decoder_png @@ -665,12 +756,14 @@ foreach(agcTestFragment IN LISTS agcTestFragments) include(${agcTestFragment}) endforeach() +target_include_directories(agc_driver_gcn_compare_exec_writes_tests PRIVATE $) + if(BUILD_TESTING) set_tests_properties(agc_driver_msaa_dcc_state ${multisampledDccClearTests} PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") set_tests_properties(${multisampledDccClearTests} PROPERTIES SKIP_RETURN_CODE 77) if(WIN32) - set_tests_properties(agc_driver_dithered_alpha_to_coverage agc_driver_two_sample_dithered_alpha_to_coverage ${multisampledDccClearTests} PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_driver_dithered_alpha_to_coverage agc_driver_two_sample_dithered_alpha_to_coverage agc_driver_alpha_to_coverage_sample_mask_export agc_driver_two_sample_alpha_to_coverage_sample_mask_export ${multisampledDccClearTests} PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endif() @@ -734,7 +827,7 @@ if(BUILD_TESTING) BUILD_WITH_INSTALL_RPATH TRUE INSTALL_RPATH "$ORIGIN") endif() - foreach(partialStoreTest agc_driver_depth_plane_partial_store_unimported agc_driver_depth_plane_partial_store_buffer_copy) + foreach(partialStoreTest agc_driver_depth_plane_partial_store_unimported agc_driver_depth_plane_partial_store_image_copy agc_driver_depth_plane_partial_store_buffer_copy) set_tests_properties(${partialStoreTest} PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_HOST_IMPORT=1" TIMEOUT 30 SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(WIN32) @@ -901,6 +994,22 @@ target_link_libraries(agc_compute_pipeline_replay PRIVATE libSceAgcDriver libc) set_target_properties(agc_compute_pipeline_replay PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_compute_pipeline_replay) +add_library(agc_oracle_support STATIC tools/OracleCodeObject.cpp tools/OracleDispatch.cpp) +target_include_directories(agc_oracle_support PUBLIC + ${LIBS_INCLUDE_DIR} + $ + ${CMAKE_SOURCE_DIR}/core/shader/recompiler + ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR} + ${ANYPS5_SPIRV_HEADERS_INCLUDE_DIR} +) +target_link_libraries(agc_oracle_support PUBLIC libSceAgcDriver libc) +set_target_properties(agc_oracle_support PROPERTIES CXX_EXTENSIONS OFF) + +add_executable(agc_oracle_runner EXCLUDE_FROM_ALL tools/OracleRunner.cpp) +target_link_libraries(agc_oracle_runner PRIVATE agc_oracle_support) +set_target_properties(agc_oracle_runner PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_oracle_runner) + if(BUILD_TESTING AND ANYPS5_ENABLE_SPIRV_TOOLS) add_test(NAME agc_compute_pipeline_inspection COMMAND ${CMAKE_COMMAND} -DREPLAY=$ diff --git a/core/libs/prx/libSceAgcDriver/Eq/src/Event.cpp b/core/libs/prx/libSceAgcDriver/Eq/src/Event.cpp index 035952a617..055e83ecbe 100644 --- a/core/libs/prx/libSceAgcDriver/Eq/src/Event.cpp +++ b/core/libs/prx/libSceAgcDriver/Eq/src/Event.cpp @@ -54,13 +54,14 @@ int AddEvent(KernelEqueue eq, std::uint32_t queue, std::uintptr_t ident, void* u } int DeleteEvent(KernelEqueue eq, std::uintptr_t ident, const char* caller) { - { - std::lock_guard lock(g_mutex); - std::erase_if(g_registrations, [&](const Registration& r) { return r.eq == eq && r.ident == ident; }); + if (eq == 0 || !EqueuePin_nid_postfix(eq)) { + throw std::runtime_error(std::string(caller) + ": invalid event queue"); } if (EqueueDeleteEvent_nid_postfix(eq, ident, EvfiltGraphicsCore) != EQUEUE_OK) { throw std::runtime_error(std::string(caller) + ": event is not registered"); } + std::lock_guard lock(g_mutex); + std::erase_if(g_registrations, [&](const Registration& r) { return r.eq == eq && r.ident == ident; }); return 0; } @@ -85,10 +86,16 @@ bool AgcDriverDeliverEopInterrupt(std::uint32_t queue) { extern "C" { int APS5_VABI sceAgcDriverAddEqEvent(KernelEqueue eq, int id, void* udata) { + if (id < 0) { + throw std::runtime_error(std::string(__func__) + ": negative event id"); + } return AddEvent(eq, static_cast(id), static_cast(id), udata, __func__); } int APS5_VABI sceAgcDriverDeleteEqEvent(KernelEqueue eq, int id) { + if (id < 0) { + throw std::runtime_error(std::string(__func__) + ": negative event id"); + } return DeleteEvent(eq, static_cast(id), __func__); } diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Dispatch/DispatchCache.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Dispatch/DispatchCache.hpp index b65f22d0d1..c7cf99a0a1 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Dispatch/DispatchCache.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Dispatch/DispatchCache.hpp @@ -3,6 +3,8 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" #include "prx/libSceAgcDriver/Execution/include/Recipe.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "IntermediateRepresentation/IrMetadata/ResourcePlan.hpp" #include #include #include @@ -15,12 +17,34 @@ #include #include #include +#include #include #include #include "prx/libc/include/general/AtomicSharedPtr.hpp" namespace AgcDriver::DriverDetail { +inline bool CaptureRegisteredViewsCurrent(std::span views) { + if (views.empty()) return true; + for (std::uint32_t attempt = 0; attempt < 2u; ++attempt) { + const auto before = GuestAllocations::GuestAllocationsGeneration_nid_postfix(); + bool current = true; + for (const auto& view : views) { + const auto range = GuestAllocations::GuestAllocationsLookup_nid_postfix(static_cast(view.address), false); + if (view.hasRange != (range != nullptr)) { + current = false; + break; + } + if (range != nullptr && (range->address != view.rangeAddress || range->bytes != view.rangeBytes || range->allocationAddress != view.allocationAddress || range->allocationBytes != view.allocationBytes || range->backingSerial != view.backingSerial || range->readable != view.readable || range->writable != view.writable || range->gpu != view.gpu)) { + current = false; + break; + } + } + if (before == GuestAllocations::GuestAllocationsGeneration_nid_postfix()) return current; + } + return false; +} + inline constexpr std::size_t MaxDispatchVariants = 8; struct DispatchVariant { @@ -30,6 +54,7 @@ struct DispatchVariant { std::vector shared; std::uint64_t forgetSerial = 0; std::shared_ptr compiled; + std::shared_ptr> registeredViews; std::array meshIndexDescriptor{}; std::shared_ptr shader; diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp index e486345e11..e1357753b8 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp @@ -124,6 +124,7 @@ enum class DrawVerdict { Drawn, Nothing, Rejected }; struct StageCapture { std::shared_ptr compiled; std::shared_ptr capture; + std::shared_ptr> registeredViews; std::vector regions; std::vector> supplied; std::uint64_t forgetSerial = 0; diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp index 38c45f9c84..18b36ea0f9 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp @@ -184,6 +184,7 @@ struct PreparedShaders : PreparedShaderState { std::atomic demanded{false}; bool pending = false; std::vector convertedSamples; + std::vector layerRowSamples; }; class SharedCode { @@ -302,7 +303,7 @@ struct ShaderSnapshot { }; -std::shared_ptr ReadRawShader(std::uint64_t address); +std::shared_ptr ReadRawShader(std::uint64_t address, const std::shared_ptr& retained = {}); std::shared_ptr ReadUserDataTailContinuation(std::span code, std::uint32_t userDataBase, std::span userData); std::shared_ptr ProgramSnapshot(const ShaderRegistry& shaders, std::uint64_t address, std::initializer_list types); void NoteRawGraphicsDraw(const ShaderSnapshot& snapshot); diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/FragmentBarycentric.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/FragmentBarycentric.hpp index 361aadb063..4dcd362276 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/FragmentBarycentric.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/FragmentBarycentric.hpp @@ -9,6 +9,10 @@ inline bool FragmentShaderBarycentricUsable(VkDriverId driver, bool reported) { return reported && driver != VK_DRIVER_ID_MOLTENVK; } +inline bool HostInterpolationPreferred(VkDriverId driver) { + return driver == VK_DRIVER_ID_MESA_RADV; +} + } #endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp index 3d32f4cdb1..f7ab9e51cc 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp @@ -54,6 +54,7 @@ void Write(std::uint64_t address, std::span source, std::size_t void WriteChanged(std::uint64_t address, std::span current, std::span original); void WriteChanged(std::uint64_t address, std::span current, std::span original, std::uint32_t byteStoreLimit); std::uint32_t ChangedByteStoreLimit(); +void WriteChangedKeepingGuest(std::uint64_t address, std::span current, std::span original); // Write tracking. The guest arena is reserved with page write watching, so CollectWrites learns which // pages of a range the CPU wrote since they were last collected, stamps their 64 KiB blocks with a new @@ -126,6 +127,7 @@ std::uint64_t CollectWritesUncached(std::uint64_t address, std::size_t bytes); // tracker mutex, so every collect that bumped before it has finished its walk and every later // collect stamps newer. std::uint64_t TrackerGeneration(); +void AdvanceTrackerGenerationForTest(std::uint64_t generation); std::uint64_t NewestStamp(std::uint64_t address, std::size_t bytes); std::uint64_t UntrackedHint(std::uint64_t address, std::size_t bytes); // Whether no CPU write touched the range since `generation`: an uncached resetting collect of the diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/KeyboardOverlay.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/KeyboardOverlay.hpp index 3553ee0cb9..1a58ce7149 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/KeyboardOverlay.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/KeyboardOverlay.hpp @@ -51,6 +51,8 @@ struct KeyboardGlyphEntry { }; inline constexpr char32_t KeyboardFallbackCode = 0xFFFD; +inline constexpr std::uint8_t KeyboardCompositionMark = 1; +inline constexpr std::uint8_t KeyboardSelectionMark = 2; std::span KeyboardGlyphTable(); bool KeyboardHasGlyph(char32_t code); diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Pm4.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Pm4.hpp index 0b9bc1f9f8..7403a695b2 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Pm4.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Pm4.hpp @@ -14,6 +14,11 @@ namespace AgcDriver::Pm4 { constexpr std::size_t GdsBytes = 0x10000; std::uint64_t GdsAddress(); +void RequestImportableGds(bool requested); +bool ImportableGdsRequested(); +bool GdsShared(); +bool GdsImportable(); +void RejectGdsImport(); struct DrawParameters { std::uint64_t indexAddress; diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/ShaderMemory.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/ShaderMemory.hpp index f0edd61fd1..3bec7a5126 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/ShaderMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/ShaderMemory.hpp @@ -91,6 +91,7 @@ public: private: std::shared_ptr capture(const ShaderRecompiler::RecompileRequest& request, const ShaderRecompiler::SourceHandle* handle, const ShaderRecompiler::PreparedShaderInvocation* invocation); + std::shared_ptr captureWaitingOnFailure(const ShaderRecompiler::RecompileRequest& request, const ShaderRecompiler::SourceHandle* handle, const ShaderRecompiler::PreparedShaderInvocation* invocation); static constexpr std::size_t PageBytes = 4096; static constexpr std::size_t PageWords = PageBytes / sizeof(std::uint32_t); @@ -169,6 +170,8 @@ private: std::size_t descriptorSpanBytes = 0; std::uint64_t rawInvalidations = 0; std::uint32_t forceSync = 0; + std::uint64_t branchReadsRefused = 0; + bool waitBranches = false; }; // The positions, among a dispatch-cache variant's stored words, of the pure flat-SRT leaves a diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp index 38bd2233f4..d6f06ae2bb 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp @@ -301,19 +301,19 @@ public: // records. Null when nothing is prepared: the resource cache may serve the dispatch (its // Revalidate stays under the mutex), or APS5_LOCKED_BUILD=1 keeps the whole build under it as // before. `shader` and `snapshots` must outlive the dispatch. - std::shared_ptr PrepareDispatch(const ShaderRecompiler::RecompileResult& shader, std::span snapshots, std::uint64_t dispatchThreads = 0, std::uint64_t programAddress = 0, std::optional> htileClear = std::nullopt); + std::shared_ptr PrepareDispatch(const ShaderRecompiler::RecompileResult& shader, std::span snapshots, std::uint64_t dispatchThreads = 0, std::uint64_t programAddress = 0, std::optional> htileClear = std::nullopt, const std::vector* registeredImageViews = nullptr); // APS5_PROFILE_DRAW: the parts of a PrepareDispatch in milliseconds, in the order key, find, // precollect, presync, stage A (the driver's 'prepare:' rows). static std::span PreparePhaseMs(const PreparedDispatch& prepared); // `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, std::optional> htileClear = std::nullopt); + 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, const std::vector* registeredImageViews = nullptr); struct IndirectOutcome { int cpuReason; double argumentReadMs; }; - IndirectOutcome DispatchIndirect(const ShaderRecompiler::RecompileResult& shader, std::uint64_t arguments, std::span snapshots = {}, std::uint64_t programAddress = 0, std::shared_ptr prepared = nullptr, std::shared_ptr* recipe = nullptr, const std::array& threadGroup = {}); + IndirectOutcome DispatchIndirect(const ShaderRecompiler::RecompileResult& shader, std::uint64_t arguments, std::span snapshots = {}, std::uint64_t programAddress = 0, std::shared_ptr prepared = nullptr, std::shared_ptr* recipe = nullptr, const std::array& threadGroup = {}, const std::vector* registeredImageViews = nullptr); // A value-equal dispatch-cache hit with a recipe (design_cpu_final M4), in two steps. // PrepareRecipe, WITHOUT GuestMemory::GpuMutex: the pre-checks (recipes on, the recipe's // device is this one, its template and pipeline objects still alive), the template's @@ -388,11 +388,12 @@ public: private: VulkanDevice(const PresentationWindow* window, bool compileOnly); friend struct VulkanDeviceTestAccess; + std::pair scratchPoolHeader(const ShaderRecompiler::RecompileResult& shader) const; VkPipeline splitComputePipeline(const Graphics::Context& context, const ShaderRecompiler::RecompileResult& shader, ComputePipelineObjects& objects, const std::array& base); 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 = {}, std::optional> htileClear = std::nullopt); + 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, const std::vector* registeredImageViews = nullptr); IndirectOutcome recordCompletedDispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::uint64_t programAddress, std::shared_ptr* recipeOut, const std::array& threadGroup, std::optional> htileClear, DispatchCompletion& completion); // The stage-A pre-sync over `surfaces` (see PrepareDispatch): the serial waited for, 0 none. std::uint64_t presync(std::span surfaces); diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/WorkerSampler.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/WorkerSampler.hpp index 57ac083143..72854adb7b 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/WorkerSampler.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/WorkerSampler.hpp @@ -8,6 +8,7 @@ namespace AgcDriver { void StartWorkerSampler(std::uint32_t queue = 0); void StartCommitterSampler(); void StopWorkerSampler(); +bool ReadableStackRegion(std::uint64_t address, std::uint64_t& low, std::uint64_t& high); } 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 32f81716d4..f6eaa8838a 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp @@ -307,7 +307,8 @@ void Driver::dispatch(QueueState& queue, std::span packet, } if (fromAhead) { if (const auto hook = aheadAdoptHook.load(std::memory_order_acquire)) hook(); - if (!CapturedCurrent(ahead.captured, snapshot)) { + const auto* aheadViews = ahead.capture != nullptr ? &ahead.capture->snapshot.registeredImages : ahead.keepVariant != nullptr && ahead.keepVariant->registeredViews != nullptr ? ahead.keepVariant->registeredViews.get() : nullptr; + if (!CapturedCurrent(ahead.captured, snapshot) || (aheadViews != nullptr && !CaptureRegisteredViewsCurrent(*aheadViews))) { fromAhead = false; ++dispatchesRejectedAhead; ++groupStats.memoryMismatches; @@ -427,6 +428,7 @@ void Driver::dispatch(QueueState& queue, std::span packet, phaseTiming.Phase(PhaseRecompile); if (!resolving) insertDispatch(address, key, noDispatchCache || !CacheableResult(*compiledResult), profile, registeredShader, forgetAtCapture, memory, shaderMemory, captured, capture, compiledResult, missedEntry, missedDiffering, attachVariant, phaseTiming); } + const auto* registeredViews = capture != nullptr ? &capture->snapshot.registeredImages : keepVariant != nullptr && keepVariant->registeredViews != nullptr ? keepVariant->registeredViews.get() : nullptr; if (resolving) { auto& parked = resolvedAhead()[currentPacketOffset()]; parked = ResolvedDispatch{}; @@ -515,7 +517,7 @@ 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, htileClearCompletion, htileMask] { + DrawPipeline::Queue0().Enqueue([this, localDevice = std::move(localDevice), result = compiledResult, capture, registeredViews, snapshots = std::move(snapshots), liveWords = std::move(liveWords), groups, address, queueId, recipe, keepVariant, dataHit, epoch, htileClearCompletion, htileMask] { DrawPipeline::FollowEpoch(epoch); GuestMemory::SetCurrentPacket(0x15, queueId); const auto& compiledCommit = *result; @@ -526,7 +528,7 @@ void Driver::dispatch(QueueState& queue, std::span packet, bool writersNoted = false; for (;;) { std::shared_ptr prepared; - if (recipeHit == nullptr || VulkanDevice::VerifyRecipes() || htileClearCompletion.has_value()) prepared = localDevice->PrepareDispatch(compiledCommit, snapshots, 0, address, htileClearCompletion); + if (recipeHit == nullptr || VulkanDevice::VerifyRecipes() || htileClearCompletion.has_value()) prepared = localDevice->PrepareDispatch(compiledCommit, snapshots, 0, address, htileClearCompletion, registeredViews); GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Dispatch); std::lock_guard gpuLock(GuestMemory::GpuMutex()); recordLabelsForPacket(localDevice.get(), queueId); @@ -544,7 +546,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), htileClearCompletion.has_value() ? nullptr : &builtRecipe, htileClearCompletion); + localDevice->Dispatch(compiledCommit, groups[0], groups[1], groups[2], snapshots, address, std::move(prepared), htileClearCompletion.has_value() ? nullptr : &builtRecipe, htileClearCompletion, registeredViews); } if (htileMask.bytes != 0u) localDevice->NoteHtileMask(htileMask); break; @@ -601,7 +603,7 @@ void Driver::dispatch(QueueState& queue, std::span packet, std::shared_ptr prepared; if (recipeHit == nullptr || VulkanDevice::VerifyRecipes()) { try { - prepared = localDevice->PrepareDispatch(compiled, snapshots, dispatchThreads, address, htileClearCompletion); + prepared = localDevice->PrepareDispatch(compiled, snapshots, dispatchThreads, address, htileClearCompletion, registeredViews); } catch (const std::exception& error) { if (rethrow(error)) return; } @@ -657,10 +659,10 @@ void Driver::dispatch(QueueState& queue, std::span packet, } if (indirectArguments != 0) countIndirect(outcome.cpuReason, outcome.argumentReadMs); } else if (indirectArguments != 0) { - const auto outcome = localDevice->DispatchIndirect(compiled, indirectArguments, snapshots, address, std::move(prepared), attachTo != nullptr ? &builtRecipe : nullptr, threadGroup); + const auto outcome = localDevice->DispatchIndirect(compiled, indirectArguments, snapshots, address, std::move(prepared), attachTo != nullptr ? &builtRecipe : nullptr, threadGroup, registeredViews); countIndirect(outcome.cpuReason, outcome.argumentReadMs); } else { - localDevice->Dispatch(compiled, groups[0], groups[1], groups[2], snapshots, address, std::move(prepared), htileClearCompletion.has_value() ? nullptr : (attachTo != nullptr ? &builtRecipe : nullptr), htileClearCompletion); + localDevice->Dispatch(compiled, groups[0], groups[1], groups[2], snapshots, address, std::move(prepared), htileClearCompletion.has_value() ? nullptr : (attachTo != nullptr ? &builtRecipe : nullptr), htileClearCompletion, registeredViews); } } catch (const std::exception& error) { if (rethrow(error)) return; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchCache.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchCache.cpp index a691951f22..7bfa057a40 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchCache.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchCache.cpp @@ -66,7 +66,7 @@ Driver::DispatchCacheFootprint Driver::DispatchCacheSize() { std::uint64_t Driver::variantBytes(const DispatchVariant& variant) { const auto held = [](const auto& values) { return static_cast(values.capacity() * sizeof(values[0])); }; - return sizeof(DispatchVariant) + held(variant.runs) + held(variant.words) + held(variant.shared) + held(variant.dataPositions) + held(variant.dataSlots) + held(variant.runBases) + held(variant.shaderDataPatches) + held(variant.captured) + held(variant.spans) + (variant.vertexInfo != nullptr ? sizeof(ShaderRecompiler::ShaderVertexStageInfo) : 0) + (variant.memory != nullptr ? variant.memory->Bytes() : 0); + return sizeof(DispatchVariant) + held(variant.runs) + held(variant.words) + held(variant.shared) + held(variant.dataPositions) + held(variant.dataSlots) + held(variant.runBases) + held(variant.shaderDataPatches) + held(variant.captured) + held(variant.spans) + (variant.registeredViews != nullptr ? held(*variant.registeredViews) : 0u) + (variant.vertexInfo != nullptr ? sizeof(ShaderRecompiler::ShaderVertexStageInfo) : 0) + (variant.memory != nullptr ? variant.memory->Bytes() : 0); } void Driver::accountVariant(const DispatchVariant& variant, bool added) { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchInsert.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchInsert.cpp index e732e518ee..6f7607eb33 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchInsert.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchInsert.cpp @@ -18,6 +18,7 @@ void Driver::insertDispatch(std::uint64_t address, std::uint64_t key, bool noDis if (!noDispatchCache) { auto fresh = std::make_shared(); fresh->compiled = compiledResult; + if (capture != nullptr && !capture->snapshot.registeredImages.empty()) fresh->registeredViews = std::make_shared>(capture->snapshot.registeredImages); fresh->shader = registeredShader; fresh->forgetSerial = forgetAtCapture; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchLookup.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchLookup.cpp index b0b37b7900..6b9bdf10f2 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchLookup.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchLookup.cpp @@ -1,5 +1,6 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "Optimization/ResourceProgram.hpp" #include namespace AgcDriver::DriverDetail { @@ -37,6 +38,7 @@ void Driver::lookupDispatch(std::uint64_t address, const Submission& submission, std::optional sampling; for (std::size_t i = 0; i < variants.size(); ++i) { + if (variants[i]->registeredViews != nullptr && !CaptureRegisteredViewsCurrent(*variants[i]->registeredViews)) continue; regions.clear(); appendEntryRegions(*variants[i], regions); ++compared; @@ -58,16 +60,20 @@ void Driver::lookupDispatch(std::uint64_t address, const Submission& submission, current = variant != nullptr; } else { variant = variants.front(); - compared = 1; - current = true; - for (const auto& [begin, bytes] : variant->spans) { - GuestMemory::CollectWrites(begin, bytes); - if (!GuestMemory::UnchangedSince(begin, bytes, generation)) { - current = false; - break; + current = variant->registeredViews == nullptr || CaptureRegisteredViewsCurrent(*variant->registeredViews); + if (!current) variant.reset(); + if (variant != nullptr) { + compared = 1; + current = true; + for (const auto& [begin, bytes] : variant->spans) { + GuestMemory::CollectWrites(begin, bytes); + if (!GuestMemory::UnchangedSince(begin, bytes, generation)) { + current = false; + break; + } } } - if (!current) { + if (!current && variant != nullptr) { std::uint64_t collected = 0; for (const auto& [begin, bytes] : variant->spans) collected = std::max(collected, GuestMemory::CollectWrites(begin, bytes)); 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 ef2435ef83..03bdda6fdf 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp @@ -1,5 +1,6 @@ #include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "Optimization/ResourceProgram.hpp" #include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" @@ -657,6 +658,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe throw; } for (std::size_t i = 0; i < matched.size(); ++i) { + if (matched[i] != nullptr) stageCaptures[i].registeredViews = matched[i]->registeredViews; if (matched[i] == nullptr || (matched[i]->shaderDataPatches.empty() && matched[i]->descriptorPatches.empty())) continue; applyShaderDataPatches(*matched[i]); if (stageData[i].compiled != nullptr) stageData[i].compiled = patchDataWords(*matched[i], stageData[i].words); @@ -923,6 +925,7 @@ DrawVerdict Driver::finishDraw(DrawTail& tail, PerformanceTimer& timing) { const auto candidate = std::find_if(dataCandidates[i].begin(), dataCandidates[i].end(), [&](const auto& kept) { return kept.first->pushOffset == pushCursorBytes; }); if (candidate != dataCandidates[i].end() && !verifyDrawDataHits()) { matched[i] = candidate->first; + stageCaptures[i].registeredViews = matched[i]->registeredViews; matchedRegions[i] = std::move(candidate->second); reused[i] = true; programResults[i] = stageData[i].compiled != nullptr ? stageData[i].compiled.get() : matched[i]->compiled.get(); @@ -955,7 +958,7 @@ DrawVerdict Driver::finishDraw(DrawTail& tail, PerformanceTimer& timing) { try { const auto slotEnd = (pushCursorBytes / Graphics::PipelinePushSlotBytes + 1u) * Graphics::PipelinePushSlotBytes; require(result.pushConstants.size() <= slotEnd - pushCursorBytes, "stage push constants exceed the pipeline push constant block"); - stages.push_back({program.binary.stage, &result, result.pushConstants.empty() ? 0u : pushCursorBytes}); + stages.push_back({program.binary.stage, &result, result.pushConstants.empty() ? 0u : pushCursorBytes, stageCaptures[i].registeredViews.get()}); pushCursorBytes += static_cast(result.pushConstants.size()); } catch (...) { TraceMaterializationFailure(trace.token, submission, packetOffset, "push-constant-validation", completedSubdraws, std::current_exception()); @@ -1058,6 +1061,7 @@ DrawVerdict Driver::finishDraw(DrawTail& tail, PerformanceTimer& timing) { if (!rejected.empty()) return DrawVerdict::Rejected; programResults[0] = compiledStages.back().get(); stages[0].program = programResults[0]; + stages[0].registeredImageViews = stageCaptures[0].registeredViews.get(); require(programResults[0]->pushConstants.size() == pushBytes && programResults[0]->parameterExports == exports, "pairing the mesh program with its pixel shader changed the push constants or the parameter exports the pixel shader was built for"); } require(programResults[0]->meshBarycentrics == meshBarycentrics, "the mesh program was compiled for a different barycentric layout than its pixel shader"); @@ -1175,6 +1179,7 @@ DrawVerdict Driver::finishDraw(DrawTail& tail, PerformanceTimer& timing) { const auto pushBytes = result.pushConstants.size(); decodeVertexInfo(programIndex); result = *materializeDrawStage(programIndex, pushOffsets[programIndex], queue, submission, packetOffset, trace.token, programs, graphics, pixel, vertexInfos, memory, linked, programs[programIndex].binary.stage == Stage::TessellationControl || programs[programIndex].binary.stage == Stage::TessellationEvaluation ? direct : drawParameters, localDevice, *shaderMemory, stageCaptures, recompiled, drawHit, matched, matchedRegions, profile, dumpTarget, dumpSlot1, captures, phaseTiming, phaseMs, rejected, meshBarycentrics); + for (auto& stage : stages) if (stage.program == &result) stage.registeredImageViews = stageCaptures[programIndex].registeredViews.get(); if (!rejected.empty()) return DrawVerdict::Rejected; require(result.pushConstants.size() == pushBytes, "patched program changed its push constant layout"); } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp index 02fc76119e..bed17faa1b 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp @@ -227,8 +227,8 @@ void Driver::eraseDrawEntry(std::uint64_t key, const std::shared_ptr& bool DecodeProgramsCurrent(const DrawDecode& decode, const ShaderRegistry&) { return std::all_of(decode.programs.begin(), decode.programs.end(), [&](const DrawProgram& program) { if (!program.snapshot->header.empty()) return true; - return ReadRawShader(program.snapshot->codeAddress + program.codeOffset * sizeof(std::uint32_t)) == program.snapshot && - (program.tailContinuation == nullptr || ReadRawShader(program.tailContinuation->codeAddress) == program.tailContinuation); + return ReadRawShader(program.snapshot->codeAddress + program.codeOffset * sizeof(std::uint32_t), program.snapshot) == program.snapshot && + (program.tailContinuation == nullptr || ReadRawShader(program.tailContinuation->codeAddress, program.tailContinuation) == program.tailContinuation); }); } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp index c33af99f90..735319763b 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp @@ -117,6 +117,7 @@ std::shared_ptr Driver::materializeDraw stageCapture.regions = shaderMemory.TakeRecentRegions(); stageCapture.supplied = shaderMemory.SuppliedBlocks(); stageCapture.userDataUse = capture->userDataUse; + stageCapture.registeredViews = std::make_shared>(capture->snapshot.registeredImages); recompiled[i] = true; memory = shaderMemory.Regions(); @@ -225,6 +226,7 @@ void Driver::cacheDrawStages(bool useDrawEntries, bool drawHit, const Pm4::DrawP if (stageCapture.compiled == nullptr || !CacheableResult(*stageCapture.compiled)) continue; auto variant = std::make_shared(); variant->compiled = stageCapture.compiled; + variant->registeredViews = stageCapture.registeredViews; if (programs[i].binary.stage == ShaderRecompiler::ShaderStage::Mesh) { std::copy_n(programs[i].userData.begin() + ShaderRecompiler::MeshIndexBufferUserWord, variant->meshIndexDescriptor.size(), variant->meshIndexDescriptor.begin()); } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp index 3976823654..c61e3112c5 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp @@ -1,6 +1,7 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "Optimization/ResourceProgram.hpp" #include #include @@ -141,6 +142,7 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptrregisteredViews != nullptr && !CaptureRegisteredViewsCurrent(*variant->registeredViews)) continue; if (variant->pushOffset != cursor) continue; if (programs[i].binary.stage == ShaderRecompiler::ShaderStage::Mesh && !std::equal(variant->meshIndexDescriptor.begin(), variant->meshIndexDescriptor.end(), programs[i].userData.begin() + ShaderRecompiler::MeshIndexBufferUserWord)) { if (!anyLayout) outcome = EntryOutcome::Differing; @@ -249,6 +251,7 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptrdecode->programs[i].userData; auto& candidates = dataCandidates[i]; for (const auto& variant : dataEntry->stages[i]) { + if (variant->registeredViews != nullptr && !CaptureRegisteredViewsCurrent(*variant->registeredViews)) continue; bool usedEqual = true; for (std::size_t w = 0; w < now.size() && usedEqual; ++w) { if (now[w] != was[w] && (w >= 64u || ((variant->userDataMask >> w) & 1u) != 0)) usedEqual = false; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp index 4043badb89..4907b257e8 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp @@ -415,6 +415,7 @@ void Driver::execute(const Submission& submission) { CheckFailure(); std::uint64_t batchesAtFlip = 0, unsignaledAtFlip = 0; bool flipInterruptDeferred = false; + Graphics::StorageTexture::NoteFlip(); if (!drains) { GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Flush); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp index 4705332a25..887e45d265 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp @@ -16,16 +16,40 @@ #include #include #include +#include namespace AgcDriver::DriverDetail { namespace { -bool gdsWritesPending(std::span packet) { +bool gdsWritesPending(std::span packet, const VulkanDevice* localDevice) { const auto address = Pm4::GdsAddress() + packet[4]; const auto bytes = static_cast(Pm4::DmaBytes(packet)); const std::array, 1> range{{{address, address + bytes}}}; - return Graphics::Recorder::SnapshotWriteOverlaps(address, bytes) || Graphics::StorageTexture::AnyPendingOverlaps(range) || Graphics::AnyShadowedOverlaps(address, bytes); + if (Graphics::Recorder::SnapshotWriteOverlaps(address, bytes) || Graphics::StorageTexture::AnyPendingOverlaps(range) || Graphics::AnyShadowedOverlaps(address, bytes)) return true; + if (localDevice == nullptr || !Pm4::GdsImportable()) return false; + std::lock_guard gpuLock(GuestMemory::GpuMutex()); + return localDevice->StoresPendingOver(address, bytes); +} + +std::uint64_t dmaAddress(std::uint32_t low, std::uint32_t high) { + return low | (static_cast(high) << 32u); +} + +bool gdsSpan(std::uint64_t offset, std::size_t bytes) { + return offset <= Pm4::GdsBytes && bytes <= Pm4::GdsBytes - offset; +} + +std::optional decodeCopy(std::span packet) { + if (auto copy = Pm4::DecodeMemoryCopy(packet)) return copy; + if (packet.size() != 7 || !Pm4::GdsImportable()) return std::nullopt; + const auto source = Pm4::DmaSource(packet); + const auto destination = Pm4::DmaDestination(packet); + const std::size_t bytes = Pm4::DmaBytes(packet); + if (bytes == 0) return std::nullopt; + if (source == Pm4::DmaSelectGds && (destination == 0 || destination == 3) && gdsSpan(packet[2], bytes)) return Pm4::MemoryCopy{Pm4::GdsAddress() + packet[2], dmaAddress(packet[4], packet[5]), bytes}; + if ((source == 0 || source == 3) && destination == Pm4::DmaSelectGds && gdsSpan(packet[4], bytes)) return Pm4::MemoryCopy{dmaAddress(packet[2], packet[3]), Pm4::GdsAddress() + packet[4], bytes}; + return std::nullopt; } } @@ -130,7 +154,7 @@ bool Driver::preparePacketMemory(const Submission& submission, QueueState& queue if (!drainAll && !cpuStores && (opcode == 0x40 || opcode == 0x50 || opcode == 0x83)) { constexpr std::size_t gpuStoreLimit = 65536; bool drained = true; - const auto copy = opcode == 0x50 ? Pm4::DecodeMemoryCopy(packet) : std::nullopt; + const auto copy = opcode == 0x50 ? decodeCopy(packet) : std::nullopt; const auto copyBehindPendingSource = [&] { if (!copy.has_value() || copy->bytes > gpuStoreLimit || !GuestMemory::Accessible(reinterpret_cast(copy->source), copy->bytes) || !GuestMemory::Accessible(reinterpret_cast(copy->destination), copy->bytes, true)) return false; const auto end = copy->source + copy->bytes; @@ -148,16 +172,16 @@ bool Driver::preparePacketMemory(const Submission& submission, QueueState& queue if (outcome.path != 1 && outcome.path != 3) throw std::runtime_error("AGC driver: a DMA_DATA copy of up to 64 KiB that GpuCopyPossible accepted took copy path " + std::to_string(outcome.path)); return true; }; - if (opcode == 0x50 && packet.size() >= 7 && Pm4::DmaDestination(packet) == Pm4::DmaSelectGds) { - if (!gdsWritesPending(packet)) { - orderedAlready = true; - drained = false; - } - } else if (copyBehindPendingSource()) { + const bool gdsDestination = opcode == 0x50 && packet.size() >= 7 && Pm4::DmaDestination(packet) == Pm4::DmaSelectGds; + const bool gdsOnGpu = gdsDestination && Pm4::GdsImportable(); + if ((!gdsDestination || gdsOnGpu) && copyBehindPendingSource()) { ++copiesBehindPendingSources; wroteOnGpu = true; drained = false; - } else if (const auto store = Pm4::ResolveStore(packet, queue, gpuStoreLimit)) { + } else if (gdsDestination && !gdsWritesPending(packet, device.Load().get())) { + orderedAlready = true; + drained = false; + } else if (const auto store = gdsDestination && !gdsOnGpu ? std::nullopt : Pm4::ResolveStore(packet, queue, gpuStoreLimit)) { const auto bytes = store->Bytes(); if (bytes.empty()) { @@ -313,7 +337,7 @@ void Driver::dumpSampleCounters(std::uint64_t address) { bool Driver::enqueueDmaPacket(std::span packet, std::uint32_t opcode, std::uint32_t queue, const QueueState& state) { if (opcode != 0x50 || queue != 0 || packet.size() < 7 || !DrawPipeline::Active() || !deferredLabels().labels.empty()) return false; const auto source = ((packet[1] >> 29u) & 3u) | ((packet[6] >> 24u) & 4u) | ((packet[6] >> 25u) & 8u); - if (Pm4::DmaDestination(packet) == Pm4::DmaSelectGds && gdsWritesPending(packet)) return false; + if (Pm4::DmaDestination(packet) == Pm4::DmaSelectGds && gdsWritesPending(packet, device.Load().get())) return false; constexpr std::size_t immediateLimit = std::size_t{16} << 20u; constexpr std::size_t gpuStoreLimit = 65536; if (source == 2 && Pm4::DmaBytes(packet) > gpuStoreLimit && Pm4::DmaDestination(packet) != Pm4::DmaSelectGds) { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/QueueWorker.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/QueueWorker.cpp index b04c9c6fdd..e30e7dfe0f 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/QueueWorker.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/QueueWorker.cpp @@ -227,8 +227,7 @@ void Driver::run(std::uint32_t id) noexcept { const auto idleEpochAtWait = idleEpoch.load(std::memory_order_acquire); changed.wait(lock, [&] { return ready() || (id == 0 && (idleEpoch.load(std::memory_order_acquire) != idleEpochAtWait || completionsPending())); }); if (id == 0) queue0Dormant.store(false, std::memory_order_relaxed); - if (id == 0 && !ready() && (idleEpoch.load(std::memory_order_acquire) != idleEpochAtWait || completionsPending())) continue; - break; + continue; } if (poll(ready)) break; lock.unlock(); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderDiagnostics.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderDiagnostics.cpp index 279a99c2b0..6dbe187d29 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderDiagnostics.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderDiagnostics.cpp @@ -1,7 +1,16 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Mutex.hpp" #include "ControlFlow/RequestSerializer.hpp" +#include "Optimization/ResourceProgram.hpp" #include +#include +#include +#include +#ifdef _WIN32 +#include +#else +#include +#endif #include #include #include @@ -11,6 +20,15 @@ namespace AgcDriver::DriverDetail { namespace { +std::string captureSession() { +#ifdef _WIN32 + const auto process = _getpid(); +#else + const auto process = getpid(); +#endif + return std::to_string(process) + "-" + std::to_string(std::chrono::system_clock::now().time_since_epoch().count()); +} + std::optional> selectedPrograms() { const char* value = std::getenv("APS5_DUMP_SHADERS"); if (value == nullptr || std::string_view(value) == "1") return std::nullopt; @@ -32,6 +50,46 @@ std::optional> selectedPrograms() { } +std::shared_ptr PrepareShaderWithDiagnostics(const ShaderRecompiler::RecompileRequest& request, ShaderRecompiler::ShaderPreparationContext* preparation) { + std::exception_ptr failure; + std::string dump; + static const bool dumpInputs = std::getenv("APS5_DUMP_PREPARE_INPUTS") != nullptr; + std::string inputDump; + if (dumpInputs) { + try { + static const auto session = captureSession(); + static std::atomic next{0}; + const auto suffix = ".input-" + session + "-" + std::to_string(next.fetch_add(1)) + ".prepare"; + inputDump = Driver::dumpRequest(request.shader.codeAddress, request, suffix); + if (!inputDump.empty()) { + std::fprintf(stderr, "[prepare-input] begin %s\n", inputDump.c_str()); + std::fflush(stderr); + } + } catch (const std::exception& error) { + std::fprintf(stderr, "[prepare-input] capture failed: %s\n", error.what()); + } + } + try { + auto handle = ShaderRecompiler::PrepareShader(request, preparation); + if (!inputDump.empty()) std::fprintf(stderr, "[prepare-input] end %s\n", inputDump.c_str()); + return handle; + } catch (...) { + failure = std::current_exception(); + try { + if (std::getenv("APS5_DUMP_SHADERS") != nullptr) dump = Driver::dumpRequest(request.shader.codeAddress, request, ".prepare"); + } catch (...) {} + } + try { + std::rethrow_exception(failure); + } catch (const std::exception& error) { + std::ostringstream where; + where << "shader 0x" << std::hex << request.shader.codeAddress << " stage " << static_cast(request.shader.stage); + if (!dump.empty()) where << " (" << dump << ")"; + where << ": "; + throw std::runtime_error(where.str() + error.what()); + } +} + void Driver::reportSkip(const char* kind, const std::string& what) { static AgcDriver::Mutex reportedMutex; static std::set reported; @@ -96,14 +154,17 @@ std::string Driver::dumpRequest(std::uint64_t address, const ShaderRecompiler::R if (written != text.size() || closed != 0) { dumped.erase(name); std::fprintf(stderr, "[gpu] incomplete write to dump file %s (%zu of %zu bytes, close status %d)\n", name.c_str(), written, text.size(), closed); + return {}; } } else { dumped.erase(name); std::fprintf(stderr, "[gpu] could not open dump file %s for writing\n", name.c_str()); + return {}; } } catch (const std::exception& error) { dumped.erase(name); std::fprintf(stderr, "[gpu] could not serialize request for 0x%llx: %s\n", static_cast(address), error.what()); + return {}; } return name; } 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 15f26e410a..f7e6b12fd0 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -60,30 +60,8 @@ namespace AgcDriver::DriverDetail { -std::shared_ptr PrepareShaderWithDiagnostics(const ShaderRecompiler::RecompileRequest& request, ShaderRecompiler::ShaderPreparationContext* preparation) { - std::exception_ptr failure; - std::string dump; - try { - auto handle = ShaderRecompiler::PrepareShader(request, preparation); - return handle; - } catch (...) { - failure = std::current_exception(); - try { - if (std::getenv("APS5_DUMP_SHADERS") != nullptr) dump = Driver::dumpRequest(request.shader.codeAddress, request, ".prepare"); - } catch (...) {} - } - try { - std::rethrow_exception(failure); - } catch (const std::exception& error) { - std::ostringstream where; - where << "shader 0x" << std::hex << request.shader.codeAddress << " stage " << static_cast(request.shader.stage); - if (!dump.empty()) where << " (" << dump << ")"; - where << ": "; - throw std::runtime_error(where.str() + error.what()); - } -} -std::shared_ptr ReadRawShader(std::uint64_t address) { +std::shared_ptr ReadRawShader(std::uint64_t address, const std::shared_ptr& retained) { const auto drain = [](std::uint64_t first, std::size_t bytes) { if (!DrawPipeline::Active()) return; auto& pipeline = DrawPipeline::Queue0(); @@ -92,8 +70,8 @@ std::shared_ptr ReadRawShader(std::uint64_t address) { static std::mutex cacheMutex; static std::list> cache; static std::size_t cacheBytes = 0; - std::shared_ptr cached; - { + std::shared_ptr cached = retained != nullptr && retained->codeAddress == address && retained->header.empty() && !retained->code.empty() ? retained : nullptr; + if (cached == nullptr) { std::lock_guard lock(cacheMutex); const auto found = std::find_if(cache.begin(), cache.end(), [address](const auto& entry) { return entry->codeAddress == address; }); if (found != cache.end()) cached = *found; @@ -902,6 +880,10 @@ void ReportPreparedAtUse(const ShaderSnapshot& snapshot, const ShaderRecompiler: APS5_LOG_OUT_DEBUG("Shader 0x%llx samples a converted image; preparing its converted-sample variant at use", static_cast(request.shader.codeAddress)); return; } + if (request.context.layerRowSamples) { + APS5_LOG_OUT_DEBUG("Shader 0x%llx samples a 1D array past maxImageArrayLayers; preparing its layer-row variant at use", static_cast(request.shader.codeAddress)); + return; + } if (snapshot.codeAddress == NullPixelProgramAddress() && request.shader.stage == ShaderRecompiler::ShaderStage::Fragment && request.context.waveSize == 32u) { APS5_LOG_ERR("The null pixel program has no wave%u artifact for this draw; preparing it at draw", request.context.waveSize); return; @@ -1163,6 +1145,7 @@ ShaderRecompiler::PreparedShaderInvocation InvocationFor(const ShaderSnapshot& s std::unique_lock lock(snapshot.prepared->mutex); AwaitRegisteredPreparation(*snapshot.prepared, lock); if (std::ranges::find(snapshot.prepared->convertedSamples, codeOffset) != snapshot.prepared->convertedSamples.end()) source.context.convertedSamples = true; + if (std::ranges::find(snapshot.prepared->layerRowSamples, codeOffset) != snapshot.prepared->layerRowSamples.end()) source.context.layerRowSamples = true; auto invocationRequest = source; struct PreparedKeyStorage {}; auto& key = HostThreadLocal, PreparedKeyStorage>(); @@ -1190,18 +1173,22 @@ ShaderRecompiler::PreparedShaderInvocation InvocationFor(const ShaderSnapshot& s } std::shared_ptr CaptureInvocation(ShaderMemory& memory, const ShaderSnapshot& snapshot, std::size_t codeOffset, const ShaderRecompiler::RecompileRequest& request, ShaderRecompiler::PreparedShaderInvocation& invocation) { - try { - return memory.Capture(invocation); - } catch (const ShaderRecompiler::ConvertedSampleRequired&) { - if (invocation.Request().context.convertedSamples) throw; - } - { + const auto record = [&](std::vector& offsets) { std::lock_guard lock(snapshot.prepared->mutex); - auto& offsets = snapshot.prepared->convertedSamples; if (std::ranges::find(offsets, codeOffset) == offsets.end()) offsets.push_back(codeOffset); + }; + while (true) { + try { + return memory.Capture(invocation); + } catch (const ShaderRecompiler::ConvertedSampleRequired&) { + if (invocation.Request().context.convertedSamples) throw; + record(snapshot.prepared->convertedSamples); + } catch (const ShaderRecompiler::LayerRowSamplesRequired&) { + if (invocation.Request().context.layerRowSamples) throw; + record(snapshot.prepared->layerRowSamples); + } + invocation = InvocationFor(snapshot, codeOffset, request); } - invocation = InvocationFor(snapshot, codeOffset, request); - return memory.Capture(invocation); } namespace { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp index 909a088315..9cd8b15bac 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp @@ -1057,11 +1057,11 @@ struct WriteTracker { static constexpr std::size_t CoarseBlocks = 64; static constexpr std::size_t LeafBlocks = std::size_t{1} << 16; struct Leaf { - std::array blocks{}; - std::array cpuBlocks{}; - std::array coarseBlocks{}; + std::array blocks{}; + std::array cpuBlocks{}; + std::array coarseBlocks{}; #ifdef _WIN32 - std::array hintBlocks{}; + std::array hintBlocks{}; #endif }; std::vector> leaves; @@ -1076,8 +1076,8 @@ struct WriteTracker { // stamp still happen under it): a hit must return the current value, including MarkWritten // bumps, or an image refreshed after a stamp would keep failing UnchangedSince until the next // walk of its range and re-upload at every use of the epoch. - std::atomic generation{1}; - std::atomic stable{1}; + std::atomic generation{1}; + std::atomic stable{1}; std::vector pages; // Collect memo: page-rounded ranges walked during the current epoch (see BumpCollectEpoch). A // collect fully inside one skips GetWriteWatch, whose cost is proportional to the range's pages, @@ -1160,24 +1160,24 @@ struct WriteTracker { return !GuestArena::GuestArenaHostRegionOverlaps_nid_postfix(static_cast(first), static_cast(stop - first)); } - std::uint32_t newestHint(std::uint64_t first, std::uint64_t stop) const { - std::uint32_t newest = 0; + std::uint64_t newestHint(std::uint64_t first, std::uint64_t stop) const { + std::uint64_t newest = 0; for (auto block = blockOf(first); block <= blockOf(stop - 1); ++block) newest = std::max(newest, hintOf(block)); return newest; } #endif struct DriverPiece { - std::uint32_t generation = 0; + std::uint64_t generation = 0; std::uint32_t begin = 0; std::uint32_t end = 0; }; struct DriverPieces { - std::uint32_t whole = 0; - std::uint32_t dropped = 0; + std::uint64_t whole = 0; + std::uint64_t dropped = 0; std::array pieces{}; - std::uint32_t possibleWhole = 0; - std::uint32_t possibleDropped = 0; + std::uint64_t possibleWhole = 0; + std::uint64_t possibleDropped = 0; std::array possiblePieces{}; bool covers(std::uint32_t begin, std::uint32_t end, std::uint64_t generation) const { @@ -1197,21 +1197,21 @@ struct WriteTracker { std::unordered_map driverPieces; struct CpuPages { - std::array pages{}; + std::array pages{}; }; std::unordered_map cpuPages; std::unordered_map walkPages; - std::map refused; + std::map refused; - void noteCpuStore(std::uint64_t begin, std::uint64_t end, std::uint32_t stampGeneration) { + void noteCpuStore(std::uint64_t begin, std::uint64_t end, std::uint64_t stampGeneration) { notePages(cpuPages, begin, end, stampGeneration); } - void noteWalkStore(std::uint64_t begin, std::uint64_t end, std::uint32_t stampGeneration) { + void noteWalkStore(std::uint64_t begin, std::uint64_t end, std::uint64_t stampGeneration) { notePages(walkPages, begin, end, stampGeneration); } - void notePages(std::unordered_map& pages, std::uint64_t begin, std::uint64_t end, std::uint32_t stampGeneration) { + void notePages(std::unordered_map& pages, std::uint64_t begin, std::uint64_t end, std::uint64_t stampGeneration) { for (auto block = blockOf(begin); block <= blockOf(end - 1); ++block) { const auto blockStart = blockBegin(block); const auto from = std::max(begin, blockStart) - blockStart; @@ -1235,7 +1235,7 @@ struct WriteTracker { #endif } - void stampWatchedPart(std::uint64_t address, std::uint64_t end, std::uint32_t stampGeneration, bool complete) { + void stampWatchedPart(std::uint64_t address, std::uint64_t end, std::uint64_t stampGeneration, bool complete) { #ifdef _WIN32 address = std::max(address, base); end = std::min(end, base + size); @@ -1253,7 +1253,7 @@ struct WriteTracker { } } - void noteDriverStore(std::uint64_t block, std::uint64_t address, std::uint64_t end, std::uint32_t stampGeneration, bool complete) { + void noteDriverStore(std::uint64_t block, std::uint64_t address, std::uint64_t end, std::uint64_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); @@ -1301,19 +1301,19 @@ struct WriteTracker { leafFor(block).hintBlocks[block % LeafBlocks] = generation; } - std::uint32_t hintOf(std::uint64_t block) const { + std::uint64_t hintOf(std::uint64_t block) const { const auto& leaf = leaves[block / LeafBlocks]; return leaf != nullptr ? leaf->hintBlocks[block % LeafBlocks] : 0; } #endif - std::uint32_t stampOf(std::uint64_t block) const { + std::uint64_t stampOf(std::uint64_t block) const { const auto& leaf = leaves[block / LeafBlocks]; return leaf != nullptr ? leaf->blocks[block % LeafBlocks] : 0; } static constexpr std::size_t UnstampedSlots = 1u << 16; - std::vector unstampedStores; + std::vector unstampedStores; void noteUnstampedStore(std::uint64_t address, std::uint64_t end) { if (unstampedStores.empty()) unstampedStores.assign(UnstampedSlots, 0); @@ -1326,8 +1326,8 @@ struct WriteTracker { for (auto block = first; block <= last; ++block) unstampedStores[block % UnstampedSlots] = generation; } - std::uint32_t newestStamp(std::uint64_t address, std::uint64_t end) const { - std::uint32_t newest = 0; + std::uint64_t newestStamp(std::uint64_t address, std::uint64_t end) const { + std::uint64_t newest = 0; if (!unstampedStores.empty()) { const auto first = address / WriteBlockBytes; const auto last = (end - 1) / WriteBlockBytes; @@ -1346,14 +1346,14 @@ struct WriteTracker { return newest; } - std::uint32_t coarseStampOf(std::uint64_t block) const { + std::uint64_t coarseStampOf(std::uint64_t block) const { const auto& leaf = leaves[block / LeafBlocks]; return leaf != nullptr ? leaf->coarseBlocks[(block % LeafBlocks) / CoarseBlocks] : 0; } bool unchanged(std::uint64_t first, std::uint64_t last, std::uint64_t since) const { if (first == last) return stampOf(first) <= since; - const auto newer = [since](std::uint32_t stamp) { return stamp > since; }; + const auto newer = [since](std::uint64_t stamp) { return stamp > since; }; while (first <= last) { const auto& leaf = leaves[first / LeafBlocks]; if (leaf == nullptr) { @@ -1370,7 +1370,7 @@ struct WriteTracker { return true; } - std::uint32_t cpuStampOf(std::uint64_t block) const { + std::uint64_t cpuStampOf(std::uint64_t block) const { const auto& leaf = leaves[block / LeafBlocks]; return leaf != nullptr ? leaf->cpuBlocks[block % LeafBlocks] : 0; } @@ -1422,7 +1422,7 @@ struct WriteTracker { return unchanged(blockOf(address), blockOf(address + bytes - 1), since) || !storedOver(address, address + bytes, since, true); } - void stamp(std::uint64_t block, std::uint32_t stampGeneration, StampKind kind) { + void stamp(std::uint64_t block, std::uint64_t stampGeneration, StampKind kind) { const bool cpu = kind == StampKind::Cpu || kind == StampKind::ImportWindow; const bool stored = kind != StampKind::ImportWindow; auto& leaf = leafFor(block); @@ -1516,7 +1516,7 @@ void watchPrivateMapping(std::uintptr_t address, std::size_t bytes, std::uint64_ tracker.noteCpuStore(address, address + bytes, tracker.generation); for (auto block = tracker.blockOf(address); block <= tracker.blockOf(address + bytes - 1); ++block) { if (const auto& leaf = tracker.leaves[block / WriteTracker::LeafBlocks]; leaf != nullptr) leaf->hintBlocks[block % WriteTracker::LeafBlocks] = 0; - if (const auto found = tracker.driverPieces.find(block); found != tracker.driverPieces.end()) found->second.dropped = std::max(found->second.dropped, tracker.generation); + if (const auto found = tracker.driverPieces.find(block); found != tracker.driverPieces.end()) found->second.dropped = std::max(found->second.dropped, tracker.generation); tracker.stamp(block, tracker.generation, StampKind::Cpu); } unwatchSerial.fetch_add(1, std::memory_order_release); @@ -1593,7 +1593,7 @@ struct StampRuns { std::vector> windowRuns; }; -void noteWrittenRun(StampRuns& runs, std::uintptr_t begin, std::uintptr_t end, std::uint32_t generation) { +void noteWrittenRun(StampRuns& runs, std::uintptr_t begin, std::uintptr_t end, std::uint64_t generation) { if (runs.kind != StampKind::Driver) runs.tracker.noteCpuStore(begin, end, generation); else runs.tracker.noteWalkStore(begin, end, generation); } @@ -1741,7 +1741,8 @@ std::uint64_t collectWrites(std::uint64_t address, std::size_t bytes, bool memoi } } const TimedAccess timed(CounterCollect, bytes); -#ifndef _WIN32 + const auto requestedFirst = first; + const auto requestedStop = stop; if (useMemo && HostThreadSlot::Get() != 0 && bytes <= WriteBlockBytes) { const auto begin = first & ~(WriteBlockBytes - 1); const auto end = (stop + WriteBlockBytes - 1) & ~(WriteBlockBytes - 1); @@ -1750,8 +1751,16 @@ std::uint64_t collectWrites(std::uint64_t address, std::size_t bytes, bool memoi stop = end; } } + if (!walkWrites(tracker, first, stop, StampKind::Cpu)) { +#ifdef _WIN32 + if (first == requestedFirst && stop == requestedStop) return 0; + first = requestedFirst; + stop = requestedStop; + if (!walkWrites(tracker, first, stop, StampKind::Cpu)) return 0; +#else + return 0; #endif - if (!walkWrites(tracker, first, stop, StampKind::Cpu)) return 0; + } // Only a completed walk is remembered; a failed one (uncommitted pages) returned 0 above. if (collectMemoEnabled()) { const auto serial = unwatchSerial.load(std::memory_order_relaxed); @@ -2299,6 +2308,12 @@ std::uint64_t TrackerGeneration() { return tracker.generation; } +void AdvanceTrackerGenerationForTest(std::uint64_t generation) { + auto& tracker = Tracker(); + const auto lock = lockTracker(tracker); + if (generation > tracker.generation.load(std::memory_order_relaxed)) tracker.generation.store(generation, std::memory_order_relaxed); +} + std::uint64_t NewestStamp(std::uint64_t address, std::size_t bytes) { auto& tracker = Tracker(); const auto lock = lockTracker(tracker); @@ -3024,7 +3039,7 @@ void WriteChanged(std::uint64_t address, std::span current, std WriteChanged(address, current, original, ChangedByteStoreLimit()); } -void WriteChanged(std::uint64_t address, std::span current, std::span original, std::uint32_t byteStoreLimit) { +static void writeChanged(std::uint64_t address, std::span current, std::span original, std::uint32_t byteStoreLimit, bool keepGuest) { require(current.size() == original.size(), "write-back snapshot sizes differ"); BreakProofs(); if (current.empty()) return; @@ -3044,7 +3059,9 @@ void WriteChanged(std::uint64_t address, std::span current, std std::size_t at = 0; for (; at + 16 <= size; at += 16) { const auto bytes = _mm_loadu_si128(reinterpret_cast(current.data() + at)); - const auto same = _mm_cmpeq_epi8(bytes, _mm_loadu_si128(reinterpret_cast(original.data() + at))); + const auto uploaded = _mm_loadu_si128(reinterpret_cast(original.data() + at)); + auto same = _mm_cmpeq_epi8(bytes, uploaded); + if (keepGuest) same = _mm_or_si128(same, _mm_xor_si128(_mm_cmpeq_epi8(_mm_loadu_si128(reinterpret_cast(destination + at)), uploaded), _mm_set1_epi8(-1))); const auto equal = static_cast(_mm_movemask_epi8(same)); if (equal == 0xffffu) continue; const auto changed = ~equal & 0xffffu; @@ -3063,7 +3080,7 @@ void WriteChanged(std::uint64_t address, std::span current, std } if (masked) _mm_sfence(); for (; at < size; ++at) { - if (current[at] == original[at]) continue; + if (current[at] == original[at] || (keepGuest && destination[at] != original[at])) continue; destination[at] = current[at]; firstChanged = std::min(firstChanged, at); lastChanged = std::max(lastChanged, at + 1); @@ -3072,6 +3089,14 @@ void WriteChanged(std::uint64_t address, std::span current, std }); } +void WriteChanged(std::uint64_t address, std::span current, std::span original, std::uint32_t byteStoreLimit) { + writeChanged(address, current, original, byteStoreLimit, false); +} + +void WriteChangedKeepingGuest(std::uint64_t address, std::span current, std::span original) { + writeChanged(address, current, original, ChangedByteStoreLimit(), true); +} + void Write(std::uint64_t address, std::span source, std::size_t alignment) { if (source.empty()) return; // A store, as in WriteChanged. diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/HostReportSources.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/HostReportSources.cpp index a3a4257fd8..e9bdb9b331 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/HostReportSources.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/HostReportSources.cpp @@ -14,6 +14,7 @@ #include #include #include +extern char** environ; #endif namespace AgcDriver { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/KeyboardOverlay.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/KeyboardOverlay.cpp index 9bd1dd45b5..077b0ea7dd 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/KeyboardOverlay.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/KeyboardOverlay.cpp @@ -372,7 +372,20 @@ OverlayLayout LayOutKeyboard(const ImeKeyboardView& view, VkExtent2D extent) { const auto inner = gridWidth(view); rect(KeyboardMetrics::Padding, KeyboardMetrics::Padding, inner, KeyboardMetrics::FieldHeight, OverlayColor::Field); std::size_t caret = 0; - const auto characters = decode(view.text, view.caret, &caret); + auto characters = decode(view.text, view.caret, &caret); + std::vector marks(characters.size(), 0); + if (!view.preedit.empty()) { + Graphics::Require(view.preeditCursor <= view.preedit.size() && view.preeditSelection <= view.preedit.size() - view.preeditCursor, "the system keyboard composition cursor is past its text"); + std::size_t cursor = 0; + std::size_t selectionEnd = 0; + const auto composed = decode(view.preedit, view.preeditCursor, &cursor); + decode(view.preedit, view.preeditCursor + view.preeditSelection, &selectionEnd); + std::vector composedMarks(composed.size(), KeyboardCompositionMark); + std::fill(composedMarks.begin() + static_cast(cursor), composedMarks.begin() + static_cast(selectionEnd), KeyboardSelectionMark); + characters.insert(characters.begin() + static_cast(caret), composed.begin(), composed.end()); + marks.insert(marks.begin() + static_cast(caret), composedMarks.begin(), composedMarks.end()); + caret += cursor; + } const auto textLeft = KeyboardMetrics::Padding + KeyboardMetrics::FieldInset; const auto textTop = KeyboardMetrics::Padding + KeyboardMetrics::FieldInset; const auto visible = static_cast((inner - 2 * KeyboardMetrics::FieldInset + 1) / KeyboardMetrics::Advance); @@ -381,6 +394,8 @@ OverlayLayout LayOutKeyboard(const ImeKeyboardView& view, VkExtent2D extent) { for (auto index = first; index < last; ++index) { const auto column = static_cast(index - first); glyph(layout, KeyboardGlyph(characters[index]), left + (textLeft + column * KeyboardMetrics::Advance) * scale, top + textTop * scale, scale, OverlayColor::Text); + if (marks[index] == KeyboardCompositionMark) rect(textLeft + column * KeyboardMetrics::Advance, textTop + KeyboardMetrics::GlyphHeight, KeyboardMetrics::Advance, 1, OverlayColor::Accent); + else if (marks[index] == KeyboardSelectionMark) rect(textLeft + column * KeyboardMetrics::Advance, textTop + KeyboardMetrics::GlyphHeight, KeyboardMetrics::Advance, 2, OverlayColor::Text); } rect(textLeft + static_cast(caret - first) * KeyboardMetrics::Advance - 1, KeyboardMetrics::Padding + 1, 1, KeyboardMetrics::FieldHeight - 2, OverlayColor::Accent); for (const auto& key : view.keys) { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp index e427f319d5..c4fc580f50 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -16,10 +17,55 @@ namespace AgcDriver::Pm4 { +namespace { + +std::atomic importableGdsRequested{false}; +std::atomic gdsShared{false}; +std::atomic gdsImportable{false}; + +} + +void RequestImportableGds(bool requested) { + importableGdsRequested.store(requested, std::memory_order_release); +} + +bool ImportableGdsRequested() { + return importableGdsRequested.load(std::memory_order_acquire); +} + +bool GdsShared() { + static_cast(GdsAddress()); + return gdsShared.load(std::memory_order_acquire); +} + +bool GdsImportable() { + static_cast(GdsAddress()); + return gdsImportable.load(std::memory_order_acquire); +} + +void RejectGdsImport() { + gdsImportable.store(false, std::memory_order_release); +} + std::uint64_t GdsAddress() { static const std::uint64_t address = [] { constexpr int ReadWrite = 0x3; constexpr int PrivateAnonymous = 0x1002; + constexpr int Shared = 0x1; + constexpr int OpenReadWrite = 0x2; + const char* const anonymousSharedMemory = reinterpret_cast(1); + if (ImportableGdsRequested() && std::getenv("APS5_NO_SHARED_GDS") == nullptr) { + if (const int descriptor = shm_open_nid_postfix(anonymousSharedMemory, OpenReadWrite, 0600); descriptor >= 0) { + void* shared = ftruncate_nid_postfix(descriptor, GdsBytes) == 0 ? mmap_nid_postfix(nullptr, GdsBytes, ReadWrite, Shared, descriptor, 0) : nullptr; + close_nid_postfix(descriptor); + if (shared != nullptr && shared != reinterpret_cast(static_cast(-1))) { + std::memset(shared, 0, GdsBytes); + gdsShared.store(true, std::memory_order_release); + gdsImportable.store(true, std::memory_order_release); + return reinterpret_cast(shared); + } + } + } void* mapped = mmap_nid_postfix(nullptr, GdsBytes, ReadWrite, PrivateAnonymous, -1, 0); if (mapped == nullptr || mapped == reinterpret_cast(static_cast(-1))) throw std::runtime_error("cannot allocate the global data share"); return reinterpret_cast(mapped); @@ -289,7 +335,7 @@ void Validate(std::span packet, std::uint32_t queue) { // Header bit 1 selects the compute shader type and bit 2 (register writes) resets the filter CAM; // neither changes what the packet writes. const auto flags = header & 0xffu; - const bool registerWrite = opcode == 0x69 || opcode == 0x76 || opcode == 0x79 || opcode == 0x7a; + const bool registerWrite = opcode == 0x69 || opcode == 0x76 || opcode == 0x79 || opcode == 0x7a || (opcode == 0x37 && ((packet[1] >> 8u) & 0xfu) == 0); auto allowedFlags = opcode == 0x11 || opcode == 0x15 || opcode == 0x16 ? 2u : registerWrite ? 6u : opcode == 0x3c || opcode == 0x93 ? 2u : 0u; if (opcode != 0x20 && opcode != 0x22) allowedFlags |= 1u; if ((flags & ~allowedFlags) != 0) throw std::runtime_error("PM4 header flags 0x" + ToHex(flags) + " are not implemented"); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/ShaderDeviceProfile.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/ShaderDeviceProfile.cpp index f9b21ce96a..8170e78591 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/ShaderDeviceProfile.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/ShaderDeviceProfile.cpp @@ -1,5 +1,6 @@ #include "prx/libSceAgcDriver/Execution/include/ShaderDeviceProfile.hpp" #include "BdaAbi.hpp" +#include "CacheKey.hpp" #include #include @@ -75,6 +76,7 @@ ShaderDeviceProfile::ShaderDeviceProfile(const ShaderRecompiler::SpirvTarget& ta Require(!target.mesh || meshEnabled, "shader device profile exposes disabled mesh shaders"); Require(!target.nonConstantImageOffsets || (maintenance8 != nullptr && maintenance8->maintenance8 == VK_TRUE), "shader device profile exposes disabled maintenance8"); Require(!target.tessellation || core.tessellationShader == VK_TRUE, "shader device profile exposes disabled tessellation"); + Require(!target.shaderImageMinLod || core.shaderResourceMinLod == VK_TRUE, "shader device profile exposes a disabled shaderResourceMinLod"); for (const auto extension : target.supportedExtensions) extensions.emplace_back(extension); std::ranges::sort(extensions); extensions.erase(std::unique(extensions.begin(), extensions.end()), extensions.end()); @@ -83,6 +85,7 @@ ShaderDeviceProfile::ShaderDeviceProfile(const ShaderRecompiler::SpirvTarget& ta Require(std::ranges::find(extensionViews, "SPV_KHR_terminate_invocation") == extensionViews.end() || (terminate != nullptr && terminate->shaderTerminateInvocation == VK_TRUE), "shader device profile advertises a disabled feature: shaderTerminateInvocation"); this->target.supportedCapabilities = capabilities; this->target.supportedExtensions = extensionViews; + ShaderRecompiler::RecompileCacheKey::SealFeatureDigest(this->target); } } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/ShaderMemory.cpp index 91380e6132..b1b40100ef 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/ShaderMemory.cpp @@ -2,10 +2,15 @@ #include "prx/libSceAgcDriver/Execution/include/ShaderMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureSwizzleEquations.hpp" #include "prx/libc/include/HostThreadLocal.hpp" #include "Optimization/RequestMemoryView.hpp" #include "Optimization/ResourceMaterializer.hpp" #include "Optimization/ResourceProgram.hpp" +#include "Optimization/SrtWalker.hpp" #include "ThreadOwned.hpp" #include #include @@ -35,6 +40,8 @@ struct CaptureProfile { // were read raw on the driver's evidence, or were read both ways (verify) and differed. std::atomic pagesWordwise{0}; std::atomic wordWaits{0}; + std::atomic branchReadsRefused{0}; + std::atomic branchCapturesRedone{0}; std::atomic wordWaitNanoseconds{0}; std::atomic wordsRaw{0}; std::atomic wordsVerified{0}; @@ -69,6 +76,55 @@ struct CaptureProfile { constexpr std::uint64_t WordWaitNanoseconds = 50000; +std::uint64_t imageViewGeneration(void*) { + return GuestAllocations::GuestAllocationsGeneration_nid_postfix(); +} + +bool normalizeImageView(void*, std::uint32_t maxArrayLayers, std::array& descriptor, ShaderRecompiler::SrtImageViewIdentity& identity) { + identity.generation = GuestAllocations::GuestAllocationsGeneration_nid_postfix(); + if (((descriptor[3] >> 28u) & 0xfu) != 12u) return false; + const auto resource = Graphics::DecodeTextureResource(descriptor); + identity.address = resource.baseAddress; + const auto range = GuestAllocations::GuestAllocationsLookup_nid_postfix(static_cast(resource.baseAddress), false); + if (range != nullptr) { + identity.hasRange = true; + identity.readable = range->readable; + identity.writable = range->writable; + identity.gpu = range->gpu; + identity.rangeAddress = range->address; + identity.rangeBytes = range->bytes; + identity.allocationAddress = range->allocationAddress; + identity.allocationBytes = range->allocationBytes; + identity.backingSerial = range->backingSerial; + } + if (maxArrayLayers == 0u || resource.dimension != Graphics::TextureDimension::k1DArray || range == nullptr) return false; + const auto rawCount = resource.depthOrLastArray >= resource.baseArray ? resource.depthOrLastArray - resource.baseArray + 1u : 0u; + const auto geometry = Graphics::DescribeSurface(resource); + if (geometry.layerBytes == 0u || geometry.thick || geometry.layers != resource.depthOrLastArray + 1u) return false; + if (const auto xorMode = Graphics::XorSwizzleMode(resource.tileMode); xorMode != 0u) { + const auto* equation = Graphics::FindTextureSwizzleEquation(xorMode, Graphics::BytesPerElement(resource.format)); + if (equation == nullptr || std::any_of(equation->bits.begin(), equation->bits.end(), [](std::uint32_t bit) { return (bit & 0xff000000u) != 0u; })) return false; + } + if (!range->readable || !range->gpu || range->bytes == 0u || range->address > std::numeric_limits::max() - range->bytes || range->allocationAddress > std::numeric_limits::max() - range->allocationBytes) return false; + const auto rangeEnd = range->address + range->bytes; + const auto allocationEnd = range->allocationAddress + range->allocationBytes; + if (resource.baseAddress < range->address || resource.baseAddress > rangeEnd || range->address < range->allocationAddress || rangeEnd > allocationEnd) return false; + const auto offset = resource.baseAddress - range->address; + if (offset % geometry.layerBytes != 0u) return false; + const auto coveredBytes = rangeEnd - resource.baseAddress; + if (coveredBytes % geometry.layerBytes != 0u) return false; + const auto completeSlices = coveredBytes / geometry.layerBytes; + if (completeSlices <= resource.baseArray) return false; + if (rawCount <= maxArrayLayers) return false; + const auto available = completeSlices - resource.baseArray; + const auto effectiveCount = std::min(rawCount, available); + if (effectiveCount == 0u || effectiveCount == rawCount) return false; + const auto lastArray = static_cast(resource.baseArray) + effectiveCount - 1u; + if (lastArray > 0x1fffu) return false; + descriptor[4] = (descriptor[4] & ~0x1fffu) | static_cast(lastArray); + return true; +} + bool WordwisePages() { static const bool wordwise = std::getenv("APS5_NO_WORDWISE_CAPTURE") == nullptr; return wordwise; @@ -94,6 +150,11 @@ CaptureProfile& CaptureTotals() { return profile; } +bool BranchesSkipPendingWords() { + static const bool skip = std::getenv("APS5_WAIT_PENDING_BRANCHES") == nullptr; + return skip; +} + std::atomic waitedMsProvider{nullptr}; double WaitedMs() { @@ -372,6 +433,10 @@ bool ShaderMemory::read(void* context, std::uint64_t address, std::uint32_t* val waited = page.wordwise && (self.hookWaits != nullptr ? self.hookWaits() != waitsBefore : true); word = verifiedWord; } + } else if (page.wordwise && policy == PendingWrite::Sync && !forcedRaw && !self.waitBranches && BranchesSkipPendingWords() && ShaderRecompiler::SrtEvaluatingBranch()) { + ++self.branchReadsRefused; + if (CaptureProfiled()) ++CaptureTotals().branchReadsRefused; + return false; } else { // On a word-wise page the bytes before the hook's wait are kept for the observer. const bool observed = page.wordwise && policy == PendingWrite::Sync && self.observe != nullptr && GuestMemory::Accessible(reinterpret_cast(address), sizeof(word)); @@ -722,11 +787,29 @@ std::vector> ShaderMemory } std::shared_ptr ShaderMemory::Capture(const ShaderRecompiler::RecompileRequest& request, const ShaderRecompiler::SourceHandle* handle) { - return capture(request, handle, nullptr); + return captureWaitingOnFailure(request, handle, nullptr); } std::shared_ptr ShaderMemory::Capture(const ShaderRecompiler::PreparedShaderInvocation& invocation) { - return capture(invocation.Request(), nullptr, &invocation); + return captureWaitingOnFailure(invocation.Request(), nullptr, &invocation); +} + +std::shared_ptr ShaderMemory::captureWaitingOnFailure(const ShaderRecompiler::RecompileRequest& request, const ShaderRecompiler::SourceHandle* handle, const ShaderRecompiler::PreparedShaderInvocation* invocation) { + branchReadsRefused = 0; + try { + return capture(request, handle, invocation); + } catch (const std::exception&) { + if (branchReadsRefused == 0u) throw; + } + if (CaptureProfiled()) ++CaptureTotals().branchCapturesRedone; + struct WaitBranches { + bool& wait; + explicit WaitBranches(bool& flag) : wait(flag) { wait = true; } + ~WaitBranches() { wait = false; } + WaitBranches(const WaitBranches&) = delete; + WaitBranches& operator=(const WaitBranches&) = delete; + } waiting(waitBranches); + return capture(request, handle, invocation); } std::shared_ptr ShaderMemory::capture(const ShaderRecompiler::RecompileRequest& request, const ShaderRecompiler::SourceHandle* handle, const ShaderRecompiler::PreparedShaderInvocation* invocation) { @@ -750,6 +833,8 @@ std::shared_ptr ShaderMemory::capture(c runtime.unchangedSince = &unchangedSince; runtime.supplyWords = &supplyWords; runtime.descriptorScope = &descriptorScope; + runtime.normalizeImageView = &normalizeImageView; + runtime.imageViewGeneration = &imageViewGeneration; auto capture = invocation != nullptr ? invocation->Capture(runtime) : handle != nullptr ? ShaderRecompiler::CaptureResources(request, runtime, *handle) : ShaderRecompiler::CaptureResources(request, runtime); if (profile) { totals.captureNanoseconds += NanosecondsSince(started); @@ -772,7 +857,7 @@ std::shared_ptr ShaderMemory::capture(c const auto materializeNs = ShaderRecompiler::ResourceMaterializer::MaterializeNanoseconds(); const auto evaluateNs = ShaderRecompiler::ResourceMaterializer::EvaluateNanoseconds(); const auto walkNs = captureNs > resolveNs + pageNs + wordNs + specializationNs ? captureNs - resolveNs - pageNs - wordNs - specializationNs : 0; - AgcDriver::ProfilePrint_nid_no_patch( "[capture] %llu captures: %llu word reads, %llu pages fetched (%llu read word by word over pending GPU writes: %llu words waited %.2f s, %llu read raw on evidence, %llu verified with %llu mismatches, %llu served from known values, %llu hook reads not observed: no GPU wait), capture (plan lookup + materialize) %.1f s, %llu KiB registered code this capture; sub-phases (s, us per capture): source resolve %.2f/%.1f, walk %.2f/%.1f, specialization (every materialize) %.2f/%.1f, page reads %.2f/%.1f, word reads %.2f/%.1f, hook GPU waits inside %.2f/%.1f, materialize %.2f/%.1f (SRT evaluate %.2f/%.1f); source handle memo %llu hits, %llu resolves\n", static_cast(totals.captures.load()), static_cast(totals.reads.load()), static_cast(totals.pages.load()), static_cast(totals.pagesWordwise.load()), static_cast(totals.wordWaits.load()), totals.wordWaitNanoseconds.load() / 1e9, static_cast(totals.wordsRaw.load()), static_cast(totals.wordsVerified.load()), static_cast(totals.wordMismatches.load()), static_cast(totals.wordsKnown.load()), static_cast(totals.observationsSkipped.load()), captureNs / 1e9, static_cast(initialBytes / 1024), resolveNs / 1e9, resolveNs / captures / 1e3, walkNs / 1e9, walkNs / captures / 1e3, specializationNs / 1e9, specializationNs / captures / 1e3, pageNs / 1e9, pageNs / captures / 1e3, wordNs / 1e9, wordNs / captures / 1e3, hookNs / 1e9, hookNs / captures / 1e3, materializeNs / 1e9, materializeNs / captures / 1e3, evaluateNs / 1e9, evaluateNs / captures / 1e3, static_cast(totals.handleHits.load()), static_cast(totals.handleMisses.load())); + AgcDriver::ProfilePrint_nid_no_patch( "[capture] %llu captures: %llu word reads, %llu pages fetched (%llu read word by word over pending GPU writes: %llu words waited %.2f s, %llu read raw on evidence, %llu verified with %llu mismatches, %llu served from known values, %llu hook reads not observed: no GPU wait, %llu branch condition reads over pending GPU writes left the branch unpruned instead of waiting, %llu captures redone waiting after one failed), capture (plan lookup + materialize) %.1f s, %llu KiB registered code this capture; sub-phases (s, us per capture): source resolve %.2f/%.1f, walk %.2f/%.1f, specialization (every materialize) %.2f/%.1f, page reads %.2f/%.1f, word reads %.2f/%.1f, hook GPU waits inside %.2f/%.1f, materialize %.2f/%.1f (SRT evaluate %.2f/%.1f); source handle memo %llu hits, %llu resolves\n", static_cast(totals.captures.load()), static_cast(totals.reads.load()), static_cast(totals.pages.load()), static_cast(totals.pagesWordwise.load()), static_cast(totals.wordWaits.load()), totals.wordWaitNanoseconds.load() / 1e9, static_cast(totals.wordsRaw.load()), static_cast(totals.wordsVerified.load()), static_cast(totals.wordMismatches.load()), static_cast(totals.wordsKnown.load()), static_cast(totals.observationsSkipped.load()), static_cast(totals.branchReadsRefused.load()), static_cast(totals.branchCapturesRedone.load()), captureNs / 1e9, static_cast(initialBytes / 1024), resolveNs / 1e9, resolveNs / captures / 1e3, walkNs / 1e9, walkNs / captures / 1e3, specializationNs / 1e9, specializationNs / captures / 1e3, pageNs / 1e9, pageNs / captures / 1e3, wordNs / 1e9, wordNs / captures / 1e3, hookNs / 1e9, hookNs / captures / 1e3, materializeNs / 1e9, materializeNs / captures / 1e3, evaluateNs / 1e9, evaluateNs / captures / 1e3, static_cast(totals.handleHits.load()), static_cast(totals.handleMisses.load())); } } return capture; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 5cdae04f99..8a373ec7df 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -368,6 +368,51 @@ struct WorkgroupMemory { } }; +struct ScratchPool { + VkDevice device = VK_NULL_HANDLE; + PFN_vkDestroyDescriptorPool destroyPool = nullptr; + std::unique_ptr buffer; + VkDescriptorPool pool = VK_NULL_HANDLE; + VkDescriptorSet set = VK_NULL_HANDLE; + std::uint64_t slots = 0; + std::uint64_t headerBytes = 0; + std::uint64_t bytes = 0; + std::uint64_t lastUse = 0; + ~ScratchPool() { + buffer.reset(); + if (pool != VK_NULL_HANDLE) destroyPool(device, pool, nullptr); + } +}; + +void ZeroScratchPoolHeader(const Graphics::Context& context, const ScratchPool& pool) { + Graphics::CommandBatch batch(context); + context.Resolved(&Graphics::DeviceFunctions::cmdFillBuffer, "vkCmdFillBuffer")(batch.Handle(), pool.buffer->Handle(), 0, pool.headerBytes, 0u); + Graphics::RecordMemoryBarrier(context, batch.Handle(), VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT); + batch.SubmitAndWait(); +} + +std::uint64_t MebibytesFromEnvironment(const char* name, std::uint64_t fallback) { + std::uint64_t mebibytes = fallback; + if (const char* text = std::getenv(name)) { + char* end = nullptr; + const auto parsed = std::strtoull(text, &end, 10); + if (end != text && *end == 0 && parsed != 0) mebibytes = parsed; + } + return mebibytes << 20u; +} + +std::uint64_t ScratchPoolCapBytes() { + static const std::uint64_t bytes = MebibytesFromEnvironment("APS5_SCRATCH_POOL_MIB", 64); + return bytes; +} + +constexpr std::uint64_t ScratchPoolBufferLimit = std::uint64_t{1} << 31u; + +std::uint64_t ScratchPoolTotalBytes() { + static const std::uint64_t bytes = MebibytesFromEnvironment("APS5_SCRATCH_POOL_TOTAL_MIB", 256); + return bytes; +} + struct VulkanDevice::State { bool compileOnly = false; void* library = nullptr; @@ -424,6 +469,7 @@ struct VulkanDevice::State { bool shaderTerminateInvocation = false; bool fusedExactArithmetic = false; bool fragmentBarycentricBuiltins = false; + bool hostInterpolationFirst = false; bool geometryShader = false; bool largePoints = false; bool sampleRateShading = false; @@ -464,8 +510,11 @@ struct VulkanDevice::State { bool graphicsPipelineLibrary = false; bool pipelineLinkTimeOptimization = true; bool imageViewMinLod = false; + bool shaderResourceMinLod = false; + bool shaderImageMinLod = false; bool pipelineExecutableInfo = false; bool maintenance8 = false; + bool pushDescriptors = false; bool maintenance5 = false; bool fillModeNonSolid = false; std::uint32_t srgbDecodeFormats = 0; @@ -536,6 +585,9 @@ struct VulkanDevice::State { std::vector, std::shared_ptr>> patternBuffers; VkDescriptorSetLayout workgroupMemoryLayout = VK_NULL_HANDLE; std::shared_ptr workgroupMemory; + std::mutex scratchPoolsMutex; + std::map> scratchPools; + std::uint64_t scratchPoolUses = 0; // Compute pipeline objects by variant (and push-constant use), under their own mutex: the // dispatch's find-or-insert and the verify switch's lookups touch the map, recipes hold weak // references to its objects. @@ -554,6 +606,7 @@ struct VulkanDevice::State { std::unique_ptr pipelineCache; std::unique_ptr descriptorCache; std::unique_ptr vertexInputs; + Graphics::VertexBufferFormats vertexFormats; std::unique_ptr samplerCache; Graphics::ResourceCache& resourceCache = Graphics::SharedResourceCache(); // Recorded dispatches that write a copied buffer (their results reach guest memory by a CPU @@ -699,6 +752,65 @@ struct VulkanDevice::State { return workgroupMemory; } + std::shared_ptr ScratchPoolFor(const Graphics::Context& context, const ShaderRecompiler::PipelineIdentity& identity, std::uint32_t slotDwords, std::uint64_t groups) { + const auto range = std::min(properties.limits.maxStorageBufferRange, ScratchPoolBufferLimit); + const auto slotBytes = (static_cast(slotDwords) + 2u) * 4u; + if (slotBytes > range) { + throw std::runtime_error("Vulkan dispatch: one scratch pool slot of " + std::to_string(slotBytes) + " bytes is over the scratch pool buffer limit of " + std::to_string(range) + " bytes"); + } + const auto capSlots = std::max(std::min({ScratchPoolCapBytes(), ScratchPoolTotalBytes(), range}) / slotBytes, 1u); + const auto wanted = std::clamp(groups, 1u, capSlots); + std::lock_guard lock(scratchPoolsMutex); + const auto use = ++scratchPoolUses; + if (const auto found = scratchPools.find(identity); found != scratchPools.end() && found->second->slots >= wanted) { + found->second->lastUse = use; + return found->second; + } + const auto size = wanted * slotBytes; + EvictScratchPools(identity, size); + auto created = std::make_shared(); + created->device = device; + created->destroyPool = DeviceFunction("vkDestroyDescriptorPool"); + created->slots = wanted; + created->headerBytes = wanted * 2u * 4u; + created->bytes = size; + created->lastUse = use; + created->buffer = std::make_unique(context, static_cast(size), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT); + const VkDescriptorPoolSize poolSize{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1}; + VkDescriptorPoolCreateInfo poolInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; + poolInfo.maxSets = 1; + poolInfo.poolSizeCount = 1; + poolInfo.pPoolSizes = &poolSize; + check(DeviceFunction("vkCreateDescriptorPool")(device, &poolInfo, nullptr, &created->pool), "vkCreateDescriptorPool scratch pool"); + VkDescriptorSetAllocateInfo allocation{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; + allocation.descriptorPool = created->pool; + allocation.descriptorSetCount = 1; + allocation.pSetLayouts = &workgroupMemoryLayout; + check(DeviceFunction("vkAllocateDescriptorSets")(device, &allocation, &created->set), "vkAllocateDescriptorSets scratch pool"); + const VkDescriptorBufferInfo described{created->buffer->Handle(), 0, size}; + VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}; + write.dstSet = created->set; + write.dstBinding = 0; + write.descriptorCount = 1; + write.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + write.pBufferInfo = &described; + DeviceFunction("vkUpdateDescriptorSets")(device, 1, &write, 0, nullptr); + ZeroScratchPoolHeader(context, *created); + scratchPools[identity] = created; + return created; + } + + void EvictScratchPools(const ShaderRecompiler::PipelineIdentity& identity, std::uint64_t needed) { + scratchPools.erase(identity); + std::uint64_t held = 0; + for (const auto& [key, pool] : scratchPools) held += pool->bytes; + while (!scratchPools.empty() && held + needed > ScratchPoolTotalBytes()) { + const auto oldest = std::min_element(scratchPools.begin(), scratchPools.end(), [](const auto& left, const auto& right) { return left.second->lastUse < right.second->lastUse; }); + held -= oldest->second->bytes; + scratchPools.erase(oldest); + } + } + void CreateMarkerBuffer() { bufferMarker = DeviceFunction("vkCmdWriteBufferMarkerAMD"); VkBufferCreateInfo buffer{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO}; @@ -960,6 +1072,7 @@ struct VulkanDevice::State { Graphics::ClearHostImports(device); patternBuffers.clear(); workgroupMemory.reset(); + scratchPools.clear(); if (workgroupMemoryLayout != VK_NULL_HANDLE) reinterpret_cast(deviceProc(device, "vkDestroyDescriptorSetLayout"))(device, workgroupMemoryLayout, nullptr); descriptorCache.reset(); vertexInputs.reset(); @@ -1362,6 +1475,8 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : state->individuallyFreeDescriptorSets = driver.driverID == VK_DRIVER_ID_MESA_LLVMPIPE; state->fusedExactArithmetic = driver.driverID == VK_DRIVER_ID_MOLTENVK; state->tessellationPipelineCache = TessellationPipelineCacheUsable(driver.driverID); + state->hostInterpolationFirst = HostInterpolationPreferred(driver.driverID) && std::getenv("APS5_NO_HOST_INTERPOLATION_FIRST") == nullptr; + APS5_LOG_OUT("Pixel interpolation driver=%d hostInterpolationFirst=%d", static_cast(driver.driverID), state->hostInterpolationFirst ? 1 : 0); state->subgroupControlBarriers = SubgroupControlBarrierUsable(driver.driverID); state->workgroupScopeFences = WorkgroupScopeFenceUsable(driver.driverID); state->ldsLockFences = LdsLockFencesRequired(driver.driverID); @@ -1633,6 +1748,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : state->drawInputsInPlace = std::getenv("APS5_NO_DRAW_INPUTS_IN_PLACE") == nullptr && (std::getenv("APS5_DRAW_INPUTS_IN_PLACE") != nullptr || DrawInputsInPlaceUsable(driverProperties.driverID, windows)); if (!compileOnly) Graphics::SetHostImportWindow(8ull << 20u); if (!compileOnly) Graphics::SetHostImportMaxBytes(maintenance3.maxMemoryAllocationSize); + if (!compileOnly) Pm4::RequestImportableGds(state->properties.deviceType == VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU); #ifdef _WIN32 // Imported pages are locked; locking is bounded by the process working-set minimum. if (const char* value = std::getenv("APS5_WORKING_SET_MIB"); value != nullptr && !compileOnly) { @@ -1677,7 +1793,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : if (hasExtension(VK_EXT_IMAGE_VIEW_MIN_LOD_EXTENSION_NAME)) { VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &minLodFeatures}; state->InstanceFunction("vkGetPhysicalDeviceFeatures2")(selected, &features); - state->imageViewMinLod = minLodFeatures.minLod == VK_TRUE; + state->imageViewMinLod = minLodFeatures.minLod == VK_TRUE && std::getenv("APS5_NO_IMAGE_VIEW_MIN_LOD") == nullptr; if (state->imageViewMinLod) deviceExtensions.push_back(VK_EXT_IMAGE_VIEW_MIN_LOD_EXTENSION_NAME); } minLodFeatures = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_IMAGE_VIEW_MIN_LOD_FEATURES_EXT}; @@ -1691,6 +1807,8 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : } maintenance8Features = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MAINTENANCE_8_FEATURES_KHR}; maintenance8Features.maintenance8 = VK_TRUE; + state->pushDescriptors = hasExtension(VK_KHR_PUSH_DESCRIPTOR_EXTENSION_NAME); + if (state->pushDescriptors) deviceExtensions.push_back(VK_KHR_PUSH_DESCRIPTOR_EXTENSION_NAME); VkPhysicalDeviceDepthClipControlFeaturesEXT depthClipFeatures{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DEPTH_CLIP_CONTROL_FEATURES_EXT}; if (hasExtension(VK_EXT_DEPTH_CLIP_CONTROL_EXTENSION_NAME)) { VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &depthClipFeatures}; @@ -1896,6 +2014,8 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : if (enabled.shaderImageGatherExtended) state->capabilities.push_back(spv::CapabilityImageGatherExtended); enabled.shaderResourceMinLod = available.shaderResourceMinLod; if (enabled.shaderResourceMinLod) state->capabilities.push_back(spv::CapabilityMinLod); + state->shaderResourceMinLod = enabled.shaderResourceMinLod == VK_TRUE; + state->shaderImageMinLod = state->shaderResourceMinLod && !state->imageViewMinLod && !state->graphicsPipelineLibrary; enabled.sampleRateShading = available.sampleRateShading; enabled.geometryShader = available.geometryShader; state->geometryShader = enabled.geometryShader == VK_TRUE; @@ -2203,12 +2323,13 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : if (domains(selected, &domainCount, available.data()) != VK_SUCCESS) available.clear(); } #ifdef _WIN32 - const auto host = VK_TIME_DOMAIN_QUERY_PERFORMANCE_COUNTER_KHR; + const std::array hosts{VK_TIME_DOMAIN_QUERY_PERFORMANCE_COUNTER_KHR}; #else - const auto host = VK_TIME_DOMAIN_CLOCK_MONOTONIC_KHR; + const std::array hosts{VK_TIME_DOMAIN_CLOCK_MONOTONIC_KHR, VK_TIME_DOMAIN_CLOCK_MONOTONIC_RAW_KHR}; #endif - if (std::ranges::find(available, VK_TIME_DOMAIN_DEVICE_KHR) != available.end() && std::ranges::find(available, host) != available.end()) { - state->hostTimeDomain = host; + const auto host = std::ranges::find_first_of(hosts, available); + if (std::ranges::find(available, VK_TIME_DOMAIN_DEVICE_KHR) != available.end() && host != hosts.end()) { + state->hostTimeDomain = *host; state->calibratedTimestamps = state->DeviceFunction(khr ? "vkGetCalibratedTimestampsKHR" : "vkGetCalibratedTimestampsEXT"); } } @@ -2257,6 +2378,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : Graphics::PrepareImportWatch(graphicsContext()); state->bufferPool = std::make_shared(graphicsContext()); state->emptyBuffer = std::make_unique(graphicsContext(), Graphics::EmptyBufferBytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + if (!compileOnly && Pm4::GdsImportable() && Graphics::HostImportFor(graphicsContext(), Pm4::GdsAddress(), Pm4::GdsBytes, Graphics::ImportRequester::Map) == nullptr) Pm4::RejectGdsImport(); state->pipelineCache = std::make_unique(graphicsContext(), state->properties, window != nullptr || compileOnly); state->detiler = std::make_unique(graphicsContext()); state->textureCache = std::make_unique(graphicsContext()); @@ -2592,7 +2714,7 @@ int VulkanDevice::WriteLabelOnGpu(std::uint64_t address, std::spandevice); if (queue == 0) static_cast(Graphics::StorageTexture::StoreDrawnLinear("label")); - Graphics::StorageTexture::StoreReadbackBeforeLabel("readback before label"); + Graphics::StorageTexture::StoreReadbackBeforeLabel("readback before label", state->device); if (!state->recorder || bytes.empty() || bytes.size() > 65536 || address % 4 != 0 || bytes.size() % 4 != 0) return 4; auto& recorder = *state->recorder; // Batches that already finished are retired before the checks: their completions ran, so they @@ -4123,7 +4245,7 @@ bool VulkanDevice::CalibrateClock(double& gpuNs, std::chrono::steady_clock::time const double sinceNs = static_cast(static_cast(counter.QuadPart) - stamps[1]) * 1e9 / static_cast(frequency.QuadPart); #else timespec monotonic{}; - clock_gettime(CLOCK_MONOTONIC, &monotonic); + clock_gettime(state->hostTimeDomain == VK_TIME_DOMAIN_CLOCK_MONOTONIC_RAW_KHR ? CLOCK_MONOTONIC_RAW : CLOCK_MONOTONIC, &monotonic); const auto now = std::chrono::steady_clock::now(); const double sinceNs = static_cast(static_cast(monotonic.tv_sec) * 1000000000ull + static_cast(monotonic.tv_nsec) - stamps[1]); #endif @@ -4151,6 +4273,7 @@ ShaderRecompiler::SpirvTarget VulkanDevice::Target() const { auto target = state->shaderProfile->Target(); target.float32FlushToZero = state->float32FlushToZero; target.fusedExactArithmetic = state->fusedExactArithmetic; + target.hostInterpolationFirst = state->hostInterpolationFirst; target.float32DenormPreserve = state->float32DenormPreserve; return target; } @@ -4166,6 +4289,7 @@ ShaderRecompiler::SpirvTarget VulkanDevice::buildTarget() const { target.fragmentShaderBarycentricEnabled = state->fragmentShaderBarycentric; target.fragmentBarycentricBuiltins = state->fragmentBarycentricBuiltins; target.nonConstantImageOffsets = state->maintenance8; + target.shaderImageMinLod = state->shaderImageMinLod; target.narrowSubgroupClock = state->narrowSubgroupClock; target.float64DenormPreserve = state->float64DenormPreserve; target.float16DenormPreserve = state->float16DenormPreserve; @@ -4182,6 +4306,7 @@ ShaderRecompiler::SpirvTarget VulkanDevice::buildTarget() const { target.ldsLockFences = state->ldsLockFences; target.meshWindowBuffer = state->driverId == VK_DRIVER_ID_NVIDIA_PROPRIETARY && std::getenv("APS5_NO_MESH_WINDOW_BUFFER") == nullptr; target.srgbDecodeFormats = state->srgbDecodeFormats & ~state->srgbCopyFormats; + target.maxImageArrayLayers = limits.maxImageArrayLayers; target.vertexSubgroups = (state->subgroup.supportedStages & VK_SHADER_STAGE_VERTEX_BIT) != 0; target.tessellationEvaluationSubgroups = (state->subgroup.supportedStages & VK_SHADER_STAGE_TESSELLATION_EVALUATION_BIT) != 0; if (state->meshShader) { @@ -4230,6 +4355,13 @@ bool VulkanDevice::GraphicsPipelineLibraries() const { return state->graphicsPipelineLibrary; } +std::pair VulkanDevice::scratchPoolHeader(const ShaderRecompiler::RecompileResult& shader) const { + std::lock_guard lock(state->scratchPoolsMutex); + const auto found = state->scratchPools.find(shader.Identity()); + if (found == state->scratchPools.end() || found->second == nullptr) return {VK_NULL_HANDLE, 0}; + return {found->second->buffer->Handle(), found->second->headerBytes}; +} + Graphics::Context VulkanDevice::graphicsContext() const { static const bool noCache = std::getenv("APS5_NO_CONTEXT_CACHE") != nullptr; if (state->contextReady && !noCache) return state->context; @@ -4283,6 +4415,7 @@ Graphics::Context VulkanDevice::buildContext() const { context.recorder = state->recorder.get(); context.descriptorCache = state->descriptorCache.get(); context.vertexInputs = state->vertexInputs.get(); + context.vertexFormats = &state->vertexFormats; context.samplerCache = state->samplerCache.get(); context.drawIndirectFirstInstance = state->drawIndirectFirstInstance; context.multiDrawIndirect = state->multiDrawIndirect; @@ -4339,8 +4472,11 @@ Graphics::Context VulkanDevice::buildContext() const { context.earlyFragmentSampleMaskTestBeforeSampleCounting = state->earlyFragmentSampleMaskTestBeforeSampleCounting; context.primitiveListRestart = state->primitiveListRestart; context.imageViewMinLod = state->imageViewMinLod; + context.shaderResourceMinLod = state->shaderResourceMinLod; + context.shaderImageMinLod = state->shaderImageMinLod; context.pipelineExecutableInfo = state->pipelineExecutableInfo; context.maintenance8 = state->maintenance8; + context.pushDescriptors = state->pushDescriptors; context.srgbDecodeFormats = state->srgbDecodeFormats; context.srgbCopyFormats = state->srgbCopyFormats; context.sampleLocationSampleCounts = state->sampleLocationSampleCounts; @@ -5049,14 +5185,14 @@ VulkanDevice::PresyncStatistics VulkanDevice::PresyncCounts() { return {counters.checked.load(std::memory_order_relaxed), counters.presyncs.load(std::memory_order_relaxed), counters.keyChecks.load(std::memory_order_relaxed), counters.keyWaits.load(std::memory_order_relaxed)}; } -std::shared_ptr VulkanDevice::PrepareDispatch(const ShaderRecompiler::RecompileResult& shader, std::span snapshots, std::uint64_t dispatchThreads, std::uint64_t programAddress, std::optional> htileClear) { +std::shared_ptr VulkanDevice::PrepareDispatch(const ShaderRecompiler::RecompileResult& shader, std::span snapshots, std::uint64_t dispatchThreads, std::uint64_t programAddress, std::optional> htileClear, const std::vector* registeredImageViews) { ShaderRecompiler::RuntimeAbi::RequireVersion(shader.runtimeAbiVersion); // APS5_LOCKED_BUILD=1: the whole build under the mutex, as before the split. static const bool lockedBuild = std::getenv("APS5_LOCKED_BUILD") != nullptr; if (lockedBuild || shader.spirv.size() < 5 || shader.spirv[0] != 0x07230203u) return nullptr; static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; const auto start = std::chrono::steady_clock::now(); - const Graphics::CompiledShader compute{ShaderRecompiler::ShaderStage::Compute, &shader, 0}; + const Graphics::CompiledShader compute{ShaderRecompiler::ShaderStage::Compute, &shader, 0, registeredImageViews}; const auto context = graphicsContext(); auto prepared = std::make_shared(); prepared->dispatchThreads = dispatchThreads; @@ -5172,12 +5308,12 @@ std::shared_ptr VulkanDevice::PrepareRecipe(const std::shared_ptr 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)); +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, const std::vector* registeredImageViews) { + static_cast(dispatch(shader, x, y, z, 0, snapshots, programAddress, std::move(prepared), recipe, {}, htileClear, registeredImageViews)); } -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) { - return dispatch(shader, 0, 0, 0, arguments, snapshots, programAddress, std::move(prepared), recipe, threadGroup); +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, const std::vector* registeredImageViews) { + return dispatch(shader, 0, 0, 0, arguments, snapshots, programAddress, std::move(prepared), recipe, threadGroup, std::nullopt, registeredImageViews); } void VulkanDevice::decideIndirect(RecordedDispatch& record, IndirectOutcome& outcome, char* groupsText) { @@ -5291,6 +5427,11 @@ void VulkanDevice::recordDispatch(RecordedDispatch& record) { if (recorder.HasQueuedKeyStores() && (resources.HoldsLease() || recorder.AnyQueuedKeyStore(touches))) recorder.FlushKeyStores(); if (recorder.HasQueuedStores() && (resources.HoldsLease() || recorder.AnyQueuedStore(touches))) recorder.FlushStores(); static_cast(Graphics::StorageTexture::StoreDrawnLinearTouching("dispatch access", touches, resources.HoldsLease())); + std::shared_ptr pool; + if (const auto slotDwords = record.shader->program->scratchPoolSlotDwords; slotDwords != 0) { + const std::uint64_t groups = argumentImport != nullptr ? std::numeric_limits::max() : static_cast(record.x) * record.y * record.z; + pool = state->ScratchPoolFor(context, record.shader->program->Identity(), slotDwords, groups); + } bool unordered = false; const auto overlapQueue = Graphics::Recorder::TakeDispatchOrder(unordered); const auto* groupLimit = state->properties.limits.maxComputeWorkGroupCount; @@ -5409,6 +5550,10 @@ void VulkanDevice::recordDispatch(RecordedDispatch& record) { context.Resolved(&Graphics::DeviceFunctions::cmdBindDescriptorSets, "vkCmdBindDescriptorSets")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, record.objects->layout, ShaderRecompiler::WorkgroupMemoryDescriptorSet, 1, &memory->set, 0, nullptr); recorder.Keep(std::move(memory)); } + if (pool != nullptr) { + context.Resolved(&Graphics::DeviceFunctions::cmdBindDescriptorSets, "vkCmdBindDescriptorSets")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, record.objects->layout, ShaderRecompiler::ScratchPoolDescriptorSet, 1, &pool->set, 0, nullptr); + recorder.Keep(std::move(pool)); + } if (record.pushStages != 0) { context.Resolved(&Graphics::DeviceFunctions::cmdPushConstants, "vkCmdPushConstants")(commands, record.objects->layout, VK_SHADER_STAGE_COMPUTE_BIT, 0, Graphics::ComputePushConstantBytes, record.pushBytes->data()); } @@ -5507,10 +5652,11 @@ std::shared_ptr VulkanDevice::createComputePipeline(cons objects->destroyPipeline = state->DeviceFunction("vkDestroyPipeline"); objects->module = Graphics::CreateShaderModule(state->DeviceFunction("vkCreateShaderModule"), state->device, state->graphicsPipelineLibrary, shader.spirv.data(), shader.spirv.size() * sizeof(std::uint32_t), "vkCreateShaderModule"); objects->resourcesLayout = setLayout; - const std::array setLayouts{setLayout, shader.workgroupMemoryDwords != 0 ? state->workgroupMemoryLayout : VK_NULL_HANDLE}; + const bool scratchPool = shader.scratchPoolSlotDwords != 0; + const std::array setLayouts{setLayout, shader.workgroupMemoryDwords != 0 || scratchPool ? state->workgroupMemoryLayout : VK_NULL_HANDLE, scratchPool ? state->workgroupMemoryLayout : VK_NULL_HANDLE}; const VkPushConstantRange push{VK_SHADER_STAGE_COMPUTE_BIT, 0, Graphics::ComputePushConstantBytes}; VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; - layoutInfo.setLayoutCount = shader.workgroupMemoryDwords != 0 ? ShaderRecompiler::WorkgroupMemoryDescriptorSet + 1 : 1; + layoutInfo.setLayoutCount = scratchPool ? ShaderRecompiler::ScratchPoolDescriptorSet + 1 : shader.workgroupMemoryDwords != 0 ? ShaderRecompiler::WorkgroupMemoryDescriptorSet + 1 : 1; layoutInfo.pSetLayouts = setLayouts.data(); layoutInfo.pushConstantRangeCount = pushStages != 0 ? 1 : 0; layoutInfo.pPushConstantRanges = pushStages != 0 ? &push : nullptr; @@ -5530,7 +5676,8 @@ std::shared_ptr VulkanDevice::createComputePipeline(cons resourcesLayout.bindings.push_back({layoutKey[index], type, layoutKey[index + 2u], layoutKey[index + 3u], updateAfterBind && type == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER ? VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT : 0u}); } capture.sets.push_back(std::move(resourcesLayout)); - if (shader.workgroupMemoryDwords != 0) capture.sets.push_back({0, {{0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT, 0}}}); + if (shader.workgroupMemoryDwords != 0 || scratchPool) capture.sets.push_back({0, {{0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT, 0}}}); + if (scratchPool) capture.sets.push_back({0, {{0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT, 0}}}); if (pushStages != 0) capture.pushConstants.push_back(push); for (const auto& value : shader.specialization) capture.specialization.push_back({value.id, value.value}); capture.requiredSubgroupSize = subgroupSize; @@ -5616,7 +5763,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, std::optional> htileClear) { +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, const std::vector* registeredImageViews) { startJournalReplay(); for (const auto& binding : shader.bindings) { if (binding.role != ShaderRecompiler::DescriptorRole::GuestBuffers) continue; @@ -5641,7 +5788,7 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec if (shader.spirv.size() < 5 || shader.spirv[0] != 0x07230203u) { throw std::runtime_error("Vulkan dispatch: invalid SPIR-V"); } - const std::array shaders{{{ShaderRecompiler::ShaderStage::Compute, &shader, 0}}}; + const std::array shaders{{{ShaderRecompiler::ShaderStage::Compute, &shader, 0, registeredImageViews}}}; const auto pushStages = Graphics::PushConstantStages(shaders); if (pushStages != 0 && state->properties.limits.maxPushConstantsSize < Graphics::ComputePushConstantBytes) { throw std::runtime_error("Vulkan dispatch: compute push constant range exceeds device limit"); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp index 7eefc1cce6..bbcee6c7db 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp @@ -26,6 +26,16 @@ namespace AgcDriver { #ifdef _WIN32 +bool ReadableStackRegion(std::uint64_t address, std::uint64_t& low, std::uint64_t& high) { + constexpr DWORD readable = PAGE_READONLY | PAGE_READWRITE | PAGE_WRITECOPY | PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY; + MEMORY_BASIC_INFORMATION memory{}; + if (VirtualQuery(reinterpret_cast(address), &memory, sizeof(memory)) == 0 || memory.State != MEM_COMMIT) return false; + if ((memory.Protect & (PAGE_GUARD | PAGE_NOACCESS)) != 0 || (memory.Protect & readable) == 0) return false; + low = reinterpret_cast(memory.BaseAddress); + high = low + memory.RegionSize; + return true; +} + namespace { std::jthread processSamplerThread; @@ -72,7 +82,7 @@ struct Sampler { } bool readStack(std::uint64_t address, std::uint64_t& value) const { - if (address < stackLow || address > stackHigh - sizeof(value)) return false; + if (stackHigh < stackLow + sizeof(value) || address < stackLow || address > stackHigh - sizeof(value)) return false; std::memcpy(&value, reinterpret_cast(address), sizeof(value)); return true; } @@ -104,6 +114,11 @@ struct Sampler { } int walk(CONTEXT& context, std::array& frames) { + if (context.Rsp < stackLow || context.Rsp >= stackHigh) { + stackLow = 0; + stackHigh = 0; + ReadableStackRegion(context.Rsp, stackLow, stackHigh); + } const bool dwarf = stackHigh != 0 && context.Rsp >= stackLow && context.Rsp < stackHigh; int count = 0; while (count < depth && context.Rip != 0) { @@ -364,6 +379,7 @@ void StopWorkerSampler() { if (committerSamplerThread.joinable()) committerSamplerThread.join(); } #else +bool ReadableStackRegion(std::uint64_t, std::uint64_t&, std::uint64_t&) { return false; } void StartWorkerSampler(std::uint32_t) {} void StartCommitterSampler() {} void StopWorkerSampler() {} diff --git a/core/libs/prx/libSceAgcDriver/Export.cpp b/core/libs/prx/libSceAgcDriver/Export.cpp index 199dad9656..83d21e8c7a 100644 --- a/core/libs/prx/libSceAgcDriver/Export.cpp +++ b/core/libs/prx/libSceAgcDriver/Export.cpp @@ -127,26 +127,6 @@ int APS5_VABI sceAgcDriverTmpInitIdhs(void) { return 0; } -int APS5_VABI sceAgcDriverUserDataWritePacket(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAgcDriverUserDataWritePopMarker(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAgcDriverUserDataWritePushMarker(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAgcDriverUserDataWriteSetMarker(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - } extern "C" { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/BdaResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/BdaResources.hpp index 2a796be046..45aa0df8aa 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/BdaResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/BdaResources.hpp @@ -3,7 +3,10 @@ #include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" #include "Recompiler.hpp" +#include +#include #include +#include #include #include #include @@ -12,13 +15,30 @@ namespace AgcDriver::Graphics { bool LoopGuardTripped(); +struct BdaFaultChunk; + class BdaResources { public: - explicit BdaResources(const Context& context); - BdaResources(const Context& context, const GuestBufferMemory& memory); + explicit BdaResources(const Context& context, bool reportOnly = false); + BdaResources(const Context& context, const GuestBufferMemory& memory, bool reportOnly = false); + ~BdaResources(); + BdaResources(const BdaResources&) = delete; + BdaResources& operator=(const BdaResources&) = delete; VkDescriptorBufferInfo Table() const; VkDescriptorBufferInfo Fault() const; void CheckFault() const; + void NoteUse(std::uint64_t serial); + bool TakeFaultSlot(const Context& context); + bool HoldsFaultSlot() const { return faultChunk != nullptr; } + static void NoteFinished(VkDevice device, std::uint64_t serial); + struct FaultSlotCounts { + std::uint64_t chunks = 0; + std::uint64_t slots = 0; + std::uint64_t held = 0; + std::uint64_t reclaimed = 0; + std::uint64_t retaken = 0; + }; + static FaultSlotCounts FaultSlots(VkDevice device); struct ImageTableFaults { std::uint32_t stage = 0; std::uint64_t shader = 0; @@ -57,8 +77,13 @@ private: // BdaResources.cpp, which refers to it weakly). void markWrittenPages() const; void checkSrtGuard() const; + std::span faultBytes() const; std::shared_ptr table; - std::unique_ptr fault; + VkDevice faultDevice = VK_NULL_HANDLE; + std::shared_ptr faultChunk; + VkDeviceSize faultOffset = 0; + bool reportOnly = false; + std::uint64_t lastUse = 0; std::size_t tableBytes = 0; std::vector imageTables; std::vector> imageTableRanges; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp index d8a450ec8c..d2f3c8aedd 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp @@ -28,6 +28,7 @@ struct BufferAllocation { bool slab = false; std::size_t bufferBytes = 0; std::uint32_t memoryType = 0; + bool hostPlaced = false; }; struct SlabSlot { @@ -71,7 +72,7 @@ public: VkDeviceMemory TakeImageMemory(const VkMemoryAllocateInfo& allocation, const char* operation, std::optional hostType = std::nullopt, std::uint32_t* allocatedType = nullptr, bool newImage = false); void PutImageMemory(VkDeviceMemory memory, VkDeviceSize bytes, std::uint32_t memoryType) noexcept; std::size_t RetainedImages(); - std::optional Take(std::size_t bytes, VkBufferUsageFlags usage, VkMemoryPropertyFlags properties = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, const char* category = nullptr); + std::optional Take(std::size_t bytes, VkBufferUsageFlags usage, VkMemoryPropertyFlags properties = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, const char* category = nullptr, bool hostPlaced = false); void Put(const BufferAllocation& allocation) noexcept; VkDeviceSize ReclaimIdleDevice(VkDeviceSize bytes, std::chrono::steady_clock::time_point now); static bool SlabEligible(std::size_t capacity, VkDeviceSize alignment, VkDeviceSize size, VkDeviceSize atom); @@ -106,11 +107,12 @@ private: VkBufferUsageFlags usage; VkMemoryPropertyFlags properties; std::uint32_t memoryType = 0; + bool hostPlaced = false; bool operator==(const SlotKey&) const = default; }; struct SlotKeyHash { std::size_t operator()(const SlotKey& key) const noexcept { - return std::hash{}(key.bytes) ^ (static_cast(key.usage) << 32u) ^ (static_cast(key.properties) << 48u) ^ (static_cast(key.memoryType) << 56u); + return std::hash{}(key.bytes) ^ (static_cast(key.usage) << 32u) ^ (static_cast(key.properties) << 48u) ^ (static_cast(key.memoryType) << 56u) ^ (static_cast(key.hostPlaced) << 63u); } }; // One retention tier: its slots by key (each list oldest first), their count and bytes, the diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp index 7af85f1777..c1710679ed 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp @@ -6,6 +6,7 @@ #endif #include #include +#include "prx/libc/include/PrimedSharedMutex.hpp" #include #include #include @@ -35,6 +36,7 @@ class SamplerCache; class ShaderResources; class PlaceholderImages; class VertexInputCache; +class VertexBufferFormats; class PipelineCacheSynchronization { public: @@ -46,7 +48,7 @@ public: SnapshotLock TryLockForSnapshot() const { return SnapshotLock(mutex, std::try_to_lock); } private: - mutable std::shared_mutex mutex; + mutable PrimedSharedMutex mutex; }; inline void Require(bool condition, const std::string& reason) { @@ -259,8 +261,11 @@ struct Context { bool earlyFragmentSampleMaskTestBeforeSampleCounting = false; bool primitiveListRestart = false; bool imageViewMinLod = false; + bool shaderResourceMinLod = false; + bool shaderImageMinLod = false; bool pipelineExecutableInfo = false; bool maintenance8 = false; + bool pushDescriptors = false; bool graphicsPipelineLibrary = false; bool pipelineLinkTimeOptimization = true; std::uint32_t srgbDecodeFormats = 0; @@ -279,6 +284,7 @@ struct Context { return pipelineCacheSynchronization != nullptr ? pipelineCacheSynchronization->LockForPipelineUse() : PipelineCacheSynchronization::PipelineUseLock{}; } VertexInputCache* vertexInputs = nullptr; + VertexBufferFormats* vertexFormats = nullptr; template TFunction Function(const char* name) const { @@ -330,35 +336,62 @@ struct Context { return type; } - std::uint32_t DeviceMemoryType(std::uint32_t mask, VkDeviceSize bytes, const char* what) const { - const auto type = MemoryType(mask, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - if (memoryProperties2 == nullptr || physical == VK_NULL_HANDLE) return type; + struct HeapReport { + VkDeviceSize budget = 0; + VkDeviceSize usage = 0; + bool Fits(VkDeviceSize bytes) const { return budget == 0 || (usage < budget && bytes <= budget - usage); } + }; + + HeapReport ReportedHeap(std::uint32_t heap) const { + if (memoryProperties2 == nullptr || physical == VK_NULL_HANDLE) return {}; VkPhysicalDeviceMemoryBudgetPropertiesEXT reported{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT}; VkPhysicalDeviceMemoryProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_PROPERTIES_2}; properties.pNext = &reported; memoryProperties2(physical, &properties); + return {reported.heapBudget[heap], reported.heapUsage[heap]}; + } + + bool DeviceLocalFits(std::uint32_t mask, VkDeviceSize bytes) const { + return ReportedHeap(memory.memoryTypes[MemoryType(mask, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT)].heapIndex).Fits(bytes); + } + + std::uint32_t DeviceMemoryType(std::uint32_t mask, VkDeviceSize bytes, const char* what) const { + const auto type = MemoryType(mask, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); const auto heap = memory.memoryTypes[type].heapIndex; - const auto budget = reported.heapBudget[heap]; - const auto usage = reported.heapUsage[heap]; - if (budget == 0 || (usage < budget && bytes <= budget - usage)) return type; + const auto report = ReportedHeap(heap); + if (report.Fits(bytes)) return type; const auto host = HostMemoryType(mask); if (!host) return type; if (cacheReclaim != nullptr) { - const auto wanted = bytes + (usage > budget ? usage - budget : 0u); + const auto wanted = bytes + (report.usage > report.budget ? report.usage - report.budget : 0u); const auto released = cacheReclaim(wanted, true); if (released >= wanted) return type; if (released != 0) { - memoryProperties2(physical, &properties); - if (reported.heapUsage[heap] < reported.heapBudget[heap] && bytes <= reported.heapBudget[heap] - reported.heapUsage[heap]) return type; + const auto after = ReportedHeap(heap); + if (after.Fits(bytes)) return type; } } static std::atomic placements{0}, placedBytes{0}; const auto count = placements.fetch_add(1) + 1; const auto total = placedBytes.fetch_add(bytes) + bytes; - if (count <= 5 || (count & (count - 1)) == 0) std::fprintf(stderr, "[gpu] %s: device-local heap %u uses %llu of its %llu MiB budget, %llu bytes placed in host memory type %u; %llu such allocations, %llu MiB\n", what, heap, static_cast(usage >> 20u), static_cast(budget >> 20u), static_cast(bytes), *host, static_cast(count), static_cast(total >> 20u)); + if (count <= 5 || (count & (count - 1)) == 0) std::fprintf(stderr, "[gpu] %s: device-local heap %u uses %llu of its %llu MiB budget, %llu bytes placed in host memory type %u; %llu such allocations, %llu MiB\n", what, heap, static_cast(report.usage >> 20u), static_cast(report.budget >> 20u), static_cast(bytes), *host, static_cast(count), static_cast(total >> 20u)); return *host; } + bool DeviceLocalHeadroom(std::uint32_t type, VkDeviceSize bytes) const { + if (memoryProperties2 == nullptr || physical == VK_NULL_HANDLE || type >= memory.memoryTypeCount) return true; + const auto heap = memory.memoryTypes[type].heapIndex; + if ((memory.memoryHeaps[heap].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) == 0) return true; + VkPhysicalDeviceMemoryBudgetPropertiesEXT reported{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT}; + VkPhysicalDeviceMemoryProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_PROPERTIES_2, &reported}; + memoryProperties2(physical, &properties); + const auto budget = reported.heapBudget[heap]; + if (budget == 0) return true; + const auto limit = budget - budget / 8u; + const auto usage = reported.heapUsage[heap]; + return usage < limit && bytes <= limit - usage; + } + std::optional HostMemoryType(std::uint32_t mask) const { for (std::uint32_t i = 0; i < memory.memoryTypeCount; ++i) { if ((mask & (1u << i)) != 0 && (memory.memoryHeaps[memory.memoryTypes[i].heapIndex].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) == 0) return i; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp index b0c9a79d39..ec8281bb25 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp @@ -70,6 +70,8 @@ DccKeys TextureClearKeys(const GuestTextureResource& resource, std::uint64_t gue struct DccKeyProof { DccKeys keys = DccKeys::Uncompressed; std::uint64_t generation = 0; + std::uint64_t pendingNote = 0; + std::uint64_t pendingRecorder = 0; }; // TextureClearKeys with the scan skipped while the key range is unchanged since `proof` was taken // (CollectWrites over the keys, then UnchangedSince its generation), so a surface's keys are scanned @@ -80,6 +82,8 @@ struct DccKeyProof { // their record), which makes the proof exactly as sound as UnchangedSince over the texels. // APS5_NO_KEY_FAST_PATH=1 scans on every call and stores nothing. DccKeys ProvedClearKeys(const GuestTextureResource& resource, std::uint64_t guestBytes, DccKeyProof& proof); +DccKeyProof PendingKeyProof(const GuestTextureResource& resource, std::uint64_t guestBytes, DccKeys keys); +bool PendingKeyProofHolds(const GuestTextureResource& resource, std::uint64_t guestBytes, const DccKeyProof& proof); bool KeyFastPath(); bool SingleKeysReadable(const GuestTextureResource& resource); bool ReadSingleElement(const GuestTextureResource& resource, std::span element); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp index 89ffea8862..c02c50d026 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp @@ -18,6 +18,7 @@ class Texture; class StorageTexture; VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target, bool depthWritten = true, bool stencilTested = false); +VkImageView DepthSurfaceViewWithStencilWriteIntent(const Context& context, const DepthTarget& target, bool depthWritten, bool stencilTested, bool stencilWritten); SampleOrderTarget DepthSurfaceOrderTarget(const Context& context, const DepthTarget& target); DepthSampleLocations DepthSurfaceSampleLocations(const Context& context, const DepthTarget& target); enum class DepthPlaneScope { Overlapped, Whole }; @@ -37,6 +38,8 @@ std::uint64_t DepthSliceBytes(VkExtent2D extent, std::uint32_t bytesPerTexel); bool DirectDepthCopy(const Context& context, VkFormat depthFormat, VkImageAspectFlagBits aspect, VkFormat colorFormat); void SetDirectDepthCopy(bool enabled); std::uint64_t DirectDepthCopyCount(); +void SetDepthTransferPasses(bool enabled); +std::uint64_t DepthTransferPassCount(); std::uint64_t MultisampledDepthBytes_nid_no_patch(VkExtent2D extent, std::uint32_t bytesPerTexel, std::uint32_t samples); void RequireNoResidentDepthRead_nid_no_patch(std::uint64_t address, std::size_t bytes, std::optional> htileClear = std::nullopt); void AppendHostWrittenDepthRanges(std::uint64_t address, std::size_t bytes, std::vector>& ranges); @@ -86,6 +89,7 @@ VkImageAspectFlags HtileFillClears(std::uint32_t pattern, bool stencilInHtile); std::uint64_t HtileMetadataBytes(VkExtent2D extent); bool HtileFillCovers(std::uint64_t htile, VkExtent2D extent, std::uint64_t address, std::size_t bytes); std::shared_ptr DepthSurfaceTexture(const Context& context, std::span words, const GuestTextureResource& resource, VkComponentMapping components, VkImageViewType multisampleViewType = VK_IMAGE_VIEW_TYPE_2D); +std::shared_ptr DepthSurfaceFirstSample(const Context& context, std::span words, const GuestTextureResource& resource, VkComponentMapping components); std::size_t DepthPlaneCopyCount(); VkDeviceSize DepthPlaneCopyBytes(); VkFormat HostDepthFormat(const Context& context, VkFormat format); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp index 64355013ca..5186c230d5 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp @@ -57,6 +57,15 @@ inline VkDeviceSize HostImportOffset(const HostImport& import, std::uint64_t add return address - import.base + import.lead; } +struct HostImportStart { + std::uint64_t guest; + VkDeviceAddress device; +}; + +inline HostImportStart HostImportBufferStart(const HostImport& import) { + return {import.base - import.lead, import.address - import.lead}; +} + enum class ImportWatch : std::uint8_t { Watch, Unwatch }; struct ImportProbe { @@ -467,7 +476,9 @@ private: bool gpuCopy = false; VkBuffer copySource = VK_NULL_HANDLE; std::uint64_t copySourceBase = 0; + VkDeviceAddress copySourceAddress = 0; bool copiedBack = false; + std::shared_ptr shadowCopy; // An element the shader updates atomically lies inside (AddWritable's `atomic`). bool atomic = false; bool swept = false; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestSamplerResource.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestSamplerResource.hpp index 55442662ba..8e1b012d47 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestSamplerResource.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestSamplerResource.hpp @@ -35,6 +35,8 @@ struct GuestSamplerResource { GuestSamplerResource DecodeSamplerResource(std::span words, bool unnormalizedProven = false, bool forceDegammaPaired = false, bool integerBorder = false); std::optional> SingleLevelSamplerWords(std::span words, bool singleLevelImage, bool mipmappedImage); +bool HoldsLodAtZero(std::span words); +std::array PinnedLodSamplerWords(std::span words, std::uint32_t lod); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp index ddb6cf6bac..e2594b15fc 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp @@ -4,6 +4,7 @@ #include "Recompiler.hpp" #include #include +#include #include namespace AgcDriver::Graphics { @@ -141,6 +142,8 @@ constexpr TextureTileMode GcnMicroTileMode(TextureTileMode mode) { float EffectiveMinLod(const GuestTextureResource& resource); bool IsNullTextureResource(std::span words); +std::optional TextureResourceAllocationAddress(std::span words); +bool IsNullSampledTextureResource(std::span words); GuestTextureResource DecodeTextureResource(std::span words); std::array EncodeTextureResource(const GuestTextureResource& resource); bool DescriptorSingleLevel(std::span words); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp index 067d251c70..f40fd3d483 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp @@ -157,7 +157,7 @@ bool ShadersWriteMemory(std::span shaders); bool DeclaresEarlyFragmentTests(const ShaderRecompiler::RecompileResult& program); void ValidateSampleMask(const Context& context, const State& state, std::span shaders); // Returns the color attachment locations the pixel shader writes. -std::set ValidateShaders(std::span shaders, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing = false, bool imageInt64Atomics = false, bool geometryShader = false, bool sampleRateShading = false, bool bufferInt64Atomics = false, bool shaderClipDistance = false, bool shaderCullDistance = false, bool viewportIndexLayer = false, bool runtimeDescriptorArray = false, bool demoteToHelperInvocation = false, bool shaderTerminateInvocation = false, bool meshShader = true); +std::set ValidateShaders(std::span shaders, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing = false, bool imageInt64Atomics = false, bool geometryShader = false, bool sampleRateShading = false, bool bufferInt64Atomics = false, bool shaderClipDistance = false, bool shaderCullDistance = false, bool viewportIndexLayer = false, bool runtimeDescriptorArray = false, bool demoteToHelperInvocation = false, bool shaderTerminateInvocation = false, bool meshShader = true, bool shaderResourceMinLod = false); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp index bf66553186..3c8d324c0d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp @@ -244,6 +244,7 @@ public: void CountSamples(); bool CountingSamples() const { return countingSamples; } bool RecordMeshArguments(VkCommandBuffer commands, VkDeviceAddress record, VkDeviceAddress arguments, std::span rules); + bool RecordChangedCopy(VkCommandBuffer commands, VkDeviceAddress result, VkDeviceAddress snapshot, VkDeviceAddress target, std::uint32_t dwords, VkDeviceAddress counter = 0); std::uint64_t SamplesTotal(); bool DumpSamples(VkDeviceAddress target, std::uint64_t address = 0); void NoteSampledDraw(VkCommandBuffer commands); @@ -394,6 +395,7 @@ public: // The function the flush hook calls, on the accessing thread (which may hold the GPU mutex), // when an access overlaps one of that thread's queued labels; it records them. Set once. static void SetQueuedLabelRecorder(void (*recorder)()); + static void FailNextSubmitsForTest(unsigned count); // Whether the lock-free pending-write snapshot (open, in-flight and finishing batches) overlaps // the range: false means no recorded work writes it, so a wait on it has nothing to submit. static bool SnapshotWriteOverlaps(std::uint64_t address, std::size_t bytes); @@ -769,6 +771,9 @@ private: int meshArgumentState = 0; VkPipelineLayout meshArgumentLayout = VK_NULL_HANDLE; VkPipeline meshArgumentPipeline = VK_NULL_HANDLE; + int changedCopyState = 0; + VkPipelineLayout changedCopyLayout = VK_NULL_HANDLE; + VkPipeline changedCopyPipeline = VK_NULL_HANDLE; int sampleCounterState = 0; std::unique_ptr sampleCounter; VkPipelineLayout sampleLayout = VK_NULL_HANDLE; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Resources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Resources.hpp index bb2322c4cf..6b0bf7a2cd 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Resources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Resources.hpp @@ -12,6 +12,7 @@ public: ~Buffer(); Buffer(const Buffer&) = delete; Buffer& operator=(const Buffer&) = delete; + static std::shared_ptr WithinDeviceLocalHeadroom(const Context& context, std::size_t size, VkBufferUsageFlags usage); VkBuffer Handle() const; VkDeviceAddress DeviceAddress() const; // Host-visible buffers only: a device-local one (properties without HOST_VISIBLE, the staging @@ -21,6 +22,7 @@ public: void Invalidate(); private: + Buffer(const Context& context, std::size_t size, VkBufferUsageFlags usage, VkMemoryPropertyFlags properties, bool headroom); void initializeAddress(VkBufferUsageFlags usage); void release() noexcept; Context context; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Sampler.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Sampler.hpp index 7212fb40c6..34dd9a2774 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Sampler.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Sampler.hpp @@ -68,10 +68,10 @@ private: std::uint64_t misses = 0; }; -void RequireFilterMinmax(const Context& context, VkFormat format, std::uint32_t samplerMask, std::span> samplers); -bool ForcesDegammaOn(std::uint32_t guestFormat, std::uint32_t samplerMask, std::span> samplers); -void RequireBorderSwizzle(std::uint32_t bcSwizzle, std::uint32_t samplerMask, std::span> samplers); -void RequireNonSeamlessCube(bool cube, std::uint32_t samplerMask, std::span> samplers); +void RequireFilterMinmax(const Context& context, VkFormat format, std::uint64_t samplerMask, std::span> samplers); +bool ForcesDegammaOn(std::uint32_t guestFormat, std::uint64_t samplerMask, std::span> samplers); +void RequireBorderSwizzle(std::uint32_t bcSwizzle, std::uint64_t samplerMask, std::span> samplers); +void RequireNonSeamlessCube(bool cube, std::uint64_t samplerMask, std::span> samplers); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index 4c664922c7..ec6195ac83 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -7,6 +7,7 @@ #include "prx/libSceAgcDriver/Graphics/include/Sampler.hpp" #include "prx/libSceAgcDriver/Execution/include/Mutex.hpp" #include "Recompiler.hpp" +#include "IntermediateRepresentation/IrMetadata/ResourcePlan.hpp" #include "prx/libSceAgcDriver/Graphics/include/Shaders.hpp" #include #include @@ -54,6 +55,7 @@ bool SampledTexturesShareEntry(std::span first, std::span words); +bool SpirvFaultReportOnly(std::span words, std::uint32_t set, std::uint32_t binding); bool ShaderStoresThroughAddresses(const ShaderRecompiler::SharedSpirv& spirv); void LastRecordIndices(std::span logged, std::span objects, std::vector& indices); @@ -157,6 +159,7 @@ struct ImageTableReuseCounts { ImageTableReuseCounts ImageTableReuses(); std::uint64_t SampledTexturesCleared(); std::uint64_t SampledLookupKeyScans(); +std::uint64_t SampledLookupKeyProofs(); struct PresyncSurface { std::uint64_t address; @@ -504,6 +507,7 @@ private: bool depthCompare = false; VkImageViewType multisampleViewType = VK_IMAGE_VIEW_TYPE_2D; bool firstSample = false; + bool layerRows = false; std::shared_ptr texture; }; PreviousSampled previousSampled; @@ -513,7 +517,7 @@ private: // lock and the cache still holds it; null sends the element to cachedTexture. std::shared_ptr fastTexture(const ImageRecord& record); void resolveImageBinding(const ShaderRecompiler::DescriptorBinding& binding, Binding& item, std::span> shaderSamplers); - void applyAnisoOverride(); + void applySamplerVariants(); void forgetDeferredInputs(); void release() noexcept; void prepareAddressBindings(std::span shaders, std::span snapshots); @@ -586,6 +590,7 @@ private: std::vector srtGuardFaults; bool usesBda = false; bool bdaReadOnly = true; + bool faultReportOnly = true; std::vector multisampleSources; bool usesFaultBuffer = false; bool faultChecks = false; @@ -618,9 +623,16 @@ private: std::shared_ptr keptTable(std::size_t set, std::shared_ptr>>& held) noexcept; std::uint32_t setOf(const Binding& binding) const; std::vector allocations; - std::vector descriptorBuffers; + mutable std::vector descriptorBuffers; std::vector descriptorImages; std::vector descriptorWrites; + struct FaultDescriptor { + std::size_t write; + std::size_t buffer; + std::uint32_t element; + }; + std::vector faultDescriptors; + void prepareFaultSlot() const; mutable std::vector drawPoolSizes; // Guest buffer elements bound read-only: each use of this object skips that many pending-write // notes (counted in MarkGpuWrites for the [buffers] line). @@ -651,11 +663,13 @@ private: bool integerBorder = false; bool singleLevelImage = false; bool mipmappedImage = false; + std::optional pinnedLod; }; std::vector samplerSources; bool reusable = false; std::vector directRegions; std::vector validatedTextures; + std::vector registeredImageViews; // The pending registry's serial at the last Revalidate that proved this object, taken before // its checks and kept only when unchanged after them (0: none): while it is still the serial, // no image other than the object's own sources has results pending over its surfaces and diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Shaders.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Shaders.hpp index 2ddac45e80..e2f279e8e4 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Shaders.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Shaders.hpp @@ -2,6 +2,7 @@ #define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_SHADERS_HPP #include "prx/libSceAgcDriver/Graphics/include/Context.hpp" +#include "IntermediateRepresentation/IrMetadata/ResourcePlan.hpp" #include "Recompiler.hpp" #include #include @@ -37,6 +38,7 @@ struct CompiledShader { ShaderRecompiler::ShaderStage stage; const ShaderRecompiler::RecompileResult* program; std::uint32_t pushConstantOffset; + const std::vector* registeredImageViews = nullptr; }; inline VkShaderStageFlagBits VulkanStage(ShaderRecompiler::ShaderStage stage) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index 5a4a1f20e9..fe18787c9a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -111,6 +111,7 @@ struct DepthTarget { bool htilePipeAligned = false; std::uint32_t htileVrsEncoding = 0; bool htileDepthOnly = false; + bool gcn = false; }; struct State { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index 9720d0df72..72581ec1ca 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -60,7 +60,7 @@ struct OwnedImage { class Texture { public: - Texture(const Context& context, TextureDetiler& detiler, const GuestTextureResource& descriptor, VkComponentMapping components, std::span snapshot, bool depthCompare = false, const VkClearColorValue* clear = nullptr); + Texture(const Context& context, TextureDetiler& detiler, const GuestTextureResource& descriptor, VkComponentMapping components, std::span snapshot, bool depthCompare = false, bool layerRows = false, const VkClearColorValue* clear = nullptr); Texture(const Context& context, const std::shared_ptr& source, const GuestTextureResource& descriptor, VkComponentMapping components); // A view of a storage image's own VkImage: the sampled texture follows the image's content, so a // compute pass writing it and the next pass sampling it share one image and copy nothing. @@ -303,9 +303,12 @@ public: static bool UnwatchHookLocksGpu(std::span> ranges); static void SetPublishHookForTest(void (*hook)()); static constexpr std::uint64_t SmallStoreBytes = 64 * 1024; + static constexpr std::uint64_t OccasionalStoreBytes = 1024 * 1024; + static constexpr std::uint64_t OccasionalWriteFlips = 8; static std::size_t StoreSmallPending(const char* reason); - static void StoreReadbackBeforeLabel(const char* reason); + static void StoreReadbackBeforeLabel(const char* reason, VkDevice device = VK_NULL_HANDLE); static std::size_t StoreDrawnLinear(const char* reason); + static void NoteFlip(); static std::size_t StoreDrawnLinearTouching(const char* reason, const std::function& touches, bool all); static bool AnyPending(); static void FlushDumpSelected(const char* reason); @@ -471,6 +474,7 @@ public: static std::uint64_t HandedOverSuperseded(); static void SetClearHandOver(bool enabled); static std::uint64_t UntrackedCompares(); + static std::uint64_t OccasionalLabelStores(); std::uint64_t GuestBytes() const; std::uint64_t BudgetBytes() const; std::size_t OriginalSnapshotBytes() const { return original.capacity(); } @@ -518,9 +522,11 @@ private: void releaseReadTraps(bool sharingPages); void storeTrappedOverlaps(const char* reason); bool unchangedSinceBaseline(std::uint64_t from, std::uint64_t to) const; + bool cpuLeftUnchanged(std::uint32_t layer, std::uint64_t from, std::uint64_t to, bool edge) const; void saveBeforeUnwatch(); void forgetKeptAtUnwatch(std::uint32_t unit); bool keptAtUnwatchHolds(std::uint64_t from, std::uint32_t unit) const; + bool keptForCpu(std::uint32_t unit) const; // Drops the saved guest bytes of the units over [address, address + bytes) (another image's fill // or copy supersedes them there); the other units' bytes stay valid. void forgetSavedOver(std::uint64_t address, std::size_t bytes); @@ -747,6 +753,7 @@ private: std::uint32_t trackedLayers = 1; std::uint64_t trackedLayerBytes = 0; std::vector layerGeneration; + std::vector layerPendingSince; std::vector completionKept; UnitFlags layerPending; std::vector layerBehind; @@ -795,6 +802,8 @@ private: bool pendingFromOthers = false; std::vector drawnRects; std::vector storeNotes; + std::uint64_t writtenFlip = 0; + std::atomic writtenOccasionally{false}; std::uint64_t version = 0; std::uint64_t writes = 0; // `original` holds the guest bytes; false after a GPU-side clear, which never read them. diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp index 93fa96c5eb..0914660be1 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp @@ -92,6 +92,8 @@ namespace AgcDriver::Graphics { VkShaderModule imageModule = VK_NULL_HANDLE; VkShaderModule repackModule = VK_NULL_HANDLE; VkPipeline repackPipeline = VK_NULL_HANDLE; + VkShaderModule imageWordModule = VK_NULL_HANDLE; + VkShaderModule wordModule = VK_NULL_HANDLE; DescriptorRing imageDescriptorSets; VkDescriptorSetLayout readbackDescriptorLayout = VK_NULL_HANDLE; VkPipelineLayout readbackPipelineLayout = VK_NULL_HANDLE; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp index cdb94e2f24..9157055443 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp @@ -11,7 +11,6 @@ #include #include #include -#include #include #include #include @@ -128,14 +127,39 @@ inline std::array VertexDescriptorFlagsMessage(const ShaderRecompiler return message; } +inline bool QueryVertexBufferFormat(const Context& context, VkFormat format) { + VkFormatProperties properties{}; + context.formatProperties(context.physical, format, &properties); + return (properties.bufferFeatures & VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT) != 0; +} + +class VertexBufferFormats { +public: + bool Supported(const Context& context, VkFormat format) { + const auto index = static_cast(format); + if (index >= known.size()) return QueryVertexBufferFormat(context, format); + auto state = known[index].load(std::memory_order_relaxed); + if (state == 0) { + state = QueryVertexBufferFormat(context, format) ? 2 : 1; + known[index].store(state, std::memory_order_relaxed); + } + return state == 2; + } + +private: + std::array, VK_FORMAT_ASTC_12x12_SRGB_BLOCK + 1> known{}; +}; + inline VertexInputLayout BuildVertexInputLayout(const Context& context, std::span attributes) { Require(attributes.size() <= context.limits.maxVertexInputBindings && attributes.size() <= context.limits.maxVertexInputAttributes, "vertex input count exceeds device limits"); VertexInputLayout result; - std::set locations; + result.bindings.reserve(attributes.size()); + result.attributes.reserve(attributes.size()); for (const auto& attribute : attributes) { const auto& fields = attribute.resource.fields; const auto format = DecodeVertexFormat(attribute); - Require(attribute.location < context.limits.maxVertexInputAttributes && locations.insert(attribute.location).second, "invalid or duplicate vertex attribute location"); + const auto duplicate = std::any_of(result.attributes.begin(), result.attributes.end(), [&](const auto& described) { return described.location == attribute.location; }); + Require(attribute.location < context.limits.maxVertexInputAttributes && !duplicate, "invalid or duplicate vertex attribute location"); Require(attribute.fetchIndex <= 1, "unsupported vertex fetch index"); if ((fields[1] & 0x80000000u) != 0 || (fields[3] & 0x00800000u) != 0 || (fields[3] >> 30u) != 0) { Require(false, VertexDescriptorFlagsMessage(attribute).data()); @@ -146,9 +170,7 @@ inline VertexInputLayout BuildVertexInputLayout(const Context& context, std::spa Require(stride <= context.limits.maxVertexInputBindingStride, "vertex stride exceeds device limits"); Require(context.formatProperties != nullptr, "missing vertex format property query"); const auto supported = [&](VkFormat candidate) { - VkFormatProperties properties{}; - context.formatProperties(context.physical, candidate, &properties); - return (properties.bufferFeatures & VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT) != 0; + return context.vertexFormats != nullptr ? context.vertexFormats->Supported(context, candidate) : QueryVertexBufferFormat(context, candidate); }; auto host = format.format; auto hostSupported = supported(host); @@ -156,7 +178,7 @@ inline VertexInputLayout BuildVertexInputLayout(const Context& context, std::spa host = DecodeVertexFormat(attribute, components).format; hostSupported = supported(host); } - Require(hostSupported, "device does not support vertex format " + std::to_string(format.format)); + if (!hostSupported) Require(false, "device does not support vertex format " + std::to_string(format.format)); const auto binding = static_cast(result.bindings.size()); result.bindings.push_back({binding, stride, attribute.fetchIndex == 0 ? VK_VERTEX_INPUT_RATE_VERTEX : VK_VERTEX_INPUT_RATE_INSTANCE}); result.attributes.push_back({attribute.location, binding, host, 0}); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/shaders/DepthToStorage.comp b/core/libs/prx/libSceAgcDriver/Graphics/shaders/DepthToStorage.comp new file mode 100644 index 0000000000..0ce8d0e28a --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/shaders/DepthToStorage.comp @@ -0,0 +1,16 @@ +#version 450 +#extension GL_EXT_samplerless_texture_functions : require + +layout(local_size_x = 8, local_size_y = 8) in; +layout(set = 0, binding = 0) uniform texture2D depthPlane; +layout(set = 0, binding = 1, r32ui) uniform writeonly uimage2D texels; +layout(push_constant) uniform Push { + uvec2 offset; + uvec2 extent; +} push; + +void main() { + if (any(greaterThanEqual(gl_GlobalInvocationID.xy, push.extent))) return; + const ivec2 position = ivec2(push.offset + gl_GlobalInvocationID.xy); + imageStore(texels, position, uvec4(floatBitsToUint(texelFetch(depthPlane, position, 0).r))); +} diff --git a/core/libs/prx/libSceAgcDriver/Graphics/shaders/StagingCopyBack.comp b/core/libs/prx/libSceAgcDriver/Graphics/shaders/StagingCopyBack.comp new file mode 100644 index 0000000000..a950cfef8d --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/shaders/StagingCopyBack.comp @@ -0,0 +1,34 @@ +#version 450 +#extension GL_EXT_buffer_reference : require +#extension GL_EXT_shader_explicit_arithmetic_types_int64 : require + +layout(local_size_x = 64) in; +layout(buffer_reference, std430, buffer_reference_align = 4) buffer Words { uint words[]; }; +layout(push_constant) uniform Parameters { + Words result; + Words snapshot; + Words target; + Words counter; + uint count; + uint always; +} parameters; + +uint differingBytes(uint difference) { + uint folded = difference | (difference >> 1u); + folded |= folded >> 2u; + folded |= folded >> 4u; + return (folded & 0x01010101u) * 0xffu; +} + +void main() { + uint index = gl_GlobalInvocationID.x; + if (index >= parameters.count) return; + uint value = parameters.result.words[index]; + uint before = parameters.snapshot.words[index]; + uint held = parameters.target.words[index]; + uint changed = differingBytes(value ^ before); + uint kept = differingBytes(held ^ before); + uint stored = parameters.always != 0u ? 0xffffffffu : changed & ~kept; + if (stored != 0u) parameters.target.words[index] = (held & ~stored) | (value & stored); + if (uint64_t(parameters.counter) != 0ul && (changed & kept) != 0u) atomicAdd(parameters.counter.words[0], 1u); +} diff --git a/core/libs/prx/libSceAgcDriver/Graphics/shaders/TextureDetile.comp b/core/libs/prx/libSceAgcDriver/Graphics/shaders/TextureDetile.comp index 6ebceb5667..6c8e6b6a78 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/shaders/TextureDetile.comp +++ b/core/libs/prx/libSceAgcDriver/Graphics/shaders/TextureDetile.comp @@ -207,34 +207,27 @@ void copyElement(uint src, uint dst) { } #endif -void main() { - uvec2 p = gl_GlobalInvocationID.xy + uvec2(params.columnBegin, params.rowBegin); - if (p.x >= params.width || p.y >= params.height) { - return; - } - +bool locate(uvec2 p, out uint src, out uint dst, out uint ranged) { uint linearOffset = p.y * params.pitchBytes + p.x * ELEMENT_BYTES; if (TILE_FAMILY == 0u) { - // kLinear: guest and host layouts match; a straight copy at the - // same pitch, no swizzle. + ranged = linearOffset; if (linearOffset < params.rangeBegin || linearOffset >= params.rangeEnd) { - return; + return false; } #ifdef LINEAR_IMAGE - if (RETILE != 0u) { - loadElement(p, params.dstBase + linearOffset - params.tiledBase); - } else { - storeElement(params.srcBase + linearOffset - params.tiledBase, p); - } + src = params.srcBase + linearOffset - params.tiledBase; + dst = params.dstBase + linearOffset - params.tiledBase; #else if (RETILE != 0u) { - copyElement(params.srcBase + linearOffset - params.linearBase, params.dstBase + linearOffset - params.tiledBase); + src = params.srcBase + linearOffset - params.linearBase; + dst = params.dstBase + linearOffset - params.tiledBase; } else { - copyElement(params.srcBase + linearOffset - params.tiledBase, params.dstBase + linearOffset - params.linearBase); + src = params.srcBase + linearOffset - params.tiledBase; + dst = params.dstBase + linearOffset - params.linearBase; } #endif - return; + return true; } uvec2 swizzle = p; @@ -251,24 +244,106 @@ void main() { uint split = equationBit(SPLIT0, p) | (equationBit(SPLIT1, p) << 1) | (equationBit(SPLIT2, p) << 2) | (equationBit(SPLIT3, p) << 3); tiledOffset += split * params.splitStride; } + ranged = tiledOffset; if (tiledOffset < params.rangeBegin || tiledOffset >= params.rangeEnd) { - return; + return false; } tiledOffset -= params.tiledBase; linearOffset -= params.linearBase; #ifdef LINEAR_IMAGE - if (RETILE != 0u) { - loadElement(p, params.dstBase + tiledOffset); - } else { - storeElement(params.srcBase + tiledOffset, p); - } + src = params.srcBase + tiledOffset; + dst = params.dstBase + tiledOffset; #else if (RETILE != 0u) { - copyElement(params.srcBase + linearOffset, params.dstBase + tiledOffset); + src = params.srcBase + linearOffset; + dst = params.dstBase + tiledOffset; } else { - copyElement(params.srcBase + tiledOffset, params.dstBase + linearOffset); + src = params.srcBase + tiledOffset; + dst = params.dstBase + linearOffset; } +#endif + return true; +} + +void moveElement(uvec2 p) { + uint src; + uint dst; + uint ranged; + if (!locate(p, src, dst, ranged)) { + return; + } +#ifdef LINEAR_IMAGE + if (RETILE != 0u) { + loadElement(p, dst); + } else { + storeElement(src, p); + } +#else + copyElement(src, dst); #endif } +#ifdef WORD_GROUPS +shared uint groupWords[32]; +shared uint groupLanes[32]; +shared uint groupDestinations[64]; + +void main() { + uvec2 p = gl_GlobalInvocationID.xy + uvec2(params.columnBegin, params.rowBegin); + bool inside = p.x < params.width && p.y < params.height; + uint lane = gl_LocalInvocationID.y * gl_WorkGroupSize.x + gl_LocalInvocationID.x; + uint count = 4u / ELEMENT_BYTES; + uint slot = lane / count; + uint index = lane % count; + uint src = 0u; + uint dst = 0u; + uint ranged = 0u; + bool valid = inside && locate(p, src, dst, ranged); + if (index == 0u) { + groupWords[slot] = 0u; + groupLanes[slot] = 0u; + } + groupDestinations[lane] = dst; + barrier(); + if (valid) { + uint mask = ELEMENT_BYTES == 1u ? 0xffu : 0xffffu; +#ifdef LINEAR_IMAGE + uint value = imageLoad(linearImage, ivec2(p)).x & mask; +#else + uint value = (inputBuffer.data[src >> 2] >> ((src & 3u) * 8u)) & mask; +#endif + atomicOr(groupWords[slot], value << (index * ELEMENT_BYTES * 8u)); + atomicOr(groupLanes[slot], 1u << index); + } + barrier(); + uint first = groupDestinations[slot * count]; + bool whole = groupLanes[slot] == (1u << count) - 1u && (first & 3u) == 0u; + for (uint i = 1u; i < count; ++i) { + whole = whole && groupDestinations[slot * count + i] == first + i * ELEMENT_BYTES; + } + if (whole) { + if (index == 0u) { +#ifdef LINEAR_IMAGE + tiledBuffer.data[first >> 2] = groupWords[slot]; +#else + outputBuffer.data[first >> 2] = groupWords[slot]; +#endif + } + } else if (valid) { +#ifdef LINEAR_IMAGE + loadElement(p, dst); +#else + copyElement(src, dst); +#endif + } +} +#else +void main() { + uvec2 p = gl_GlobalInvocationID.xy + uvec2(params.columnBegin, params.rowBegin); + if (p.x >= params.width || p.y >= params.height) { + return; + } + moveElement(p); +} +#endif diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp index 3b0498e7b8..bfd803f262 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp @@ -4,6 +4,7 @@ #include "ImageTableAbi.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include +#include #include #include #include @@ -13,7 +14,12 @@ #include #include #include +#include #include +#include +#include +#include +#include namespace AgcDriver::Graphics { @@ -104,15 +110,207 @@ std::uint64_t hashRanges(const std::vector& ran } -BdaResources::BdaResources(const Context& context) { - static_assert(std::endian::native == std::endian::little); - Require(ShaderRecompiler::BdaAbi::FaultBufferBytes <= context.limits.maxStorageBufferRange, "BDA fault buffer exceeds storage buffer range limit"); - fault = std::make_unique(context, ShaderRecompiler::BdaAbi::FaultBufferBytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); - std::memset(fault->Bytes().data(), 0, ShaderRecompiler::BdaAbi::WrittenListWord * sizeof(std::uint32_t)); - std::memset(fault->Bytes().data() + ShaderRecompiler::BdaAbi::SrtGuardWord * sizeof(std::uint32_t), 0, sizeof(std::uint32_t)); +struct BdaFaultChunk { + std::unique_ptr buffer; + std::vector free; + std::size_t slots = 0; + VkDeviceSize slotBytes = 0; +}; + +namespace { + +struct FaultArena { + std::vector> chunks; + std::unordered_set holders; + std::uint64_t finished = 0; + BdaResources::FaultSlotCounts counts; +}; + +struct FaultArenas { + std::mutex mutex; + std::unordered_map devices; +}; + +FaultArenas& faultArenas() { + static auto* arenas = new FaultArenas(); + return *arenas; } -BdaResources::BdaResources(const Context& context, const GuestBufferMemory& memory) : BdaResources(context) { +bool sharedFaultSlots() { + static const bool shared = std::getenv("APS5_NO_SHARED_FAULT_SLOTS") == nullptr; + return shared; +} + +constexpr VkDeviceSize FaultChunkBytes = VkDeviceSize{512} * 1024; + +VkDeviceSize faultSlotBytes(bool reportOnly) { + return reportOnly ? sizeof(ShaderRecompiler::BdaAbi::Fault) : ShaderRecompiler::BdaAbi::FaultBufferBytes; +} + +VkDeviceSize faultStride(const Context& context, VkDeviceSize bytes) { + const auto alignment = std::max(context.limits.minStorageBufferOffsetAlignment, sizeof(std::uint32_t)); + return (bytes + alignment - 1) / alignment * alignment; +} + +bool faultSlotClean(std::span bytes) { + namespace Abi = ShaderRecompiler::BdaAbi; + if (bytes.size() < Abi::FaultBufferBytes) return std::all_of(bytes.begin(), bytes.end(), [](std::byte value) { return value == std::byte{0}; }); + std::array head{}; + std::memcpy(head.data(), bytes.data(), sizeof(head)); + std::uint32_t guard = 0; + std::memcpy(&guard, bytes.data() + Abi::SrtGuardWord * sizeof(std::uint32_t), sizeof(guard)); + return guard == 0 && std::all_of(head.begin(), head.end(), [](std::uint32_t word) { return word == 0; }); +} + +void clearFaultSlot(std::span bytes) { + namespace Abi = ShaderRecompiler::BdaAbi; + if (bytes.size() < Abi::FaultBufferBytes) { + std::memset(bytes.data(), 0, bytes.size()); + return; + } + std::memset(bytes.data(), 0, Abi::WrittenListWord * sizeof(std::uint32_t)); + std::memset(bytes.data() + Abi::SrtGuardWord * sizeof(std::uint32_t), 0, sizeof(std::uint32_t)); +} + +void dropChunk(FaultArena& arena, const std::shared_ptr& chunk, std::vector>& dropped) { + const auto found = std::find(arena.chunks.begin(), arena.chunks.end(), chunk); + if (found == arena.chunks.end()) return; + arena.counts.slots -= (*found)->slots; + --arena.counts.chunks; + dropped.push_back(std::move(*found)); + arena.chunks.erase(found); +} + +} + +BdaResources::BdaResources(const Context& context, bool reportOnly) : reportOnly(reportOnly) { + static_assert(std::endian::native == std::endian::little); + Require(ShaderRecompiler::BdaAbi::FaultBufferBytes <= context.limits.maxStorageBufferRange, "BDA fault buffer exceeds storage buffer range limit"); + TakeFaultSlot(context); +} + +BdaResources::~BdaResources() { + std::vector> dropped; + auto& arenas = faultArenas(); + std::lock_guard lock(arenas.mutex); + if (faultChunk == nullptr) return; + const auto bytes = faultBytes(); + if (!faultSlotClean(bytes)) clearFaultSlot(bytes); + const auto chunk = faultChunk; + dropped.push_back(std::move(faultChunk)); + const auto found = arenas.devices.find(faultDevice); + if (found == arenas.devices.end()) return; + auto& arena = found->second; + arena.holders.erase(this); + --arena.counts.held; + if (!sharedFaultSlots()) { + arena.counts.slots -= chunk->slots; + --arena.counts.chunks; + } else { + chunk->free.push_back(faultOffset); + if (chunk->free.size() == chunk->slots) dropChunk(arena, chunk, dropped); + } + if (arena.holders.empty()) { + while (!arena.chunks.empty()) dropChunk(arena, arena.chunks.back(), dropped); + arenas.devices.erase(found); + } +} + +std::span BdaResources::faultBytes() const { + return faultChunk->buffer->Bytes().subspan(static_cast(faultOffset), static_cast(faultChunk->slotBytes)); +} + +void BdaResources::NoteUse(std::uint64_t serial) { + auto& arenas = faultArenas(); + std::lock_guard lock(arenas.mutex); + lastUse = std::max(lastUse, serial); +} + +void BdaResources::NoteFinished(VkDevice device, std::uint64_t serial) { + auto& arenas = faultArenas(); + std::lock_guard lock(arenas.mutex); + const auto found = arenas.devices.find(device); + if (found != arenas.devices.end()) found->second.finished = serial; +} + +BdaResources::FaultSlotCounts BdaResources::FaultSlots(VkDevice device) { + auto& arenas = faultArenas(); + std::lock_guard lock(arenas.mutex); + const auto found = arenas.devices.find(device); + return found != arenas.devices.end() ? found->second.counts : FaultSlotCounts{}; +} + +bool BdaResources::TakeFaultSlot(const Context& context) { + if (faultChunk != nullptr) return false; + std::vector> dropped; + auto& arenas = faultArenas(); + std::lock_guard lock(arenas.mutex); + auto& arena = arenas.devices[context.device]; + const bool retake = faultDevice != VK_NULL_HANDLE; + if (!retake) lastUse = arena.finished + 1; + faultDevice = context.device; + const bool shared = sharedFaultSlots(); + const auto slotBytes = shared ? faultSlotBytes(reportOnly) : VkDeviceSize{ShaderRecompiler::BdaAbi::FaultBufferBytes}; + const auto withFree = [&] { return std::find_if(arena.chunks.rbegin(), arena.chunks.rend(), [&](const std::shared_ptr& chunk) { return chunk->slotBytes == slotBytes && !chunk->free.empty(); }); }; + auto source = shared ? withFree() : arena.chunks.rend(); + bool reclaimed = false; + if (shared && source == arena.chunks.rend() && GuestMemory::GpuMutex().HeldByThisThread()) { + std::vector idle; + for (auto* holder : arena.holders) { + if (holder->faultChunk->slotBytes == slotBytes && holder->lastUse <= arena.finished && faultSlotClean(holder->faultBytes())) idle.push_back(holder); + } + if (!idle.empty()) { + const auto reclaim = (idle.size() + 1) / 2; + std::nth_element(idle.begin(), idle.begin() + static_cast(reclaim - 1), idle.end(), [](const BdaResources* left, const BdaResources* right) { return left->lastUse < right->lastUse; }); + for (std::size_t index = 0; index < reclaim; ++index) { + auto& holder = *idle[index]; + holder.faultChunk->free.push_back(holder.faultOffset); + holder.faultChunk.reset(); + arena.holders.erase(&holder); + --arena.counts.held; + ++arena.counts.reclaimed; + } + reclaimed = true; + source = withFree(); + } + } + std::shared_ptr chunk; + if (source != arena.chunks.rend()) { + chunk = *source; + } else { + const auto stride = shared ? faultStride(context, slotBytes) : slotBytes; + const auto slots = shared ? std::max(1, FaultChunkBytes / stride) : 1; + chunk = std::make_shared(); + chunk->buffer = std::make_unique(context, static_cast(stride * slots), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + std::memset(chunk->buffer->Bytes().data(), 0, chunk->buffer->Bytes().size()); + chunk->slots = static_cast(slots); + chunk->slotBytes = slotBytes; + for (auto slot = slots; slot-- > 0;) chunk->free.push_back(slot * stride); + if (shared) arena.chunks.push_back(chunk); + arena.counts.slots += chunk->slots; + ++arena.counts.chunks; + } + faultOffset = chunk->free.back(); + chunk->free.pop_back(); + faultChunk = chunk; + arena.holders.insert(this); + ++arena.counts.held; + if (retake) ++arena.counts.retaken; + bool spare = false; + for (auto it = arena.chunks.begin(); reclaimed && it != arena.chunks.end();) { + if ((*it)->free.size() != (*it)->slots || !std::exchange(spare, true)) { + ++it; + continue; + } + arena.counts.slots -= (*it)->slots; + --arena.counts.chunks; + dropped.push_back(std::move(*it)); + it = arena.chunks.erase(it); + } + return true; +} + +BdaResources::BdaResources(const Context& context, const GuestBufferMemory& memory, bool reportOnly) : BdaResources(context, reportOnly) { const auto cached = memory.CachedAddressTable(); std::vector built; if (!cached.has_value()) built = memory.AddressRanges(); @@ -228,7 +426,7 @@ VkDescriptorBufferInfo BdaResources::Table() const { } VkDescriptorBufferInfo BdaResources::Fault() const { - return {fault->Handle(), 0, ShaderRecompiler::BdaAbi::FaultBufferBytes}; + return {faultChunk->buffer->Handle(), faultOffset, faultChunk->slotBytes}; } namespace { @@ -240,12 +438,13 @@ bool LoopGuardTripped() { } void BdaResources::CheckFault() const { - markWrittenPages(); + if (faultChunk == nullptr) return; + if (!reportOnly) markWrittenPages(); ShaderRecompiler::BdaAbi::Fault report{}; - std::memcpy(&report, fault->Bytes().data(), sizeof(report)); + std::memcpy(&report, faultBytes().data(), sizeof(report)); if (report.state == ShaderRecompiler::BdaAbi::FaultState::Empty) { Require(static_cast(report.reason) == 0 && report.address == 0 && report.bytes == 0 && report.stage == 0 && report.instruction == 0 && report.otherFaults == 0, "BDA fault record has data without publication"); - checkSrtGuard(); + if (!reportOnly) checkSrtGuard(); return; } Require(report.state == ShaderRecompiler::BdaAbi::FaultState::Ready && report.otherFaults <= 1, "incomplete or invalid BDA fault record"); @@ -254,7 +453,7 @@ void BdaResources::CheckFault() const { // APS5_LOOP_GUARD: the shader left a loop that ran past the guard; the dispatch result is kept. loopGuardTripped.store(true, std::memory_order_relaxed); std::fprintf(stderr, "[gpu] loop guard: the loop exit at pc 0x%x of shader 0x%llx ran past %u evaluations\n", report.instruction, static_cast(report.address), report.bytes); - std::memset(fault->Bytes().data(), 0, fault->Bytes().size()); + std::memset(faultBytes().data(), 0, faultBytes().size()); return; } if (report.reason == ShaderRecompiler::BdaAbi::FaultReason::ImageTable) { @@ -321,7 +520,7 @@ void BdaResources::SetSrtGuards(std::vector guards) { void BdaResources::checkSrtGuard() const { namespace Abi = ShaderRecompiler::BdaAbi; std::uint32_t guard = 0; - std::memcpy(&guard, fault->Bytes().data() + Abi::SrtGuardWord * sizeof(std::uint32_t), sizeof(guard)); + std::memcpy(&guard, faultBytes().data() + Abi::SrtGuardWord * sizeof(std::uint32_t), sizeof(guard)); if (guard == 0) return; const auto stage = guard >> Abi::SrtGuardStageShift; const auto record = guard & ((1u << Abi::SrtGuardStageShift) - 1u); @@ -337,7 +536,7 @@ void BdaResources::checkSrtGuard() const { void BdaResources::markWrittenPages() const { namespace Abi = ShaderRecompiler::BdaAbi; - auto* words = reinterpret_cast(fault->Bytes().data()); + auto* words = reinterpret_cast(faultBytes().data()); Require(words[Abi::WrittenOverflowWord] == 0, "more than " + std::to_string(Abi::WrittenPageSlots) + " chunks of " + std::to_string(Abi::WrittenChunkPages) + " pages stored to through the BDA table in one use are not implemented"); const auto count = words[Abi::WrittenCountWord]; if (count == 0) return; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp index dae52e0264..7b418132af 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp @@ -399,7 +399,7 @@ BufferPool::Tier& BufferPool::tierFor(std::size_t capacity, VkMemoryPropertyFlag return !sharedTiers() && capacity < classLimit ? smallTier : largeTier; } -std::optional BufferPool::Take(std::size_t bytes, VkBufferUsageFlags usage, VkMemoryPropertyFlags properties, const char* category) { +std::optional BufferPool::Take(std::size_t bytes, VkBufferUsageFlags usage, VkMemoryPropertyFlags properties, const char* category, bool hostPlaced) { static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; const auto capacity = Capacity(bytes); std::lock_guard lock(mutex); @@ -415,9 +415,9 @@ std::optional BufferPool::Take(std::size_t bytes, VkBufferUsag auto& tier = tierFor(capacity, properties); const auto largest = &tier == &deviceTier && !exactSizes() && bytes <= std::numeric_limits::max() / 2 ? std::max(capacity, bytes * 2) : capacity; auto found = tier.free.end(); - for (auto size = capacity; size <= largest && found == tier.free.end(); size = NextCapacity(size)) found = tier.free.find({size, usage, properties}); + for (auto size = capacity; size <= largest && found == tier.free.end(); size = NextCapacity(size)) found = tier.free.find({size, usage, properties, 0, hostPlaced}); if (found == tier.free.end() || found->second.empty()) { - ++tier.misses; + if (!hostPlaced) ++tier.misses; return std::nullopt; } auto& slots = found->second; @@ -497,7 +497,7 @@ VkDeviceSize BufferPool::ReclaimIdleDevice(VkDeviceSize bytes, std::chrono::stea for (auto it = deviceTier.free.begin(); it != deviceTier.free.end(); ++it) { if (it->second.empty()) continue; const auto& slot = it->second.front(); - if (slot.allocation.buffer == VK_NULL_HANDLE || slot.allocation.slab || slot.lastUse > reclaimBefore) continue; + if (slot.allocation.buffer == VK_NULL_HANDLE || slot.allocation.slab || slot.allocation.hostPlaced || slot.lastUse > reclaimBefore) continue; if (oldest == deviceTier.free.end() || slot.lastUse < oldest->second.front().lastUse) oldest = it; } if (oldest == deviceTier.free.end()) { @@ -586,7 +586,7 @@ void BufferPool::Put(const BufferAllocation& allocation) noexcept { evicted.push_back(allocation); } else { while (tier.slots != 0 && (tier.retainedBytes + allocation.allocationBytes > tier.budget || tier.slots >= maxSlots)) evictOldest(tier, evicted); - tier.free[{allocation.bytes, allocation.usage, allocation.properties}].push_back({allocation, ++clock, {}}); + tier.free[{allocation.bytes, allocation.usage, allocation.properties, 0, allocation.hostPlaced}].push_back({allocation, ++clock, {}}); ++tier.slots; tier.retainedBytes += allocation.allocationBytes; if (allocation.slab) MemoryAccounting::RetainPart(device, allocation.memory, allocation.allocationBytes, true); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp index 2526b73538..be42c8cf2c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp @@ -729,7 +729,7 @@ DccKeys ProvedClearKeys(const GuestTextureResource& resource, std::uint64_t gues // The collect precedes the scan: a write landing between them is stamped above `collected`, so // the proof it would invalidate is never taken as current. const auto collected = count == 0 ? 0 : GuestMemory::CollectWrites(resource.dccAddress, count); - if (proof.generation != 0 && collected != 0 && GuestMemory::UnchangedSince(resource.dccAddress, count, proof.generation)) { + if (proof.generation != 0 && collected != 0 && GuestMemory::UnchangedSince(resource.dccAddress, count, proof.generation) && PendingKeyProofHolds(resource, guestBytes, proof)) { counters.proved.fetch_add(1, std::memory_order_relaxed); return proof.keys; } @@ -748,12 +748,36 @@ DccKeys ProvedClearKeys(const GuestTextureResource& resource, std::uint64_t gues stable = !Recorder::SnapshotWriteOverlaps(resource.dccAddress, count); } } - proof = stable ? DccKeyProof{keys, collected} : DccKeyProof{}; + proof = stable ? DccKeyProof{keys, collected} : memoized && collected != 0 ? PendingKeyProof(resource, guestBytes, keys) : DccKeyProof{}; counters.scanned.fetch_add(1, std::memory_order_relaxed); if (!stable) counters.unstable.fetch_add(1, std::memory_order_relaxed); return keys; } +DccKeyProof PendingKeyProof(const GuestTextureResource& resource, std::uint64_t guestBytes, DccKeys keys) { + const auto count = static_cast(guestBytes / KeyBytes); + if (!KeyFastPath() || resource.dccAddress == 0 || count == 0 || keys == DccKeys::Mixed || keys == DccKeys::Unreadable || !GuestMemory::GpuMutex().HeldByThisThread()) return {}; + auto* recorder = Recorder::Active(); + if (recorder == nullptr) return {}; + const auto info = recorder->DescribePendingWrite(resource.dccAddress, count); + if (!info.has_value() || info->signaled) return {}; + const auto note = recorder->NewestWriteNote(resource.dccAddress, count); + if (note == 0) return {}; + const auto collected = GuestMemory::CollectWrites(resource.dccAddress, count); + if (collected == 0) return {}; + return {keys, collected, note, recorder->Id()}; +} + +bool PendingKeyProofHolds(const GuestTextureResource& resource, std::uint64_t guestBytes, const DccKeyProof& proof) { + if (proof.pendingNote == 0) return true; + const auto count = static_cast(guestBytes / KeyBytes); + if (!GuestMemory::GpuMutex().HeldByThisThread()) return false; + auto* recorder = Recorder::Active(); + if (recorder == nullptr || recorder->Id() != proof.pendingRecorder || recorder->NewestWriteNote(resource.dccAddress, count) != proof.pendingNote) return false; + const auto info = recorder->DescribePendingWrite(resource.dccAddress, count); + return info.has_value() && !info->signaled; +} + DccKeyProofCounts KeyProofCounts() { const auto& counters = Proofs(); return {counters.proved.load(std::memory_order_relaxed), counters.scanned.load(std::memory_order_relaxed), counters.unstable.load(std::memory_order_relaxed), counters.rangeProved.load(std::memory_order_relaxed), counters.rangeScanned.load(std::memory_order_relaxed)}; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index ba6221ba79..94c079da3a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -2,7 +2,9 @@ #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/PrimedSharedMutex.hpp" #include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp" +#include "prx/libSceAgcDriver/Graphics/include/FirstSampleCopy.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" #include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" @@ -13,6 +15,7 @@ #include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" #include "prx/libSceAgcDriver/Execution/include/Mutex.hpp" +#include "prx/libSceAgcDriver/Graphics/shaders/DepthToStorage_spv.h" #include "RdnaDecoder/RdnaInstructionDecoder.hpp" #include "RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp" #include @@ -74,6 +77,121 @@ bool singleComponent(VkFormat format, std::uint32_t bytes) { } } +std::atomic depthTransferPasses{std::getenv("APS5_NO_DEPTH_TRANSFER_PASS") == nullptr}; +std::atomic depthTransferPassCount{0}; + +bool depthToStoragePass(const Context& context, VkFormat depthFormat, VkFormat colorFormat) { + if (!context.pushDescriptors || !depthTransferPasses.load(std::memory_order_relaxed)) return false; + return aspectCopyBytes(depthFormat, VK_IMAGE_ASPECT_DEPTH_BIT) == 4 && singleComponent(colorFormat, 4); +} + +struct RectPush { + std::array offset; + std::array extent; +}; + +class DepthToStoragePass { +public: + explicit DepthToStoragePass(const Context& context) : context(context) { + this->context.bufferPool.reset(); + try { + pushSet = context.Function("vkCmdPushDescriptorSetKHR"); + const std::array bindings{{{0, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr}, {1, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr}}}; + VkDescriptorSetLayoutCreateInfo setInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO}; + setInfo.flags = VK_DESCRIPTOR_SET_LAYOUT_CREATE_PUSH_DESCRIPTOR_BIT_KHR; + setInfo.bindingCount = static_cast(bindings.size()); + setInfo.pBindings = bindings.data(); + Check(context.Function("vkCreateDescriptorSetLayout")(context.device, &setInfo, nullptr, &setLayout), "vkCreateDescriptorSetLayout depth to storage"); + const VkPushConstantRange rect{VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(RectPush)}; + VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; + layoutInfo.setLayoutCount = 1; + layoutInfo.pSetLayouts = &setLayout; + layoutInfo.pushConstantRangeCount = 1; + layoutInfo.pPushConstantRanges = ▭ + Check(context.Function("vkCreatePipelineLayout")(context.device, &layoutInfo, nullptr, &layout), "vkCreatePipelineLayout depth to storage"); + const auto compute = CreateShaderModule(context, DEPTH_TO_STORAGE_SPV, sizeof(DEPTH_TO_STORAGE_SPV), "vkCreateShaderModule depth to storage"); + VkComputePipelineCreateInfo info{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; + info.stage = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, nullptr, 0, VK_SHADER_STAGE_COMPUTE_BIT, compute, "main", nullptr}; + info.layout = layout; + VkResult created; + { + auto pipelineCacheLock = context.LockPipelineCacheForUse(); + created = context.Function("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &info, nullptr, &pipeline); + } + context.Function("vkDestroyShaderModule")(context.device, compute, nullptr); + Check(created, "vkCreateComputePipelines depth to storage"); + } catch (...) { + release(); + throw; + } + } + ~DepthToStoragePass() { release(); } + DepthToStoragePass(const DepthToStoragePass&) = delete; + DepthToStoragePass& operator=(const DepthToStoragePass&) = delete; + + VkDevice Device() const { return context.device; } + + void Record(VkCommandBuffer commands, VkImageView depthPlane, VkImageView texels, std::span rects) const { + const VkDescriptorImageInfo depthInfo{VK_NULL_HANDLE, depthPlane, VK_IMAGE_LAYOUT_GENERAL}; + const VkDescriptorImageInfo texelInfo{VK_NULL_HANDLE, texels, VK_IMAGE_LAYOUT_GENERAL}; + const std::array writes{imageWrite(0, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, depthInfo), imageWrite(1, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, texelInfo)}; + context.Resolved(&DeviceFunctions::cmdBindPipeline, "vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline); + pushSet(commands, VK_PIPELINE_BIND_POINT_COMPUTE, layout, 0, static_cast(writes.size()), writes.data()); + const auto pushConstants = context.Resolved(&DeviceFunctions::cmdPushConstants, "vkCmdPushConstants"); + const auto dispatch = context.Resolved(&DeviceFunctions::cmdDispatch, "vkCmdDispatch"); + for (const auto& rect : rects) { + const RectPush push{{static_cast(rect.offset.x), static_cast(rect.offset.y)}, {rect.extent.width, rect.extent.height}}; + pushConstants(commands, layout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(push), &push); + dispatch(commands, (rect.extent.width + 7u) / 8u, (rect.extent.height + 7u) / 8u, 1); + } + } + +private: + static VkWriteDescriptorSet imageWrite(std::uint32_t binding, VkDescriptorType type, const VkDescriptorImageInfo& info) { + VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}; + write.dstBinding = binding; + write.descriptorCount = 1; + write.descriptorType = type; + write.pImageInfo = &info; + return write; + } + + void release() noexcept { + context.Function("vkDestroyPipeline")(context.device, pipeline, nullptr); + context.Function("vkDestroyPipelineLayout")(context.device, layout, nullptr); + context.Function("vkDestroyDescriptorSetLayout")(context.device, setLayout, nullptr); + pipeline = VK_NULL_HANDLE; + layout = VK_NULL_HANDLE; + setLayout = VK_NULL_HANDLE; + } + + Context context; + PFN_vkCmdPushDescriptorSetKHR pushSet = nullptr; + VkDescriptorSetLayout setLayout = VK_NULL_HANDLE; + VkPipelineLayout layout = VK_NULL_HANDLE; + VkPipeline pipeline = VK_NULL_HANDLE; +}; + +std::mutex& depthToStoragePassesMutex() { + static std::mutex mutex; + return mutex; +} + +std::vector>& depthToStoragePasses() { + static auto* list = new std::vector>(); + return *list; +} + +DepthToStoragePass& depthToStoragePassFor(const Context& context) { + std::lock_guard lock(depthToStoragePassesMutex()); + auto& list = depthToStoragePasses(); + for (const auto& pass : list) { + if (pass->Device() == context.device) return *pass; + } + list.push_back(std::make_unique(context)); + return *list.back(); +} + struct DepthGuard { VkDevice device; std::uint64_t address; @@ -84,7 +202,7 @@ struct DepthGuard { }; std::shared_mutex& depthGuardMutex() { - static std::shared_mutex mutex; + static PrimedSharedMutex mutex; return mutex; } @@ -100,6 +218,8 @@ std::atomic& anyDepthGuard() { struct DepthSamplingState { std::vector initial; + std::optional initialDepth; + std::optional initialStencil; std::string refusal; std::optional aliasStencil; std::optional unprovenStencil; @@ -152,6 +272,68 @@ std::uint64_t fillNoteAfter(const Recorder& recorder, const GuestMemory::Unchang return found; } +bool uniformPlaneTarget(const DepthTarget& target) { + return target.format == VK_FORMAT_D32_SFLOAT_S8_UINT && target.stencilAddress != 0u && target.htileAddress == 0u; +} + +template +std::optional uniformPlaneValue(std::uint64_t address, std::size_t bytes) { + std::array chunk{}; + std::optional value; + for (std::size_t offset = 0u; offset < bytes;) { + const auto count = std::min(chunk.size(), (bytes - offset) / sizeof(Element)); + const auto elements = std::span(chunk).first(count); + GuestMemory::Read(address + offset, std::as_writable_bytes(elements)); + if (!value) value = elements.front(); + if (std::ranges::any_of(elements, [&](Element element) { return element != *value; })) return std::nullopt; + offset += count * sizeof(Element); + } + return value; +} + +DepthSamplingState prepareUniformPlaneSampling(const Context& context, const DepthTarget& target, DepthSamplingState state) { + const auto refuse = [&](const char* reason) { + state.refusal = reason; + return state; + }; + if (target.depthSwizzle != 24u || target.firstSlice != 0u || target.lastSlice != 0u) return refuse("native multisample depth arrays or non-Z_X layout are not implemented"); + if ((target.pipeBankXor & ~0xcf00u) != 0u) return refuse("unsupported native multisample depth XOR"); + if (target.address == 0u || (target.address & 65535u) != 0u || (target.stencilAddress & 65535u) != 0u) return refuse("native multisample depth base is not aligned"); + if (target.htileStencil) return refuse("native multisample stencil import is not implemented"); + state.initial = {{target.address, static_cast(MultisampledDepthBytes_nid_no_patch(target.extent, 4u, target.samples)), 0u}, {target.stencilAddress, static_cast(MultisampledDepthBytes_nid_no_patch(target.extent, 1u, target.samples)), 0u}}; + for (const auto& range : state.initial) { + if (range.address > std::numeric_limits::max() - range.bytes) return refuse("native depth import range overflows"); + if (!GuestMemory::Accessible(reinterpret_cast(range.address), range.bytes) || !GuestMemory::Watched(range.address, range.bytes)) return refuse("native multisample depth import requires watched accessible guest memory"); + } + const auto& depth = state.initial[0]; + const auto& stencil = state.initial[1]; + if (depth.address + depth.bytes > stencil.address && stencil.address + stencil.bytes > depth.address) return refuse("native depth and stencil initial ranges overlap"); + for (std::size_t index = 0u; index < state.initial.size(); ++index) { + const auto alias = MultisampleColorTargetOver(state.initial[index].address, state.initial[index].bytes); + if (!alias) continue; + reportAlias(*alias, state.initial[index], index == 1u ? "stencil plane" : "depth"); + return refuse(index == 1u ? stencilAliasRefusal : depthAliasRefusal); + } + for (auto& range : state.initial) { + if (auto* recorder = RecorderFor(context)) recorder->SyncThrough(range.address, range.bytes); + StorageTexture::FlushPending(range.address, range.bytes); + GuestMemory::FlushGpuWrites(range.address, range.bytes); + if (!GuestMemory::Accessible(reinterpret_cast(range.address), range.bytes) || !GuestMemory::Watched(range.address, range.bytes)) return refuse("native multisample depth import requires watched accessible guest memory"); + range.generation = GuestMemory::CollectWritesUncached(range.address, range.bytes); + if (range.generation == 0u) return refuse("native multisample depth import requires tracked generations"); + } + const auto depthWord = uniformPlaneValue(depth.address, depth.bytes); + const auto depthValue = depthWord ? std::bit_cast(*depthWord) : 0.0f; + if (!depthWord || !(depthValue >= 0.0f && depthValue <= 1.0f)) return refuse("native multisample depth plane without HTILE does not hold one depth value in [0, 1]"); + const auto stencilByte = uniformPlaneValue(stencil.address, stencil.bytes); + if (!stencilByte) return refuse("native multisample stencil plane without HTILE does not hold one stencil value"); + for (const auto& range : state.initial) GuestMemory::CollectWritesUncached(range.address, range.bytes); + if (!GuestMemory::UnchangedSinceAll(state.initial)) return refuse("guest writes raced native multisample depth import"); + state.initialDepth = depthValue; + state.initialStencil = *stencilByte; + return state; +} + DepthSamplingState prepareDepthSampling(const Context& context, const DepthTarget& target) { DepthSamplingState state; const auto refuse = [&](const char* reason) { @@ -159,6 +341,7 @@ DepthSamplingState prepareDepthSampling(const Context& context, const DepthTarge return state; }; if (target.samples != 2u && target.samples != 4u && target.samples != 8u) return refuse("unsupported native depth sample count"); + if (uniformPlaneTarget(target)) return prepareUniformPlaneSampling(context, target, std::move(state)); const bool stencil = target.format == VK_FORMAT_D32_SFLOAT_S8_UINT; const bool stencilPlane = stencil && !target.htileStencil && target.htileDepthOnly; if (target.format != VK_FORMAT_D16_UNORM && target.format != VK_FORMAT_D32_SFLOAT && !stencil) return refuse("native multisample stencil or depth format import is not implemented"); @@ -337,6 +520,10 @@ std::optional decodeOverwrite(const StorageImageClear& clea return overwrite; } +class DepthSurface; +std::pair staleStencilRange(const DepthSurface& surface); +void noteStalePlane(std::uint64_t begin, std::uint64_t end); + class DepthSurface { public: DepthSurface(const Context& context, const DepthTarget& target, DepthSamplingState sampling = {}) : context(context), target(target), hostFormat(HostDepthFormat(context, target.format)), sampling(std::move(sampling)) { @@ -410,7 +597,7 @@ public: toGeneral.subresourceRange = viewInfo.subresourceRange; const auto barrier = context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier"); barrier(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &toGeneral); - const VkClearDepthStencilValue clear{target.clearDepth, this->sampling.aliasStencil.value_or(target.clearStencil)}; + const VkClearDepthStencilValue clear{this->sampling.initialDepth.value_or(target.clearDepth), this->sampling.initialStencil.value_or(this->sampling.aliasStencil.value_or(target.clearStencil))}; context.Function("vkCmdClearDepthStencilImage")(commands, image, VK_IMAGE_LAYOUT_GENERAL, &clear, 1, &toGeneral.subresourceRange); RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT); if (batch) batch->SubmitAndWait(); @@ -449,8 +636,9 @@ public: if (!alias || (index == 2u && sampling.unprovenStencil && stencilClearGeneration == 0u)) continue; const auto value = index == 2u && separatePlane ? aliasedStencil(*alias, range, target.samples) : std::nullopt; const bool kept = value.has_value() && (stencilClearGeneration != 0u ? *value == *stencilClearValue : value == sampling.aliasStencil); - if (!kept) reportAlias(*alias, range, index == 2u ? "stencil plane" : index == 1u ? "HTILE" : "depth"); - Require(kept, index == 2u ? stencilAliasRefusal : depthAliasRefusal); + const bool stencilRange = index == 2u || (index == 1u && uniformPlaneTarget(target)); + if (!kept) reportAlias(*alias, range, stencilRange ? "stencil plane" : index == 1u ? "HTILE" : "depth"); + Require(kept, stencilRange ? stencilAliasRefusal : depthAliasRefusal); planeAliased = true; } if (stencilClearGeneration != 0u) { @@ -507,6 +695,10 @@ public: const bool identity = stencil ? resource.format == 5u : resource.baseAddress == target.address && resource.format == (target.format == VK_FORMAT_D16_UNORM ? 7u : 22u); Require(identity && resource.width == target.extent.width && resource.height == target.extent.height && resource.samples == target.samples, "native multisample depth sampled identity or format mismatch"); Require(resource.dimension == TextureDimension::k2DMsaa && resource.tileMode == TextureTileMode::kZ64KBX && resource.depthOrLastArray == 0u && resource.baseArray == 0u && resource.baseLevel == 0u && resource.lastLevel == 0u && resource.mipCount == 1u && resource.allocatedMipCount == 1u && resource.minLod == 0u, "native multisample depth sampled array, mip or layout is not implemented"); + if (uniformPlaneTarget(target)) { + Require(resource.pipeBankXor == target.pipeBankXor && resource.dccAddress == 0u && !resource.dccAlphaOnMsb, "native multisample depth sampled metadata or XOR mismatch"); + return; + } const bool htile = resource.dccAddress == target.htileAddress && resource.dccPipeAligned == target.htilePipeAligned; const bool bare = resource.dccAddress == 0u && !resource.dccPipeAligned; if (stencil) { @@ -629,21 +821,48 @@ public: return texture; } + std::shared_ptr FirstSample(const Texture& source, VkComponentMapping components) { + auto* recorder = RecorderFor(context); + Require(recorder != nullptr, "a native multisampled depth target is sampled only by recorded draws and dispatches"); + const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT; + const auto format = d16 ? VK_FORMAT_R16_UNORM : VK_FORMAT_R32_SFLOAT; + if (firstSample == nullptr) firstSample = std::make_shared(context, format, target.extent); + firstSample->copy.Record(*recorder, source.View(), source.Layout()); + const std::array key{static_cast(components.r), static_cast(components.g), static_cast(components.b), static_cast(components.a)}; + auto& texture = firstSample->textures[key]; + if (texture == nullptr) { + texture = std::make_shared(context, firstSample->copy.Image(), format, VK_IMAGE_ASPECT_COLOR_BIT, components); + texture->MarkRefreshedPerUse(); + } + return {firstSample, texture.get()}; + } + void ForgetClear(VkImageAspectFlags aspects) { pendingClear &= ~aspects; if ((aspects & VK_IMAGE_ASPECT_DEPTH_BIT) != 0) depthFill.reset(); if ((aspects & VK_IMAGE_ASPECT_STENCIL_BIT) != 0) stencilFill.reset(); } + void MarkStencilStale() { + if (!StencilMirrorable()) return; + stencilStale = true; + const auto [begin, end] = staleStencilRange(*this); + noteStalePlane(begin, end); + } + void NoteHtileClear(VkImageAspectFlags aspects) { ForgetClear(aspects); pendingClear |= aspects; + if ((aspects & VK_IMAGE_ASPECT_STENCIL_BIT) != 0u) MarkStencilStale(); } void NotePlaneFill(VkImageAspectFlagBits aspect, float depth, std::uint8_t stencil) { pendingClear |= aspect; if (aspect == VK_IMAGE_ASPECT_DEPTH_BIT) depthFill = depth; - else stencilFill = stencil; + else { + stencilFill = stencil; + MarkStencilStale(); + } } void CopyAspectFrom(const DepthSurface& source, VkImageAspectFlagBits aspect) { @@ -861,6 +1080,7 @@ public: auto& plane = stencil ? stencilPlane : depthPlane; const auto source = std::const_pointer_cast(candidate.shared_from_this()); copyPlane(plane, source, stencil, false); + ForgetClear(stencil ? VK_IMAGE_ASPECT_STENCIL_BIT : VK_IMAGE_ASPECT_DEPTH_BIT); written = ++depthWrites; NoteHostWrite(); return true; @@ -1214,11 +1434,18 @@ private: bool filled = false; }; std::map, std::shared_ptr> textures; + struct FirstSampleTextures { + FirstSampleTextures(const Context& context, VkFormat format, VkExtent2D extent) : copy(context, format, extent) {} + FirstSampleCopy copy; + std::map, std::shared_ptr> textures; + }; + std::shared_ptr firstSample; PlaneStorage depthPlane; PlaneStorage stencilPlane; std::uint64_t depthMemory = 0; std::uint64_t stencilMemory = 0; StencilCopy stencilCopy; + VkImageView transferView = VK_NULL_HANDLE; void createSampleShadow() { VkImage image = VK_NULL_HANDLE; @@ -1407,7 +1634,8 @@ private: const auto bytes = static_cast(target.extent.width) * target.extent.height * texelBytes; const auto aspect = stencil ? VK_IMAGE_ASPECT_STENCIL_BIT : VK_IMAGE_ASPECT_DEPTH_BIT; const bool direct = !stencil && DirectDepthCopy(context, hostFormat, aspect, storage->ImageFormat()); - if (!direct && plane.staging == nullptr) plane.staging = std::make_unique(context, bytes, VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT); + const bool passes = toStorage && !stencil && target.samples == 1 && depthToStoragePass(context, hostFormat, storage->ImageFormat()); + if (!direct && !passes && plane.staging == nullptr) plane.staging = std::make_unique(context, bytes, VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT); const auto commands = recorder != nullptr ? recorder->Commands() : batch->Handle(); if (recorder != nullptr) recorder->Keep(storage); const std::uint32_t timing = recorder != nullptr ? recorder->BeginGpuTiming(Recorder::CommandClass::Copy) : 0u; @@ -1428,6 +1656,22 @@ private: moved += static_cast(rect.extent.width) * rect.extent.height * texelBytes; } } + if (passes) { + const std::vector whole{{{0, 0}, target.extent}}; + const auto& areas = rects != nullptr ? *rects : whole; + const auto texels = storage->AtomicView(0, storage->Descriptor().dimension == TextureDimension::k2DArray); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT); + depthToStoragePassFor(context).Record(commands, depthTransferView(), texels, areas); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT); + depthTransferPassCount.fetch_add(1, std::memory_order_relaxed); + if (batch) { + batch->SubmitAndWait(); + return; + } + Recorder::CountBarriers(Recorder::CommandClass::Copy, 2); + recorder->EndGpuTiming(timing, moved); + return; + } auto colorRegions = planeRegions; for (auto& region : colorRegions) region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; const auto count = static_cast(planeRegions.size()); @@ -1465,6 +1709,17 @@ private: recorder->EndGpuTiming(timing, moved); } + VkImageView depthTransferView() { + if (transferView != VK_NULL_HANDLE) return transferView; + VkImageViewCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + info.image = image; + info.viewType = VK_IMAGE_VIEW_TYPE_2D; + info.format = hostFormat; + info.subresourceRange = {VK_IMAGE_ASPECT_DEPTH_BIT, 0, 1, 0, 1}; + Check(context.Function("vkCreateImageView")(context.device, &info, nullptr, &transferView), "vkCreateImageView depth to storage"); + return transferView; + } + void release() noexcept { depthPlane = {}; stencilPlane = {}; @@ -1472,6 +1727,8 @@ private: mirrorPlane = {}; stencilMirror = {}; textures.clear(); + if (transferView) context.Function("vkDestroyImageView")(context.device, transferView, nullptr); + transferView = VK_NULL_HANDLE; if (view) context.Function("vkDestroyImageView")(context.device, view, nullptr); if (owned != nullptr) owned.reset(); else { @@ -2436,6 +2693,10 @@ float HostDepthBiasConstant(const Context& context, VkFormat format, float const } VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target, bool depthWritten, bool stencilTested) { + return DepthSurfaceViewWithStencilWriteIntent(context, target, depthWritten, stencilTested, true); +} + +VkImageView DepthSurfaceViewWithStencilWriteIntent(const Context& context, const DepthTarget& target, bool depthWritten, bool stencilTested, bool stencilWritten) { const auto planeBytes = [&](std::uint32_t bytesPerTexel) { return target.samples == 1u ? DepthSliceBytes(target.extent, bytesPerTexel) : MultisampledDepthBytes_nid_no_patch(target.extent, bytesPerTexel, target.samples); }; ClaimMultisampleTargetMemory(target.address, planeBytes(target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT ? 2u : 4u)); if (target.stencilAddress != 0u) ClaimMultisampleTargetMemory(target.stencilAddress, planeBytes(1u)); @@ -2486,6 +2747,8 @@ VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target, const bool inherits = owner != depthOwners().end() && owner->second.surface->active->load(std::memory_order_acquire) && !owner->second.surface->retired; surface->clearDepth = target.clearDepth; surface->clearStencil = target.clearStencil; + const auto stencilOwner = stencilOwners().find({context.device, target.stencilAddress}); + const bool stencilRebound = stencilOwner == stencilOwners().end() || stencilOwner->second.surface != surface; takePlanes(*surface, created, htileCleared | (cleared ? VK_IMAGE_ASPECT_DEPTH_BIT : 0u)); if (cleared || htileCleared != 0u) surface->expanded = false; if (created && !inherits && target.htileAddress == 0) surface->TakeStorage(); @@ -2518,11 +2781,7 @@ VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target, surface->MarkAllStale(planeBytes); noteStalePlane(plane, plane + planeBytes); } - if (surface->StencilMirrorable()) { - surface->stencilStale = true; - const auto [begin, end] = staleStencilRange(*surface); - noteStalePlane(begin, end); - } + if (stencilWritten || created || stencilRebound) surface->MarkStencilStale(); drawnDepth = surface; return surface->view; } @@ -2900,6 +3159,10 @@ void ClearDepthSurfaces(VkDevice device) { std::erase_if(surfaces(), [&](const auto& surface) { return surface->context.device == device; }); std::erase_if(planeCopies(), [&](const auto& entry) { return std::get<0>(entry.first) == device; }); std::erase_if(layeredStorages(), [&](const auto& entry) { return std::get<0>(entry.first) == device; }); + { + std::lock_guard passesLock(depthToStoragePassesMutex()); + std::erase_if(depthToStoragePasses(), [&](const auto& pass) { return pass->Device() == device; }); + } std::unique_lock guardLock(depthGuardMutex()); std::erase_if(depthGuards(), [&](const auto& range) { return range.device == device; }); anyDepthGuard().store(!depthGuards().empty(), std::memory_order_release); @@ -2922,6 +3185,16 @@ thread_local bool depthCopySourceActive = false; } +std::shared_ptr DepthSurfaceFirstSample(const Context& context, std::span words, const GuestTextureResource& resource, VkComponentMapping components) { + Require(resource.dimension == TextureDimension::k2DMsaa, "reading sample 0 of a depth surface needs a multisampled descriptor"); + const auto source = DepthSurfaceTexture(context, words, resource, VkComponentMapping{}, VK_IMAGE_VIEW_TYPE_2D); + Require(source != nullptr, "reading sample 0 of a multisampled depth surface whose depth plane is not at the descriptor's address is not implemented"); + std::lock_guard lock(surfacesMutex()); + auto* found = planeSurface(context.device, resource.baseAddress); + Require(found != nullptr && found->target.address == resource.baseAddress, "reading sample 0 of a multisampled depth surface whose depth plane is not at the descriptor's address is not implemented"); + return found->FirstSample(*source, components); +} + std::shared_ptr DepthSurfaceTexture(const Context& context, std::span words, const GuestTextureResource& resource, VkComponentMapping components, VkImageViewType multisampleViewType) { const auto held = [&](const auto& surface) { return surface->active->load(std::memory_order_acquire) && surface->context.device == context.device && !surface->retired && (surface->target.address == resource.baseAddress || (surface->target.stencilAddress != 0 && surface->target.stencilAddress == resource.baseAddress)); @@ -2956,7 +3229,9 @@ std::shared_ptr DepthSurfaceTexture(const Context& context, std::spanSampled(words, resource, components, found == drawnDepth, multisampleViewType, depthCopySourceActive); } - if (resource.width != base.extent.width || resource.height != base.extent.height || resource.baseLevel != 0) return nullptr; + const bool exact = resource.width == base.extent.width && resource.height == base.extent.height; + const bool cropped = !exact && base.gcn && !stencil && found->hostFormat == base.format && (resource.gcnPitch == 0 || resource.gcnPitch == base.extent.width) && resource.width <= base.extent.width && resource.height <= base.extent.height && resource.baseArray == 0 && resource.lastLevel == 0 && resource.dimension == TextureDimension::k2D; + if ((!exact && !cropped) || resource.baseLevel != 0) return nullptr; const bool d16 = base.format == VK_FORMAT_D16_UNORM || base.format == VK_FORMAT_D16_UNORM_S8_UINT; const auto format = ResolveTextureFormat(resource.format); const bool depthBits = !stencil && words.size() >= 4 && ShaderRecompiler::DepthBitsTextureWidth(words[1], words[3]) == (d16 ? 16u : 32u); @@ -2964,9 +3239,9 @@ std::shared_ptr DepthSurfaceTexture(const Context& context, std::span resource.depthOrLastArray)) && !(cube && resource.depthOrLastArray != 0 && (resource.depthOrLastArray + 1u) % 6u != 0u) && (resource.dimension == TextureDimension::k2D || resource.dimension == TextureDimension::k2DArray || cube); + const bool planes = (format == copyFormat || depthBits) && resource.lastLevel == 0 && !(resource.baseArray != 0 && (cube || !layered || resource.baseArray > resource.depthOrLastArray)) && !(cube && resource.depthOrLastArray != 0 && (resource.depthOrLastArray + 1u) % 6u != 0u) && (resource.dimension == TextureDimension::k2D || resource.dimension == TextureDimension::k2DArray || cube); if (!(sampled ? found->SampledAccepts(words, resource) : planes) && (found->OverwrittenInMemory() || found->OverwrittenOnGpu())) { retireStoringStale(*found); return nullptr; @@ -2992,10 +3267,11 @@ std::shared_ptr DepthSurfaceTexture(const Context& context, std::spanimage, slice == drawnDepth ? 0u : slice->written}); } const auto imageFormat = integer ? VK_FORMAT_R16_UINT : d16 ? VK_FORMAT_D16_UNORM : VK_FORMAT_D32_SFLOAT; + const VkExtent2D extent{resource.width, resource.height}; auto& copies = planeCopies(); - const std::tuple copyKey{context.device, base.address, base.extent.width, base.extent.height, imageFormat}; + const std::tuple copyKey{context.device, base.address, extent.width, extent.height, imageFormat}; auto& copy = copies[copyKey]; - if (copy == nullptr || copy->layers < layers) copy = std::make_unique(context, base.extent, imageFormat, layers); + if (copy == nullptr || copy->layers < layers) copy = std::make_unique(context, extent, imageFormat, layers); copy->lastUse = ++planeCopyClock(); copy->lastPresent = Recorder::Presents(); auto texture = copy->Refresh(slices, resource, components, cube || resource.dimension == TextureDimension::k2DArray ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D, layered && !cube ? resource.baseArray : 0u); @@ -3293,4 +3569,12 @@ void SetDirectDepthCopy(bool enabled) { directDepthCopy.store(enabled && std::getenv("APS5_NO_DIRECT_DEPTH_COPY") == nullptr, std::memory_order_relaxed); } +void SetDepthTransferPasses(bool enabled) { + depthTransferPasses.store(enabled && std::getenv("APS5_NO_DEPTH_TRANSFER_PASS") == nullptr, std::memory_order_relaxed); +} + +std::uint64_t DepthTransferPassCount() { + return depthTransferPassCount.load(std::memory_order_relaxed); +} + } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 5d58ac17f4..38e8fb93c2 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -388,8 +388,15 @@ GuestTextureResource ArraySurfaceForTarget(const ColorTarget& color) { namespace { +bool writesStencilTarget(const State& state) { + const auto keeps = [](const VkStencilOpState& face) { + return face.writeMask == 0u || (face.failOp == VK_STENCIL_OP_KEEP && face.passOp == VK_STENCIL_OP_KEEP && face.depthFailOp == VK_STENCIL_OP_KEEP); + }; + return state.stencilTest && (!keeps(state.stencilFront) || !keeps(state.stencilBack)); +} + bool writesDepthTarget(const State& state) { - return state.depthWrite || (state.stencilTest && (state.stencilFront.writeMask != 0u || state.stencilBack.writeMask != 0u)); + return state.depthWrite || writesStencilTarget(state); } std::size_t colorKeyCount(const ColorTarget& color, std::size_t bytes) { @@ -1092,7 +1099,7 @@ std::uint32_t CachedFragmentOutputs(const Context& context, std::span zeroVertexBuffer(const Context& context, std::size_t byt // Draw's validation, index and vertex phases. With `recipe` the fragment outputs, the pipeline // stages and the vertex input layout are the recipe's (derived from the same compiled stages) // instead of computed. -DrawInputs prepareDrawInputs(const Context& context, const State& state, const Pm4::DrawParameters& draw, std::span shaders, DrawOutcome& outcome, DrawTimer& timer, const DrawRecipe* recipe) { +DrawInputs prepareDrawInputs(const Context& context, const State& state, const Pm4::DrawParameters& draw, std::span shaders, DrawOutcome& outcome, DrawTimer& timer, PerformanceTimer& timing, const DrawRecipe* recipe) { DrawInputs inputs; APS5_LOG_OUT_DEBUG("Draw indices=%u instances=%u indexSize=%u flags=%u indexAddress=0x%llx", draw.indexCount, draw.instanceCount, draw.indexSize, draw.flags, static_cast(draw.indexAddress)); APS5_LOG_OUT_DEBUG("State colorTarget=%u render=%ux%u colorAddress=0x%llx colorBytes=%llu colorExtent=%ux%u", state.hasColorTarget ? 1u : 0u, state.renderExtent.width, state.renderExtent.height, static_cast(state.color.address), static_cast(state.color.bytes), state.color.extent.width, state.color.extent.height); @@ -1547,6 +1554,7 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P ValidateDepthBounds(context, state); ValidateSampleMask(context, state, shaders); timer.phase(PhaseValidate); + timing.Mark("validate"); inputs.maxIndex = draw.indexed ? 0u : draw.firstVertex + draw.indexCount - 1u; 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"); @@ -1645,7 +1653,7 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P const bool restartLeft = draw.indirect.has_value(); auto highest = HighestDrawIndex(kept, draw.indexSize, restartLeft); constexpr std::uint32_t ZeroFetchSlack = 0x10000u; - if (const auto zeroFrom = highest ? ZeroFetchVertex(shaders.front().program->vertexAttributes) : std::nullopt) { + if (const auto zeroFrom = highest && !shaders.front().program->vertexAttributes.empty() ? ZeroFetchVertex(shaders.front().program->vertexAttributes) : std::nullopt) { const auto limit = *zeroFrom > draw.firstVertex ? *zeroFrom - draw.firstVertex : 0u; if (*highest >= limit && *highest - limit >= ZeroFetchSlack) highest = ClampDrawIndices(kept, draw.indexSize, restartLeft, limit); } @@ -1685,7 +1693,7 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P return inputs; } constexpr std::uint32_t ZeroFetchSlack = 0x10000u; - if (const auto zeroFrom = state.stages.mesh ? std::nullopt : ZeroFetchVertex(shaders.front().program->vertexAttributes)) { + if (const auto zeroFrom = state.stages.mesh || shaders.front().program->vertexAttributes.empty() ? std::nullopt : ZeroFetchVertex(shaders.front().program->vertexAttributes)) { const auto limit = *zeroFrom > draw.firstVertex ? *zeroFrom - draw.firstVertex : 0u; if (*highest >= limit && *highest - limit >= ZeroFetchSlack) { GuestMemory::FlushGpuWrites(draw.indexAddress, static_cast(indexBytes)); @@ -1719,6 +1727,7 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P } } APS5_LOG_CHARS_OUT_DEBUG("Index validation OK"); + timing.Mark("index_upload"); const auto& attributes = shaders.front().program->vertexAttributes; // Validates the vertex descriptors; the layout also keys and builds the pipeline. if (recipe != nullptr) { @@ -1822,6 +1831,7 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P inputs.vertexOffsets[i] = rangeOffsets[plan.copyOf[j]] + plan.offsets[j]; } timer.phase(PhaseVertex); + timing.Mark("vertex_upload"); return inputs; } @@ -2889,6 +2899,10 @@ void rememberShape(const Context& context, std::uint64_t shape, std::span& image) { return image.first == resident.Image() && image.second; }); +} + 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; @@ -2935,12 +2949,10 @@ bool prepareDraw(const Context& context, const State& state, const Pm4::DrawPara auto& ownWaitedMs = prepared.ownWaitedMs; const auto report = [&](const char* suffix) { reportDrawEnd(state, timer, built, outcome, waitedBefore, ownWaitedMs, suffix); }; auto& inputs = prepared.inputs; - inputs = prepareDrawInputs(context, state, draw, shaders, outcome, timer, nullptr); + inputs = prepareDrawInputs(context, state, draw, shaders, outcome, timer, timing, 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"); const int dumpLimit = prepared.dumpLimit = DumpTargetLimit(); auto& targets = prepared.targets; targets.resize(state.colors.size()); @@ -3065,7 +3077,7 @@ bool prepareDraw(const Context& context, const State& state, const Pm4::DrawPara targetViews.push_back(binding.target->View()); } if (state.depth) { - targetViews.push_back(DepthSurfaceView(context, *state.depth, state.depthWrite, state.stencilTest)); + targetViews.push_back(DepthSurfaceViewWithStencilWriteIntent(context, *state.depth, state.depthWrite, state.stencilTest, writesStencilTarget(state))); if (writesDepthTarget(state)) NoteDepthSurfaceWrite(context, *state.depth); } timer.phase(PhasePrepare); @@ -3075,6 +3087,13 @@ bool prepareDraw(const Context& context, const State& state, const Pm4::DrawPara auto& resolved = prepared.resolved; resolved = resolveDrawResources(context, state, draw, shaders, snapshots, indexBytes, recordable, outcome, timer); auto& resources = resolved.resources; + for (std::size_t index = 0; index < targets.size(); ++index) { + auto& binding = targets[index]; + if (binding.color.partialWriteMask == 0 || binding.multisampled || binding.resident == nullptr || binding.proxied || storesIntoResident(*resources, *binding.resident)) continue; + const auto& color = binding.color; + binding.resident = refreshResidentTarget(context, state, color, outcome, profile, [&] { return color.arrayLastSlice != 0 ? CachedStorageLayers(context, ArraySurfaceForTarget(color), color.arraySlice, color.layers) : CachedStorageSurface(context, SurfaceForTarget(color)); }); + if (binding.resident != nullptr) targetViews[index] = binding.resident->AttachmentView(color.format, color.mip, color.depthSlice, color.layers, color.arraySlice); + } if (!stencilReferences.empty() && resources->ChecksFaults()) recordable = false; if (!stencilReferences.empty()) { const auto readImages = resources->StorageImages(); @@ -3224,8 +3243,7 @@ bool prepareDraw(const Context& context, const State& state, const Pm4::DrawPara for (const auto& binding : targets) { if (binding.color.partialWriteMask == 0 || binding.multisampled) continue; Require(lean && binding.resident != nullptr && !binding.proxied, "a partial write of a packed color target is merged into its resident image by a recorded draw only"); - const auto images = resources->StorageImages(); - Require(std::ranges::any_of(images, [&](const std::pair& image) { return image.first == binding.resident->Image() && image.second; }), "the partial write of a packed color target does not store into the target's resident image"); + Require(storesIntoResident(*resources, *binding.resident), "the partial write of a packed color target does not store into the target's resident image"); } if (!lean && !resolved.moved.empty()) { resolved.resources = std::make_shared(context, shaders, state.color, static_cast(state.colors.size()), draw.indexAddress, static_cast(indexBytes), snapshots); @@ -3805,7 +3823,7 @@ DrawRecipeOutcome DrawWithRecipe(const Context& context, const State& state, con 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"); - auto inputs = prepareDrawInputs(context, state, draw, shaders, outcome, timer, &recipe); + auto inputs = prepareDrawInputs(context, state, draw, shaders, outcome, timer, timing, &recipe); if (inputs.nothing) { result.recorded = true; return result; @@ -3834,7 +3852,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, state.stencilTest); + const auto depthView = DepthSurfaceViewWithStencilWriteIntent(context, *state.depth, state.depthWrite, state.stencilTest, writesStencilTarget(state)); 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 f1d5bece70..c4a70179f5 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -32,8 +32,20 @@ struct udmabuf_create { std::uint64_t offset; std::uint64_t size; }; +struct udmabuf_create_item { + std::uint32_t memfd; + std::uint32_t pad; + std::uint64_t offset; + std::uint64_t size; +}; +struct udmabuf_create_list { + std::uint32_t flags; + std::uint32_t count; + udmabuf_create_item list[]; +}; constexpr std::uint32_t UDMABUF_FLAGS_CLOEXEC = 0x01; constexpr unsigned long UDMABUF_CREATE = 0; +constexpr unsigned long UDMABUF_CREATE_LIST = 0; #else #include #endif @@ -1499,7 +1511,7 @@ ImportPages importPages(std::uint64_t base, std::uint64_t bytes, bool opening = } const auto protection = info.Protect & 0xffu; const bool pageWritable = (info.Type == MEM_PRIVATE || info.Type == MEM_IMAGE) && writableProtection(protection); - const bool pageReadOnly = info.Type != MEM_MAPPED && (protection == PAGE_READONLY || protection == PAGE_EXECUTE_READ); + const bool pageReadOnly = (info.Type != MEM_MAPPED || (info.AllocationProtect & 0xffu) == PAGE_READONLY) && (protection == PAGE_READONLY || protection == PAGE_EXECUTE_READ); if (!pageReadOnly) pages.readOnly = false; if (info.Type != MEM_MAPPED) pages.shared = false; if (info.Type != MEM_MAPPED && !pageWritable && !pageReadOnly && pages.refused.BaseAddress == nullptr) pages.refused = info; @@ -1907,6 +1919,12 @@ std::optional hostHeapIndex(const VkPhysicalDeviceMemoryPropertie for (std::uint32_t index = 0; index < memory.memoryHeapCount; ++index) { if ((memory.memoryHeaps[index].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) == 0 && (!heap || memory.memoryHeaps[index].size > memory.memoryHeaps[*heap].size)) heap = index; } + if (heap) return heap; + constexpr VkMemoryPropertyFlags shared = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; + for (std::uint32_t index = 0; index < std::min(memory.memoryTypeCount, VK_MAX_MEMORY_TYPES); ++index) { + const auto candidate = memory.memoryTypes[index].heapIndex; + if ((memory.memoryTypes[index].propertyFlags & shared) == shared && candidate < memory.memoryHeapCount && (!heap || memory.memoryHeaps[candidate].size > memory.memoryHeaps[*heap].size)) heap = candidate; + } return heap; } @@ -3681,7 +3699,7 @@ bool WaitForLeasesAndImports(std::uintptr_t address, std::size_t bytes) { const auto end = static_cast(address) + bytes; auto& imports = Imports(); if (releaseParkedImports(imports, address, end)) retiredImport = true; - const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); + const auto lease = GuestAllocations::GuestAllocationsAcquireRegistered_nid_postfix(); { std::lock_guard importsLock(imports.mutex); for (auto it = imports.imports.begin(); it != imports.imports.end();) { @@ -3746,7 +3764,7 @@ bool WaitForLeasesAndImports(std::uintptr_t address, std::size_t bytes) { synced = true; drained = true; } - const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); + const auto lease = GuestAllocations::GuestAllocationsAcquireRegistered_nid_postfix(); { std::lock_guard importsLock(imports.mutex); for (auto it = imports.imports.begin(); it != imports.imports.end();) { @@ -4524,7 +4542,7 @@ bool ImportMappedRanges(const Context& context, const GuestAllocations::Mapped& bool RegisteredReadableCovers(std::uint64_t address, std::size_t bytes, std::vector* pieces) { if (pieces != nullptr) pieces->clear(); if (bytes == 0 || bytes > std::numeric_limits::max() - address) return false; - const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); + const auto lease = GuestAllocations::GuestAllocationsAcquireRegistered_nid_postfix(); const auto end = address + bytes; for (auto at = address; at < end;) { const auto* range = containingRange(lease, at, at + 1); @@ -4537,7 +4555,7 @@ bool RegisteredReadableCovers(std::uint64_t address, std::size_t bytes, std::vec std::uint64_t RegisteredReadableEnd(std::uint64_t address) { if (address == std::numeric_limits::max()) return 0; - const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); + const auto lease = GuestAllocations::GuestAllocationsAcquireRegistered_nid_postfix(); const auto* range = containingRange(lease, address, address + 1); return range != nullptr ? range->address + range->bytes : 0; } @@ -5325,8 +5343,13 @@ std::uint64_t gpuCopyLimit() { return limit; } -VkBufferUsageFlags gpuCopyUsage(bool addressable) { - return VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | (addressable ? VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT : 0u); +bool changedCopyBacks(const Context& context) { + static const bool disabled = std::getenv("APS5_NO_CHANGED_COPY_BACK") != nullptr; + return !disabled && context.bufferDeviceAddress; +} + +VkBufferUsageFlags gpuCopyUsage(const Context& context, bool addressable) { + return VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | (addressable || changedCopyBacks(context) ? VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT : 0u); } // Device-local staging. A written V# element bound in place makes the shader store into host @@ -5345,10 +5368,11 @@ VkBufferUsageFlags gpuCopyUsage(bool addressable) { // (resolveOverlays), and AddressRanges cuts the import's range around it so that its pointers reach // the shadow as its descriptor does. The // shadow is per use, not resident: a reused build records its copy-in anew (RecordStagingCopies). -// Accepted trade-off, as for the misaligned copies above: a CPU write into a staged range between -// the copy-in and the copy-back is rolled back by the copy-back (the whole written element range, -// since store extents are not known), for the batch's latency; the staging window keeps the -// exposure to the elements named on the [buffers] staging line and APS5_TRACE_STAGING. +// Accepted trade-off, as for the misaligned copies above: the copy-back stores only the bytes the +// shader changed where the import still holds the copy-in (RecordChangedCopy), but a CPU write +// between the copy-in and the copy-back is rolled back at the unaligned ends of a range, in a region +// that is not 4-byte aligned and without a shadow copy; the staging window keeps the exposure to +// the elements named on the [buffers] staging line and APS5_TRACE_STAGING. // APS5_NO_WRITTEN_SHADOW=1 and APS5_NO_ATOMIC_STAGING=1 bind those elements in place as before; // APS5_WRITTEN_SHADOW_MIN_KIB / APS5_WRITTEN_SHADOW_MAX_KIB (16 / 2048, 8192 on a discrete GPU) bound // the written window: on an integrated GPU a kernel streaming once through a large buffer (the @@ -5432,7 +5456,11 @@ CopyStats& Copies() { // region takes the path it would take without staging (Region::unstaged). std::shared_ptr stagingBuffer(const Context& context, std::size_t bytes, VkBufferUsageFlags usage) { try { - return std::make_shared(context, bytes, usage, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + if (auto buffer = Buffer::WithinDeviceLocalHeadroom(context, bytes, usage)) return buffer; + static std::atomic reported{0}; + if (reported.fetch_add(1, std::memory_order_relaxed) < 4) std::fprintf(stderr, "[buffers] staging shadow of %zu bytes refused: its device-local heap is within an eighth of its budget\n", bytes); + Copies().stagingRefused.fetch_add(1, std::memory_order_relaxed); + return nullptr; } catch (const std::exception& error) { static std::atomic reported{0}; if (reported.fetch_add(1, std::memory_order_relaxed) < 4) std::fprintf(stderr, "[buffers] staging shadow of %zu bytes refused: %s\n", bytes, error.what()); @@ -5725,7 +5753,7 @@ void GuestBufferMemory::UploadPrepare(bool addressable) { const bool misaligned = !bindableInPlace(HostImportOffset(*entry, region.begin), addressable); if (staged) { const auto allocateStart = std::chrono::steady_clock::now(); - region.buffer = stagingBuffer(context, static_cast(region.end - region.begin), gpuCopyUsage(addressable)); + region.buffer = stagingBuffer(context, static_cast(region.end - region.begin), gpuCopyUsage(context, addressable)); if (profile) allocateUs.fetch_add(microsecondsSince(allocateStart), std::memory_order_relaxed); if (region.buffer == nullptr) { // No shadow to be had: in place when aligned, else the host copy below. @@ -5737,7 +5765,7 @@ void GuestBufferMemory::UploadPrepare(bool addressable) { if (!staged) { if (!gpuCopyEligible(region)) continue; const auto allocateStart = std::chrono::steady_clock::now(); - region.buffer = std::make_shared(context, static_cast(region.end - region.begin), gpuCopyUsage(addressable), copyBufferProperties(false)); + region.buffer = std::make_shared(context, static_cast(region.end - region.begin), gpuCopyUsage(context, addressable), copyBufferProperties(false)); if (profile) allocateUs.fetch_add(microsecondsSince(allocateStart), std::memory_order_relaxed); } region.deviceLocal = staged; @@ -5957,7 +5985,9 @@ void GuestBufferMemory::UploadFinish(bool addressable, std::optionalbuffer; - region.copySourceBase = entry->base - entry->lead; + const auto start = HostImportBufferStart(*entry); + region.copySourceBase = start.guest; + region.copySourceAddress = start.device; } } if (region.direct != nullptr) { @@ -6157,12 +6187,12 @@ void GuestBufferMemory::recordGpuCopies(std::span copies, bool ad if (region->buffer == nullptr) { // Not made by UploadPrepare (the registry was stale then): made here, under the lock. const auto allocateStart = std::chrono::steady_clock::now(); - if (region->deviceLocal) region->buffer = stagingBuffer(context, static_cast(bytes), gpuCopyUsage(addressable)); + if (region->deviceLocal) region->buffer = stagingBuffer(context, static_cast(bytes), gpuCopyUsage(context, addressable)); if (region->buffer == nullptr) { // Or no shadow to be had: a host copy serves the region instead, aligned or not. region->deviceLocal = false; region->unstaged = true; - region->buffer = std::make_shared(context, static_cast(bytes), gpuCopyUsage(addressable), copyBufferProperties(false)); + region->buffer = std::make_shared(context, static_cast(bytes), gpuCopyUsage(context, addressable), copyBufferProperties(false)); } if (profile) allocateUs.fetch_add(microsecondsSince(allocateStart), std::memory_order_relaxed); } @@ -6225,6 +6255,28 @@ void GuestBufferMemory::recordGpuCopies(std::span copies, bool ad } } } + std::vector shadowed; + for (auto* region : copies) { + const auto bytes = static_cast(region->end - region->begin); + if (!changedCopyBacks(context) || region->buffer->DeviceAddress() == 0 || !WritesOverlap(region->begin, bytes)) { + region->shadowCopy = nullptr; + continue; + } + if (region->shadowCopy == nullptr) region->shadowCopy = stagingBuffer(context, bytes, gpuCopyUsage(context, true)); + if (region->shadowCopy == nullptr || region->shadowCopy->DeviceAddress() == 0) { + region->shadowCopy = nullptr; + continue; + } + shadowed.push_back(region); + } + if (!shadowed.empty()) { + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); + Recorder::CountBarriers(Recorder::CommandClass::StagingIn); + for (auto* region : shadowed) { + CopyBuffer(context, commands, region->buffer->Handle(), 0, region->shadowCopy->Handle(), 0, region->end - region->begin); + recorder->Keep(region->shadowCopy); + } + } if (stagedAny) reportStaging(); // The copied bytes are visible to the shaders bound to the buffers, which may also store into // them. Every stage: dispatches and draws share this path, and draws run mesh, task and @@ -6283,7 +6335,7 @@ void GuestBufferMemory::RecordCopyBacks(Recorder& recorder) { commands = recorder.Commands(&covered); timing = recorder.BeginGpuTiming(StagingCopyBackKey); // The shader's stores into the buffer (whichever stage made them) precede the copy. - constexpr BarrierMask leading{VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT}; + constexpr BarrierMask leading{VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_SHADER_READ_BIT}; if (!Recorder::ElideCovered(Recorder::CommandClass::StagingOut, leading, covered)) { RecordMemoryBarrier(context, commands, leading.srcStage, leading.dstStage, leading.srcAccess, leading.dstAccess); Recorder::CountBarriers(Recorder::CommandClass::StagingOut); @@ -6292,7 +6344,34 @@ void GuestBufferMemory::RecordCopyBacks(Recorder& recorder) { } // Exactly the sub-range the shader may write, in place in the import: what the shader // would have stored there itself had the range been bindable. - CopyBuffer(context, commands, region.buffer->Handle(), from - region.begin, region.copySource, from - region.copySourceBase, to - from); + auto whole = from; + auto wholeEnd = to; + if (region.shadowCopy != nullptr && region.copySourceAddress != 0 && region.begin % 4u == 0 && region.copySourceBase % 4u == 0) { + const auto alignedFrom = (from + 3u) & ~std::uint64_t{3}; + const auto alignedTo = to & ~std::uint64_t{3}; + static const bool traceDivergence = std::getenv("APS5_TRACE_COPY_BACK") != nullptr; + std::shared_ptr counter; + if (traceDivergence && alignedFrom < alignedTo) { + counter = std::make_shared(context, 16, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT); + std::memset(counter->Bytes().data(), 0, 16); + } + if (alignedFrom < alignedTo && recorder.RecordChangedCopy(commands, region.buffer->DeviceAddress() + (alignedFrom - region.begin), region.shadowCopy->DeviceAddress() + (alignedFrom - region.begin), region.copySourceAddress + (alignedFrom - region.copySourceBase), static_cast((alignedTo - alignedFrom) / 4u), counter != nullptr ? counter->DeviceAddress() : 0)) { + if (counter != nullptr) { + recorder.Keep(counter); + recorder.OnComplete([counter, alignedFrom, alignedTo, regionBegin = region.begin, regionEnd = region.end] { + std::uint32_t diverged = 0; + std::memcpy(&diverged, counter->Bytes().data(), sizeof(diverged)); + static const bool alwaysCopy = std::getenv("APS5_CHANGED_COPY_ALWAYS") != nullptr; + if (diverged != 0) std::fprintf(stderr, "[copy-back] 0x%llx+0x%llx (region 0x%llx+0x%llx): %u dwords where the shader and the CPU changed the same bytes; %s\n", static_cast(alignedFrom), static_cast(alignedTo - alignedFrom), static_cast(regionBegin), static_cast(regionEnd - regionBegin), diverged, alwaysCopy ? "the shader's bytes were stored (APS5_CHANGED_COPY_ALWAYS)" : "the CPU's bytes were kept"); + }); + } + if (from < alignedFrom) CopyBuffer(context, commands, region.buffer->Handle(), from - region.begin, region.copySource, from - region.copySourceBase, alignedFrom - from); + if (alignedTo < to) CopyBuffer(context, commands, region.buffer->Handle(), alignedTo - region.begin, region.copySource, alignedTo - region.copySourceBase, to - alignedTo); + whole = wholeEnd; + recorder.Keep(region.shadowCopy); + } + } + if (whole < wholeEnd) CopyBuffer(context, commands, region.buffer->Handle(), whole - region.begin, region.copySource, whole - region.copySourceBase, wholeEnd - whole); copiedBytes += to - from; recorder.Keep(region.buffer); if (profile) { @@ -6312,7 +6391,7 @@ void GuestBufferMemory::RecordCopyBacks(Recorder& recorder) { // indirect dispatch's arguments) and to the host once the batch completed; the caller's // pending-write note makes CPU readers wait for that. constexpr VkAccessFlags copiedAccess = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_INDIRECT_COMMAND_READ_BIT | VK_ACCESS_HOST_READ_BIT; - RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, copiedAccess); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_SHADER_WRITE_BIT, copiedAccess); Recorder::CountBarriers(Recorder::CommandClass::StagingOut); recorder.MarkCovered(copiedAccess); recorder.EndGpuTiming(timing, copiedBytes); @@ -6588,10 +6667,12 @@ void GuestBufferMemory::MarkDirectWrites() const { namespace { -void storeChanged(std::uint64_t address, std::span current, std::span original) { +using ChangedStore = void (*)(std::uint64_t, std::span, std::span); + +void storeChanged(std::uint64_t address, std::span current, std::span original, ChangedStore store) { const auto writable = GuestMemory::DescribeCommitted(address, current.size(), true); if (writable.whole) { - GuestMemory::WriteChanged(address, current, original); + store(address, current, original); return; } const auto unchanged = [&](std::uint64_t from, std::uint64_t to) { @@ -6606,7 +6687,7 @@ void storeChanged(std::uint64_t address, std::span current, std if (cursor < first) unchanged(cursor, first); const auto at = static_cast(first - address); const auto length = static_cast(last - first); - GuestMemory::WriteChanged(first, current.subspan(at, length), original.subspan(at, length)); + store(first, current.subspan(at, length), original.subspan(at, length)); cursor = last; } if (cursor < address + current.size()) unchanged(cursor, address + current.size()); @@ -6676,7 +6757,8 @@ void GuestBufferMemory::WriteBack() { Require(uploaded && !committed, "guest memory cannot be committed twice or before upload"); const auto merged = mergeWrites(writes); // The CPU keeps running while the GPU works, so storing whole ranges would roll back its writes to - // bytes the shader never touched. Only runs that differ from the uploaded copy are stored. + // bytes the shader never touched. Only runs that differ from the uploaded copy are stored, and a + // host copy leaves the bytes the CPU changed since its upload as they are. for (const auto& [begin, end] : merged) { const auto* found = owner(begin, end); Require(found != nullptr, "write-back range has no GPU owner"); @@ -6704,7 +6786,7 @@ void GuestBufferMemory::WriteBack() { if (mirror.resident == nullptr) { const auto current = mirror.buffer->Bytes().subspan(offset, length); if (mirror.sparse) storeSparse(mirror, begin, current, reference->second); - else storeChanged(begin, current, reference->second); + else storeChanged(begin, current, reference->second, GuestMemory::WriteChanged); continue; } std::vector current(length); @@ -6716,7 +6798,7 @@ void GuestBufferMemory::WriteBack() { } mirror.resident->Store(begin, current); if (mirror.sparse) storeSparse(mirror, begin, current, reference->second); - else storeChanged(begin, current, reference->second); + else storeChanged(begin, current, reference->second, GuestMemory::WriteChanged); continue; } const auto current = mirror.buffer->Bytes().subspan(offset, length); @@ -6752,7 +6834,7 @@ void GuestBufferMemory::WriteBack() { if (!region.sparse) { const auto first = static_cast(begin - region.begin); const auto length = static_cast(end - begin); - storeChanged(begin, region.buffer->Bytes().subspan(first, length), std::span(region.uploaded).subspan(first, length)); + storeChanged(begin, region.buffer->Bytes().subspan(first, length), std::span(region.uploaded).subspan(first, length), GuestMemory::WriteChangedKeepingGuest); continue; } std::size_t packed = 0; @@ -6761,7 +6843,7 @@ void GuestBufferMemory::WriteBack() { const auto to = std::min(last, end); if (from < to) { const auto length = static_cast(to - from); - storeChanged(from, region.buffer->Bytes().subspan(static_cast(from - region.begin), length), std::span(region.uploaded).subspan(packed + static_cast(from - first), length)); + storeChanged(from, region.buffer->Bytes().subspan(static_cast(from - region.begin), length), std::span(region.uploaded).subspan(packed + static_cast(from - first), length), GuestMemory::WriteChangedKeepingGuest); } packed += static_cast(last - first); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp index 4c46e8abe7..76ac17848e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp @@ -172,6 +172,16 @@ GuestSamplerResource DecodeSamplerResource(std::span words, return result; } +bool HoldsLodAtZero(std::span words) { + return (words[0] & (1u << 15u)) == 0u && (((words[2] >> 26u) & 3u) == 0u || ((words[1] >> 12u) & 0xfffu) == 0u); +} + +std::array PinnedLodSamplerWords(std::span words, std::uint32_t lod) { + std::array pinned{words[0], (words[1] & ~0xffffffu) | lod | (lod << 12u), words[2], words[3]}; + if (((pinned[2] >> 26u) & 3u) == 0u) pinned[2] |= 1u << 26u; + return pinned; +} + std::optional> SingleLevelSamplerWords(std::span words, bool singleLevelImage, bool mipmappedImage) { const auto filters = words[2]; if (((filters >> 29u) & 1u) == 0 || !singleLevelImage || (!isAnisoFilter((filters >> 20u) & 3u) && !isAnisoFilter((filters >> 22u) & 3u))) return std::nullopt; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp index b1e5044131..400d8bc588 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp @@ -64,6 +64,26 @@ TextureTileMode resolveGcnTileMode(std::uint32_t index) { } } +TextureTileMode resolveDescriptorTileMode(std::span words, bool gcn) { + return gcn ? resolveGcnTileMode(ShaderRecompiler::GcnImageTilingIndex(words[7])) : resolveTileMode((words[3] >> 20u) & 0x1fu); +} + +std::uint64_t descriptorBaseAddress(std::span words) { + Require(words.size() == 8, "guest texture descriptor must contain 8 dwords"); + const auto base40 = (static_cast(words[0]) | (static_cast(words[1]) << 32u)) & 0xffffffffffull; + return base40 << 8u; +} + +std::uint64_t normalizedDescriptorBaseAddress(std::uint64_t baseAddress, TextureTileMode tileMode, std::uint32_t* pipeBankXor) { + auto xorBytes = 0u; + if (XorSwizzleMode(tileMode) != 0) { + xorBytes = static_cast(baseAddress & (tileMode == TextureTileMode::kS4KBX || tileMode == TextureTileMode::kD4KBX ? 0xfffu : 0xffffu)); + baseAddress -= xorBytes; + } + if (pipeBankXor != nullptr) *pipeBankXor = xorBytes; + return baseAddress; +} + TextureDimension resolveDimension(std::uint32_t raw) { switch (raw) { case 8: return TextureDimension::k1D; @@ -118,6 +138,21 @@ std::optional> R128ResidentLevelWords(std::span TextureResourceAllocationAddress(std::span words) { + const auto baseAddress = descriptorBaseAddress(words); + if (baseAddress == 0) return std::nullopt; + const bool gcn = ShaderRecompiler::GcnGuestShaders(); + TextureTileMode tileMode; + try { + tileMode = resolveDescriptorTileMode(words, gcn); + } catch (const std::runtime_error&) { + return std::nullopt; + } + const auto normalized = normalizedDescriptorBaseAddress(baseAddress, tileMode, nullptr); + if (normalized == 0) return std::nullopt; + return normalized; +} + bool IsNullTextureResource(std::span words) { Require(words.size() == 8, "guest texture descriptor must contain 8 dwords"); const auto base40 = (static_cast(words[0]) | (static_cast(words[1]) << 32u)) & 0xffffffffffull; @@ -125,6 +160,14 @@ bool IsNullTextureResource(std::span words) { return base40 == 0 || type <= 1; } +bool IsNullSampledTextureResource(std::span words) { + if (IsNullTextureResource(words)) return true; + const auto type = static_cast((words[3] >> 28u) & 0xfu); + if (type < ShaderRecompiler::ImageType::Color1D || type > ShaderRecompiler::ImageType::Color2DMsaa) return false; + const auto format = (words[1] >> 20u) & 0x1ffu; + return format == 0u || format == 0x1ffu; +} + GuestTextureResource DecodeTextureResource(std::span descriptor) { Require(descriptor.size() == 8, "guest texture descriptor must contain 8 dwords"); const bool gcn = ShaderRecompiler::GcnGuestShaders(); @@ -134,8 +177,7 @@ GuestTextureResource DecodeTextureResource(std::span descri if (gcn) local[6] = local[7] = 0u; const std::span words(local); - const auto base40 = (static_cast(words[0]) | (static_cast(words[1]) << 32u)) & 0xffffffffffull; - auto baseAddress = base40 << 8u; + auto baseAddress = descriptorBaseAddress(words); Require(baseAddress != 0, "guest texture descriptor has a null base address"); const auto minLod = (words[1] >> 8u) & 0xfffu; @@ -225,7 +267,7 @@ GuestTextureResource DecodeTextureResource(std::span descri TextureTileMode tileMode; TextureDimension dimension; try { - tileMode = gcn ? resolveGcnTileMode(ShaderRecompiler::GcnImageTilingIndex(gcnTiling)) : resolveTileMode(tileModeRaw); + tileMode = resolveDescriptorTileMode(descriptor, gcn); dimension = resolveDimension(typeRaw); } catch (const std::exception& error) { throw std::runtime_error(std::string(error.what()) + describe()); @@ -241,11 +283,8 @@ GuestTextureResource DecodeTextureResource(std::span descri // hardware never addresses them, so the view ends at the surface's last level. if (baseLevel <= maxMip) lastLevel = std::min(lastLevel, maxMip); std::uint32_t pipeBankXor = 0; - if (XorSwizzleMode(tileMode) != 0) { - pipeBankXor = static_cast(baseAddress & (tileMode == TextureTileMode::kS4KBX || tileMode == TextureTileMode::kD4KBX ? 0xfffu : 0xffffu)); - baseAddress -= pipeBankXor; - Require(baseAddress != 0, "guest texture descriptor has a null base address"); - } + baseAddress = normalizedDescriptorBaseAddress(baseAddress, tileMode, &pipeBankXor); + Require(baseAddress != 0, "guest texture descriptor has a null base address"); std::uint32_t linearPitch = 0; switch (dimension) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp index dbe18f1bde..44f96ea533 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -13,9 +13,11 @@ #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" +#include "prx/libSceAgcDriver/Graphics/include/MultisampleTextureLayout.hpp" #include "IntermediateRepresentation/IrMetadata/BufferFormat.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/PrimedSharedMutex.hpp" #include #include #include @@ -23,6 +25,7 @@ #include #include #include +#include #include #include #include @@ -174,7 +177,7 @@ struct TargetUse { class MultisampleTarget : public std::enable_shared_from_this { public: - MultisampleTarget(const Context& context, const ColorTarget& target) : context(context), target(target), cmask(target.cmaskAddress, target.cmaskBytes), colorFill(target) { + MultisampleTarget(const Context& context, const ColorTarget& target, bool guestSamples = false) : context(context), target(target), cmask(target.cmaskAddress, target.cmaskBytes), colorFill(target) { this->context.bufferPool.reset(); if (target.dccAddress != 0) metadataValid = std::make_shared>(true); const auto initial = initialDccClear(target); @@ -207,25 +210,64 @@ public: return (uintProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT) != 0 && storageSupports(uintFormat, VK_IMAGE_USAGE_STORAGE_BIT, 0u); }; if (storageFormat != VK_FORMAT_UNDEFINED && (!context.storageImageMultisample || !storageSupports(storageFormat, VK_IMAGE_USAGE_STORAGE_BIT, 0u) || !storageSupports(target.format, usage | VK_IMAGE_USAGE_STORAGE_BIT, storageFlags) || !uintStorageSupported())) storageFormat = VK_FORMAT_UNDEFINED; + VkImageCreateFlags flags = orderFlags; + std::shared_ptr upload; + const auto elementFormat = guestSamples ? TextureDetiler::ImageElementFormat(target.elementBytes) : VK_FORMAT_UNDEFINED; + const auto alignment = std::max({4u, target.elementBytes, context.limits.minStorageBufferOffsetAlignment}); + const auto stride = context.limits.maxStorageBufferRange / alignment * alignment; + if (guestSamples) { + Require(context.detiler != nullptr && context.storageImageMultisample && context.storageImageWriteWithoutFormat, "uploading multisampled guest color requires formatless multisampled storage image writes"); + Require(stride != 0, "uploading multisampled guest color needs a storage range of at least one aligned element"); + flags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT | VK_IMAGE_CREATE_EXTENDED_USAGE_BIT; + Require(storageSupports(elementFormat, VK_IMAGE_USAGE_STORAGE_BIT, 0u) && storageSupports(target.format, usage | VK_IMAGE_USAGE_STORAGE_BIT, flags), "the device cannot upload multisampled guest color through an unsigned storage view"); + usage |= VK_IMAGE_USAGE_STORAGE_BIT; + upload = guestSampleUpload(target); + } 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; + const VkImageCreateFlags twinFlags = flags | 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); + const auto created = allocate(usage, flags); image = created.image; memory = created.memory; view = created.view; allocationBytes = created.bytes; + if (upload) { + VkImageViewUsageCreateInfo storageUsage{VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO}; + storageUsage.usage = VK_IMAGE_USAGE_STORAGE_BIT; + VkImageViewCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + info.pNext = &storageUsage; + info.image = image; + info.viewType = VK_IMAGE_VIEW_TYPE_2D_ARRAY; + info.format = elementFormat; + info.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + Check(context.Function("vkCreateImageView")(context.device, &info, nullptr, &uploadView), "vkCreateImageView multisampled guest color upload"); + } recordCommands(context, [&](VkCommandBuffer commands, Recorder* recorder) { - recordToGeneral(commands, image); - const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; - context.Resolved(&DeviceFunctions::cmdClearColorImage, "vkCmdClearColorImage")(commands, image, VK_IMAGE_LAYOUT_GENERAL, &initial, 1, &range); - RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT); - if (recorder != nullptr) Recorder::CountBarriers(Recorder::CommandClass::Draw, 2); + if (!upload) { + recordToGeneral(commands, image); + const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + context.Resolved(&DeviceFunctions::cmdClearColorImage, "vkCmdClearColorImage")(commands, image, VK_IMAGE_LAYOUT_GENERAL, &initial, 1, &range); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT); + if (recorder != nullptr) Recorder::CountBarriers(Recorder::CommandClass::Draw, 2); + return; + } + recordToGeneral(commands, image, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_SHADER_WRITE_BIT); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT); + const auto bytes = static_cast(target.extent.width) * target.extent.height * target.samples * target.elementBytes; + for (std::uint64_t offset = 0; offset < bytes; offset += stride) { + const auto count = std::min(stride, bytes - offset); + context.detiler->DispatchMultisampleImage(commands, target.elementBytes, upload->Handle(), offset, (count + 3u) & ~std::uint64_t{3}, uploadView, target.extent.width, target.extent.height, 1u, target.samples, static_cast(offset / target.elementBytes), static_cast(count / target.elementBytes)); + } + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + if (recorder != nullptr) { + recorder->Keep(upload, upload->Bytes().size()); + Recorder::CountBarriers(Recorder::CommandClass::Draw, 3); + } }); } catch (...) { release(); @@ -299,16 +341,16 @@ public: if (allocation.memory) MemoryAccounting::Free(context, allocation.memory); } - void recordToGeneral(VkCommandBuffer commands, VkImage destination) { + void recordToGeneral(VkCommandBuffer commands, VkImage destination, VkPipelineStageFlags destinationStage = VK_PIPELINE_STAGE_TRANSFER_BIT, VkAccessFlags destinationAccess = VK_ACCESS_TRANSFER_WRITE_BIT) { VkImageMemoryBarrier toGeneral{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; - toGeneral.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + toGeneral.dstAccessMask = destinationAccess; toGeneral.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; toGeneral.newLayout = VK_IMAGE_LAYOUT_GENERAL; toGeneral.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; toGeneral.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; toGeneral.image = destination; toGeneral.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; - context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &toGeneral); + context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, destinationStage, 0, 0, nullptr, 0, nullptr, 1, &toGeneral); } void makeStorable(Recorder& recorder) { @@ -698,6 +740,22 @@ private: return true; } + VkImageView uploadView = VK_NULL_HANDLE; + + std::shared_ptr guestSampleUpload(const ColorTarget& use) { + Require(use.cmaskAddress == 0 && use.dccAddress == 0, "uploading multisampled guest color under compression metadata is not implemented"); + Require(use.layers == 1u && use.arraySlice == 0u && use.arrayLastSlice == 0u && use.depth == 1u && use.depthSlice == 0u && use.mipCount == 1u && use.mip == 0u && !use.mipTail, "uploading multisampled guest color of arrays, volumes or mips is not implemented"); + Require(use.tileMode == ColorTileMode::RenderTarget || use.tileMode == ColorTileMode::Z64KBX, "non-uniform multisampled guest color tiling is not implemented"); + const MultisampleTextureLayout layout(use.tileMode == ColorTileMode::Z64KBX ? TextureTileMode::kZ64KBX : TextureTileMode::kR64KBX, use.elementBytes, use.samples, use.extent.width, use.extent.height, 1u, use.pipeBankXor); + Require(layout.Bytes() == MultisampledColorBytes(use) && layout.Bytes() <= std::numeric_limits::max(), "the multisampled guest color layout and footprint differ"); + Require(layout.LinearBytes() / use.elementBytes <= std::numeric_limits::max(), "the multisampled guest color element count is unsupported"); + std::vector guest(static_cast(layout.Bytes())); + GuestMemory::Read(use.address, guest); + auto staging = std::make_shared(context, static_cast((layout.LinearBytes() + 3u) & ~std::uint64_t{3}), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + layout.Detile(guest, staging->Bytes().first(static_cast(layout.LinearBytes()))); + return staging; + } + VkClearColorValue initialDccClear(const ColorTarget& use) { if (use.dccAddress == 0) return {}; const auto dccBytes = MultisampledDccBytes(use); @@ -809,6 +867,7 @@ private: firstSampleTextures.clear(); firstSampleCopy.reset(); for (const auto& [format, resolvePass] : resolvePasses) context.Function("vkDestroyRenderPass")(context.device, resolvePass, nullptr); + if (uploadView) context.Function("vkDestroyImageView")(context.device, uploadView, 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); @@ -829,14 +888,6 @@ struct GuardedRange { bool fmask = false; }; -class PrimedSharedMutex : public std::shared_mutex { -public: - PrimedSharedMutex() { - lock(); - unlock(); - } -}; - std::shared_mutex& guardedMutex() { static PrimedSharedMutex mutex; return mutex; @@ -1066,21 +1117,16 @@ MultisampleTarget& createFilled(const Context& context, const GuestTextureResour if (!htileCleared) std::memcpy(&value, memory, sizeof(value)); char held[16]; std::snprintf(held, sizeof(held), "0x%08x", value); - const auto repeated = htileCleared ? memory + bytes : std::mismatch(memory + sizeof(value), memory + bytes, memory).first; - if (repeated != memory + bytes) { - char offset[32]; - std::snprintf(offset, sizeof(offset), "+0x%llx", static_cast(repeated - memory)); - refuse(std::string("whose color memory ") + range + " does not hold one repeated 32-bit value (" + held + " until " + offset + ")"); - } + const bool uniform = htileCleared || std::equal(memory + sizeof(value), memory + bytes, memory); VkClearColorValue clear{}; - if (!MultisampledFillClearColor(use, value, clear)) refuse(std::string("whose color memory ") + range + " holds " + held + ", which has no exact clear value in its format"); - auto target = std::make_shared(context, use); + if (uniform && !MultisampledFillClearColor(use, value, clear)) refuse(std::string("whose color memory ") + range + " holds " + held + ", which has no exact clear value in its format"); + auto target = std::make_shared(context, use, !uniform); target->Keep(); targets().push_back(std::move(target)); auto& created = *targets().back(); created.use->hostOnly.store(written, std::memory_order_release); guard(context, use, created.active, created.metadataValid, created.use); - created.colorFill.NoteFill(use.address, static_cast(bytes), value); + if (uniform) created.colorFill.NoteFill(use.address, static_cast(bytes), value); created.fromTexture = true; created.htileAddress = resource.dccAddress; created.htileBytes = htileBytes; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index ba8c114f75..144e2f4325 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -67,6 +68,12 @@ void LogPipelineStatistics_nid_no_patch(const Context& context, VkPipeline pipel std::vector properties(count, {VK_STRUCTURE_TYPE_PIPELINE_EXECUTABLE_PROPERTIES_KHR}); Check(getProperties(context.device, &pipelineInfo, &count, properties.data()), "vkGetPipelineExecutablePropertiesKHR"); properties.resize(count); + std::string report; + const auto append = [&report](const char* format, auto... values) { + std::array line{}; + std::snprintf(line.data(), line.size(), format, values...); + report += line.data(); + }; for (std::uint32_t index = 0; index < properties.size(); ++index) { VkPipelineExecutableInfoKHR executable{VK_STRUCTURE_TYPE_PIPELINE_EXECUTABLE_INFO_KHR}; executable.pipeline = pipeline; @@ -77,28 +84,27 @@ void LogPipelineStatistics_nid_no_patch(const Context& context, VkPipeline pipel Check(getStatistics(context.device, &executable, &count, statistics.data()), "vkGetPipelineExecutableStatisticsKHR"); statistics.resize(count); const auto& property = properties[index]; - std::fprintf(stderr, "[pipeline-stats] executable=%u stages=0x%x subgroup=%u name=%s\n", index, property.stages, property.subgroupSize, property.name); + append("[pipeline-stats] executable=%u stages=0x%x subgroup=%u name=%s\n", index, property.stages, property.subgroupSize, property.name); for (const auto& statistic : statistics) { - std::fprintf(stderr, "[pipeline-stats] executable=%u %s=", index, statistic.name); switch (statistic.format) { case VK_PIPELINE_EXECUTABLE_STATISTIC_FORMAT_BOOL32_KHR: - std::fprintf(stderr, "%s", statistic.value.b32 ? "true" : "false"); + append("[pipeline-stats] executable=%u %s=%s (%s)\n", index, statistic.name, statistic.value.b32 ? "true" : "false", statistic.description); break; case VK_PIPELINE_EXECUTABLE_STATISTIC_FORMAT_INT64_KHR: - std::fprintf(stderr, "%lld", static_cast(statistic.value.i64)); + append("[pipeline-stats] executable=%u %s=%lld (%s)\n", index, statistic.name, static_cast(statistic.value.i64), statistic.description); break; case VK_PIPELINE_EXECUTABLE_STATISTIC_FORMAT_UINT64_KHR: - std::fprintf(stderr, "%llu", static_cast(statistic.value.u64)); + append("[pipeline-stats] executable=%u %s=%llu (%s)\n", index, statistic.name, static_cast(statistic.value.u64), statistic.description); break; case VK_PIPELINE_EXECUTABLE_STATISTIC_FORMAT_FLOAT64_KHR: - std::fprintf(stderr, "%.17g", statistic.value.f64); + append("[pipeline-stats] executable=%u %s=%.17g (%s)\n", index, statistic.name, statistic.value.f64, statistic.description); break; default: throw std::runtime_error("unsupported pipeline statistic format"); } - std::fprintf(stderr, " (%s)\n", statistic.description); } } + std::fputs(report.c_str(), stderr); } bool HasStencil(VkFormat format) { @@ -233,7 +239,6 @@ void ValidateProvokingVertex(const Context& context, const State& state, std::sp Require(context.provokingVertexLast, "last provoking vertex requires VK_EXT_provoking_vertex with provokingVertexLast enabled"); const bool meshPrimitives = state.stages.path == ShaderPath::Geometry && state.stages.mesh; Require(!state.stages.tessellation && (meshPrimitives || (state.stages.path == ShaderPath::Vertex && !state.stages.mesh)), "last provoking vertex is unsupported for generated primitive pipelines"); - Require(!meshPrimitives || state.topology != VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN, "last provoking vertex is unsupported for triangle fans assembled for a geometry program"); Require(state.rectList || state.topology == VK_PRIMITIVE_TOPOLOGY_POINT_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_LINE_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_LINE_STRIP || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN, "last provoking vertex is unsupported for this primitive topology"); for (const auto& shader : shaders) { if (shader.stage != ShaderRecompiler::ShaderStage::Fragment) continue; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp index 672ef16088..90805553cf 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp @@ -4,6 +4,7 @@ #include "prx/libSceAgcDriver/Execution/include/Mutex.hpp" #include "prx/libc/include/HostThreadLocal.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libSceAgcDriver/Graphics/include/BdaResources.hpp" #include "prx/libSceAgcDriver/Graphics/include/ReferenceStorage.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libkernel/Time/include/SleepWake.hpp" @@ -13,6 +14,7 @@ #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" #include "prx/libSceAgcDriver/Graphics/include/DeferredDestruction.hpp" #include "prx/libSceAgcDriver/Graphics/shaders/MeshArguments_spv.h" +#include "prx/libSceAgcDriver/Graphics/shaders/StagingCopyBack_spv.h" #include "prx/libSceAgcDriver/Graphics/shaders/SampleCounter_spv.h" #include "prx/libSceAgcDriver/Graphics/shaders/SampleDumps_spv.h" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" @@ -552,6 +554,7 @@ bool ProcTable() { // set and cleared under this mutex, so a lookup never touches a table being destroyed. Nothing // under this mutex takes the GPU mutex (no lock-order cycle). AgcDriver::Mutex labelTableMutex; +std::atomic injectedSubmitFailures{0}; Recorder* labelTableOwner = nullptr; // The ranges of the calling worker's queued labels (NoteQueuedLabel, cleared by // ForgetQueuedLabels): the flush hook, on the same thread, has them recorded before an access that @@ -1235,6 +1238,8 @@ Recorder::~Recorder() { } if (meshArgumentPipeline != VK_NULL_HANDLE) context.Function("vkDestroyPipeline")(context.device, meshArgumentPipeline, nullptr); if (meshArgumentLayout != VK_NULL_HANDLE) context.Function("vkDestroyPipelineLayout")(context.device, meshArgumentLayout, nullptr); + if (changedCopyPipeline != VK_NULL_HANDLE) context.Function("vkDestroyPipeline")(context.device, changedCopyPipeline, nullptr); + if (changedCopyLayout != VK_NULL_HANDLE) context.Function("vkDestroyPipelineLayout")(context.device, changedCopyLayout, nullptr); if (sampleCounterState > 0) { samplesPassed.store(SamplesTotal(), std::memory_order_release); if (samplePools != nullptr) std::static_pointer_cast(samplePools)->close(); @@ -1876,7 +1881,7 @@ bool Recorder::closeStoreRun(bool atSubmit) { // pollers read results after the label); in place, later work sees the stores as well. std::uint32_t barriers = 2; if (atSubmit) { - recordBarrier(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_HOST_READ_BIT); + recordBarrier(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_HOST_READ_BIT); } else if (passEndLast && BarrierBatch() && Covers(PassEndBarrier, LabelLeadingBarrier)) { CountMerged(CommandClass::LabelRun); barriers = 1; @@ -2755,6 +2760,58 @@ bool Recorder::RecordMeshArguments(VkCommandBuffer commands, VkDeviceAddress rec return true; } +bool Recorder::RecordChangedCopy(VkCommandBuffer commands, VkDeviceAddress result, VkDeviceAddress snapshot, VkDeviceAddress target, std::uint32_t dwords, VkDeviceAddress counter) { + constexpr std::uint32_t PushBytes = 40; + constexpr std::uint32_t GroupSize = 64; + if (changedCopyState == 0) { + changedCopyState = -1; + VkShaderModule module = VK_NULL_HANDLE; + try { + const VkPushConstantRange push{VK_SHADER_STAGE_COMPUTE_BIT, 0, PushBytes}; + VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; + layoutInfo.pushConstantRangeCount = 1; + layoutInfo.pPushConstantRanges = &push; + Check(context.Function("vkCreatePipelineLayout")(context.device, &layoutInfo, nullptr, &changedCopyLayout), "vkCreatePipelineLayout changed copy"); + VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; + moduleInfo.codeSize = sizeof(STAGING_COPY_BACK_SPV); + moduleInfo.pCode = STAGING_COPY_BACK_SPV; + Check(context.Function("vkCreateShaderModule")(context.device, &moduleInfo, nullptr, &module), "vkCreateShaderModule changed copy"); + VkComputePipelineCreateInfo pipelineInfo{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; + pipelineInfo.stage = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}; + pipelineInfo.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT; + pipelineInfo.stage.module = module; + pipelineInfo.stage.pName = "main"; + pipelineInfo.layout = changedCopyLayout; + Check(context.Function("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &changedCopyPipeline), "vkCreateComputePipelines changed copy"); + context.Function("vkDestroyShaderModule")(context.device, module, nullptr); + changedCopyState = 1; + } catch (const std::exception& error) { + if (module != VK_NULL_HANDLE) context.Function("vkDestroyShaderModule")(context.device, module, nullptr); + std::fprintf(stderr, "[gpu] staged write-backs copy their whole ranges: %s\n", error.what()); + } + } + if (changedCopyState < 0) return false; + static const bool alwaysCopy = std::getenv("APS5_CHANGED_COPY_ALWAYS") != nullptr; + function(cmdBindPipeline, "vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, changedCopyPipeline); + const auto perDispatch = static_cast(std::min(std::uint64_t{context.limits.maxComputeWorkGroupCount[0]} * GroupSize, std::numeric_limits::max() - GroupSize)); + for (std::uint32_t done = 0; done < dwords;) { + const auto count = std::min(dwords - done, perDispatch); + struct { + VkDeviceAddress result; + VkDeviceAddress snapshot; + VkDeviceAddress target; + VkDeviceAddress counter; + std::uint32_t count; + std::uint32_t always; + } parameters{result + VkDeviceAddress{done} * 4u, snapshot + VkDeviceAddress{done} * 4u, target + VkDeviceAddress{done} * 4u, counter, count, alwaysCopy ? 1u : 0u}; + static_assert(sizeof(parameters) == PushBytes); + function(cmdPushConstants, "vkCmdPushConstants")(commands, changedCopyLayout, VK_SHADER_STAGE_COMPUTE_BIT, 0, PushBytes, ¶meters); + function(cmdDispatch, "vkCmdDispatch")(commands, (count + GroupSize - 1) / GroupSize, 1, 1); + done += count; + } + return true; +} + void Recorder::beginSamples(Batch& batch) { auto cache = std::static_pointer_cast(samplePools); if (sampleCounterState > 0 && cache == nullptr) { @@ -3810,6 +3867,10 @@ void Recorder::HoldNextSubmit(VkSemaphore semaphore, std::uint64_t value) { submitHoldValue = value; } +void Recorder::FailNextSubmitsForTest(unsigned count) { + injectedSubmitFailures.store(count, std::memory_order_relaxed); +} + void Recorder::Submit() { PerformanceTimer frameTiming("Recorder.Submit"); // The work count is cleared even when nothing is open: the driver counts a dispatch after its @@ -3853,6 +3914,7 @@ void Recorder::Submit() { } } discardUnsubmitted{*this, batch}; Check(function(endCommandBuffer, "vkEndCommandBuffer")(batch->commands), "vkEndCommandBuffer recorder"); + if (injectedSubmitFailures.load(std::memory_order_relaxed) != 0 && injectedSubmitFailures.fetch_sub(1, std::memory_order_relaxed) != 0) throw std::runtime_error("vkQueueSubmit recorder: injected failure"); VkSubmitInfo submission{VK_STRUCTURE_TYPE_SUBMIT_INFO}; submission.commandBufferCount = 1; submission.pCommandBuffers = &batch->commands; @@ -4328,6 +4390,10 @@ void Recorder::finish(std::unique_ptr batch, bool wait, int source, bool recorder.publishPendingWrites(); } catch (...) { } + try { + BdaResources::NoteFinished(recorder.context.device, batch->serial); + } catch (...) { + } } } finishingScope{*this, batch.get()}; finishing.push_back(batch.get()); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ResidentMirror.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ResidentMirror.cpp index 74144e49fd..cd0c5df699 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ResidentMirror.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ResidentMirror.cpp @@ -29,6 +29,7 @@ namespace AgcDriver::Graphics { namespace { constexpr std::uint64_t BlockSize = ResidentBuffer::BlockBytes; +constexpr VkDeviceSize StagingChunkBytes = VkDeviceSize{32} << 20u; constexpr std::uint64_t SlabBytes = std::uint64_t{16} << 20u; constexpr std::uint32_t SlabBlocks = static_cast(SlabBytes / BlockSize); constexpr std::uint64_t ZeroBytes = SlabBytes; @@ -573,18 +574,34 @@ void ResidentBuffer::flush() { cursor = std::max(cursor, last); } if (regions.empty()) return; - auto staging = std::make_shared(context, static_cast(total), VK_BUFFER_USAGE_TRANSFER_SRC_BIT); + struct Chunk { + std::shared_ptr staging; + std::vector copies; + }; + std::vector chunks; + VkDeviceSize filled = StagingChunkBytes; for (const auto& region : regions) { for (VkDeviceSize done = 0; done < region.size;) { + if (filled == StagingChunkBytes) { + const auto remaining = total - (region.srcOffset + done); + chunks.push_back({std::make_shared(context, static_cast(std::min(StagingChunkBytes, remaining)), VK_BUFFER_USAGE_TRANSFER_SRC_BIT), {}}); + filled = 0; + } + auto& chunk = chunks.back(); const auto at = origin + region.dstOffset + done; - const auto length = std::min(region.size - done, BlockSize - (at - origin) % BlockSize); - std::memcpy(staging->Bytes().data() + region.srcOffset + done, Block(at), static_cast(length)); + const auto length = std::min({region.size - done, BlockSize - (at - origin) % BlockSize, StagingChunkBytes - filled}); + std::memcpy(chunk.staging->Bytes().data() + filled, Block(at), static_cast(length)); + if (!chunk.copies.empty() && chunk.copies.back().srcOffset + chunk.copies.back().size == filled && chunk.copies.back().dstOffset + chunk.copies.back().size == region.dstOffset + done) chunk.copies.back().size += length; + else chunk.copies.push_back({filled, region.dstOffset + done, length}); + filled += length; done += length; } } recordTransfer(context, [&](VkCommandBuffer commands, std::vector>& kept) { - context.Function("vkCmdCopyBuffer")(commands, staging->Handle(), buffer, static_cast(regions.size()), regions.data()); - kept.push_back(staging); + for (const auto& chunk : chunks) { + context.Function("vkCmdCopyBuffer")(commands, chunk.staging->Handle(), buffer, static_cast(chunk.copies.size()), chunk.copies.data()); + kept.push_back(chunk.staging); + } kept.push_back(shared_from_this()); }); } catch (...) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp index abc6ba3de3..d956541b20 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp @@ -49,7 +49,14 @@ VkBuffer BindExactBuffer(const Context& context, std::size_t size, VkBufferUsage } -Buffer::Buffer(const Context& context, std::size_t size, VkBufferUsageFlags usage, VkMemoryPropertyFlags properties) : context(context), size(size), capacity(BufferPool::Capacity(size)), usage(BufferPool::Usage(usage, properties)), properties(properties) { +Buffer::Buffer(const Context& context, std::size_t size, VkBufferUsageFlags usage, VkMemoryPropertyFlags properties) : Buffer(context, size, usage, properties, false) {} + +std::shared_ptr Buffer::WithinDeviceLocalHeadroom(const Context& context, std::size_t size, VkBufferUsageFlags usage) { + std::shared_ptr buffer(new Buffer(context, size, usage, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, true)); + return buffer->ready ? buffer : nullptr; +} + +Buffer::Buffer(const Context& context, std::size_t size, VkBufferUsageFlags usage, VkMemoryPropertyFlags properties, bool headroom) : context(context), size(size), capacity(BufferPool::Capacity(size)), usage(BufferPool::Usage(usage, properties)), properties(properties) { Require(size != 0, "zero-sized GPU buffer"); const bool addressable = (usage & VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT) != 0; Require(!addressable || context.bufferDeviceAddress, "buffer device address is not enabled"); @@ -98,6 +105,11 @@ Buffer::Buffer(const Context& context, std::size_t size, VkBufferUsageFlags usag // write-combined memory, so the default host properties prefer cached host memory. const bool useSlab = (properties & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) != 0 && BufferPool::SlabEligible(capacity, requirements.alignment, requirements.size, context.limits.nonCoherentAtomSize); allocation.memoryTypeIndex = context.BufferMemoryType(requirements.memoryTypeBits, properties, useSlab ? BufferPool::SlabBlockBytes(capacity) : allocation.allocationSize); + if (headroom && !context.DeviceLocalHeadroom(allocation.memoryTypeIndex, useSlab ? BufferPool::SlabBlockBytes(capacity) : allocation.allocationSize)) { + context.Function("vkDestroyBuffer")(context.device, buffer, nullptr); + buffer = VK_NULL_HANDLE; + return; + } if (useSlab) { const auto slot = cache->TakeSlot(context, allocation.memoryTypeIndex, capacity, addressable); memory = slot.memory; @@ -164,12 +176,21 @@ void Buffer::Invalidate() { DeviceBuffer::DeviceBuffer(const Context& context, std::size_t size, VkBufferUsageFlags usage) : context(context), size(size), capacity(BufferPool::Capacity(size)), usage(BufferPool::Usage(usage, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT)) { Require(size != 0, "zero-sized device buffer"); cache = GetBufferPool(context); - if (const auto allocation = cache->Take(size, this->usage, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, "vkAllocateMemory device buffer")) { - buffer = allocation->buffer; - memory = allocation->memory; - allocationBytes = allocation->allocationBytes; - capacity = allocation->bytes; - if (allocation->bufferBytes == size) { + auto pooled = cache->Take(size, this->usage, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, "vkAllocateMemory device buffer"); + if (!pooled) { + pooled = cache->Take(size, this->usage, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, "vkAllocateMemory device buffer", true); + if (pooled && context.DeviceLocalFits(~0u, capacity)) { + cache->Put(*pooled); + pooled.reset(); + } + } + if (pooled) { + hostPlaced = pooled->hostPlaced; + buffer = pooled->buffer; + memory = pooled->memory; + allocationBytes = pooled->allocationBytes; + capacity = pooled->bytes; + if (pooled->bufferBytes == size) { ready = true; return; } @@ -213,8 +234,10 @@ DeviceBuffer::~DeviceBuffer() { } void DeviceBuffer::release() noexcept { - if (ready && buffer && memory && cache && !hostPlaced) { - cache->Put({buffer, memory, nullptr, 0, allocationBytes, capacity, usage, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, 0, false, size}); + if (ready && buffer && memory && cache) { + BufferAllocation allocation{buffer, memory, nullptr, 0, allocationBytes, capacity, usage, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, 0, false, size}; + allocation.hostPlaced = hostPlaced; + cache->Put(allocation); return; } if (buffer) context.Function("vkDestroyBuffer")(context.device, buffer, nullptr); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Sampler.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Sampler.cpp index d7b1fb0056..80d99fcf93 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Sampler.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Sampler.cpp @@ -1,6 +1,7 @@ #include "prx/libSceAgcDriver/Graphics/include/Sampler.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" #include +#include #include #include @@ -102,10 +103,10 @@ namespace AgcDriver::Graphics { return sampler; } - void RequireFilterMinmax(const Context& context, VkFormat format, std::uint32_t samplerMask, std::span> samplers) { + void RequireFilterMinmax(const Context& context, VkFormat format, std::uint64_t samplerMask, std::span> samplers) { bool filtered = false; - for (std::uint32_t element = 0; element < 32u; ++element) { - if (((samplerMask >> element) & 1u) == 0u) continue; + for (auto mask = samplerMask; mask != 0u; mask &= mask - 1u) { + const auto element = static_cast(std::countr_zero(mask)); if (element >= samplers.size()) Require(false, "a sampled texture is paired with sampler element " + std::to_string(element) + ", which its shader does not bind"); filtered = filtered || samplers[element]->RequiresFilterMinmax(); } @@ -123,29 +124,31 @@ namespace AgcDriver::Graphics { return requiresNonSeamlessCube; } - bool ForcesDegammaOn(std::uint32_t guestFormat, std::uint32_t samplerMask, std::span> samplers) { + bool ForcesDegammaOn(std::uint32_t guestFormat, std::uint64_t samplerMask, std::span> samplers) { if (IsSrgbTextureFormat(guestFormat) || IgnoresForcedDegamma(guestFormat)) return false; - for (std::uint32_t element = 0; element < 32u && element < samplers.size(); ++element) { - if (((samplerMask >> element) & 1u) != 0u && samplers[element]->ForcesDegamma()) return true; + for (auto mask = samplerMask; mask != 0u; mask &= mask - 1u) { + const auto element = static_cast(std::countr_zero(mask)); + if (element < samplers.size() && samplers[element]->ForcesDegamma()) return true; } return false; } - void RequireBorderSwizzle(std::uint32_t bcSwizzle, std::uint32_t samplerMask, std::span> samplers) { + void RequireBorderSwizzle(std::uint32_t bcSwizzle, std::uint64_t samplerMask, std::span> samplers) { constexpr std::uint32_t BcSwizzleXyzw = 0u; constexpr std::uint32_t BcSwizzleZyxw = 4u; if (bcSwizzle == BcSwizzleXyzw || bcSwizzle == BcSwizzleZyxw) return; - for (std::uint32_t element = 0; element < 32u; ++element) { - if (((samplerMask >> element) & 1u) == 0u) continue; + for (auto mask = samplerMask; mask != 0u; mask &= mask - 1u) { + const auto element = static_cast(std::countr_zero(mask)); if (element >= samplers.size()) Require(false, "a sampled texture is paired with sampler element " + std::to_string(element) + ", which its shader does not bind"); if (samplers[element]->ReadsOpaqueBlackBorder()) Require(false, "a sampled texture whose BC swizzle " + std::to_string(bcSwizzle) + " moves the alpha channel is sampled through an opaque black border, which is not implemented"); } } - void RequireNonSeamlessCube(bool cube, std::uint32_t samplerMask, std::span> samplers) { + void RequireNonSeamlessCube(bool cube, std::uint64_t samplerMask, std::span> samplers) { if (!cube) return; - for (std::uint32_t element = 0; element < 32u && element < samplers.size(); ++element) { - if (((samplerMask >> element) & 1u) != 0u && samplers[element]->RequiresNonSeamlessCube()) Require(false, "a cube texture is sampled through a sampler that disables seamless cube filtering, which needs VK_EXT_non_seamless_cube_map"); + for (auto mask = samplerMask; mask != 0u; mask &= mask - 1u) { + const auto element = static_cast(std::countr_zero(mask)); + if (element < samplers.size() && samplers[element]->RequiresNonSeamlessCube()) Require(false, "a cube texture is sampled through a sampler that disables seamless cube filtering, which needs VK_EXT_non_seamless_cube_map"); } } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderAddressResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderAddressResources.cpp index 01cdc5661d..84de0fb00a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderAddressResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderAddressResources.cpp @@ -5,6 +5,8 @@ #include #include #include +#include +#include #include #include @@ -65,6 +67,37 @@ bool definesNoPointerValue(std::uint32_t opcode) { } } +bool referencesNoValue(std::uint32_t opcode) { + switch (static_cast(opcode)) { + case spv::OpCapability: + case spv::OpExtension: + case spv::OpExtInstImport: + case spv::OpMemoryModel: + case spv::OpEntryPoint: + case spv::OpExecutionMode: + case spv::OpExecutionModeId: + case spv::OpSource: + case spv::OpSourceExtension: + case spv::OpString: + case spv::OpLine: + case spv::OpNoLine: + case spv::OpModuleProcessed: + case spv::OpConstantTrue: + case spv::OpConstantFalse: + case spv::OpConstant: + case spv::OpConstantComposite: + case spv::OpConstantNull: + case spv::OpSpecConstantTrue: + case spv::OpSpecConstantFalse: + case spv::OpSpecConstant: + case spv::OpSpecConstantComposite: + case spv::OpSpecConstantOp: + return true; + default: + return definesNoPointerValue(opcode) || (opcode >= spv::OpTypeVoid && opcode <= spv::OpTypeForwardPointer); + } +} + template bool forEachInstruction(std::span words, const TVisit& visit) { for (std::size_t at = 5; at < words.size();) { @@ -91,6 +124,21 @@ bool storesThroughAddressesCached(const ShaderRecompiler::SharedSpirv& spirv) { return stores; } +bool faultReportOnlyCached(const ShaderRecompiler::SharedSpirv& spirv, std::uint32_t set, std::uint32_t binding) { + static std::mutex mutex; + static std::map, bool> known; + const auto hash = spirv.ContentHash(); + const std::tuple key{hash.low, hash.high, set, binding}; + { + std::lock_guard lock(mutex); + if (const auto found = known.find(key); found != known.end()) return found->second; + } + const bool reportOnly = SpirvFaultReportOnly(spirv.Words(), set, binding); + std::lock_guard lock(mutex); + known.emplace(key, reportOnly); + return reportOnly; +} + } bool ShaderStoresThroughAddresses(const ShaderRecompiler::SharedSpirv& spirv) { @@ -118,6 +166,45 @@ bool SpirvStoresThroughAddresses(std::span words) { return stores; } +bool SpirvFaultReportOnly(std::span words, std::uint32_t set, std::uint32_t binding) { + constexpr std::uint32_t reportWords = sizeof(ShaderRecompiler::BdaAbi::Fault) / sizeof(std::uint32_t); + if (words.size() < 5 || words[0] != spv::MagicNumber) return false; + std::unordered_map sets; + std::unordered_map bindings; + std::unordered_map constants; + const bool readable = forEachInstruction(words, [&](std::uint32_t opcode, std::span instruction) { + if (opcode == spv::OpDecorate && instruction.size() == 4 && instruction[2] == spv::DecorationDescriptorSet) sets[instruction[1]] = instruction[3]; + if (opcode == spv::OpDecorate && instruction.size() == 4 && instruction[2] == spv::DecorationBinding) bindings[instruction[1]] = instruction[3]; + if (opcode == spv::OpConstant && instruction.size() == 4) constants[instruction[2]] = instruction[3]; + }); + if (!readable) return false; + std::unordered_set buffers; + for (const auto& [id, number] : bindings) { + if (const auto found = sets.find(id); number == binding && found != sets.end() && found->second == set) buffers.insert(id); + } + const auto constantBelow = [&](std::uint32_t id, std::uint32_t limit) { + const auto found = constants.find(id); + return found != constants.end() && found->second < limit; + }; + const auto chainsBuffer = [&](std::uint32_t opcode, std::span instruction) { + return (opcode == spv::OpAccessChain || opcode == spv::OpInBoundsAccessChain) && instruction.size() > 3 && buffers.contains(instruction[3]); + }; + bool reportOnly = !buffers.empty(); + std::unordered_set reportPointers; + forEachInstruction(words, [&](std::uint32_t opcode, std::span instruction) { + if (!chainsBuffer(opcode, instruction)) return; + reportOnly = reportOnly && instruction.size() == 6 && constantBelow(instruction[4], 1) && constantBelow(instruction[5], reportWords); + reportPointers.insert(instruction[2]); + }); + forEachInstruction(words, [&](std::uint32_t opcode, std::span instruction) { + if (!reportOnly || referencesNoValue(opcode) || chainsBuffer(opcode, instruction) || (opcode == spv::OpVariable && instruction.size() > 2 && buffers.contains(instruction[2]))) return; + const auto accessed = opcode == spv::OpLoad || opcode == spv::OpAtomicLoad ? (instruction.size() > 3 ? instruction[3] : 0u) : pointerOperandStored(opcode, instruction); + const auto references = std::count_if(instruction.begin() + 1, instruction.end(), [&](std::uint32_t word) { return buffers.contains(word) || reportPointers.contains(word); }); + reportOnly = references == (reportPointers.contains(accessed) ? 1 : 0); + }); + return reportOnly; +} + void ShaderResources::prepareAddressBindings(std::span shaders, std::span snapshots) { static const bool loopGuarded = std::getenv("APS5_LOOP_GUARD") != nullptr; for (const auto& shader : shaders) { @@ -127,8 +214,12 @@ void ShaderResources::prepareAddressBindings(std::span sha for (const auto& binding : shader.program->bindings) { if (binding.role != ShaderRecompiler::DescriptorRole::BdaPagetable && binding.role != ShaderRecompiler::DescriptorRole::FaultBuffer) continue; Require(binding.kind == ShaderRecompiler::DescriptorKind::StorageBuffer && binding.count == 1 && binding.guestDescriptor.empty() && !binding.readOnly, "invalid BDA descriptor contract"); - if (binding.role == ShaderRecompiler::DescriptorRole::BdaPagetable) ++tables; - else ++faults; + if (binding.role == ShaderRecompiler::DescriptorRole::BdaPagetable) { + ++tables; + } else { + ++faults; + faultReportOnly = faultReportOnly && faultReportOnlyCached(shader.program->spirv, binding.descriptorSet, binding.binding); + } } const bool faultOnly = tables == 0 && faults == 1 && shader.program->bdaAbiVersion == ShaderRecompiler::BdaAbi::Version; Require((tables == faults || faultOnly) && tables <= 1 && faults <= 1, "invalid BDA table and fault descriptors"); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 4ec60ded0a..449e10cd8a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -16,6 +16,7 @@ #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" #include #include +#include #include #include #include @@ -31,6 +32,7 @@ #include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/GuestAllocations.hpp" #include "Optimization/include/Optimization/ShaderStageInputInfo.hpp" #include "RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp" #include "IntermediateRepresentation/IrMetadata/ShaderStage.hpp" @@ -54,6 +56,28 @@ bool overlap(std::uint64_t first, std::size_t firstSize, std::uint64_t second, s return first < second + secondSize && second < first + firstSize; } +bool registeredImageViewsCurrent(std::span views) { + if (views.empty()) return true; + for (std::uint32_t attempt = 0; attempt < 2u; ++attempt) { + const auto before = GuestAllocations::GuestAllocationsGeneration_nid_postfix(); + bool current = true; + for (const auto& view : views) { + const auto range = GuestAllocations::GuestAllocationsLookup_nid_postfix(static_cast(view.address), false); + if (view.hasRange != (range != nullptr)) { + current = false; + break; + } + if (range != nullptr && (range->address != view.rangeAddress || range->bytes != view.rangeBytes || range->allocationAddress != view.allocationAddress || range->allocationBytes != view.allocationBytes || range->backingSerial != view.backingSerial || range->readable != view.readable || range->writable != view.writable || range->gpu != view.gpu)) { + current = false; + break; + } + } + const auto after = GuestAllocations::GuestAllocationsGeneration_nid_postfix(); + if (before == after) return current; + } + return false; +} + VkComponentSwizzle ComponentSwizzleFor(std::uint8_t dstSel) { switch (dstSel) { case 0: return VK_COMPONENT_SWIZZLE_ZERO; @@ -75,11 +99,30 @@ VkComponentMapping ViewComponents(const GuestTextureResource& resource) { // Sampled textures are reused across draws and dispatches while their guest bytes are unchanged; a // byte copy of the guest surface validates each reuse, so CPU or GPU writes to it force a re-upload. // The key is everything a lookup matches: the device, the eight descriptor words and the swizzle. +struct AllocationIdentity { + std::uint64_t rangeAddress = 0; + std::uint64_t rangeBytes = 0; + std::uint64_t allocationAddress = 0; + std::uint64_t allocationBytes = 0; + std::uint64_t backingSerial = 0; + std::uint32_t flags = 0; + bool operator==(const AllocationIdentity&) const = default; +}; + +AllocationIdentity ImageAllocationIdentity(std::span words) { + const auto address = TextureResourceAllocationAddress(words); + if (!address) return {}; + const auto range = GuestAllocations::GuestAllocationsLookup_nid_postfix(static_cast(*address), false); + if (range == nullptr) return {}; + return {range->address, range->bytes, range->allocationAddress, range->allocationBytes, range->backingSerial, (range->readable ? 1u : 0u) | (range->writable ? 2u : 0u) | (range->gpu ? 4u : 0u)}; +} + struct TextureKey { VkDevice device; std::array words; std::array components; bool depthCompare = false; + AllocationIdentity allocation; bool operator==(const TextureKey&) const = default; }; @@ -92,6 +135,7 @@ struct TextureKeyHash { std::size_t operator()(const TextureKey& key) const noexcept { auto hash = 14695981039346656037ull ^ reinterpret_cast(key.device); hash = hashWords(hash, key.words.data(), key.words.size()) ^ static_cast(key.depthCompare); + for (const auto value : {key.allocation.rangeAddress, key.allocation.rangeBytes, key.allocation.allocationAddress, key.allocation.allocationBytes, key.allocation.backingSerial, static_cast(key.allocation.flags)}) hash = (hash ^ value) * 1099511628211ull; return static_cast(hashWords(hash, key.components.data(), key.components.size())); } }; @@ -99,6 +143,7 @@ struct TextureKeyHash { TextureKey MakeTextureKey(VkDevice device, std::span words, VkComponentMapping components, bool depthCompare = false) { TextureKey key{device, {}, {static_cast(components.r), static_cast(components.g), static_cast(components.b), static_cast(components.a)}}; key.depthCompare = depthCompare; + key.allocation = ImageAllocationIdentity(words); std::copy(words.begin(), words.end(), key.words.begin()); key.words[5] &= ~((0xfffu << 8u) | (1u << 25u)); key.words[6] &= ~0xffu; @@ -349,6 +394,7 @@ struct ImageMemoEntry { std::array words{}; GuestTextureResource resource{}; std::uint64_t guestBytes = 0; + AllocationIdentity allocation; VkComponentMapping components{}; bool hasEntry = false; std::list::iterator entry{}; @@ -365,12 +411,32 @@ ImageMemoEntry& imageMemoSlot(std::span words) { return (*imageMemo)[(hash ^ (hash >> 29u)) % ImageMemoSlots]; } +struct SampledKeyProofEntry { + std::uint64_t dccAddress = 0; + std::uint64_t guestBytes = 0; + std::uint32_t format = 0; + bool dccAlphaOnMsb = false; + DccKeyProof proof; +}; + +constexpr std::size_t SampledKeyProofSlots = 1024; +struct SampledKeyProofTag {}; + +SampledKeyProofEntry& sampledKeyProofSlot(std::uint64_t dccAddress, std::uint64_t guestBytes, std::uint32_t format, bool dccAlphaOnMsb) { + auto& proofs = HostThreadLocal>, SampledKeyProofTag>(); + if (proofs == nullptr) proofs = std::make_unique>(); + const auto hash = dccAddress ^ (guestBytes * 0x9e3779b97f4a7c15ull) ^ (static_cast(format) << 32u) ^ (dccAlphaOnMsb ? 0x94d049bb133111ebull : 0ull); + return (*proofs)[(hash ^ (hash >> 29u)) % SampledKeyProofSlots]; +} + const ImageMemoEntry& decodedImage(VkDevice device, std::span words) { auto& slot = imageMemoSlot(words); - if (slot.device == device && std::equal(words.begin(), words.end(), slot.words.begin())) return slot; + const auto allocation = ImageAllocationIdentity(words); + if (slot.device == device && slot.allocation == allocation && std::equal(words.begin(), words.end(), slot.words.begin())) return slot; slot.device = VK_NULL_HANDLE; slot.resource = DecodeTextureResource(words); slot.guestBytes = DescribeSurface(slot.resource).guestBytes; + slot.allocation = allocation; slot.components = ViewComponents(slot.resource); std::copy(words.begin(), words.end(), slot.words.begin()); slot.hasEntry = false; @@ -380,7 +446,7 @@ const ImageMemoEntry& decodedImage(VkDevice device, std::span::iterator findSampledEntry(TextureCache& cache, VkDevice device, const std::array& words, VkComponentMapping components) { auto& slot = imageMemoSlot(words); - const bool same = slot.device == device && slot.words == words && slot.components.r == components.r && slot.components.g == components.g && slot.components.b == components.b && slot.components.a == components.a; + const bool same = slot.device == device && slot.allocation == ImageAllocationIdentity(words) && slot.words == words && slot.components.r == components.r && slot.components.g == components.g && slot.components.b == components.b && slot.components.a == components.a; if (same && slot.hasEntry && slot.erasures == cache.erasures) return slot.entry; const auto it = findTexture(cache, MakeTextureKey(device, words, components)); if (same) { @@ -428,6 +494,7 @@ TextureCounters& TextureCounts() { } thread_local std::uint64_t sampledLookupKeyScans = 0; +thread_local std::uint64_t sampledLookupKeyProofs = 0; void reportTextureCounters() { static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; @@ -617,7 +684,8 @@ std::uint64_t forcedCacheBudget() { return forced; } -std::uint64_t storageBudget(const Context& context, std::uint64_t required = 0) { +std::uint64_t storageBudget(const Context& context, std::uint64_t required = 0, std::optional* calculatedAt = nullptr) { + if (calculatedAt != nullptr) calculatedAt->reset(); const auto forced = forcedCacheBudget(); if (forced != 0) return forced; if (context.memoryProperties2 == nullptr || context.physical == VK_NULL_HANDLE) return TextureCacheBudget(context.memory); @@ -627,6 +695,7 @@ std::uint64_t storageBudget(const Context& context, std::uint64_t required = 0) std::chrono::steady_clock::time_point read{}; std::uint64_t budget{0}; std::uint64_t ceiling{0}; + std::chrono::steady_clock::time_point calculatedAt{}; } cached; const auto now = std::chrono::steady_clock::now(); std::lock_guard lock(cached.mutex); @@ -644,10 +713,12 @@ std::uint64_t storageBudget(const Context& context, std::uint64_t required = 0) storage = storageCacheBytes(); cached.budget = StorageTextureBudget(context.memory, &*reported, sampled, storage); cached.ceiling = StorageTextureCeiling(context.memory, &*reported); + cached.calculatedAt = std::chrono::steady_clock::now(); }; if (cached.device != context.physical || now - cached.read >= std::chrono::seconds(1)) read(); const auto needed = std::min(required, cached.ceiling); if (needed > cached.budget && context.bufferPool != nullptr && context.bufferPool->ReclaimIdleDevice(needed - cached.budget, now) != 0) read(); + if (calculatedAt != nullptr) *calculatedAt = cached.calculatedAt; if (reported) MemoryAccounting::Report(context, *reported, sampled, storage, cached.budget); return cached.budget; } @@ -665,15 +736,21 @@ std::uint64_t sampledBudget(const Context& context, TextureCache& cache) { const auto storage = storageCacheBytes(); const auto budget = SampledTextureBudget(context.memory, &reported, sampled + storage, storage); const bool fresh = cache.budgetDevice != context.physical; - const auto storageLimit = storageBudget(context); + std::optional storageCalculatedAt; + const auto storageLimit = storageBudget(context, 0, &storageCalculatedAt); if (const auto heap = largestDeviceLocalHeap(context.memory); heap && (fresh || SampledBudgetReportDue(cache.reportedBudget, budget, now - cache.reportedAt) || SampledBudgetReportDue(cache.reportedStorage, storageLimit, now - cache.reportedAt))) { char was[48] = ""; char wasStorage[48] = ""; + char storageLimitAge[32] = "unavailable"; + if (storageCalculatedAt) { + const auto age = std::chrono::duration_cast(std::chrono::steady_clock::now() - *storageCalculatedAt); + std::snprintf(storageLimitAge, sizeof(storageLimitAge), "%llu", static_cast(age.count())); + } if (!fresh) { std::snprintf(was, sizeof(was), " (was %llu MiB)", static_cast(cache.reportedBudget >> 20u)); std::snprintf(wasStorage, sizeof(wasStorage), " (was %llu MiB)", static_cast(cache.reportedStorage >> 20u)); } - std::fprintf(stderr, "[gpu] sampled texture cache budget %llu MiB%s: device-local heap %u has a VK_EXT_memory_budget budget of %llu MiB and uses %llu MiB, %llu MiB of it sampled textures (%llu MiB in the cache) and %llu MiB cached storage images; storage image cache budget %llu MiB%s\n", static_cast(budget >> 20u), was, *heap, static_cast(reported.heapBudget[*heap] >> 20u), static_cast(reported.heapUsage[*heap] >> 20u), static_cast(sampled >> 20u), static_cast(cache.bytes >> 20u), static_cast(storage >> 20u), static_cast(storageLimit >> 20u), wasStorage); + std::fprintf(stderr, "[gpu] sampled texture cache budget %llu MiB%s: device-local heap %u has a VK_EXT_memory_budget budget of %llu MiB and uses %llu MiB, %llu MiB of it sampled textures (%llu MiB in the cache) and %llu MiB cached storage images; active storage image cache budget %llu MiB%s; storage_limit_age_ms=%s\n", static_cast(budget >> 20u), was, *heap, static_cast(reported.heapBudget[*heap] >> 20u), static_cast(reported.heapUsage[*heap] >> 20u), static_cast(sampled >> 20u), static_cast(cache.bytes >> 20u), static_cast(storage >> 20u), static_cast(storageLimit >> 20u), wasStorage, storageLimitAge); cache.reportedBudget = budget; cache.reportedStorage = storageLimit; cache.reportedAt = now; @@ -698,7 +775,7 @@ std::array untrackedKey(std::uint64_t address, std::size_t byt } std::span firstLayerWords(std::span words, std::optional shape, std::array& narrowed, bool report) { - if (shape != ShaderRecompiler::DescriptorImageShape::Image2D || words.size() != narrowed.size() || IsNullTextureResource(words)) return words; + if (shape != ShaderRecompiler::DescriptorImageShape::Image2D || words.size() != narrowed.size() || IsNullSampledTextureResource(words)) return words; try { if (!FirstLayerDescriptor(words, narrowed)) return words; } catch (const std::runtime_error&) { @@ -715,6 +792,10 @@ std::span firstLayerWords(std::span wo return narrowed; } +bool layerRowsBinding(std::optional shape, const GuestTextureResource& resource, bool depthCompare) { + return !depthCompare && shape == ShaderRecompiler::DescriptorImageShape::Image2D && resource.dimension == TextureDimension::k1DArray; +} + void reportGuestAllocation(const GuestTextureResource& resource, std::uint64_t guestBytes) { const auto range = GuestAllocations::GuestAllocationsLookup_nid_postfix(resource.baseAddress, false); const auto geometry = DescribeSurface(resource); @@ -735,11 +816,12 @@ struct PendingLookup { bool repackCopy = false; }; -PendingLookup pendingLookupSource(const Context& context, const GuestTextureResource& resource, std::uint64_t guestBytes, bool depthCompare) { +PendingLookup pendingLookupSource(const Context& context, const GuestTextureResource& resource, std::uint64_t guestBytes, bool depthCompare, bool layerRows = false) { PendingLookup lookup; lookup.depthCopy = depthCompare && guestBytes >= DepthCopyMinBytes() && Texture::DepthCopyFormat(ResolveTextureFormat(resource.format)) != VK_FORMAT_UNDEFINED && resource.dimension != TextureDimension::k3D; lookup.srgbExpand = !depthCompare && SrgbExpandsToRgba8(context, resource.format); lookup.source = StorageTexture::FindPending(resource.baseAddress, guestBytes); + if (layerRows) lookup.source.reset(); auto& source = lookup.source; lookup.blockCopy = source != nullptr && !depthCompare && !lookup.srgbExpand && Texture::CanBlockCopyFrom(*source, resource); lookup.repackCopy = source != nullptr && !depthCompare && Texture::CanRepackCopyFrom(*source, resource); @@ -749,8 +831,9 @@ PendingLookup pendingLookupSource(const Context& context, const GuestTextureReso } // `guestBytes` is the surface size when the caller described the surface already (0: described here). -std::shared_ptr cachedTexture(const Context& context, std::span words, const GuestTextureResource& requested, VkComponentMapping components, std::uint64_t guestBytes = 0, bool depthCompare = false, VkImageViewType multisampleViewType = VK_IMAGE_VIEW_TYPE_2D) { - const auto resource = ArrayLayerWindow(requested, context.limits.maxImageArrayLayers); +std::shared_ptr cachedTexture(const Context& context, std::span words, const GuestTextureResource& requested, VkComponentMapping components, std::uint64_t guestBytes = 0, bool depthCompare = false, VkImageViewType multisampleViewType = VK_IMAGE_VIEW_TYPE_2D, bool layerRows = false) { + Require(!layerRows || (requested.dimension == TextureDimension::k1DArray && !depthCompare), "only a sampled 1D array is laid out as rows of a 2D image"); + const auto resource = layerRows ? requested : ArrayLayerWindow(requested, context.limits.maxImageArrayLayers); if (resource.baseAddress != requested.baseAddress) guestBytes = 0; if (resource.dimension == TextureDimension::k2DMsaa) { Require(!depthCompare, "native multisampled depth comparison sampling is not implemented"); @@ -773,7 +856,8 @@ 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(); - if (auto depth = DepthSurfaceTexture(context, words, resource, components)) { + Require(!layerRows || !DepthSurfaceAt(resource.baseAddress), "sampling a native depth surface as a 1D array past maxImageArrayLayers is not implemented"); + if (auto depth = layerRows ? nullptr : DepthSurfaceTexture(context, words, resource, components)) { logLookup({depth.get(), resource, 0, DccKeys::Uncompressed, 0, nullptr, true}); return depth; } @@ -782,13 +866,13 @@ std::shared_ptr cachedTexture(const Context& context, std::span(ShaderRecompiler::IrBufferFormat::Format16UNorm); if (depthBitsWidth == 16u && resource.format != unorm16) { auto normalized = resource; normalized.format = unorm16; - return cachedTexture(context, words, normalized, components, guestBytes, depthCompare); + return cachedTexture(context, words, normalized, components, guestBytes, depthCompare, VK_IMAGE_VIEW_TYPE_2D, layerRows); } const auto viewedWindow = [&] { const auto window = ViewedArrayWindow(resource, false); @@ -801,7 +885,7 @@ std::shared_ptr cachedTexture(const Context& context, std::span windows{0}; const auto count = windows.fetch_add(1) + 1; if (count <= 5 || (count & (count - 1)) == 0) std::fprintf(stderr, "[gpu] sampled array view 0x%llx (%ux%u format %u, dimension %d, slices %u..%u) served from its viewed slices at 0x%llx; %llu such views\n", static_cast(resource.baseAddress), resource.width, resource.height, resource.format, static_cast(resource.dimension), resource.baseArray, resource.depthOrLastArray, static_cast(window.baseAddress), static_cast(count)); - return cachedTexture(context, words, window, components, 0, depthCompare); + return cachedTexture(context, words, window, components, 0, depthCompare, VK_IMAGE_VIEW_TYPE_2D, layerRows); } if (guestBytes == 0) guestBytes = DescribeSurface(resource).guestBytes; if (guestBytes >= (1ull << 30u)) { @@ -818,10 +902,25 @@ std::shared_ptr cachedTexture(const Context& context, std::span scanned; const auto scanKeys = [&] { if (scanned.has_value()) return *scanned; + if (resource.dccAddress == 0) { + scanned = DccKeys::Uncompressed; + return *scanned; + } const auto scanStart = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; - scanned = TextureClearKeys(resource, guestBytes); - if (resource.dccAddress != 0) ++sampledLookupKeyScans; - if (profile && resource.dccAddress != 0) LookupOutcomes::Add(LookupOutcomes::DccScan, scanStart); + auto& memo = sampledKeyProofSlot(resource.dccAddress, guestBytes, resource.format, resource.dccAlphaOnMsb); + if (memo.dccAddress != resource.dccAddress || memo.guestBytes != guestBytes || memo.format != resource.format || memo.dccAlphaOnMsb != resource.dccAlphaOnMsb) { + memo.dccAddress = resource.dccAddress; + memo.guestBytes = guestBytes; + memo.format = resource.format; + memo.dccAlphaOnMsb = resource.dccAlphaOnMsb; + memo.proof = {}; + } + const auto scans = KeyProofCounts().scanned; + const auto proofs = KeyProofCounts().proved; + scanned = ProvedClearKeys(resource, guestBytes, memo.proof); + if (KeyProofCounts().scanned != scans) ++sampledLookupKeyScans; + if (KeyProofCounts().proved != proofs) ++sampledLookupKeyProofs; + if (profile) LookupOutcomes::Add(LookupOutcomes::DccScan, scanStart); return *scanned; }; auto& counters = TextureCounts(); @@ -831,7 +930,7 @@ std::shared_ptr cachedTexture(const Context& context, std::span cachedTexture(const Context& context, std::span cachedTexture(const Context& context, std::span(context, source, resource, components); - if (const auto clear = SampledClearColor(context, resource, *keys, depthCompare)) return std::make_shared(context, *context.detiler, resource, components, std::span{}, false, &*clear); + if (const auto clear = SampledClearColor(context, resource, *keys, depthCompare)) return std::make_shared(context, *context.detiler, resource, components, std::span{}, false, layerRows, &*clear); std::vector snapshot(bytes); ReadTextureSurface(resource, *keys, snapshot); - return std::make_shared(context, *context.detiler, resource, components, snapshot, depthCompare); + return std::make_shared(context, *context.detiler, resource, components, snapshot, depthCompare, layerRows); } auto snapshotGeneration = generation; std::optional>> committed; @@ -1043,7 +1142,7 @@ std::shared_ptr cachedTexture(const Context& context, std::spanCached() ? 0u : heldBytes(*source); counters.fromStorage.fetch_add(1, std::memory_order_relaxed); } else if (clear) { - entry.texture = createTexture(*context.detiler, resource, components, std::span{}, false, &*clear); + entry.texture = createTexture(*context.detiler, resource, components, std::span{}, false, layerRows, &*clear); entry.accounted = entry.texture->AllocationBytes(); counters.cleared.fetch_add(1, std::memory_order_relaxed); } else { @@ -1058,7 +1157,7 @@ std::shared_ptr cachedTexture(const Context& context, std::span(resource.baseAddress), resource.width, resource.height, resource.format, static_cast(resource.tileMode), nonzero, entry.bytes.size() / 64); } - entry.texture = createTexture(*context.detiler, resource, components, entry.bytes, depthCompare); + entry.texture = createTexture(*context.detiler, resource, components, entry.bytes, depthCompare, layerRows); entry.accounted = entry.texture->AllocationBytes(); constexpr std::size_t SnapshotCompareBytes = 256u << 10u; if (entry.bytes.size() > SnapshotCompareBytes && (entry.generation != 0 || entry.untracked[0] != 0)) parkSnapshot(cache, entry.bytes); @@ -1923,7 +2022,7 @@ struct ReusedTable { std::vector elements; std::vector queries; std::vector> ranges; - std::vector imageSamplers; + std::vector imageSamplers; std::vector samplers; std::vector bound; bool replayable = false; @@ -2058,6 +2157,9 @@ void ValidateRuntimeResources(const CompiledShader& shader, std::span shaders, const ColorTarget* target, std::uint32_t colorAttachments, std::uint64_t indexAddress, std::size_t indexBytes) { + registeredImageViews.clear(); + for (const auto& shader : shaders) if (shader.registeredImageViews != nullptr) registeredImageViews.insert(registeredImageViews.end(), shader.registeredImageViews->begin(), shader.registeredImageViews->end()); + Require(registeredImageViewsCurrent(registeredImageViews), "AGC graphics: registered image range changed after resource capture"); PerformanceTimer frameTiming("Resources.Prepare"); const auto stageStart = std::chrono::steady_clock::now(); phaseStart = stageStart; @@ -2454,17 +2556,21 @@ void ShaderResources::buildComplete() { PerformanceTimer frameTiming("Resources.Complete"); BuildLookups lookups; Require(!completed, "cannot update a published descriptor heap"); + Require(registeredImageViewsCurrent(registeredImageViews), "AGC graphics: registered image range changed after resource preparation"); const auto stageStart = std::chrono::steady_clock::now(); phaseStart = stageStart; try { // The lookups run in plan order: Revalidate walks the bindings the same way, and consecutive // storage elements of one mip chain share the previous element's image. previousSampled = {}; - for (auto& source : samplerSources) source.singleLevelImage = source.mipmappedImage = false; + for (auto& source : samplerSources) { + source.singleLevelImage = source.mipmappedImage = false; + source.pinnedLod.reset(); + } for (const auto& deferred : deferredImages) resolveImageBinding(*deferred.binding, bindings[deferred.index], std::span>(samplers).subspan(deferred.firstSampler, deferred.samplerCount)); previousSampled = {}; deferredImages.clear(); - applyAnisoOverride(); + applySamplerVariants(); // The records served their purpose: each holds the cache entry's objects as of stage A, // which would otherwise keep a replaced texture or an evicted storage image (and its device // memory) alive, outside the caches' budgets, for as long as this object is cached. @@ -2473,12 +2579,13 @@ void ShaderResources::buildComplete() { nextImageRecord = 0; Require(textures.size() == plannedSampledImages && storageTextures.size() == plannedStorageImages, "image lookups disagree with the descriptor plan"); resolveImageTables(); + Require(registeredImageViewsCurrent(registeredImageViews), "AGC graphics: registered image range changed during resource materialization"); timing.bindingsMs += phase(BuildPhase::Images); requireGuestBuffers(); guestMemory.UploadFinish(usesBda, htileClear); timing.uploadMs += phase(BuildPhase::Upload); - if (usesBda) bda = std::make_unique(context, guestMemory); - else if (usesFaultBuffer) bda = std::make_unique(context); + if (usesBda) bda = std::make_unique(context, guestMemory, faultReportOnly); + else if (usesFaultBuffer) bda = std::make_unique(context, faultReportOnly); if (bda) bda->SetSrtGuards(std::move(srtGuardFaults)); finishImageTables(); phase(BuildPhase::Bda); @@ -2557,7 +2664,9 @@ void ShaderResources::buildComplete() { switch (binding.layout.descriptorType) { case VK_DESCRIPTOR_TYPE_STORAGE_BUFFER: write.pBufferInfo = buffers.data() + buffers.size(); - for (const auto index : binding.allocations) { + for (std::size_t element = 0; element < binding.allocations.size(); ++element) { + const auto index = binding.allocations[element]; + if (allocations[index].role == ShaderRecompiler::DescriptorRole::FaultBuffer) faultDescriptors.push_back({writes.size(), buffers.size(), static_cast(element)}); buffers.push_back(descriptor(allocations[index])); } break; @@ -2782,7 +2891,10 @@ void VisitContentKey(const CompiledShader& shader, bool dataWords, bool movableB // must not serve a build with another (the variant implies it, this makes it explicit). packBits(binding.bufferWritten); visit(static_cast(binding.imageSamplers.size())); - for (const auto sampler : binding.imageSamplers) visit(sampler); + for (const auto sampler : binding.imageSamplers) { + visit(static_cast(sampler)); + visit(static_cast(sampler >> 32u)); + } visit(static_cast(binding.imageShapes.size())); for (const auto shape : binding.imageShapes) visit(static_cast(shape)); } @@ -2803,7 +2915,7 @@ void ShaderResources::AppendContentKey(const CompiledShader& shader, bool dataWo }; for (const auto& binding : shader.program->bindings) { addWords(20); - addWords(binding.imageSamplers.size()); + addWords(binding.imageSamplers.size() * 2u); addWords(binding.imageShapes.size()); if ((dataWords && !movableBuffers) || !DataRole(binding.role)) addWords(binding.guestDescriptor.size()); for (const auto* bits : {&binding.imageWritten, &binding.samplerDepthCompare, &binding.imageDepthCompare, &binding.imageAtomic, &binding.samplerUnnormalized, &binding.samplerIntegerBorder, &binding.samplerCoordinatesIgnored, &binding.imageUnnormalized, &binding.imageUnnormalizedBaseLevel, &binding.bufferWritten}) addWords(bits->size() / 32u + (bits->size() % 32u != 0 ? 1u : 0u)); @@ -3506,7 +3618,7 @@ ShaderResources::OwnRefreshFallback ShaderResources::refreshOwnObjects(std::span if (binding.kind == ShaderRecompiler::DescriptorKind::SampledImage) { for (std::uint32_t element = 0; element < binding.count; ++element) { const auto words = std::span(binding.guestDescriptor).subspan(static_cast(element) * 8u, 8u); - if (IsNullTextureResource(words)) continue; + if (IsNullSampledTextureResource(words)) continue; if (textureIndex >= textures.size()) return OwnRefreshFallback::Uncached; const auto index = textureIndex++; const auto overlap = listed(index, false); @@ -3591,6 +3703,11 @@ void noteEpochProof(bool reused, bool verified, bool failed) { } bool ShaderResources::Revalidate(std::span shaders, ProofReport* report) { + if (!registeredImageViewsCurrent(registeredImageViews)) { + if (report != nullptr) *report = {ProofPath::Full, ProofFailure::Other}; + provedEpoch = 0; + return false; + } if (EpochProofs()) { const auto epoch = GuestMemory::CollectEpoch(); const auto proof = GuestMemory::ProofSerial(); @@ -3669,11 +3786,12 @@ bool ShaderResources::revalidateFull(std::span shaders, Pr const auto raw = std::span(binding.guestDescriptor).subspan(static_cast(element) * elementWords, elementWords); std::array narrowed{}; const auto words = firstLayerWords(raw, binding.imageShape, narrowed, false); - if (IsNullTextureResource(words)) continue; + if (IsNullSampledTextureResource(words)) continue; const auto resource = DecodeTextureResource(words); const VkComponentMapping components = ViewComponents(resource); const VkImageViewType multisampleViewType = binding.imageShape == ShaderRecompiler::DescriptorImageShape::Image2DMultisampleArray ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D; - if (textureIndex >= textures.size() || cachedTexture(context, words, resource, components, 0, !binding.imageDepthCompare.empty() && binding.imageDepthCompare.at(element), multisampleViewType) != textures[textureIndex]) return false; + const bool depthCompare = !binding.imageDepthCompare.empty() && binding.imageDepthCompare.at(element); + if (textureIndex >= textures.size() || cachedTexture(context, words, resource, components, 0, depthCompare, multisampleViewType, layerRowsBinding(binding.imageShape, resource, depthCompare)) != textures[textureIndex]) return false; ++textureIndex; } } else if (binding.kind == ShaderRecompiler::DescriptorKind::StorageImage) { @@ -4894,7 +5012,7 @@ bool ShaderResources::precollectImages() { } ImageRecord record; record.sampled = sampled; - if (!IsNullTextureResource(words)) { + if (sampled ? !IsNullSampledTextureResource(words) : !IsNullTextureResource(words)) { const auto* memo = words.size() == 8 ? &decodedImage(context.device, words) : nullptr; record.resource = memo != nullptr ? memo->resource : DecodeTextureResource(words); record.guestBytes = memo != nullptr ? memo->guestBytes : DescribeSurface(record.resource).guestBytes; @@ -4978,12 +5096,12 @@ std::shared_ptr ShaderResources::fastTexture(const ImageRecord& record) return record.texture; } -void ShaderResources::applyAnisoOverride() { +void ShaderResources::applySamplerVariants() { for (std::size_t index = 0; index < samplerSources.size(); ++index) { const auto& source = samplerSources[index]; const auto variant = SingleLevelSamplerWords(source.words, source.singleLevelImage, source.mipmappedImage); - if (!variant.has_value()) continue; - const auto& words = *variant; + if (!variant.has_value() && !source.pinnedLod.has_value()) continue; + const auto words = source.pinnedLod.has_value() ? PinnedLodSamplerWords(variant.has_value() ? *variant : source.words, *source.pinnedLod) : *variant; if (context.samplerCache != nullptr && !SamplerCacheDisabled()) { samplers[index] = context.samplerCache->Get(context, words, source.compareEnable, source.unnormalized, true, source.integerBorder); } else { @@ -5006,35 +5124,47 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind std::array narrowed{}; const auto words = firstLayerWords(raw, binding.imageShape, narrowed, true); const auto* record = nextRecord(); - if (IsNullTextureResource(words)) { + if (IsNullSampledTextureResource(words)) { item.imageAllocations.push_back(std::numeric_limits::max()); --plannedSampledImages; continue; } const auto resource = record != nullptr && record->decoded ? record->resource : DecodeTextureResource(words); + const auto minLod = context.shaderImageMinLod && resource.minLod > resource.baseLevel * 256u && resource.lastLevel > resource.baseLevel ? std::min(resource.minLod - resource.baseLevel * 256u, (resource.lastLevel - resource.baseLevel) * 256u) : 0u; + if (minLod != 0u && !shaderSamplers.empty()) { + const auto firstSampler = static_cast(shaderSamplers.data() - samplers.data()); + for (std::uint32_t sampler = 0; sampler < shaderSamplers.size() && sampler < 32u; ++sampler) { + auto& source = samplerSources.at(firstSampler + sampler); + if (((binding.imageSamplers[element] >> sampler) & 1u) == 0 || !HoldsLodAtZero(source.words)) continue; + Require(!source.pinnedLod.has_value() || *source.pinnedLod == minLod, "an S# that holds the LOD at 0 is paired with images of different minimum LODs"); + source.pinnedLod = minLod; + } + } const bool forcedSrgb = ForcesDegammaOn(resource.format, binding.imageSamplers[element], shaderSamplers); RequireNonSeamlessCube(resource.dimension == TextureDimension::kCube, binding.imageSamplers[element], shaderSamplers); const bool depthCompare = !binding.imageDepthCompare.empty() && binding.imageDepthCompare.at(element); const bool firstLayer = binding.imageShape == ShaderRecompiler::DescriptorImageShape::Image2D && (resource.dimension == TextureDimension::k2DArray || (depthCompare && resource.dimension == TextureDimension::k1DArray)); const bool depth1D = depthCompare && ((binding.imageShape == ShaderRecompiler::DescriptorImageShape::Image2D && resource.dimension == TextureDimension::k1D) || (binding.imageShape == ShaderRecompiler::DescriptorImageShape::Image2DArray && resource.dimension == TextureDimension::k1DArray)); const bool firstSample = binding.imageShape == ShaderRecompiler::DescriptorImageShape::Image2D && resource.dimension == TextureDimension::k2DMsaa; - if (!firstLayer && !depth1D && !firstSample && !MatchesGuestDimension(*binding.imageShape, resource.dimension)) throw std::runtime_error("AGC graphics: guest texture dimension disagrees with the shader's declared image shape (shape " + std::to_string(static_cast(*binding.imageShape)) + ", dimension " + std::to_string(static_cast(resource.dimension)) + ")"); + const bool layerRows = layerRowsBinding(binding.imageShape, resource, depthCompare); + Require(!layerRows || (resource.baseArray <= resource.depthOrLastArray && resource.depthOrLastArray - resource.baseArray >= context.limits.maxImageArrayLayers), "a 1D array within maxImageArrayLayers was bound as rows of a 2D image"); + if (!firstLayer && !depth1D && !firstSample && !layerRows && !MatchesGuestDimension(*binding.imageShape, resource.dimension)) throw std::runtime_error("AGC graphics: guest texture dimension disagrees with the shader's declared image shape (shape " + std::to_string(static_cast(*binding.imageShape)) + ", dimension " + std::to_string(static_cast(resource.dimension)) + ")"); const VkComponentMapping components = ViewComponents(resource); const auto guestBytes = record != nullptr && record->decoded ? record->guestBytes : DescribeSurface(resource).guestBytes; CompletePendingCpuWrites(context, resource.baseAddress, static_cast(guestBytes)); if (resource.dccAddress != 0) CompletePendingCpuWrites(context, resource.dccAddress, DccKeyBytes(guestBytes)); std::shared_ptr texture; const VkImageViewType multisampleViewType = binding.imageShape == ShaderRecompiler::DescriptorImageShape::Image2DMultisampleArray ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D; - if (previousSampled.texture != nullptr && previousSampled.depthCompare == depthCompare && previousSampled.multisampleViewType == multisampleViewType && previousSampled.firstSample == firstSample && words.size() == 8 && std::equal(words.begin(), words.end(), previousSampled.words.begin())) texture = previousSampled.texture; + if (previousSampled.texture != nullptr && previousSampled.depthCompare == depthCompare && previousSampled.multisampleViewType == multisampleViewType && previousSampled.firstSample == firstSample && previousSampled.layerRows == layerRows && words.size() == 8 && std::equal(words.begin(), words.end(), previousSampled.words.begin())) texture = previousSampled.texture; if (texture == nullptr && !firstSample && record != nullptr && record->texture != nullptr) { texture = fastTexture(*record); (texture != nullptr ? counters.fastHits : counters.fastMisses).fetch_add(1, std::memory_order_relaxed); } if (texture == nullptr && firstSample) { - Require(!depthCompare && !DepthSurfaceAt(resource.baseAddress), "reading sample 0 of a multisampled depth surface through a 2D image is not implemented"); - texture = MultisampleTargetFirstSample(context, resource, components); + Require(!depthCompare, "reading sample 0 of a multisampled texture with a depth comparison through a 2D image is not implemented"); + texture = DepthSurfaceAt(resource.baseAddress) ? DepthSurfaceFirstSample(context, words, resource, components) : MultisampleTargetFirstSample(context, resource, components); } - if (texture == nullptr) texture = cachedTexture(context, words, resource, components, guestBytes, depthCompare, multisampleViewType); + if (texture == nullptr) texture = cachedTexture(context, words, resource, components, guestBytes, depthCompare, multisampleViewType, layerRows); if (resource.dimension == TextureDimension::k2DMsaa) multisampleSources.push_back(resource.baseAddress); bool baseLevel = false; if (element < binding.imageUnnormalized.size() && binding.imageUnnormalized[element]) { @@ -5051,8 +5181,9 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind if (!shaderSamplers.empty()) { const bool singleLevel = DescriptorSingleLevel(words); const auto firstSampler = static_cast(shaderSamplers.data() - samplers.data()); - for (std::uint32_t sampler = 0; sampler < shaderSamplers.size() && sampler < 32u; ++sampler) { - if (((binding.imageSamplers[element] >> sampler) & 1u) == 0) continue; + for (auto mask = binding.imageSamplers[element]; mask != 0u; mask &= mask - 1u) { + const auto sampler = static_cast(std::countr_zero(mask)); + if (sampler >= shaderSamplers.size()) break; auto& source = samplerSources.at(firstSampler + sampler); (singleLevel ? source.singleLevelImage : source.mipmappedImage) = true; } @@ -5062,6 +5193,7 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind previousSampled.depthCompare = depthCompare; previousSampled.multisampleViewType = multisampleViewType; previousSampled.firstSample = firstSample; + previousSampled.layerRows = layerRows; previousSampled.texture = texture; } textures.push_back(std::move(texture)); @@ -5305,8 +5437,9 @@ void ShaderResources::resolveImageTables() { RequireNonSeamlessCube(resource.dimension == TextureDimension::kCube, binding.imageSamplers.at(element), stageSamplers); const auto numeric = ShaderRecompiler::DepthBitsTextureWidth(words[1], words[3]) != 0u ? ShaderRecompiler::IrTextureNumericClass::Float : ShaderRecompiler::SampledTextureNumericClass(static_cast(resource.format)); const bool integerImage = numeric == ShaderRecompiler::IrTextureNumericClass::Uint || numeric == ShaderRecompiler::IrTextureNumericClass::Sint; - for (std::uint32_t sampler = 0; sampler < stageSamplers.size() && sampler < 32u; ++sampler) { - if (((binding.imageSamplers.at(element) >> sampler) & 1u) == 0) continue; + for (auto mask = binding.imageSamplers.at(element); mask != 0u; mask &= mask - 1u) { + const auto sampler = static_cast(std::countr_zero(mask)); + if (sampler >= stageSamplers.size()) break; const auto& source = samplerSources.at(table.firstSampler + sampler); const auto descriptor = DecodeSamplerResource(source.words, source.unnormalized, true, source.integerBorder); const bool border = descriptor.addressModeU == VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER || descriptor.addressModeV == VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER || descriptor.addressModeW == VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER; @@ -5437,6 +5570,7 @@ void ShaderResources::resolveImageTables() { const bool depthCompare = binding.imageDepthCompare.at(element); const VkImageViewType multisampleViewType = shape == ShaderRecompiler::DescriptorImageShape::Image2DMultisampleArray ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D; try { + Require(context.imageViewMinLod || EffectiveMinLod(resource) == 0.0f, "a sampled image table entry has a minimum LOD clamp, which needs VK_EXT_image_view_min_lod"); texture = cachedTexture(context, sampledWords, resource, components, guestBytes, depthCompare, multisampleViewType); } catch (const std::runtime_error& error) { table.map[faults + element] = rejectTableElement(table, words, error.what()); @@ -5476,6 +5610,10 @@ std::uint64_t SampledLookupKeyScans() { return sampledLookupKeyScans; } +std::uint64_t SampledLookupKeyProofs() { + return sampledLookupKeyProofs; +} + std::uint64_t SampledTexturesCleared() { return TextureCounts().cleared.load(std::memory_order_relaxed); } @@ -5800,6 +5938,7 @@ std::size_t ShaderResources::DrawUploadLimit() { std::shared_ptr ShaderResources::PrepareDrawBindings(Recorder& recorder, std::span moved, bool movedOnly) const { requireGuestBuffers(moved); if ((_set == VK_NULL_HANDLE && fragment.set == VK_NULL_HANDLE) || usesBda) return {}; + prepareFaultSlot(); thread_local std::vector>* readsSlot = nullptr; auto& reads = ShaderRecompiler::ThreadOwned(readsSlot); guestMemory.InPlaceReads(reads); @@ -5947,6 +6086,24 @@ std::shared_ptr ShaderResources::PrepareDrawBindi return result; } +void ShaderResources::prepareFaultSlot() const { + if (bda == nullptr) return; + const auto* recorder = Recorder::Active(); + bda->NoteUse(recorder != nullptr && recorder->Device() == context.device ? recorder->OpenSerial() : std::numeric_limits::max()); + if (!bda->TakeFaultSlot(context)) return; + std::vector writes; + writes.reserve(faultDescriptors.size()); + for (const auto& fault : faultDescriptors) { + descriptorBuffers[fault.buffer] = bda->Fault(); + auto write = descriptorWrites[fault.write]; + write.dstArrayElement = fault.element; + write.descriptorCount = 1; + write.pBufferInfo = &descriptorBuffers[fault.buffer]; + writes.push_back(write); + } + if (!writes.empty()) context.Resolved(&DeviceFunctions::updateDescriptorSets, "vkUpdateDescriptorSets")(context.device, static_cast(writes.size()), writes.data(), 0, nullptr); +} + void ShaderResources::requireGuestBuffers(std::span moved) const { for (std::size_t index = 0; index < allocations.size(); ++index) { const auto& item = allocations[index]; @@ -5961,6 +6118,7 @@ void ShaderResources::requireGuestBuffers(std::span moved) co void ShaderResources::Bind(VkCommandBuffer commands, VkPipelineBindPoint bindPoint, VkPipelineLayout layout) const { requireGuestBuffers(); + prepareFaultSlot(); if (_set != VK_NULL_HANDLE) context.Resolved(&DeviceFunctions::cmdBindDescriptorSets, "vkCmdBindDescriptorSets")(commands, bindPoint, layout, 0, 1, &_set, 0, nullptr); if (fragment.set != VK_NULL_HANDLE) context.Resolved(&DeviceFunctions::cmdBindDescriptorSets, "vkCmdBindDescriptorSets")(commands, bindPoint, layout, ShaderRecompiler::RuntimeAbi::FragmentDescriptorSet, 1, &fragment.set, 0, nullptr); } @@ -5992,6 +6150,7 @@ void ShaderResources::WriteBack() { } void ShaderResources::MarkGpuWrites(Recorder& recorder) { + if (bda != nullptr && recorder.Device() == context.device) bda->NoteUse(recorder.OpenSerial()); // The ranges this use reads in place through their host imports (read-only and written elements // alike, and an address-based build's whole leased heaps), before the writes: a CPU store into // one of them (the copy HLE) must not land before the recorded work read it. diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp index 8b8bd0a0d4..a29670c0c3 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp @@ -168,7 +168,7 @@ struct Module { } }; -Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing, bool imageInt64Atomics, bool geometryShader, bool sampleRateShading, bool bufferInt64Atomics, bool shaderClipDistance, bool shaderCullDistance, bool viewportIndexLayer, bool runtimeDescriptorArray, bool demoteToHelperInvocation, bool shaderTerminateInvocation, std::span primitiveLocations = {}) { +Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing, bool imageInt64Atomics, bool geometryShader, bool sampleRateShading, bool bufferInt64Atomics, bool shaderClipDistance, bool shaderCullDistance, bool viewportIndexLayer, bool runtimeDescriptorArray, bool demoteToHelperInvocation, bool shaderTerminateInvocation, bool shaderResourceMinLod, std::span primitiveLocations = {}) { using Stage = ShaderRecompiler::ShaderStage; Require(compiled.program != nullptr, "missing compiled shader"); const auto& shader = *compiled.program; @@ -282,6 +282,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi capability == spv::CapabilityImage1D || capability == spv::CapabilityImageGatherExtended || capability == spv::CapabilityImageQuery || + (shaderResourceMinLod && capability == spv::CapabilityMinLod) || ((vertex || control || evaluation || mesh) && shaderClipDistance && capability == spv::CapabilityClipDistance) || (vertex && capability == spv::CapabilityDrawParameters) || (fragment && geometryShader && capability == spv::CapabilityGeometry) || @@ -668,7 +669,7 @@ bool DeclaresEarlyFragmentTests(const ShaderRecompiler::RecompileResult& program return false; } -std::set ValidateShaders(std::span shaders, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing, bool imageInt64Atomics, bool geometryShader, bool sampleRateShading, bool bufferInt64Atomics, bool shaderClipDistance, bool shaderCullDistance, bool viewportIndexLayer, bool runtimeDescriptorArray, bool demoteToHelperInvocation, bool shaderTerminateInvocation, bool meshShader) { +std::set ValidateShaders(std::span shaders, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing, bool imageInt64Atomics, bool geometryShader, bool sampleRateShading, bool bufferInt64Atomics, bool shaderClipDistance, bool shaderCullDistance, bool viewportIndexLayer, bool runtimeDescriptorArray, bool demoteToHelperInvocation, bool shaderTerminateInvocation, bool meshShader, bool shaderResourceMinLod) { using Stage = ShaderRecompiler::ShaderStage; const bool tessellation = state.stages.path == ShaderPath::Tessellation; const bool mesh = state.stages.path == ShaderPath::Geometry; @@ -706,7 +707,7 @@ std::set ValidateShaders(std::span shaders, Require(shaders[i].program != nullptr, "missing compiled shader"); Require(shaders[i].stage == expected, "graphics stage order disagrees"); for (const auto& binding : shaders[i].program->bindings) Require(binding.descriptorSet == ShaderRecompiler::RuntimeAbi::DescriptorSet || (shaders[i].stage == ShaderRecompiler::ShaderStage::Fragment && binding.descriptorSet == ShaderRecompiler::RuntimeAbi::FragmentDescriptorSet), "graphics resource uses a descriptor set other than its stage's"); - const auto current = Inspect(shaders[i], state, subgroup, fragmentShaderBarycentric, descriptorIndexing, imageInt64Atomics, geometryShader, sampleRateShading, bufferInt64Atomics, shaderClipDistance, shaderCullDistance, viewportIndexLayer, runtimeDescriptorArray, demoteToHelperInvocation, shaderTerminateInvocation, primitiveLocations); + const auto current = Inspect(shaders[i], state, subgroup, fragmentShaderBarycentric, descriptorIndexing, imageInt64Atomics, geometryShader, sampleRateShading, bufferInt64Atomics, shaderClipDistance, shaderCullDistance, viewportIndexLayer, runtimeDescriptorArray, demoteToHelperInvocation, shaderTerminateInvocation, shaderResourceMinLod, primitiveLocations); if (i != 0) { for (const auto& [location, signature] : current.inputs) { const auto output = previous.outputs.find(location); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 5f117bfec7..bd7c7e9e2d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -451,6 +451,7 @@ DepthTarget decodeDepthPlane(const Registers& cx) { DepthTarget depth{}; depth.samples = 1u << ((read(cx, 0x010) >> 2u) & 3u); const bool gcn = ShaderRecompiler::GcnGuestShaders(); + depth.gcn = gcn; depth.depthSwizzle = gcn ? 0u : (read(cx, 0x010) >> 4u) & 31u; depth.iterate256 = !gcn && (read(cx, 0x010) & (1u << 20u)) != 0; depth.firstSlice = view & 0x1fffu; @@ -1010,12 +1011,22 @@ struct DecodedColorFormat { ShaderRecompiler::ColorExportPacking packing = ShaderRecompiler::ColorExportPacking::None; }; +bool UintPacked(ShaderRecompiler::ColorExportPacking packing) { + return packing == ShaderRecompiler::ColorExportPacking::Uint || packing == ShaderRecompiler::ColorExportPacking::UintClamp8 || packing == ShaderRecompiler::ColorExportPacking::UintClamp16; +} + const char* PackedColorTargetName(ShaderRecompiler::ColorExportPacking packing) { if (packing == ShaderRecompiler::ColorExportPacking::Unorm4_4_4_4) return "4_4_4_4 unorm"; - if (packing == ShaderRecompiler::ColorExportPacking::Uint) return "unsigned integer"; + if (UintPacked(packing)) return "unsigned integer"; return packing == ShaderRecompiler::ColorExportPacking::Float11_11_10 ? "11_11_10 float" : "10_11_11 unorm"; } +ShaderRecompiler::ColorExportPacking HalfExportPacking(VkFormat format) { + if (format == VK_FORMAT_R8_UINT || format == VK_FORMAT_R8G8_UINT || format == VK_FORMAT_R8G8B8A8_UINT) return ShaderRecompiler::ColorExportPacking::UintClamp8; + if (format == VK_FORMAT_R16_UINT || format == VK_FORMAT_R16G16_UINT || format == VK_FORMAT_R16G16B16A16_UINT) return ShaderRecompiler::ColorExportPacking::UintClamp16; + return ShaderRecompiler::ColorExportPacking::Uint; +} + bool WordPacked(ShaderRecompiler::ColorExportPacking packing) { return packing == ShaderRecompiler::ColorExportPacking::Unorm10_11_11 || packing == ShaderRecompiler::ColorExportPacking::Float11_11_10 || packing == ShaderRecompiler::ColorExportPacking::Unorm4_4_4_4; } @@ -1839,7 +1850,7 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { if ((weighted(state.colorBlendOp) && (constantColor(state.srcColorBlendFactor) || constantColor(state.dstColorBlendFactor))) || (weighted(state.alphaBlendOp) && (constantColor(state.srcAlphaBlendFactor) || constantColor(state.dstAlphaBlendFactor)))) throw std::runtime_error("AGC graphics: blending into an 8_8 color target with the alternate component order with a constant color factor, which Vulkan applies to the second channel with the green blend constant instead of the alpha one, is not implemented"); } } - if (color.packing == ShaderRecompiler::ColorExportPacking::Uint) state.blendEnable = VK_FALSE; + if (UintPacked(color.packing)) state.blendEnable = VK_FALSE; if (WordPacked(color.packing)) { const std::string packed = std::string(color.packing == ShaderRecompiler::ColorExportPacking::Float11_11_10 ? "an " : "a ") + PackedColorTargetName(color.packing); const auto slotExport = (exportFormat >> (4u * color.exportIndex)) & 0xfu; @@ -1868,7 +1879,8 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { } if (color.packing == ShaderRecompiler::ColorExportPacking::Uint) { const auto slotExport = (exportFormat >> (4u * color.exportIndex)) & 0xfu; - if (slotExport != 1 && slotExport != 2 && slotExport != 3 && slotExport != 9) throw std::runtime_error("AGC graphics: color export format " + std::to_string(slotExport) + " into an unsigned integer target is unsupported"); + if (slotExport == 4) color.packing = HalfExportPacking(color.format); + else if (slotExport != 1 && slotExport != 2 && slotExport != 3 && slotExport != 9) throw std::runtime_error("AGC graphics: color export format " + std::to_string(slotExport) + " into an unsigned integer target is unsupported"); } if (state.blendEnable && !reversed && (secondSource(state.srcColorBlendFactor) || secondSource(state.dstColorBlendFactor) || secondSource(state.srcAlphaBlendFactor) || secondSource(state.dstAlphaBlendFactor))) { const auto addition = [](VkBlendOp op) { return op == VK_BLEND_OP_ADD || op == VK_BLEND_OP_SUBTRACT || op == VK_BLEND_OP_REVERSE_SUBTRACT; }; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 10bb34fedc..14a15c3f38 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -89,6 +89,8 @@ struct TextureProfile { // batch of their own (APS5_NO_RECORDED_CLEAR=1, or no recorder). std::uint64_t storageRecordedClears = 0; std::uint64_t storageWaitedClears = 0; + std::uint64_t layerRows = 0; + double layerRowsMs = 0; }; TextureProfile& Profile() { @@ -132,6 +134,7 @@ std::atomic queuedReadHeld{0}; std::atomic flushedAfterSkip{0}, flushedAfterSkipSameSite{0}, staleEvicted{0}; std::atomic depthCopyTextures{0}, depthCopyRefreshes{0}; std::atomic refreshesProved{0}; +std::atomic refreshesProvedPendingKeys{0}; std::atomic untrackedCompares{0}; struct StorageTraffic { @@ -188,7 +191,7 @@ void reportStorageTraffic(StorageTraffic& traffic) { const auto take = [](std::atomic& counter) { return static_cast(counter.exchange(0, std::memory_order_relaxed)); }; const auto partialUploadCount = take(partialUploads), partialWriteBackCount = take(partialWriteBacks); const auto uploadMiB = take(partialUploadBytes) / 1048576.0, writeBackMiB = take(partialWriteBackBytes) / 1048576.0; - AgcDriver::ProfilePrint_nid_no_patch( "[storage] uploads by path (count/MiB, 10 s):%s; %llu write-backs GPU-direct, %llu stored nothing; block units: partial uploads %llu/%.1f, partial write-backs %llu/%.1f, %llu widened to every pending unit, units dropped %llu, superseded %llu, handed over at %llu clear-key refreshes (%llu units superseded), dropped unregistered %llu, kept over a key flip %llu%s, pretest skipped %llu, held for a queued draw read %llu, evicted stale %llu; drawn linear targets: stores by reason (count/MiB):%s, pending %zu images / %.1f MiB, oldest %.1f ms\n", line.c_str(), static_cast(traffic.directWriteBacks), take(emptyWriteBacks), partialUploadCount, uploadMiB, partialWriteBackCount, writeBackMiB, take(coalescedWriteBacks), take(unitsDropped), take(unitsSuperseded), take(clearHandOvers), take(handedOverSuperseded), take(unregisteredDropped), take(keyFlipKept), ShadowReport().c_str(), take(pretestSkipped), take(queuedReadHeld), take(staleEvicted), drawnLine.c_str(), drawn.images, drawn.bytes / 1048576.0, drawn.oldestMs); + AgcDriver::ProfilePrint_nid_no_patch( "[storage] uploads by path (count/MiB, 10 s):%s; %llu write-backs GPU-direct, %llu stored nothing; block units: partial uploads %llu/%.1f, partial write-backs %llu/%.1f, %llu widened to every pending unit, units dropped %llu, superseded %llu, handed over at %llu clear-key refreshes (%llu units superseded), dropped unregistered %llu, kept over a key flip %llu%s, pretest skipped %llu, held for a queued draw read %llu, evicted stale %llu; refreshes proved %llu, %llu of them over pending driver key stores (cumulative); drawn linear targets: stores by reason (count/MiB):%s, pending %zu images / %.1f MiB, oldest %.1f ms\n", line.c_str(), static_cast(traffic.directWriteBacks), take(emptyWriteBacks), partialUploadCount, uploadMiB, partialWriteBackCount, writeBackMiB, take(coalescedWriteBacks), take(unitsDropped), take(unitsSuperseded), take(clearHandOvers), take(handedOverSuperseded), take(unregisteredDropped), take(keyFlipKept), ShadowReport().c_str(), take(pretestSkipped), take(queuedReadHeld), take(staleEvicted), static_cast(refreshesProved.load(std::memory_order_relaxed)), static_cast(refreshesProvedPendingKeys.load(std::memory_order_relaxed)), drawnLine.c_str(), drawn.images, drawn.bytes / 1048576.0, drawn.oldestMs); traffic.writeBacks.clear(); traffic.uploadReasons.clear(); traffic.drawnStores.clear(); @@ -293,7 +296,8 @@ namespace { void ChainMinLod(const Context& context, const GuestTextureResource& descriptor, VkImageViewCreateInfo& viewInfo, VkImageViewMinLodCreateInfoEXT& minLod) { const auto clamp = EffectiveMinLod(descriptor); if (clamp == 0.0f) return; - Require(context.imageViewMinLod, "guest texture descriptor uses a minimum LOD clamp, which needs VK_EXT_image_view_min_lod"); + Require(context.imageViewMinLod || context.shaderImageMinLod, "guest texture descriptor uses a minimum LOD clamp, which needs VK_EXT_image_view_min_lod, or shaderResourceMinLod without graphics pipeline libraries"); + if (!context.imageViewMinLod) return; minLod.minLod = clamp; minLod.pNext = viewInfo.pNext; viewInfo.pNext = &minLod; @@ -357,10 +361,11 @@ std::uint64_t SampledTextureMemory() { return SampledMemoryCounter().load(std::memory_order_relaxed); } -Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTextureResource& descriptor, VkComponentMapping components, std::span snapshot, bool depthCompare, const VkClearColorValue* clear) : context(context) { +Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTextureResource& descriptor, VkComponentMapping components, std::span snapshot, bool depthCompare, bool layerRows, const VkClearColorValue* clear) : context(context) { static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; PhaseTimer timer; + const auto started = profile && layerRows ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; try { const auto colorFormat = SampledTextureFormat(context, descriptor.format); const bool srgbExpand = !depthCompare && SrgbExpandsToRgba8(context, descriptor.format); @@ -371,6 +376,12 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex if (IsBlockCompressed(descriptor.format)) { Require(context.textureCompressionBC, "device does not support BC compressed textures"); } + if (layerRows) { + Require(!depthCompare && descriptor.dimension == TextureDimension::k1DArray, "only a sampled 1D array is laid out as rows of a 2D image"); + Require(!srgbExpand, "a 1D array laid out as rows of a 2D image in a format expanded to RGBA8 is not implemented"); + Require(!IsBlockCompressed(descriptor.format), "a block-compressed 1D array laid out as rows of a 2D image is not implemented"); + Require(descriptor.baseLevel == 0u && std::min(descriptor.lastLevel, descriptor.mipCount - 1u) == 0u, "a 1D array past maxImageArrayLayers viewed at mip levels other than level 0 alone is not implemented"); + } const auto geometry = DescribeSurface(descriptor); const auto& mips = geometry.mips; @@ -391,14 +402,16 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex const auto linearBytes = sliceLinearBytes * arrayLayers; const bool depth1D = depthCompare && (descriptor.dimension == TextureDimension::k1D || descriptor.dimension == TextureDimension::k1DArray); + const auto rowLayers = layerRows ? geometry.imageLayers - descriptor.baseArray : 0u; + Require(!layerRows || (descriptor.baseArray < geometry.imageLayers && rowLayers <= context.limits.maxImageDimension2D / 2u), "a 1D array past maxImageArrayLayers whose viewed slices need more than maxImageDimension2D rows is not implemented"); VkImageCreateInfo imageInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; imageInfo.flags = (descriptor.dimension == TextureDimension::kCube ? VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT : 0u) | (SrgbTwinFormat(context, vkFormat) != VK_FORMAT_UNDEFINED ? VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT : 0u); - imageInfo.imageType = depth1D ? VK_IMAGE_TYPE_2D : ImageTypeFor(descriptor.dimension); + imageInfo.imageType = depth1D || layerRows ? VK_IMAGE_TYPE_2D : ImageTypeFor(descriptor.dimension); imageInfo.format = vkFormat; - imageInfo.extent = {descriptor.width, depth1D ? 1u : descriptor.height, geometry.imageDepth}; - imageInfo.mipLevels = descriptor.mipCount; - Require(geometry.imageLayers <= context.limits.maxImageArrayLayers, "a texture array with more layers than maxImageArrayLayers is not implemented"); - imageInfo.arrayLayers = geometry.imageLayers; + imageInfo.extent = {descriptor.width, layerRows ? rowLayers * 2u : depth1D ? 1u : descriptor.height, geometry.imageDepth}; + imageInfo.mipLevels = layerRows ? 1u : descriptor.mipCount; + Require(layerRows || geometry.imageLayers <= context.limits.maxImageArrayLayers, "a texture array with more layers than maxImageArrayLayers is not implemented"); + imageInfo.arrayLayers = layerRows ? 1u : geometry.imageLayers; imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | (srgbExpand ? VK_IMAGE_USAGE_TRANSFER_SRC_BIT : 0u); @@ -438,7 +451,7 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex batch = std::make_unique(context); commands = batch->Handle(); } - const VkImageSubresourceRange range{aspect, 0, descriptor.mipCount, 0, geometry.imageLayers}; + const VkImageSubresourceRange range{aspect, 0, imageInfo.mipLevels, 0, imageInfo.arrayLayers}; VkImageMemoryBarrier toTransferDst{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; toTransferDst.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; toTransferDst.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; @@ -544,12 +557,22 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex toTransferDst.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; toTransferDst.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; toTransferDst.image = uploadImage; - toTransferDst.subresourceRange = {aspect, 0, descriptor.mipCount, 0, geometry.imageLayers}; + toTransferDst.subresourceRange = layerRows ? VkImageSubresourceRange{aspect, 0, 1, 0, 1} : VkImageSubresourceRange{aspect, 0, descriptor.mipCount, 0, geometry.imageLayers}; context.Function("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 1, &linearReadBarrier, 1, &toTransferDst); std::vector regions; regions.reserve(static_cast(arrayLayers) * mips.size()); - for (std::uint32_t layer = 0; layer < arrayLayers; ++layer) { + for (std::uint32_t row = 0; row < rowLayers * 2u; ++row) { + const auto layer = descriptor.baseArray + row / 2u; + VkBufferImageCopy region{}; + region.bufferOffset = static_cast(layer) * sliceLinearBytes + mips[0].linearOffset; + region.bufferRowLength = mips[0].pitchBytes / BytesPerElement(descriptor.format) * BlockWidth(descriptor.format); + region.imageSubresource = {aspect, 0, 0, 1}; + region.imageOffset = {0, static_cast(row), 0}; + region.imageExtent = {descriptor.width, 1u, 1u}; + regions.push_back(region); + } + for (std::uint32_t layer = 0; layer < (layerRows ? 0u : arrayLayers); ++layer) { const auto linearLayerOffset = static_cast(layer) * sliceLinearBytes; for (std::uint32_t level = 0; level < descriptor.mipCount; ++level) { if (!geometry.HasLayer(level, layer)) continue; @@ -626,11 +649,11 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; viewInfo.image = image; - viewInfo.viewType = depth1D ? (descriptor.dimension == TextureDimension::k1DArray ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D) : ViewTypeFor(descriptor.dimension, viewLayerCount); + viewInfo.viewType = layerRows ? VK_IMAGE_VIEW_TYPE_2D : depth1D ? (descriptor.dimension == TextureDimension::k1DArray ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D) : ViewTypeFor(descriptor.dimension, viewLayerCount); viewInfo.format = srgbExpand ? VK_FORMAT_R8G8B8A8_SRGB : vkFormat; viewFormat = viewInfo.format; viewInfo.components = depthCompare ? VkComponentMapping{} : components; - viewInfo.subresourceRange = {aspect, descriptor.baseLevel, viewLevelCount, descriptor.baseArray, viewLayerCount}; + viewInfo.subresourceRange = layerRows ? VkImageSubresourceRange{aspect, 0, 1, 0, 1} : VkImageSubresourceRange{aspect, descriptor.baseLevel, viewLevelCount, descriptor.baseArray, viewLayerCount}; VkImageViewMinLodCreateInfoEXT minLod{VK_STRUCTURE_TYPE_IMAGE_VIEW_MIN_LOD_CREATE_INFO_EXT}; ChainMinLod(context, descriptor, viewInfo, minLod); @@ -643,6 +666,11 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex auto& totals = Profile(); totals.view += timer.lap(); if (++totals.count % 200 == 0) std::fprintf(stderr, "[texture] %llu textures (%llu copied from storage images, %llu uploads recorded): allocate+image %.0f ms, guest read %.0f ms, detile+copy %.0f ms, view+buffer release %.0f ms\n", static_cast(totals.count), static_cast(totals.fromStorage), static_cast(totals.recordedUploads), totals.allocate, totals.read, totals.gpu, totals.view); + if (layerRows) { + const auto ms = std::chrono::duration(std::chrono::steady_clock::now() - started).count(); + totals.layerRowsMs += ms; + std::fprintf(stderr, "[texture] layer rows 0x%llx: %u slices of %u texels as a %ux%u image in %.2f ms (%llu layer-row textures, %.0f ms in all)\n", static_cast(descriptor.baseAddress), rowLayers, descriptor.width, descriptor.width, rowLayers * 2u, ms, static_cast(++totals.layerRows), totals.layerRowsMs); + } } countedBytes = allocationBytes; SampledMemoryCounter().fetch_add(countedBytes, std::memory_order_relaxed); @@ -1210,7 +1238,7 @@ void Texture::prepareSrgbView(const GuestTextureResource& descriptor, VkImageVie viewInfo.pNext = nullptr; viewInfo.format = format; srgbInfo = viewInfo; - srgbMinLod = EffectiveMinLod(descriptor); + srgbMinLod = context.imageViewMinLod ? EffectiveMinLod(descriptor) : 0.0f; } VkImageView Texture::SrgbView(bool firstLayer, bool baseLevel) const { @@ -1531,6 +1559,7 @@ StorageTexture::StorageTexture(const Context& context, TextureDetiler& detiler, trackedLayers = blockUnits ? static_cast((guestBytes + 65535) / 65536) : layered && LayerTrackingEnabled() ? geometry.layers : 1u; trackedLayerBytes = blockUnits ? 65536 : guestBytes / trackedLayers; layerGeneration.assign(trackedLayers, 0); + layerPendingSince.assign(trackedLayers, 0); unwatchHash.assign(trackedLayers, {}); pendingMiss.assign(trackedLayers, BaselineMiss::None); layerPending.assign(trackedLayers, false); @@ -2243,6 +2272,7 @@ bool StorageTexture::refresh(std::uint32_t firstLayer, std::uint32_t lastLayer) } NoteProved(); refreshesProved.fetch_add(1, std::memory_order_relaxed); + if (profile && keyProof.pendingNote != 0) refreshesProvedPendingKeys.fetch_add(1, std::memory_order_relaxed); if (profile) LookupOutcomes::Add(LookupOutcomes::RefreshProved, start); return true; } @@ -2552,7 +2582,7 @@ bool StorageTexture::refresh(std::uint32_t firstLayer, std::uint32_t lastLayer) DccKeys StorageTexture::ProvedKeys() const { std::optional keys; if (descriptor.dccAddress != 0 && uploadedKeys == DccKeys::Uncompressed) keys = WaitForKeyWriters(descriptor, guestBytes); - if (keys.has_value()) keyProof = {}; + if (keys.has_value()) keyProof = PendingKeyProof(descriptor, guestBytes, *keys); else keys = ProvedClearKeys(descriptor, guestBytes, keyProof); if (IsDccClear(filledKeys) && *keys != filledKeys && !anyLayerPending()) filledKeys = DccKeys::Uncompressed; return *keys; @@ -2587,7 +2617,7 @@ bool StorageTexture::refreshProved() { queries[used++] = {descriptor.baseAddress, bytes, generation}; if (descriptor.dccAddress != 0) { const auto keyCount = static_cast(guestBytes / 256); - if (keyCount == 0 || keyProof.generation != refreshProof.keyGeneration || keyProof.keys != uploadedKeys) return false; + if (keyCount == 0 || keyProof.generation != refreshProof.keyGeneration || keyProof.keys != uploadedKeys || !PendingKeyProofHolds(descriptor, guestBytes, keyProof)) return false; if (GuestMemory::CollectWrites(descriptor.dccAddress, keyCount) == 0) return false; queries[used++] = {descriptor.dccAddress, keyCount, keyProof.generation}; } @@ -2692,7 +2722,7 @@ bool StorageTexture::keepWrittenSince(std::uint32_t unit) { const auto from = std::max(at, begin); const auto to = std::min(at + block, end); const bool edge = from != at || to != at + block; - if (changedBlocks[static_cast((at - spanBegin) / block)] == GuestMemory::BlockWritten && (!edge || GuestMemory::StoredOver(from, static_cast(to - from), unitBlockGeneration(unit, at))) && !unchangedSinceBaseline(from, to)) keptAtUnwatch.emplace(from, layerGeneration[unit]); + if (changedBlocks[static_cast((at - spanBegin) / block)] == GuestMemory::BlockWritten && (!edge || GuestMemory::StoredOver(from, static_cast(to - from), unitBlockGeneration(unit, at))) && !cpuLeftUnchanged(unit, from, to, edge)) keptAtUnwatch.emplace(from, layerGeneration[unit]); } return true; } @@ -4067,10 +4097,27 @@ void StorageTexture::noteLayersBehind(std::uint32_t firstLayer, std::uint32_t la if (std::none_of(layerBehind.begin(), layerBehind.end(), [](bool behind) { return behind; })) layerBehind.clear(); } +namespace { +std::atomic flipSerial{0}; +std::atomic occasionalLabelStores{0}; +} + +void StorageTexture::NoteFlip() { + flipSerial.fetch_add(1, std::memory_order_relaxed); +} + +std::uint64_t StorageTexture::OccasionalLabelStores() { + return occasionalLabelStores.load(std::memory_order_relaxed); +} + void StorageTexture::markLayersPending(std::uint32_t first, std::uint32_t count) { CaptureTrace::Log("image-write image=%llx first=%u count=%u generation=%llu", static_cast(descriptor.baseAddress), first, count, static_cast(generation)); ++writes; static const bool eager = std::getenv("APS5_EAGER_WRITEBACK") != nullptr || std::getenv("APS5_NO_TEXTURE_CACHE") != nullptr; + if (const auto flip = flipSerial.load(std::memory_order_relaxed); writes == 1u || flip != writtenFlip) { + writtenOccasionally = writes == 1u || flip - writtenFlip > OccasionalWriteFlips; + writtenFlip = flip; + } std::optional publication; if (!layerPending.Any()) publication.emplace(descriptor.baseAddress, guestBytes); if (!layerPending.AllIn(first, count)) { @@ -4078,7 +4125,7 @@ void StorageTexture::markLayersPending(std::uint32_t first, std::uint32_t count) bool anyUnsaved = false; const bool trims = CommitTrims(); if (trims) { - for (std::uint32_t layer = first; layer < first + count && !anyUnsaved; ++layer) anyUnsaved = !layerPending[layer] && !layerOriginal[layer] && (layerGeneration[layer] == 0 || savedTicket[layer] == 0); + for (std::uint32_t layer = first; layer < first + count && !anyUnsaved; ++layer) anyUnsaved = !layerPending[layer] && !layerOriginal[layer] && (layerGeneration[layer] == 0 || savedTicket[layer] == 0 || keptForCpu(layer)); } if (!trims || anyUnsaved) { unsaved.assign(trackedLayers, false); @@ -4092,7 +4139,7 @@ void StorageTexture::markLayersPending(std::uint32_t first, std::uint32_t count) auto miss = BaselineMiss::LeftWatchWhilePending; if (layerGeneration[layer] != 0 && savedTicket[layer] != 0) miss = BaselineMiss::SaveInFlight; else if (layerGeneration[layer] != 0 && !GuestMemory::Watched(layerBegin(layer), static_cast(layerBytes(layer)))) miss = baselineMiss(layer); - unsaved[layer] = miss == BaselineMiss::LeftWatchWhilePending || miss == BaselineMiss::None; + unsaved[layer] = keptForCpu(layer) || miss == BaselineMiss::LeftWatchWhilePending || miss == BaselineMiss::None; pendingMiss[layer] = miss == BaselineMiss::None ? BaselineMiss::SavedBytesDropped : miss; anyUnsaved = anyUnsaved || unsaved[layer]; } @@ -4107,6 +4154,7 @@ void StorageTexture::markLayersPending(std::uint32_t first, std::uint32_t count) if (unsaved[layer]) pendingMiss[layer] = BaselineMiss::SavedBytesDropped; } } + GuestMemory::CollectWritesUncached(layerBegin(first), static_cast(layerBegin(first + count - 1) + layerBytes(first + count - 1) - layerBegin(first))); if (!completionKept.empty()) { for (std::uint32_t layer = first; layer < first + count; ++layer) { if (layerPending[layer]) continue; @@ -4116,6 +4164,10 @@ void StorageTexture::markLayersPending(std::uint32_t first, std::uint32_t count) } layerPending.SetRange(first, count, true); } + const auto pendingSince = GuestMemory::TrackerGeneration(); + for (std::uint32_t layer = first; layer < first + count; ++layer) { + if (!keptForCpu(layer)) layerPendingSince[layer] = pendingSince; + } // Results of this image now cover the alias's results borrowed into these units: this image // stores them, the alias no longer has to (its results, unchanged since the borrow, are in // this image's content). An alias written since keeps its own newer results pending. @@ -4744,6 +4796,15 @@ bool StorageTexture::keptAtUnwatchHolds(std::uint64_t from, std::uint32_t unit) return found != keptAtUnwatch.end() && layerOriginal[unit] && found->second == layerGeneration[unit]; } +bool StorageTexture::keptForCpu(std::uint32_t unit) const { + const auto begin = layerBegin(unit); + const auto end = begin + layerBytes(unit); + for (auto it = keptAtUnwatch.lower_bound(begin); it != keptAtUnwatch.end() && it->first < end; ++it) { + if (it->second == layerGeneration[unit]) return true; + } + return false; +} + void StorageTexture::forgetSavedOver(std::uint64_t address, std::size_t bytes) { // Only these units' bytes changed under the saved copy. Units outside keep it: results still // pending there (the flush before the fill or copy stored only the units it overlaps) are @@ -5014,10 +5075,11 @@ std::size_t StorageTexture::StoreSmallPending(const char* reason) { return stored; } -void StorageTexture::StoreReadbackBeforeLabel(const char* reason) { +void StorageTexture::StoreReadbackBeforeLabel(const char* reason, VkDevice device) { static const bool disabled = std::getenv("APS5_NO_LABEL_READBACK_STORE") != nullptr; if (disabled) return; std::vector> ranges; + std::vector> occasional; { auto& pending = Pending(); std::lock_guard lock(pending.mutex); @@ -5043,8 +5105,16 @@ void StorageTexture::StoreReadbackBeforeLabel(const char* reason) { #endif ranges.emplace_back(texture->descriptor.baseAddress, static_cast(texture->guestBytes)); } + for (auto* texture : pending.textures) { + if (texture->descriptor.tileMode == TextureTileMode::kLinear || texture->guestBytes <= SmallStoreBytes || texture->guestBytes > OccasionalStoreBytes) continue; + if (device != VK_NULL_HANDLE && texture->context.device != device) continue; + if (!texture->writtenOccasionally.exchange(false, std::memory_order_relaxed)) continue; + occasionalLabelStores.fetch_add(1, std::memory_order_relaxed); + occasional.emplace_back(texture->descriptor.baseAddress, static_cast(texture->guestBytes)); + } } for (const auto& [address, bytes] : ranges) static_cast(FlushPending(address, bytes, nullptr, reason)); + for (const auto& [address, bytes] : occasional) static_cast(FlushPending(address, bytes, nullptr, reason, PublishScope::Whole, nullptr, false, device)); } std::size_t StorageTexture::StoreDrawnLinear(const char* reason) { @@ -6415,6 +6485,16 @@ bool StorageTexture::unchangedSinceBaseline(std::uint64_t from, std::uint64_t to return GuestMemory::EqualsCommittedUnsynced(from, std::span(baseline).subspan(offset, static_cast(to - from))); } +bool StorageTexture::cpuLeftUnchanged(std::uint32_t layer, std::uint64_t from, std::uint64_t to, bool edge) const { + if (!unchangedSinceBaseline(from, to)) return false; + if (edge || layerPendingSince[layer] == 0) return true; + const std::array since{layerPendingSince[layer]}; + std::array changed{}; + std::array cpu{}; + GuestMemory::ChangedBlocks(from, static_cast(to - from), since, changed, cpu); + return cpu[0] == 0; +} + void StorageTexture::writeBackLayers(const std::vector& layers) { CaptureTrace::Log("writeback image=%llx generation=%llu reason=%s units=%zu", static_cast(descriptor.baseAddress), static_cast(generation), flushReason, static_cast(std::count(layers.begin(), layers.end(), true))); static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; @@ -6435,6 +6515,7 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { // may have reused the memory for something else entirely (see layerGeneration). A generation of // zero means no tracking, and the layer is stored whole. std::vector> keep; + std::vector> keptBlocks; std::vector skippedLayer(trackedLayers, false); bool skippedAny = false; std::size_t skipped = 0; @@ -6507,10 +6588,11 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { const bool edge = from != at || to != at + block; const auto index = static_cast((at - spanBegin) / block); const bool kept = keptAtUnwatchHolds(from, layer); - const bool written = kept || unknown || (changedBlocks[index] == GuestMemory::BlockWritten && (compared[layer] || !edge || GuestMemory::StoredOver(from, static_cast(to - from), unitBlockGeneration(layer, at))) && !unchangedSinceBaseline(from, to)); + const bool written = kept || unknown || (changedBlocks[index] == GuestMemory::BlockWritten && (compared[layer] || !edge || GuestMemory::StoredOver(from, static_cast(to - from), unitBlockGeneration(layer, at))) && !cpuLeftUnchanged(layer, from, to, edge)); if (written && (!drawn.rect || (tracked && !kept && !unknown && cpuBlocks[index] == 0))) { skippedAny = true; skippedLayer[layer] = true; + keptBlocks.emplace_back(from, layer); ++skipped; continue; } @@ -6522,6 +6604,7 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) { if (layers[layer]) forgetKeptAtUnwatch(layer); } + for (const auto& [from, layer] : keptBlocks) keptAtUnwatch.emplace(from, layerGeneration[layer]); // Debug aid: APS5_TRACE_FLUSH also names the blocks a write-back leaves to the CPU. static const bool traceKept = std::getenv("APS5_TRACE_FLUSH") != nullptr; if (traceKept && skippedAny) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureDetiler.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureDetiler.cpp index 35fb2aeadb..246720ca34 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureDetiler.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureDetiler.cpp @@ -6,6 +6,8 @@ #include "prx/libSceAgcDriver/Graphics/shaders/CmaskClear_spv.h" #include "prx/libSceAgcDriver/Graphics/shaders/TextureDetileImage_spv.h" #include "prx/libSceAgcDriver/Graphics/shaders/TextureRepack_spv.h" +#include "prx/libSceAgcDriver/Graphics/shaders/TextureDetileImageWords_spv.h" +#include "prx/libSceAgcDriver/Graphics/shaders/TextureDetileWords_spv.h" #include #include #include @@ -122,6 +124,8 @@ void TextureDetiler::release() noexcept { if (imageModule) context.Function("vkDestroyShaderModule")(context.device, imageModule, nullptr); if (repackPipeline) context.Function("vkDestroyPipeline")(context.device, repackPipeline, nullptr); if (repackModule) context.Function("vkDestroyShaderModule")(context.device, repackModule, nullptr); + if (imageWordModule) context.Function("vkDestroyShaderModule")(context.device, imageWordModule, nullptr); + if (wordModule) context.Function("vkDestroyShaderModule")(context.device, wordModule, nullptr); if (imagePipelineLayout) context.Function("vkDestroyPipelineLayout")(context.device, imagePipelineLayout, nullptr); if (imageDescriptorLayout) context.Function("vkDestroyDescriptorSetLayout")(context.device, imageDescriptorLayout, nullptr); for (const auto& entry : pipelines) context.Function("vkDestroyPipeline")(context.device, entry.second, nullptr); @@ -176,8 +180,12 @@ VkPipeline TextureDetiler::pipeline(TextureTileMode tileMode, std::uint32_t elem values[2] = 2u; std::copy(equation->bits.begin(), equation->bits.end(), values.begin() + 4); } - if (image && imageModule == VK_NULL_HANDLE) { - imageModule = CreateShaderModule(context, TEXTURE_DETILE_IMAGE_SPV, sizeof(TEXTURE_DETILE_IMAGE_SPV), "vkCreateShaderModule"); + const bool words = elementBytes < 4u && (image ? retile && values[2] != 2u : !retile || values[2] != 2u); + auto& shader = image ? (words ? imageWordModule : imageModule) : (words ? wordModule : module); + if (shader == VK_NULL_HANDLE) { + if (!image) shader = CreateShaderModule(context, TEXTURE_DETILE_WORDS_SPV, sizeof(TEXTURE_DETILE_WORDS_SPV), "vkCreateShaderModule detile words"); + else if (words) shader = CreateShaderModule(context, TEXTURE_DETILE_IMAGE_WORDS_SPV, sizeof(TEXTURE_DETILE_IMAGE_WORDS_SPV), "vkCreateShaderModule detile image words"); + else shader = CreateShaderModule(context, TEXTURE_DETILE_IMAGE_SPV, sizeof(TEXTURE_DETILE_IMAGE_SPV), "vkCreateShaderModule"); } std::array entries{}; for (std::uint32_t index = 0; index < entries.size(); ++index) entries[index] = {index, index * 4u, 4}; @@ -188,7 +196,7 @@ VkPipeline TextureDetiler::pipeline(TextureTileMode tileMode, std::uint32_t elem specialization.pData = values.data(); VkPipelineShaderStageCreateInfo stage{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}; stage.stage = VK_SHADER_STAGE_COMPUTE_BIT; - stage.module = image ? imageModule : module; + stage.module = shader; stage.pName = "main"; stage.pSpecializationInfo = &specialization; VkComputePipelineCreateInfo createInfo{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; diff --git a/core/libs/prx/libSceAgcDriver/Submit/src/CommandBuffer.cpp b/core/libs/prx/libSceAgcDriver/Submit/src/CommandBuffer.cpp index a2c4534fdc..f2de301ece 100644 --- a/core/libs/prx/libSceAgcDriver/Submit/src/CommandBuffer.cpp +++ b/core/libs/prx/libSceAgcDriver/Submit/src/CommandBuffer.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include "SceTypes.hpp" @@ -15,7 +16,8 @@ extern "C" { int APS5_VABI sceAgcDriverSubmitCommandBuffer(void* queue_context, const Packet* packet) { if (!queue_context || !packet) return EINVAL; - const auto queue = *reinterpret_cast(static_cast(queue_context) + 4); + std::uint32_t queue = 0; + std::memcpy(&queue, static_cast(queue_context) + 4, sizeof(queue)); if (queue != 0 && (queue < 0x20 || queue >= AgcDriver::DriverDetail::UnmeasuredSubmissionQueueEnd)) { throw std::runtime_error(std::string(__func__) + ": unsupported queue"); } @@ -26,7 +28,8 @@ int APS5_VABI sceAgcDriverSubmitCommandBuffer(void* queue_context, const Packet* int APS5_VABI sceAgcDriverSubmitMultiCommandBuffers(void* queue_context, uint32_t* const* command_buffers, const uint32_t* sizes_in_dwords, uint32_t count) { if (count == 0) return 0; if (!queue_context || !command_buffers || !sizes_in_dwords) return EINVAL; - const auto queue = *reinterpret_cast(static_cast(queue_context) + 4); + std::uint32_t queue = 0; + std::memcpy(&queue, static_cast(queue_context) + 4, sizeof(queue)); if (queue != 0 && (queue < 0x20 || queue >= AgcDriver::DriverDetail::UnmeasuredSubmissionQueueEnd)) { throw std::runtime_error(std::string(__func__) + ": unsupported queue"); } diff --git a/core/libs/prx/libSceAgcDriver/Submit/src/Dcb.cpp b/core/libs/prx/libSceAgcDriver/Submit/src/Dcb.cpp index f07d5c39c5..c2c1c6b4ce 100644 --- a/core/libs/prx/libSceAgcDriver/Submit/src/Dcb.cpp +++ b/core/libs/prx/libSceAgcDriver/Submit/src/Dcb.cpp @@ -3,6 +3,7 @@ #include "prx/libSceAgcDriver/Execution/include/VideoOutput.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include +#include #include #include #include @@ -42,6 +43,53 @@ std::uint32_t WorkloadDataPacketDwords(std::uint32_t bytes) { return payload == 1 ? 4 : payload + 7; } +constexpr std::uint32_t UserDataRegisterPacket = 0xC0017900u; +constexpr std::uint32_t UserDataInlineDword = 0x10000u; +constexpr std::uint32_t UserDataRegister = 0x342u; +constexpr std::uint32_t UserDataWritePacket = 0xC0003700u; +constexpr std::uint32_t UserDataWriteControl = 0x06010000u; +constexpr std::uint32_t UserDataWriteRegister = 0xC343u; +constexpr std::uint32_t UserDataDrawQueue = 4u; +constexpr std::uint32_t UserDataPopMarker = 0xC2000000u; +constexpr std::uint32_t UserDataPushMarker = 0xC1000000u; +constexpr std::uint32_t UserDataSetMarker = 0xC0000000u; + +std::uint32_t WriteUserData(std::uint32_t* commands, std::uint32_t asyncCompute, std::uint32_t value, const std::uint8_t* data, std::uint32_t size) { + const bool inlineData = data != nullptr && size != 0; + AgcDriver::GuestMemory::CheckRange(commands, std::size_t{inlineData ? WorkloadDataPacketDwords(size) : 3u} * sizeof(std::uint32_t), alignof(std::uint32_t), true); + if (inlineData) AgcDriver::GuestMemory::CheckRange(data, size, 1); + const std::uint32_t queue = asyncCompute == 0 ? UserDataDrawQueue : 0u; + commands[0] = UserDataRegisterPacket + (inlineData ? UserDataInlineDword : 0u) + queue; + commands[1] = UserDataRegister; + commands[2] = value; + auto* next = commands + 3; + if (inlineData) { + const auto first = std::min(size, 4u); + commands[3] = 0; + std::memcpy(commands + 3, data, first); + size -= first; + data += first; + next = commands + 4; + } + if (data == nullptr || size == 0) return static_cast(next - commands); + const std::uint64_t padded = std::uint64_t{size} + 3u; + next[3] = 0; + next[4] = 0; + next[0] = UserDataWritePacket + queue + (((static_cast(padded) << 14u) + 0x20000u) & 0x3FFF0000u); + next[1] = UserDataWriteControl; + next[2] = UserDataWriteRegister; + next[4 + static_cast((padded >> 2u) - 1u)] = 0; + std::memcpy(next + 4, data, size); + next += 4 + (padded >> 2u); + return static_cast(next - commands); +} + +std::uint32_t MarkerValue(std::uint32_t kind, std::uint32_t nameSize, std::uint32_t color) { + const auto dwords = ((nameSize << 13u) + 0x6000u) & 0x7F8000u; + const auto packedColor = ((color >> 1u) & 0x7C00u) | ((color >> 14u) & 0x3E0u) | (color >> 27u); + return kind + dwords + packedColor; +} + } extern "C" { @@ -74,6 +122,22 @@ std::uint32_t APS5_VABI sceAgcDriverUserDataGetPacketSize(std::uint32_t bytes) { return WorkloadDataPacketDwords(bytes); } +std::uint32_t APS5_VABI sceAgcDriverUserDataWritePacket(std::uint32_t* commands, std::uint32_t asyncCompute, std::uint32_t value, const void* data, std::uint32_t size) { + return WriteUserData(commands, asyncCompute, value, static_cast(data), size); +} + +std::uint32_t APS5_VABI sceAgcDriverUserDataWritePushMarker(std::uint32_t* commands, std::uint32_t asyncCompute, const char* name, std::uint32_t nameSize, std::uint32_t color) { + return WriteUserData(commands, asyncCompute, MarkerValue(UserDataPushMarker, nameSize, color), reinterpret_cast(name), nameSize); +} + +std::uint32_t APS5_VABI sceAgcDriverUserDataWriteSetMarker(std::uint32_t* commands, std::uint32_t asyncCompute, const char* name, std::uint32_t nameSize, std::uint32_t color) { + return WriteUserData(commands, asyncCompute, MarkerValue(UserDataSetMarker, nameSize, color), reinterpret_cast(name), nameSize); +} + +std::uint32_t APS5_VABI sceAgcDriverUserDataWritePopMarker(std::uint32_t* commands, std::uint32_t asyncCompute) { + return WriteUserData(commands, asyncCompute, UserDataPopMarker, nullptr, 0); +} + std::uint32_t APS5_VABI sceAgcDriverWaitUntilSafeForRendering(std::uint32_t** command, std::uint32_t capacity, std::uint32_t mode, std::uint32_t handle, int index) { if (!command || capacity < AgcDriver::RenderingWaitPacketWords || mode != 0 || index < 0) diff --git a/core/libs/prx/libSceAgcDriver/include/AgcDriver.hpp b/core/libs/prx/libSceAgcDriver/include/AgcDriver.hpp index 6c6e8fea9b..73600d9ffb 100644 --- a/core/libs/prx/libSceAgcDriver/include/AgcDriver.hpp +++ b/core/libs/prx/libSceAgcDriver/include/AgcDriver.hpp @@ -35,10 +35,10 @@ int APS5_VABI sceAgcDriverSysEnableSubmitDone45Exception(void); int APS5_VABI sceAgcDriverSysGetClientNumber(void); int APS5_VABI sceAgcDriverSysIsGameClosed(void); int APS5_VABI sceAgcDriverTmpInitIdhs(void); -int APS5_VABI sceAgcDriverUserDataWritePacket(void); -int APS5_VABI sceAgcDriverUserDataWritePopMarker(void); -int APS5_VABI sceAgcDriverUserDataWritePushMarker(void); -int APS5_VABI sceAgcDriverUserDataWriteSetMarker(void); +std::uint32_t APS5_VABI sceAgcDriverUserDataWritePacket(std::uint32_t* commands, std::uint32_t asyncCompute, std::uint32_t value, const void* data, std::uint32_t size); +std::uint32_t APS5_VABI sceAgcDriverUserDataWritePopMarker(std::uint32_t* commands, std::uint32_t asyncCompute); +std::uint32_t APS5_VABI sceAgcDriverUserDataWritePushMarker(std::uint32_t* commands, std::uint32_t asyncCompute, const char* name, std::uint32_t nameSize, std::uint32_t color); +std::uint32_t APS5_VABI sceAgcDriverUserDataWriteSetMarker(std::uint32_t* commands, std::uint32_t asyncCompute, const char* name, std::uint32_t nameSize, std::uint32_t color); int APS5_VABI sceAgcDriverGetShaderDebuggingStatus(); int APS5_VABI sceAgcDriverRegisterDefaultOwner(uint32_t owner); int APS5_VABI sceAgcDriverSetValidationErrorOutputFrequency(uint32_t frequency); diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp index ad805c3f66..3a51ca5e7f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp @@ -68,6 +68,75 @@ bool ScanStores(const std::function& main, const std::function return AgcDriver::Graphics::SpirvStoresThroughAddresses(module.words); } +enum FaultScanId : std::uint32_t { + FaultVoid = 1, FaultVoidFunction, FaultUint, FaultWords, FaultBlock, FaultBlockPointer, FaultWordPointer, FaultHelperFunction, FaultVariable, FaultZero, FaultReportLast, FaultOverflowWord, FaultScope, FaultSemantics, FaultMain, FaultMainLabel, FaultHelper, FaultHelperParameter, FaultHelperLabel, FaultBody, +}; + +bool ScanFaultReport(const std::function& main, const std::function& after = {}) { + ScanWords module{{spv::MagicNumber, 0x00010300u, 0u, 100u, 0u}}; + module.Op(spv::OpCapability, {spv::CapabilityShader}); + module.Op(spv::OpMemoryModel, {spv::AddressingModelLogical, spv::MemoryModelGLSL450}); + module.OpString(spv::OpEntryPoint, {spv::ExecutionModelGLCompute, FaultMain}, "main"); + module.Op(spv::OpDecorate, {FaultVariable, spv::DecorationDescriptorSet, 0u}); + module.Op(spv::OpDecorate, {FaultVariable, spv::DecorationBinding, 5u}); + module.Op(spv::OpTypeVoid, {FaultVoid}); + module.Op(spv::OpTypeFunction, {FaultVoidFunction, FaultVoid}); + module.Op(spv::OpTypeInt, {FaultUint, 32u, 0u}); + module.Op(spv::OpTypeRuntimeArray, {FaultWords, FaultUint}); + module.Op(spv::OpTypeStruct, {FaultBlock, FaultWords}); + module.Op(spv::OpTypePointer, {FaultBlockPointer, spv::StorageClassStorageBuffer, FaultBlock}); + module.Op(spv::OpTypePointer, {FaultWordPointer, spv::StorageClassStorageBuffer, FaultUint}); + module.Op(spv::OpTypeFunction, {FaultHelperFunction, FaultVoid, FaultBlockPointer}); + module.Op(spv::OpConstant, {FaultUint, FaultZero, 0u}); + module.Op(spv::OpConstant, {FaultUint, FaultReportLast, 7u}); + module.Op(spv::OpConstant, {FaultUint, FaultOverflowWord, ShaderRecompiler::BdaAbi::WrittenOverflowWord}); + module.Op(spv::OpConstant, {FaultUint, FaultScope, spv::ScopeDevice}); + module.Op(spv::OpConstant, {FaultUint, FaultSemantics, 0u}); + module.Op(spv::OpVariable, {FaultBlockPointer, FaultVariable, spv::StorageClassStorageBuffer}); + module.Op(spv::OpFunction, {FaultVoid, FaultMain, spv::FunctionControlMaskNone, FaultVoidFunction}); + module.Op(spv::OpLabel, {FaultMainLabel}); + main(module); + module.Op(spv::OpReturn, {}); + module.Op(spv::OpFunctionEnd, {}); + if (after) after(module); + return AgcDriver::Graphics::SpirvFaultReportOnly(module.words, 0, 5); +} + +void RunFaultReportScanTests() { + using AgcDriver::Graphics::Require; + Require(ScanFaultReport([](ScanWords& m) { + m.Op(spv::OpAccessChain, {FaultWordPointer, FaultBody, FaultVariable, FaultZero, FaultZero}); + m.Op(spv::OpAtomicCompareExchange, {FaultUint, FaultBody + 1u, FaultBody, FaultScope, FaultSemantics, FaultSemantics, FaultReportLast, FaultZero}); + m.Op(spv::OpAccessChain, {FaultWordPointer, FaultBody + 2u, FaultVariable, FaultZero, FaultReportLast}); + m.Op(spv::OpStore, {FaultBody + 2u, FaultZero}); + m.Op(spv::OpAtomicOr, {FaultUint, FaultBody + 3u, FaultBody + 2u, FaultScope, FaultSemantics, FaultReportLast}); + m.Op(spv::OpLoad, {FaultUint, FaultBody + 4u, FaultBody + 2u}); + }), "fault scan: loads, stores and atomics on the report words were taken for writes past the report"); + Require(!ScanFaultReport([](ScanWords& m) { + m.Op(spv::OpAccessChain, {FaultWordPointer, FaultBody, FaultVariable, FaultZero, FaultOverflowWord}); + m.Op(spv::OpStore, {FaultBody, FaultZero}); + }), "fault scan: a store to the written-page overflow word was missed"); + Require(!ScanFaultReport([](ScanWords& m) { + m.Op(spv::OpIAdd, {FaultUint, FaultBody, FaultZero, FaultReportLast}); + m.Op(spv::OpAccessChain, {FaultWordPointer, FaultBody + 1u, FaultVariable, FaultZero, FaultBody}); + m.Op(spv::OpStore, {FaultBody + 1u, FaultZero}); + }), "fault scan: a store at a computed word was taken for a report store"); + Require(!ScanFaultReport([](ScanWords& m) { + m.Op(spv::OpAccessChain, {FaultWordPointer, FaultBody, FaultVariable, FaultZero, FaultZero}); + m.Op(spv::OpCopyObject, {FaultWordPointer, FaultBody + 1u, FaultBody}); + m.Op(spv::OpStore, {FaultBody + 1u, FaultZero}); + }), "fault scan: a copied pointer to a report word was not followed"); + Require(!ScanFaultReport([](ScanWords& m) { m.Op(spv::OpFunctionCall, {FaultVoid, FaultBody, FaultHelper, FaultVariable}); }, [](ScanWords& m) { + m.Op(spv::OpFunction, {FaultVoid, FaultHelper, spv::FunctionControlMaskNone, FaultHelperFunction}); + m.Op(spv::OpFunctionParameter, {FaultBlockPointer, FaultHelperParameter}); + m.Op(spv::OpLabel, {FaultHelperLabel}); + m.Op(spv::OpAccessChain, {FaultWordPointer, FaultBody + 1u, FaultHelperParameter, FaultZero, FaultOverflowWord}); + m.Op(spv::OpStore, {FaultBody + 1u, FaultZero}); + m.Op(spv::OpReturn, {}); + m.Op(spv::OpFunctionEnd, {}); + }), "fault scan: the fault buffer handed to a function was not followed"); +} + void RunAddressStoreScanTests() { using AgcDriver::Graphics::Require; Require(!ScanStores([](ScanWords& m) { m.Op(spv::OpLoad, {IdUint, IdBody, IdPointer}); }), "store scan: a load through a physical pointer was taken for a store"); @@ -143,6 +212,7 @@ void RunBdaContractTests() { using namespace ShaderRecompiler; using AgcDriver::Graphics::SpirvStoresThroughAddresses; RunAddressStoreScanTests(); + RunFaultReportScanTests(); AgcDriver::Graphics::Require(!SpirvStoresThroughAddresses(MakeBdaTestShader(0x1000, 32)), "a BDA read was taken for a store through an address"); AgcDriver::Graphics::Require(!SpirvStoresThroughAddresses(MakeBdaDwordReadTestShader(0x1000, 4, false, false)), "BDA dword reads were taken for a store through an address"); AgcDriver::Graphics::Require(SpirvStoresThroughAddresses(MakeBdaStoreTestShader(0x1000, 7, 32, false)), "a BDA store was not seen"); @@ -156,6 +226,10 @@ void RunBdaContractTests() { auto unparsed = MakeBdaStoreTestShader(0x1000, 7, 32, false); unparsed[5] |= 0xffff0000u; AgcDriver::Graphics::Require(SpirvStoresThroughAddresses(unparsed), "a module with an instruction past its end was taken for one without stores"); + using AgcDriver::Graphics::SpirvFaultReportOnly; + AgcDriver::Graphics::Require(SpirvFaultReportOnly(MakeBdaTestShader(0x1000, 32), 0, 1) && SpirvFaultReportOnly(MakeBdaDwordReadTestShader(0x1000, 4, false, false), 0, 1) && SpirvFaultReportOnly(MakeBdaFaultSequenceTestShader({BdaAbi::FaultReason::Unmapped, BdaAbi::FaultReason::Unaligned}), 0, 1), "a BDA read program, which writes only the fault report, was given a full fault buffer"); + AgcDriver::Graphics::Require(!SpirvFaultReportOnly(MakeBdaStoreTestShader(0x1000, 7, 32, false), 0, 1) && !SpirvFaultReportOnly(MakeBdaWriteTestShader(0x1000, 7, 32, false), 0, 1), "a BDA store program, which records written pages, was given a report-only fault buffer"); + AgcDriver::Graphics::Require(!SpirvFaultReportOnly(MakeBdaTestShader(0x1000, 32), 0, 3) && !SpirvFaultReportOnly(headerOnly, 0, 1) && !SpirvFaultReportOnly(truncated, 0, 1) && !SpirvFaultReportOnly(unparsed, 0, 1), "a module without the fault buffer, or one that cannot be read, was taken for a report-only one"); const std::array capabilities{spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess}; const std::array extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; SpirvTargetOptions target{0x00401000u, 0x00010300u, 32, BdaAbi::Version, capabilities, extensions}; diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaDevice.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaDevice.cpp index 0231d44364..561fba4fd5 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaDevice.cpp @@ -265,6 +265,8 @@ int main(int argc, char** argv) { } RunBdaStoreExecutionTests(device->GetContext()); std::cout << "BDA store boundary tests passed\n"; + RunBdaFaultSlotExecutionTests(device->GetContext()); + std::cout << "BDA report-only fault slot tests passed\n"; RunColorTransferTests(device->GetContext()); std::cout << "Vulkan BDA allocation and execution tests passed\n"; return 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaExecution.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaExecution.cpp index 098d311465..13f0c33c83 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaExecution.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaExecution.cpp @@ -1,6 +1,7 @@ #include "BdaShader.hpp" #include "BdaAbi.hpp" #include "IntermediateRepresentation/IrMetadata/ShaderStage.hpp" +#include "prx/libSceAgcDriver/Graphics/include/BdaResources.hpp" #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" #include #include @@ -26,7 +27,9 @@ using namespace AgcDriver::Graphics; class Pipeline { public: - Pipeline(const Context& context, std::span code, const std::array& buffers) : context(context) { + Pipeline(const Context& context, std::span code, const std::array& buffers) : Pipeline(context, code, std::array{whole(*buffers[0]), whole(*buffers[1]), whole(*buffers[2])}) {} + + Pipeline(const Context& context, std::span code, const std::array& buffers) : context(context) { try { std::array bindings{}; for (std::uint32_t i = 0; i < bindings.size(); ++i) bindings[i] = {i, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr}; @@ -43,13 +46,12 @@ public: VkDescriptorSetAllocateInfo allocation{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO, nullptr, pool, 1, &setLayout}; Check(context.Function("vkAllocateDescriptorSets")(context.device, &allocation, &set), "vkAllocateDescriptorSets"); for (std::uint32_t i = 0; i < buffers.size(); ++i) { - const VkDescriptorBufferInfo buffer{buffers[i]->Handle(), 0, buffers[i]->Bytes().size()}; VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}; write.dstSet = set; write.dstBinding = i; write.descriptorCount = 1; write.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; - write.pBufferInfo = &buffer; + write.pBufferInfo = &buffers[i]; context.Function("vkUpdateDescriptorSets")(context.device, 1, &write, 0, nullptr); } VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; @@ -69,20 +71,26 @@ public: ~Pipeline() { release(); } - void Run(std::uint32_t groups) { + void Run(std::uint32_t groups) { RunTogether(context, {this}, groups); } + + static void RunTogether(const Context& context, std::initializer_list pipelines, std::uint32_t groups = 1) { CommandBatch batch(context); const auto commands = batch.Handle(); VkMemoryBarrier upload{VK_STRUCTURE_TYPE_MEMORY_BARRIER, nullptr, VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT}; context.Function("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 1, &upload, 0, nullptr, 0, nullptr); - context.Function("vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline); - context.Function("vkCmdBindDescriptorSets")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, layout, 0, 1, &set, 0, nullptr); - context.Function("vkCmdDispatch")(commands, groups, 1, 1); + for (const auto* pipeline : pipelines) { + context.Function("vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline->pipeline); + context.Function("vkCmdBindDescriptorSets")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline->layout, 0, 1, &pipeline->set, 0, nullptr); + context.Function("vkCmdDispatch")(commands, groups, 1, 1); + } VkMemoryBarrier download{VK_STRUCTURE_TYPE_MEMORY_BARRIER, nullptr, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT}; context.Function("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_HOST_BIT, 0, 1, &download, 0, nullptr, 0, nullptr); batch.SubmitAndWait(); } private: + static VkDescriptorBufferInfo whole(Buffer& buffer) { return {buffer.Handle(), 0, buffer.Bytes().size()}; } + void release() noexcept { if (pipeline) context.Function("vkDestroyPipeline")(context.device, pipeline, nullptr); if (module) context.Function("vkDestroyShaderModule")(context.device, module, nullptr); @@ -610,3 +618,30 @@ void RunBdaStoreExecutionTests(const Context& context) { run("split aligned dword", guest + 3, 32, coherent, {0xcd, 0xcd, 0xcd, 0xcd, 0xcd}, {}, Abi::FaultReason::Unmapped, guest + 3, 4); } } + +void RunBdaFaultSlotExecutionTests(const Context& context) { + namespace Abi = ShaderRecompiler::BdaAbi; + Buffer table(context, sizeof(Abi::Header), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + const Abi::Header header{Abi::Version, 0, sizeof(Abi::Range), 0}; + std::memcpy(table.Bytes().data(), &header, sizeof(header)); + Buffer output(context, 4, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + const VkDescriptorBufferInfo tableInfo{table.Handle(), 0, table.Bytes().size()}; + const VkDescriptorBufferInfo outputInfo{output.Handle(), 0, output.Bytes().size()}; + BdaResources first(context, true); + BdaResources second(context, true); + const auto firstSlot = first.Fault(); + const auto secondSlot = second.Fault(); + Require(firstSlot.buffer == secondSlot.buffer && firstSlot.range == sizeof(Abi::Fault) && secondSlot.range == sizeof(Abi::Fault) && firstSlot.offset != secondSlot.offset, "two report-only BdaResources did not take 32-byte slots of one fault chunk"); + Pipeline firstPipeline(context, MakeBdaFaultSequenceTestShader({Abi::FaultReason::Unmapped}, 0x7fff11110000ULL), {tableInfo, firstSlot, outputInfo}); + Pipeline secondPipeline(context, MakeBdaFaultSequenceTestShader({Abi::FaultReason::Unmapped}, 0x7fff22220000ULL), {tableInfo, secondSlot, outputInfo}); + Pipeline::RunTogether(context, {&firstPipeline, &secondPipeline}); + const auto reports = [](const BdaResources& resources, const char* address) { + try { + resources.CheckFault(); + } catch (const std::runtime_error& error) { + return std::string(error.what()).find(address) != std::string::npos; + } + return false; + }; + Require(reports(first, "address=0x7fff11110000") && reports(second, "address=0x7fff22220000"), "two dispatches of one batch faulting into neighbouring report-only fault slots did not each report their own address"); +} diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index fc2babbe95..8b01cc19db 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -7,6 +7,7 @@ #include "prx/libc/include/GuestArena.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libc/include/GuestWriteWatch.hpp" +#include #ifdef _WIN32 #ifndef NOMINMAX #define NOMINMAX @@ -24,6 +25,7 @@ #include #include #include +#include #include #include #include @@ -2423,6 +2425,111 @@ ShaderRecompiler::DescriptorBinding binding(Role role, std::uint32_t slot) { return {ShaderRecompiler::DescriptorKind::StorageBuffer, role, 0, slot, 1, {}, false}; } +namespace { + +std::span slotBytes(const BdaTestAccess& access, const VkDescriptorBufferInfo& descriptor) { + return access.bytes(descriptor.buffer).subspan(static_cast(descriptor.offset), static_cast(descriptor.range)); +} + +std::span descriptorBytes(const BdaTestAccess& access, std::uint32_t slot) { + return slotBytes(access, access.descriptor(slot)); +} + +void writeUnmappedFault(std::span bytes, std::uint64_t address) { + const ShaderRecompiler::BdaAbi::Fault report{ShaderRecompiler::BdaAbi::FaultState::Ready, ShaderRecompiler::BdaAbi::FaultReason::Unmapped, address, 4, 0, 0x40, 0}; + std::memcpy(bytes.data(), &report, sizeof(report)); +} + +bool sameFaultMemory(const VkDescriptorBufferInfo& left, const VkDescriptorBufferInfo& right) { + return left.buffer == right.buffer && left.offset < right.offset + right.range && right.offset < left.offset + left.range; +} + +void faultSlotTests(const Context& shared, const BdaTestAccess& access) { + const Context context = shared; + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + constexpr std::size_t cached = 256; + std::uint64_t serial = 1000; + std::vector> idle; + for (std::size_t index = 0; index < cached; ++index) { + idle.push_back(std::make_unique(context)); + idle.back()->NoteUse(++serial); + BdaResources::NoteFinished(context.device, serial); + } + const auto counts = BdaResources::FaultSlots(context.device); + char message[256]; + std::snprintf(message, sizeof(message), "%zu idle cached BdaResources hold %llu fault slots in %llu chunks (%llu KiB of host-visible memory)", cached, static_cast(counts.held), static_cast(counts.chunks), static_cast(counts.slots * ShaderRecompiler::BdaAbi::FaultBufferBytes / 1024)); + Require(counts.held < cached / 4 && counts.slots < cached / 4 && counts.reclaimed != 0, message); + auto& first = *idle[0]; + auto& second = *idle[1]; + Require(!first.HoldsFaultSlot() && !second.HoldsFaultSlot(), "the oldest idle BdaResources kept their fault slots"); + first.NoteUse(serial + 1); + second.NoteUse(serial + 1); + Require(first.TakeFaultSlot(context) && second.TakeFaultSlot(context), "a reused BdaResources did not take a fault slot again"); + for (std::size_t index = 0; index < 64; ++index) idle.push_back(std::make_unique(context)); + Require(first.HoldsFaultSlot() && second.HoldsFaultSlot(), "a BdaResources whose batch is in flight lost its fault slot"); + const auto firstSlot = first.Fault(); + const auto secondSlot = second.Fault(); + Require(!sameFaultMemory(firstSlot, secondSlot), "two BdaResources in one batch share fault memory"); + writeUnmappedFault(slotBytes(access, firstSlot), 0x7fff11110000ULL); + writeUnmappedFault(slotBytes(access, secondSlot), 0x7fff22220000ULL); + reject([&] { first.CheckFault(); }, "address=0x7fff11110000"); + reject([&] { second.CheckFault(); }, "address=0x7fff22220000"); + idle.clear(); + ShaderRecompiler::RecompileResult control; + control.bdaAbiVersion = ShaderRecompiler::BdaAbi::Version; + control.bindings = {binding(Role::FaultBuffer, 5)}; + const std::array stages{{{ShaderRecompiler::ShaderStage::TessellationControl, &control, 0}}}; + auto disabled = context; + disabled.bufferDeviceAddress = false; + ShaderResources resources(disabled, stages, ColorTarget{}, 0, 0, 0, {}); + const auto built = access.descriptor(5); + Require(built.range == ShaderRecompiler::BdaAbi::FaultBufferBytes, "a build whose program could not be read was bound less than a full fault buffer"); + BdaResources::NoteFinished(context.device, 1); + std::vector> pressure; + const auto reclaimedBefore = BdaResources::FaultSlots(context.device).reclaimed; + while (BdaResources::FaultSlots(context.device).reclaimed == reclaimedBefore && pressure.size() < cached) pressure.push_back(std::make_unique(context)); + Require(BdaResources::FaultSlots(context.device).reclaimed != reclaimedBefore, "an idle fault-only build kept its fault slot under pressure"); + Require(sameFaultMemory(pressure.back()->Fault(), built), "the reclaimed fault slot did not serve the build that needed one"); + resources.Bind(VK_NULL_HANDLE, VK_PIPELINE_BIND_POINT_GRAPHICS, VK_NULL_HANDLE); + const auto rebound = access.descriptor(5); + Require(!sameFaultMemory(rebound, built), "a reused build was bound to the fault slot it gave back"); + writeUnmappedFault(slotBytes(access, built), 0x7fff33330000ULL); + writeUnmappedFault(slotBytes(access, rebound), 0x7fff44440000ULL); + reject([&] { resources.CheckFaults(); }, "address=0x7fff44440000"); + reject([&] { pressure.back()->CheckFault(); }, "address=0x7fff33330000"); + std::vector> reports; + std::set reportChunks; + for (std::size_t index = 0; index < 64; ++index) { + reports.push_back(std::make_unique(context, true)); + reportChunks.insert(reports.back()->Fault().buffer); + } + std::snprintf(message, sizeof(message), "64 report-only BdaResources hold %zu fault chunks with %llu-byte slots", reportChunks.size(), static_cast(reports.front()->Fault().range)); + Require(reportChunks.size() == 1 && reports.front()->Fault().range == sizeof(ShaderRecompiler::BdaAbi::Fault), message); + Require(!sameFaultMemory(reports[0]->Fault(), reports[1]->Fault()), "two report-only BdaResources share fault memory"); + writeUnmappedFault(slotBytes(access, reports[0]->Fault()), 0x7fff55550000ULL); + writeUnmappedFault(slotBytes(access, reports[1]->Fault()), 0x7fff66660000ULL); + reject([&] { reports[0]->CheckFault(); }, "address=0x7fff55550000"); + reject([&] { reports[1]->CheckFault(); }, "address=0x7fff66660000"); + ShaderRecompiler::RecompileResult vertex; + ShaderRecompiler::RecompileResult fragment; + const std::array capabilities{spv::CapabilityShader, spv::CapabilityTessellation}; + ShaderRecompiler::SpirvTarget target{}; + target.vulkanVersion = VK_API_VERSION_1_1; + target.spirvVersion = 0x00010300u; + target.supportedCapabilities = capabilities; + target.tessellation = ShaderRecompiler::TessellationTargetLimits{32, 128, 128, 120, 4096, 128, 128}; + const auto rectList = ShaderRecompiler::BuildRectListShaders(vertex, fragment, target); + const std::array rectStages{{{ShaderRecompiler::ShaderStage::TessellationControl, &rectList.control, 0}}}; + ShaderResources rectResources(disabled, rectStages, ColorTarget{}, 0, 0, 0, {}); + const auto rectSlot = access.descriptor(rectList.control.bindings.front().binding); + std::snprintf(message, sizeof(message), "the rect-list check, which writes only the fault report, was bound %llu bytes of fault buffer", static_cast(rectSlot.range)); + Require(rectSlot.range == sizeof(ShaderRecompiler::BdaAbi::Fault), message); + writeUnmappedFault(slotBytes(access, rectSlot), 0x7fff77770000ULL); + reject([&] { rectResources.CheckFaults(); }, "address=0x7fff77770000"); +} + +} + void partialHeapImportBudgetCase(const Context& context, const BdaTestAccess& access, std::size_t bytes, std::size_t budgetBytes, std::size_t prefixBytes) { if (!GuestArena::GuestArenaAvailable_nid_postfix() || !GuestArena::GuestArenaWriteWatched_nid_postfix()) return; constexpr std::size_t window = 2 * 65536; @@ -4436,6 +4543,7 @@ void sparseHeapMirrorTests(const Context& context, const BdaTestAccess& access) } void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { + faultSlotTests(context, access); alignas(64) std::array guest{}; guest[0] = 123; const auto address = reinterpret_cast(guest.data()); @@ -4486,7 +4594,7 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { Require(!ShaderResources::NeverReusable(stages), "a fault-buffer build is refused the resource cache"); Require(!resources.Reusable(), "a fault-buffer build with copied writes is reusable"); Require(resources.NeedsCompletion() && !resources.UsesAddressTable(), "a fault-buffer build reaches guest memory through the BDA table"); - const auto fault = access.bytes(access.descriptor(5).buffer); + const auto fault = descriptorBytes(access, 5); for (const auto byte : fault) Require(byte == std::byte{}, "rect-list fault buffer was not initialized"); const ShaderRecompiler::BdaAbi::Fault report{ShaderRecompiler::BdaAbi::FaultState::Ready, ShaderRecompiler::BdaAbi::FaultReason::InvalidRectangle, 0, 0, 0, 0, 0}; std::memcpy(fault.data(), &report, sizeof(report)); @@ -4505,7 +4613,7 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { ShaderResources resources(disabled, stages, ColorTarget{}, 0, 0, 0, {}); Require(!ShaderResources::NeverReusable(stages) && resources.Reusable() && resources.ChecksFaults(), "a fault-only build is not reusable"); resources.CheckFaults(); - const auto fault = access.bytes(access.descriptor(5).buffer); + const auto fault = descriptorBytes(access, 5); const ShaderRecompiler::BdaAbi::Fault report{ShaderRecompiler::BdaAbi::FaultState::Ready, ShaderRecompiler::BdaAbi::FaultReason::InvalidRectangle, 0, 0, 0, 0, 0}; std::memcpy(fault.data(), &report, sizeof(report)); reject([&] { resources.CheckFaults(); }, "rect-list requires"); @@ -4525,7 +4633,7 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { Require(header.version == ShaderRecompiler::BdaAbi::Version && header.count == 1 && header.entryBytes == sizeof(range), "BDA header layout mismatch"); std::memcpy(&range, table.data() + sizeof(header), sizeof(range)); Require(range.begin == snapshots[0].address && range.end == range.begin + source.size(), "64-bit guest address was truncated"); - const auto fault = access.bytes(access.descriptor(5).buffer); + const auto fault = descriptorBytes(access, 5); for (const auto byte : fault) Require(byte == std::byte{}, "fault buffer was not initialized"); const ShaderRecompiler::BdaAbi::Fault denied{ShaderRecompiler::BdaAbi::FaultState::Ready, ShaderRecompiler::BdaAbi::FaultReason::Permission, snapshots[0].address + 4, 4, 0, 0x88, 0}; std::memcpy(fault.data(), &denied, sizeof(denied)); @@ -4541,11 +4649,11 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { changed = 999; std::memcpy(access.bytes(access.descriptor(6).buffer).data(), &changed, sizeof(changed)); const ShaderRecompiler::BdaAbi::Fault report{ShaderRecompiler::BdaAbi::FaultState::Ready, ShaderRecompiler::BdaAbi::FaultReason::Unmapped, 0x7fff99880000ULL, 4, 0, 0x44, 0}; - std::memcpy(access.bytes(access.descriptor(5).buffer).data(), &report, sizeof(report)); + std::memcpy(descriptorBytes(access, 5).data(), &report, sizeof(report)); reject([&] { resources.WriteBack(); }, "BDA access failed"); auto invalidRectangle = report; invalidRectangle.reason = ShaderRecompiler::BdaAbi::FaultReason::InvalidRectangle; - std::memcpy(access.bytes(access.descriptor(5).buffer).data(), &invalidRectangle, sizeof(invalidRectangle)); + std::memcpy(descriptorBytes(access, 5).data(), &invalidRectangle, sizeof(invalidRectangle)); reject([&] { resources.WriteBack(); }, "rect-list requires"); Require(guest[0] == 123, "failed GPU command published writes"); } @@ -4554,7 +4662,7 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { ShaderResources resources(context, compiled); std::memcpy(access.bytes(access.descriptor(6).buffer).data() + word * sizeof(std::uint32_t), &value, sizeof(value)); const ShaderRecompiler::BdaAbi::Fault report{ShaderRecompiler::BdaAbi::FaultState::Ready, reason, 0x7fff99880002ULL, 4, 0, 0x5c8, otherFaults}; - std::memcpy(access.bytes(access.descriptor(5).buffer).data(), &report, sizeof(report)); + std::memcpy(descriptorBytes(access, 5).data(), &report, sizeof(report)); try { resources.WriteBack(); } catch (const BdaUnalignedAccess& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaShader.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaShader.cpp index 9ca39a5b01..924122965c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaShader.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaShader.cpp @@ -134,9 +134,9 @@ std::vector MakeBdaWriteTestShader(std::uint64_t address, std::ui return shader; } -std::vector MakeBdaFaultSequenceTestShader(std::initializer_list reasons) { +std::vector MakeBdaFaultSequenceTestShader(std::initializer_list reasons, std::uint64_t address) { using namespace ShaderRecompiler; - return MakeShader(0u, false, true, [&](SpirvValueEmitContext& ctx, const IrValue&, std::uint32_t base) { + return MakeShader(address, false, true, [&](SpirvValueEmitContext& ctx, const IrValue&, std::uint32_t base) { for (const auto reason : reasons) RecordBdaFault(ctx.state, base, ConstantU32(ctx.state, 4u), ConstantU32(ctx.state, 0x1234u), reason); return std::vector{ConstantU32(ctx.state, 1u)}; }); diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaShader.hpp b/core/libs/prx/libSceAgcDriver/tests/BdaShader.hpp index e31020caf7..87c236c922 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaShader.hpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaShader.hpp @@ -10,7 +10,7 @@ std::vector MakeBdaTestShader(std::uint64_t address, std::uint32_t bits, std::int64_t offset = 0); std::vector MakeBdaDwordReadTestShader(std::uint64_t address, std::uint32_t dwords, bool coherent, bool stops); std::vector MakeBdaSpanReadTestShader(std::uint64_t address, std::uint32_t offset, std::uint32_t extracted, bool coherent, bool stops); -std::vector MakeBdaFaultSequenceTestShader(std::initializer_list reasons); +std::vector MakeBdaFaultSequenceTestShader(std::initializer_list reasons, std::uint64_t address = 0); std::vector MakeBdaDwordWriteTestShader(std::uint64_t address, std::uint32_t dwords, const std::uint32_t* values); std::vector MakeBdaStoreTestShader(std::uint64_t address, std::uint32_t value, std::uint32_t bits, bool coherent); std::vector MakeBdaWriteTestShader(std::uint64_t address, std::uint32_t value, std::uint32_t bits, bool coherent); @@ -32,6 +32,7 @@ struct BdaAnchoredReadProgram { inline constexpr std::uint32_t BdaAnchoredReadSentinel = 0xdeadbeefu; std::vector MakeBdaAnchoredReadTestShader(const BdaAnchoredReadProgram& reads, bool anchored); void RunBdaStoreExecutionTests(const AgcDriver::Graphics::Context& context); +void RunBdaFaultSlotExecutionTests(const AgcDriver::Graphics::Context& context); void RunBdaExecutionTests(const AgcDriver::Graphics::Context& context); void RunBdaContractTests(); diff --git a/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp b/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp index d7a718c419..8cc8fc1aa0 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp @@ -64,6 +64,7 @@ struct MockDevice { unsigned binds = 0; unsigned maps = 0; unsigned unmaps = 0; + std::uint32_t typeBits = 3; }; MockDevice mock; @@ -77,7 +78,7 @@ VKAPI_ATTR VkResult VKAPI_CALL mockCreateBuffer(VkDevice, const VkBufferCreateIn } VKAPI_ATTR void VKAPI_CALL mockGetBufferMemoryRequirements(VkDevice, VkBuffer buffer, VkMemoryRequirements* requirements) { - *requirements = {mock.sizes.at(buffer), 256, 3}; + *requirements = {mock.sizes.at(buffer), 256, mock.typeBits}; } VKAPI_ATTR VkResult VKAPI_CALL mockAllocateMemory(VkDevice, const VkMemoryAllocateInfo* info, const VkAllocationCallbacks*, VkDeviceMemory* memory) { @@ -218,11 +219,15 @@ Context mockContext() { VkDeviceSize reportedBudget = 0; VkDeviceSize reportedUsage = 0; +VkDeviceSize reportedSecondBudget = 0; +VkDeviceSize reportedSecondUsage = 0; VKAPI_ATTR void VKAPI_CALL mockMemoryProperties2(VkPhysicalDevice, VkPhysicalDeviceMemoryProperties2* properties) { auto* reported = static_cast(properties->pNext); reported->heapBudget[0] = reportedBudget; reported->heapUsage[0] = reportedUsage; + reported->heapBudget[2] = reportedSecondBudget; + reported->heapUsage[2] = reportedSecondUsage; } std::uint64_t diagnosticBudgetQueries = 0; @@ -260,19 +265,53 @@ void HostPlacement() { placed = MemoryOf(large.Handle()); Expect(mock.memoryTypes.at(placed) == 1, "a device buffer over the device-local budget was not placed in the host memory type"); } - Expect(!mock.memoryBytes.contains(placed) && mock.liveBuffers.empty(), "a device buffer placed in host memory was kept in the device pool after its release"); + Expect(mock.memoryBytes.contains(placed), "a device buffer placed in host memory was not kept in the pool after its release"); + { + DeviceBuffer again(context, 176 * MiB, usage); + Expect(MemoryOf(again.Handle()) == placed, "a device buffer over the device-local budget did not reuse the retained host-placed buffer"); + } reportedUsage = 200 * MiB; VkDeviceMemory device = VK_NULL_HANDLE; { DeviceBuffer large(context, 176 * MiB, usage); device = MemoryOf(large.Handle()); - Expect(mock.memoryTypes.at(device) == 0, "a device buffer within the device-local budget was not placed in device-local memory"); + Expect(mock.memoryTypes.at(device) == 0 && device != placed, "a device buffer within the device-local budget reused host memory instead of device-local memory"); } Expect(mock.memoryBytes.contains(device), "a device-local buffer was not kept in the device pool after its release"); DeviceBuffer again(context, 176 * MiB, usage); Expect(MemoryOf(again.Handle()) == device, "a retained device-local buffer was not reused"); } +std::uint64_t cacheReleases = 0; + +VkDeviceSize ReleaseEverything(VkDeviceSize bytes, bool) { + ++cacheReleases; + return bytes; +} + +void HostSlotBeforeCacheRelease() { + mock = MockDevice{}; + auto context = budgetContext(); + constexpr auto usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT; + reportedBudget = 1620 * MiB; + reportedUsage = 1600 * MiB; + VkDeviceMemory placed = VK_NULL_HANDLE; + { + DeviceBuffer large(context, 176 * MiB, usage); + placed = MemoryOf(large.Handle()); + Expect(mock.memoryTypes.at(placed) == 1, "a device buffer over the device-local budget was not placed in the host memory type"); + } + context.cacheReclaim = ReleaseEverything; + cacheReleases = 0; + { + DeviceBuffer again(context, 176 * MiB, usage); + Expect(MemoryOf(again.Handle()) == placed, "a retained host-placed buffer was passed over for a texture-cache release"); + Expect(cacheReleases == 0, "taking a retained host-placed buffer asked the texture caches to release memory"); + } + DeviceBuffer fresh(context, 64 * MiB, usage); + Expect(mock.memoryTypes.at(MemoryOf(fresh.Handle())) == 0 && cacheReleases == 1, "a new device buffer over the device-local budget did not get device-local memory after the texture caches released room"); +} + void RefusedDevicePlacement() { mock = MockDevice{}; auto context = budgetContext(); @@ -286,9 +325,13 @@ void RefusedDevicePlacement() { placed = MemoryOf(refused.Handle()); Expect(mock.memoryTypes.at(placed) == 1, "a device buffer whose device-local allocation ran out of device memory within the reported budget was not placed in the host memory type"); } - Expect(!mock.memoryBytes.contains(placed) && mock.liveBuffers.empty(), "a device buffer placed in host memory after a refused device-local allocation was kept in the device pool after its release"); + { + DeviceBuffer within(context, 4 * MiB, usage); + Expect(MemoryOf(within.Handle()) != placed && mock.memoryTypes.at(MemoryOf(within.Handle())) == 0, "a device buffer within the device-local budget reused a host-placed buffer"); + } mock.results = {VK_ERROR_OUT_OF_DEVICE_MEMORY, VK_ERROR_OUT_OF_DEVICE_MEMORY}; context.memory.memoryHeaps[1].flags = VK_MEMORY_HEAP_DEVICE_LOCAL_BIT; + const auto live = mock.liveBuffers.size(); bool threw = false; try { DeviceBuffer refused(context, 9 * MiB, usage); @@ -296,7 +339,59 @@ void RefusedDevicePlacement() { threw = true; } Expect(threw, "a refused device-local allocation without a host memory type did not fail"); - Expect(mock.liveBuffers.empty(), "a failed device buffer left its VkBuffer alive"); + Expect(mock.liveBuffers.size() <= live, "a failed device buffer left its VkBuffer alive"); +} + +void StagingHeadroom() { + mock = MockDevice{}; + { + auto context = budgetContext(); + constexpr auto usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT; + reportedBudget = 2048 * MiB; + reportedUsage = 1000 * MiB; + VkDeviceMemory retained = VK_NULL_HANDLE; + { + const auto shadow = Buffer::WithinDeviceLocalHeadroom(context, 24 * MiB, usage); + Expect(shadow != nullptr && mock.memoryTypes.at(MemoryOf(shadow->Handle())) == 0, "a staging buffer with room in its heap's budget was not made device-local"); + if (shadow) retained = MemoryOf(shadow->Handle()); + } + Expect(mock.memoryBytes.contains(retained), "a released staging buffer was not retained by the pool"); + reportedUsage = 1900 * MiB; + const auto made = mock.allocations; + const auto buffers = mock.liveBuffers.size(); + { + const auto reused = Buffer::WithinDeviceLocalHeadroom(context, 24 * MiB, usage); + Expect(reused != nullptr && MemoryOf(reused->Handle()) == retained && mock.allocations == made, "a retained staging buffer was refused within the last eighth of the budget, although reusing it allocates nothing"); + } + const auto refused = Buffer::WithinDeviceLocalHeadroom(context, 40 * MiB, usage); + Expect(refused == nullptr && mock.allocations == made, "a staging buffer that needs a new allocation within the last eighth of its heap's budget was not refused"); + Expect(mock.liveBuffers.size() == buffers, "a refused staging buffer left its VkBuffer alive"); + } + mock = MockDevice{}; + { + auto context = budgetContext(); + context.memory.memoryHeapCount = 3; + context.memory.memoryHeaps[2] = {2048 * MiB, VK_MEMORY_HEAP_DEVICE_LOCAL_BIT}; + context.memory.memoryTypeCount = 3; + context.memory.memoryTypes[2] = {VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, 2}; + constexpr auto usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT; + reportedBudget = 2048 * MiB; + reportedSecondBudget = 2048 * MiB; + mock.typeBits = 6; + const auto make = [&](const char* what, VkDeviceSize firstUsage, VkDeviceSize secondUsage, std::size_t bytes, bool expected) { + reportedUsage = firstUsage; + reportedSecondUsage = secondUsage; + const auto made = mock.allocations; + const auto shadow = Buffer::WithinDeviceLocalHeadroom(context, bytes, usage); + Expect((shadow != nullptr) == expected, std::string(what) + (expected ? ": refused" : ": made")); + if (shadow) Expect(mock.memoryTypes.at(MemoryOf(shadow->Handle())) == 2, std::string(what) + ": not allocated from the second device-local type"); + else Expect(mock.allocations == made, std::string(what) + ": allocated although refused"); + }; + make("the first device-local heap is full, the buffer's mask allows only the second", 1900 * MiB, 1000 * MiB, 24 * MiB + 4096, true); + make("the second device-local heap is full, the buffer's mask allows only the second", 1000 * MiB, 1900 * MiB, 40 * MiB, false); + } + reportedSecondBudget = 0; + reportedSecondUsage = 0; } void AttachmentReclaim() { @@ -979,6 +1074,35 @@ void IdleDeviceReclaim() { Expect(mock.liveBytes == 0 && mock.liveBuffers.empty(), "idle device reclaim leaked an allocation"); } +void IdleReclaimSkipsHostPlaced() { + mock = MockDevice{}; + { + auto context = budgetContext(); + constexpr auto usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT; + reportedBudget = 1620 * MiB; + reportedUsage = 1600 * MiB; + VkDeviceMemory placed = VK_NULL_HANDLE; + { + DeviceBuffer large(context, 176 * MiB, usage); + placed = MemoryOf(large.Handle()); + Expect(mock.memoryTypes.at(placed) == 1, "the older device buffer was not placed in host memory"); + } + reportedUsage = 200 * MiB; + VkDeviceMemory device = VK_NULL_HANDLE; + { + DeviceBuffer small(context, 4 * MiB, usage); + device = MemoryOf(small.Handle()); + Expect(mock.memoryTypes.at(device) == 0, "the younger device buffer was not placed in device-local memory"); + } + auto& pool = *context.bufferPool; + const auto start = std::chrono::steady_clock::time_point{std::chrono::seconds(1)}; + Expect(pool.ReclaimIdleDevice(MiB, start) == 0, "the first pressure observation reclaimed a buffer"); + Expect(pool.ReclaimIdleDevice(MiB, start + std::chrono::seconds(1)) == 4 * MiB, "idle device reclaim counted a host-placed buffer as device memory"); + Expect(mock.memoryBytes.contains(placed) && !mock.memoryBytes.contains(device), "idle device reclaim freed the host-placed buffer instead of the idle device-local one"); + } + Expect(mock.liveBytes == 0 && mock.liveBuffers.empty(), "host-placed reclaim test leaked an allocation"); +} + void IdleReclaimPreservesWorkingSet() { mock = MockDevice{}; { @@ -1477,7 +1601,10 @@ int main(int argc, char** argv) { RenderTargetMemoryReuse(); ImageMemoryOverBudget(); HostPlacement(); + HostSlotBeforeCacheRelease(); RefusedDevicePlacement(); + IdleReclaimSkipsHostPlaced(); + StagingHeadroom(); AttachmentReclaim(); } catch (const std::exception& error) { std::fprintf(stderr, "FAIL: %s\n", error.what()); diff --git a/core/libs/prx/libSceAgcDriver/tests/DeviceMemoryFallback.cpp b/core/libs/prx/libSceAgcDriver/tests/DeviceMemoryFallback.cpp index 02d00ffcdf..1f89e602d8 100644 --- a/core/libs/prx/libSceAgcDriver/tests/DeviceMemoryFallback.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/DeviceMemoryFallback.cpp @@ -18,12 +18,15 @@ void Expect(bool condition, const std::string& what) { constexpr std::uint32_t HostType = 0; constexpr std::uint32_t DeviceType = 1; +constexpr std::uint32_t SecondDeviceType = 2; constexpr std::uint64_t MiB = 1ull << 20u; std::uint64_t reportedBudget = 0; std::uint64_t reportedUsage = 0; std::uint64_t reportedHostBudget = 0; std::uint64_t reportedHostUsage = 0; +std::uint64_t reportedSecondBudget = 0; +std::uint64_t reportedSecondUsage = 0; VKAPI_ATTR void VKAPI_CALL mockMemoryProperties2(VkPhysicalDevice, VkPhysicalDeviceMemoryProperties2* properties) { auto* reported = static_cast(properties->pNext); @@ -31,6 +34,8 @@ VKAPI_ATTR void VKAPI_CALL mockMemoryProperties2(VkPhysicalDevice, VkPhysicalDev reported->heapUsage[0] = reportedUsage; reported->heapBudget[1] = reportedHostBudget; reported->heapUsage[1] = reportedHostUsage; + reported->heapBudget[2] = reportedSecondBudget; + reported->heapUsage[2] = reportedSecondUsage; } Context mockContext(bool budgetExtension) { @@ -138,6 +143,52 @@ void BufferPlacementTests() { choose("no physical device retains cached preference", 15, coherent, 0); } +void HeadroomTests() { + Context context = mockContext(true); + const auto check = [&](const char* what, std::uint32_t type, std::uint64_t budget, std::uint64_t usage, std::uint64_t bytes, bool expected) { + reportedBudget = budget; + reportedUsage = usage; + Expect(context.DeviceLocalHeadroom(type, bytes) == expected, std::string(what) + ": expected " + (expected ? "room" : "no room")); + }; + check("room below seven eighths of the budget", DeviceType, 4096 * MiB, 3000 * MiB, 64 * MiB, true); + check("allocation ends at seven eighths exactly", DeviceType, 4096 * MiB, 3520 * MiB, 64 * MiB, true); + check("allocation crosses seven eighths", DeviceType, 4096 * MiB, 3521 * MiB, 64 * MiB, false); + check("usage already past seven eighths", DeviceType, 4096 * MiB, 3700 * MiB, 1 * MiB, false); + check("budget not reported", DeviceType, 0, 4000 * MiB, 64 * MiB, true); + check("a host memory type is not held to the device-local budget", HostType, 4096 * MiB, 4000 * MiB, 64 * MiB, true); + check("unknown memory type", 7, 4096 * MiB, 4000 * MiB, 64 * MiB, true); + context.memoryProperties2 = nullptr; + check("no VK_EXT_memory_budget", DeviceType, 4096 * MiB, 4000 * MiB, 64 * MiB, true); + context.memoryProperties2 = mockMemoryProperties2; + context.memory.memoryHeaps[0].flags = 0; + check("no device-local heap", DeviceType, 4096 * MiB, 4000 * MiB, 64 * MiB, true); +} + +void SecondHeapHeadroomTests() { + Context context = mockContext(true); + context.memory.memoryHeapCount = 3; + context.memory.memoryHeaps[2] = {4096 * MiB, VK_MEMORY_HEAP_DEVICE_LOCAL_BIT}; + context.memory.memoryTypeCount = 3; + context.memory.memoryTypes[SecondDeviceType] = {VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, 2}; + reportedBudget = 4096 * MiB; + reportedSecondBudget = 4096 * MiB; + const auto check = [&](const char* what, std::uint32_t mask, std::uint64_t firstUsage, std::uint64_t secondUsage, std::uint32_t expectedType, bool expected) { + reportedUsage = firstUsage; + reportedSecondUsage = secondUsage; + const auto type = context.BufferMemoryType(mask, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, 64 * MiB); + Expect(type == expectedType, std::string(what) + ": chose memory type " + std::to_string(type) + ", expected " + std::to_string(expectedType)); + Expect(context.DeviceLocalHeadroom(type, 64 * MiB) == expected, std::string(what) + ": expected " + (expected ? "room" : "no room")); + }; + const std::uint32_t secondOnly = (1u << HostType) | (1u << SecondDeviceType); + const std::uint32_t all = secondOnly | (1u << DeviceType); + check("mask excludes the first device-local type, first heap under pressure", secondOnly, 3700 * MiB, 1000 * MiB, SecondDeviceType, true); + check("mask excludes the first device-local type, second heap under pressure", secondOnly, 1000 * MiB, 3700 * MiB, SecondDeviceType, false); + check("mask allows both device-local types, first heap under pressure", all, 3700 * MiB, 1000 * MiB, DeviceType, false); + check("mask allows both device-local types, second heap under pressure", all, 1000 * MiB, 3700 * MiB, DeviceType, true); + reportedSecondBudget = 0; + reportedSecondUsage = 0; +} + } int main() { @@ -153,6 +204,8 @@ int main() { BufferPlacementTests(); RefusedFallbackTests(); ReclaimTests(); + HeadroomTests(); + SecondHeapHeadroomTests(); } catch (const std::exception& error) { std::fprintf(stderr, "FAIL: %s\n", error.what()); return 1; diff --git a/core/libs/prx/libSceAgcDriver/tests/FragmentBarycentric.cpp b/core/libs/prx/libSceAgcDriver/tests/FragmentBarycentric.cpp index 6b67d8629f..e65077a142 100644 --- a/core/libs/prx/libSceAgcDriver/tests/FragmentBarycentric.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/FragmentBarycentric.cpp @@ -29,5 +29,9 @@ int main() { ++failures; } } + if (!AgcDriver::HostInterpolationPreferred(VK_DRIVER_ID_MESA_RADV) || AgcDriver::HostInterpolationPreferred(VK_DRIVER_ID_NVIDIA_PROPRIETARY) || AgcDriver::HostInterpolationPreferred(VK_DRIVER_ID_AMD_PROPRIETARY)) { + std::fprintf(stderr, "host interpolation must be preferred on RADV only\n"); + ++failures; + } return failures == 0 ? 0 : 1; } diff --git a/core/libs/prx/libSceAgcDriver/tests/FrameLimit.cpp b/core/libs/prx/libSceAgcDriver/tests/FrameLimit.cpp index 9337c47873..1b84c99965 100644 --- a/core/libs/prx/libSceAgcDriver/tests/FrameLimit.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/FrameLimit.cpp @@ -1,5 +1,6 @@ #include "prx/libSceAgcDriver/Execution/include/FrameLimit.hpp" #include +#include #include #include #include @@ -15,6 +16,11 @@ void require(bool condition, const std::string& reason) { if (!condition) throw std::runtime_error(reason); } +template +void require(bool condition, Reason&& reason) { + if (!condition) throw std::runtime_error(reason()); +} + Nanos exactNanos(std::uint64_t frames, std::uint64_t milliFps) { return static_cast(static_cast(frames) * 1'000'000'000'000ull / milliFps); } @@ -26,7 +32,7 @@ void deadlineArithmeticTests() { require(pacer.Admit(now) == 0, "the first frame of a run is delayed at " + std::to_string(milliFps)); for (std::uint64_t frame = 1; frame <= 1'000'000; ++frame) { now += pacer.Admit(now); - require(now == exactNanos(frame, milliFps), "frame " + std::to_string(frame) + " at limit " + std::to_string(milliFps) + " is presented at " + std::to_string(now) + " instead of " + std::to_string(exactNanos(frame, milliFps))); + require(now == exactNanos(frame, milliFps), [&] { return "frame " + std::to_string(frame) + " at limit " + std::to_string(milliFps) + " is presented at " + std::to_string(now) + " instead of " + std::to_string(exactNanos(frame, milliFps)); }); } } } @@ -42,7 +48,7 @@ void overshootDoesNotAccumulateTests() { const Nanos overshoot = static_cast((seed >> 33) % 2'000'000); now += pacer.Admit(now) + overshoot; const Nanos late = now - exactNanos(frame, milliFps); - require(late >= 0 && late < 2'000'000, "a sleep that overshoots by under 2 ms leaves frame " + std::to_string(frame) + " " + std::to_string(late) + " ns after its deadline"); + require(late >= 0 && late < 2'000'000, [&] { return "a sleep that overshoots by under 2 ms leaves frame " + std::to_string(frame) + " " + std::to_string(late) + " ns after its deadline"; }); } } @@ -91,7 +97,7 @@ void slowGameIsNeverDelayedTests() { const Nanos period = exactNanos(1, milliFps); Nanos now = 5'000'000'000; for (int frame = 0; frame < 10'000; ++frame) { - require(pacer.Admit(now) == 0, "a game slower than the limit " + std::to_string(milliFps) + " is delayed at frame " + std::to_string(frame)); + require(pacer.Admit(now) == 0, [&] { return "a game slower than the limit " + std::to_string(milliFps) + " is delayed at frame " + std::to_string(frame); }); now += period + period / 1000 + 1; } } diff --git a/core/libs/prx/libSceAgcDriver/tests/GcnDecode.cpp b/core/libs/prx/libSceAgcDriver/tests/GcnDecode.cpp index 855c0aecdb..3499fa7ed7 100644 --- a/core/libs/prx/libSceAgcDriver/tests/GcnDecode.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/GcnDecode.cpp @@ -31,11 +31,32 @@ void SetregImmKeepsItsLiteral() { Check(Decode(movk).wordCount == 1u, "s_movk_i32 took a literal word"); } +void Vop3FormsOfMappedVop2AndVopcDecode() { + const std::array subb{0xd2520401u, 0x001a0702u, 0xbf810000u}; + const auto borrow = Decode(subb); + Check(borrow.op == RdnaOpcode::VSubCoCiU32, "VOP3 v_subb_u32 did not decode as v_sub_co_ci_u32"); + Check(borrow.wordCount == 2u, "VOP3 v_subb_u32 is not two words"); + Check(borrow.destination.kind == RdnaOperandKind::VectorRegister && borrow.destination.reg == 1u, "VOP3 v_subb_u32 lost its vector destination"); + Check(borrow.destination2.kind == RdnaOperandKind::ScalarRegister && borrow.destination2.reg == 4u, "VOP3 v_subb_u32 lost its borrow-out SGPR pair"); + Check(borrow.source2.kind == RdnaOperandKind::ScalarRegister && borrow.source2.reg == 6u, "VOP3 v_subb_u32 lost its borrow-in SGPR pair"); + const std::array subbrev{0xd2540401u, 0x001a0702u, 0xbf810000u}; + Check(Decode(subbrev).op == RdnaOpcode::VSubrevCoCiU32, "VOP3 v_subbrev_u32 did not decode as v_subrev_co_ci_u32"); + const std::array truF32{0xd01e0004u, 0x00020300u, 0xbf810000u}; + const auto always = Decode(truF32); + Check(always.op == RdnaOpcode::VCmpTruF32, "VOP3 v_cmp_tru_f32 did not decode as its RDNA opcode"); + Check(always.destination.kind == RdnaOperandKind::ScalarRegister && always.destination.reg == 4u, "VOP3 v_cmp_tru_f32 lost its scalar destination"); + const std::array cmpsLtF32{0xd0820004u, 0x00020300u, 0xbf810000u}; + Check(Decode(cmpsLtF32).op == RdnaOpcode::VCmpLtF32, "VOP3 v_cmps_lt_f32 did not decode as v_cmp_lt_f32"); + const std::array cmpsTruF64{0xd0de0004u, 0x00020500u, 0xbf810000u}; + Check(Decode(cmpsTruF64).op == RdnaOpcode::VCmpTruF64, "VOP3 v_cmps_tru_f64 did not decode as v_cmp_tru_f64"); +} + } int main() { try { SetregImmKeepsItsLiteral(); + Vop3FormsOfMappedVop2AndVopcDecode(); } catch (const std::exception& error) { std::fprintf(stderr, "%s\n", error.what()); return 1; diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 6dd387e17b..941a074032 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -3,11 +3,13 @@ #include "BdaTests.hpp" #include "GraphicsTests.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Memory/DrawWriteRanges.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" #include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp" #include "prx/libSceAgcDriver/Graphics/include/PipelineJournal.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureCache.hpp" #include "prx/libSceAgcDriver/Graphics/include/VertexInput.hpp" #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" #include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp" @@ -6982,6 +6984,161 @@ void resourceTests() { } } +void unsetFormatStorageImageTests() { + using ShaderRecompiler::DescriptorBinding; + using ShaderRecompiler::DescriptorImageShape; + using ShaderRecompiler::DescriptorKind; + using ShaderRecompiler::DescriptorRole; + constexpr std::uint32_t Format8888UNorm = 56; + constexpr std::uint32_t Type2D = 9; + alignas(256) std::array guest{}; + const auto base = reinterpret_cast(guest.data()); + const auto words = [](std::uint32_t format, std::uint64_t baseAddress) { + return std::vector{ + static_cast(baseAddress >> 8u), + static_cast((baseAddress >> 40u) & 0xffu) | ((format & 0x1ffu) << 20u) | (3u << 30u), + 3u | (15u << 14u), + 0xfacu | (Type2D << 28u), + 0u, + 0u, + 0u, + 0u, + }; + }; + const auto storage = [&](std::uint32_t format, std::uint64_t baseAddress) { + DescriptorBinding binding{}; + binding.kind = DescriptorKind::StorageImage; + binding.role = DescriptorRole::GuestImages; + binding.binding = ShaderRecompiler::RuntimeAbi::FirstStorageImageBinding; + binding.count = 1; + binding.imageWritten = {true}; + binding.guestDescriptor = words(format, baseAddress); + return binding; + }; + for (const std::uint32_t format : {0u, 0x1ffu}) { + mock = MockVulkan{}; + auto context = mockContext(); + context.device = makeHandle(); + context.limits.maxPerStageDescriptorStorageImages = 8; + context.limits.maxDescriptorSetStorageImages = 8; + ShaderRecompiler::RecompileResult vertex; + ShaderRecompiler::RecompileResult fragment; + vertex.bindings.push_back(storage(format, base)); + const auto color = AgcDriver::Graphics::DecodeState(makeState()).color; + { + AgcDriver::Graphics::TextureDetiler detiler(context); + context.detiler = &detiler; + expectFailure([&] { AgcDriver::Graphics::ShaderResources resources(context, vertex, fragment, color, 0, 0); }, "unsupported guest texture format"); + } + Require(mock.live == 0, "a rejected unset-format storage image leaked Vulkan objects"); + } + { + mock = MockVulkan{}; + auto context = mockContext(); + context.device = makeHandle(); + context.limits.maxPerStageDescriptorSampledImages = 8; + context.limits.maxDescriptorSetSampledImages = 8; + ShaderRecompiler::RecompileResult vertex; + ShaderRecompiler::RecompileResult fragment; + auto sampled = storage(0u, base); + sampled.kind = DescriptorKind::SampledImage; + sampled.binding = ShaderRecompiler::RuntimeAbi::FirstImageBinding; + sampled.imageWritten.clear(); + sampled.imageShape = DescriptorImageShape::Image2D; + sampled.imageSamplers = {0}; + vertex.bindings.push_back(sampled); + const auto color = AgcDriver::Graphics::DecodeState(makeState()).color; + { + AgcDriver::Graphics::TextureDetiler detiler(context); + context.detiler = &detiler; + AgcDriver::Graphics::TextureCache textureCache(context); + context.textureCache = &textureCache; + AgcDriver::Graphics::ShaderResources resources(context, vertex, fragment, color, 0, 0); + } + Require(mock.live == 0, "a sampled unset-format image leaked Vulkan objects"); + } + { + mock = MockVulkan{}; + auto context = mockContext(); + context.device = makeHandle(); + context.limits.maxPerStageDescriptorSampledImages = 8; + context.limits.maxDescriptorSetSampledImages = 8; + ShaderRecompiler::RecompileResult vertex; + ShaderRecompiler::RecompileResult fragment; + auto sampled = storage(0u, base); + sampled.kind = DescriptorKind::SampledImage; + sampled.binding = ShaderRecompiler::RuntimeAbi::FirstImageBinding; + sampled.imageWritten.clear(); + sampled.imageShape = DescriptorImageShape::Image1DArray; + sampled.imageSamplers = {0}; + sampled.guestDescriptor[3] = (sampled.guestDescriptor[3] & ~(0xfu << 28u)) | (12u << 28u); + vertex.bindings.push_back(sampled); + const auto color = AgcDriver::Graphics::DecodeState(makeState()).color; + { + AgcDriver::Graphics::TextureDetiler detiler(context); + context.detiler = &detiler; + AgcDriver::Graphics::TextureCache textureCache(context); + context.textureCache = &textureCache; + AgcDriver::Graphics::ShaderResources resources(context, vertex, fragment, color, 0, 0); + } + Require(mock.live == 0, "a sampled TYPE 12 image with an unset format leaked Vulkan objects"); + } + for (const std::uint32_t format : {0u, 0x1ffu}) { + mock = MockVulkan{}; + auto context = mockContext(); + context.device = makeHandle(); + context.limits.maxPerStageDescriptorSampledImages = 8; + context.limits.maxDescriptorSetSampledImages = 8; + ShaderRecompiler::RecompileResult vertex; + ShaderRecompiler::RecompileResult fragment; + auto sampled = storage(format, base); + sampled.kind = DescriptorKind::SampledImage; + sampled.binding = ShaderRecompiler::RuntimeAbi::FirstImageBinding; + sampled.imageWritten.clear(); + sampled.imageShape = DescriptorImageShape::Image2D; + sampled.imageSamplers = {0}; + sampled.guestDescriptor[3] = (sampled.guestDescriptor[3] & ~(0xfu << 28u)) | (2u << 28u); + vertex.bindings.push_back(sampled); + const auto color = AgcDriver::Graphics::DecodeState(makeState()).color; + { + AgcDriver::Graphics::TextureDetiler detiler(context); + context.detiler = &detiler; + AgcDriver::Graphics::TextureCache textureCache(context); + context.textureCache = &textureCache; + expectFailure([&] { AgcDriver::Graphics::ShaderResources resources(context, vertex, fragment, color, 0, 0); }, "unsupported image type 2"); + } + Require(mock.live == 0, "a rejected reserved-type sampled image leaked Vulkan objects"); + } + { + mock = MockVulkan{}; + auto context = mockContext(); + context.device = makeHandle(); + context.limits.maxPerStageDescriptorStorageImages = 8; + context.limits.maxDescriptorSetStorageImages = 8; + ShaderRecompiler::RecompileResult vertex; + ShaderRecompiler::RecompileResult fragment; + vertex.bindings.push_back(storage(0u, 0)); + const auto color = AgcDriver::Graphics::DecodeState(makeState()).color; + { + AgcDriver::Graphics::TextureDetiler detiler(context); + context.detiler = &detiler; + AgcDriver::Graphics::ShaderResources resources(context, vertex, fragment, color, 0, 0); + } + Require(mock.live == 0, "a null storage image with an unset format leaked Vulkan objects"); + } + { + ShaderRecompiler::RecompileResult compiled; + compiled.bindings.push_back(storage(Format8888UNorm, base)); + bool bounded = false; + const auto ranges = AgcDriver::DriverDetail::DispatchWriteRanges(compiled, bounded); + Require(ranges.size() == 1 && ranges[0].first == base && ranges[0].second == base + 4096u, "a written storage image did not contribute its whole surface to the dispatch write ranges"); + compiled.bindings.front().guestDescriptor = words(0u, base); + expectFailure([&] { static_cast(AgcDriver::DriverDetail::DispatchWriteRanges(compiled, bounded)); }, "unsupported guest texture format"); + compiled.bindings.front().guestDescriptor = words(0u, 0u); + Require(AgcDriver::DriverDetail::DispatchWriteRanges(compiled, bounded).empty(), "a null written storage image contributed dispatch write ranges"); + } +} + void descriptorCacheTests() { mock = MockVulkan{}; auto context = mockContext(); @@ -7351,6 +7508,49 @@ void hostImportBudgetTests() { Require(HostImportBudget(memory, 16 * GiB) == 8 * GiB, "a host heap past memoryHeapCount limits the import budget"); } +void unifiedHeapImportBudgetTests() { + using AgcDriver::Graphics::HostImportBudgetFor; + constexpr std::uint64_t MiB = 1ull << 20u; + constexpr std::uint64_t GiB = 1ull << 30u; + VkPhysicalDeviceMemoryProperties unified{}; + unified.memoryHeapCount = 1; + unified.memoryHeaps[0] = {24 * GiB, VK_MEMORY_HEAP_DEVICE_LOCAL_BIT}; + unified.memoryTypeCount = 3; + unified.memoryTypes[0] = {VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, 0}; + unified.memoryTypes[1] = {VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT | VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT | VK_MEMORY_PROPERTY_HOST_CACHED_BIT, 0}; + unified.memoryTypes[2] = {VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT | VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT, 0}; + VkPhysicalDeviceMemoryBudgetPropertiesEXT report{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT}; + report.heapBudget[0] = 18 * GiB; + report.heapUsage[0] = 0; + Require(HostImportBudgetFor(unified, &report, 12 * GiB, 0) == 12 * GiB, "an idle unified heap lowered the import budget"); + report.heapUsage[0] = 8 * GiB; + Require(HostImportBudgetFor(unified, &report, 12 * GiB, 0) == 7936 * MiB, "a unified heap used by other allocations did not lower the import budget"); + Require(HostImportBudgetFor(unified, &report, 12 * GiB, 4 * GiB) == 12032 * MiB, "a unified heap counted its own imports as other allocations"); + report.heapUsage[0] = 17 * GiB; + Require(HostImportBudgetFor(unified, &report, 12 * GiB, 0) == 0, "a unified heap past its budget left room for imports"); + Require(HostImportBudgetFor(unified, nullptr, 12 * GiB, 0) == 12 * GiB, "a missing budget report lowered the import budget"); + auto deviceOnly = unified; + deviceOnly.memoryTypeCount = 2; + deviceOnly.memoryTypes[1].propertyFlags = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT; + report.heapUsage[0] = 8 * GiB; + Require(HostImportBudgetFor(deviceOnly, &report, 12 * GiB, 0) == 12 * GiB, "a heap without host-coherent memory was taken as the host heap"); + + VkPhysicalDeviceMemoryProperties discrete{}; + discrete.memoryHeapCount = 2; + discrete.memoryHeaps[0] = {8 * GiB, VK_MEMORY_HEAP_DEVICE_LOCAL_BIT}; + discrete.memoryHeaps[1] = {16 * GiB, 0}; + discrete.memoryTypeCount = 3; + discrete.memoryTypes[0] = {VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, 0}; + discrete.memoryTypes[1] = {VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT | VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, 0}; + discrete.memoryTypes[2] = {VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, 1}; + VkPhysicalDeviceMemoryBudgetPropertiesEXT split{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT}; + split.heapBudget[0] = 6 * GiB; + split.heapUsage[0] = 6 * GiB; + split.heapBudget[1] = 12 * GiB; + split.heapUsage[1] = 0; + Require(HostImportBudgetFor(discrete, &split, 20 * GiB, 0) == 10752 * MiB, "a device with a separate host heap used its device-local heap for the import budget"); +} + void tableUpdateAfterBindTests() { namespace Abi = ShaderRecompiler::ImageTableAbi; using namespace AgcDriver::Graphics; @@ -10359,7 +10559,7 @@ void ProvokingVertexTests() { ValidateProvokingVertex(supported, meshDraw, shaders); expectFailure([&] { ValidateProvokingVertex(supported, meshDraw, slotShaders); }, "last provoking vertex with explicit per-vertex triangle-strip interpolation is unsupported"); meshDraw.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN; - expectFailure([&] { ValidateProvokingVertex(supported, meshDraw, flatShaders); }, "last provoking vertex is unsupported for triangle fans assembled for a geometry program"); + for (const auto& drawn : {std::span{}, std::span(flatShaders), std::span(flatP10Shaders), std::span(smoothShaders), std::span(slotShaders)}) ValidateProvokingVertex(supported, meshDraw, drawn); for (const auto topology : {VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN}) { auto firstDraw = meshDraw; firstDraw.topology = topology; @@ -10682,7 +10882,11 @@ void uintTargetTests() { Require(uint16.color.packing == ColorExportPacking::None && uint16.color.uintExport, "a UINT16_ABGR export into a UINT target was retyped twice"); auto halves = queue; halves.context[0x1c5] = 4u; - expectFailure([&] { static_cast(AgcDriver::Graphics::DecodeState(halves)); }, "color export format 4 into an unsigned integer target is unsupported"); + for (const auto& [format, vkFormat, packing] : std::array, 5>{{{4u, VK_FORMAT_R32_UINT, ColorExportPacking::Uint}, {1u, VK_FORMAT_R8_UINT, ColorExportPacking::UintClamp8}, {10u, VK_FORMAT_R8G8B8A8_UINT, ColorExportPacking::UintClamp8}, {2u, VK_FORMAT_R16_UINT, ColorExportPacking::UintClamp16}, {12u, VK_FORMAT_R16G16B16A16_UINT, ColorExportPacking::UintClamp16}}}) { + halves.context[0x31c] = (queue.context.at(0x31c) & ~0x1f7cu) | (format << 2u) | (4u << 8u); + const auto half = AgcDriver::Graphics::DecodeState(halves); + Require(half.color.format == vkFormat && half.color.packing == packing && !half.color.uintExport && AgcDriver::Graphics::ExportPackings(half)[0] == packing && !half.blends.at(0).blendEnable, "an FP16_ABGR export into a UINT target did not get the packing that clamps to its channel width"); + } auto unorm = queue; unorm.context[0x31c] = (unorm.context.at(0x31c) & ~0x1f7cu) | (4u << 2u) | (7u << 8u); Require(AgcDriver::Graphics::DecodeState(unorm).color.packing == ColorExportPacking::None, "a 32-bit float target got the unsigned integer export packing"); @@ -11179,7 +11383,7 @@ void float11_11_10TargetTests() { const std::array shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &packed, 0}}}; Require(AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false) == std::set{0u}, "the packed float export did not reach attachment 0"); auto invalid = request; - invalid.context.pixel->targetExportPacking[0] = static_cast(5); + invalid.context.pixel->targetExportPacking[0] = static_cast(7); expectFailure([&] { static_cast(ShaderRecompiler::Recompile(invalid)); }, "invalid prepared fragment export packing"); } @@ -11662,6 +11866,15 @@ void validationTests() { expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false); }, "unsupported device capability 12"); AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, false, false, false, true); } + { + ShaderRecompiler::RecompileResult vertex; + vertex.spirv = makeModule({}); + vertex.spirv.insert(vertex.spirv.begin() + 5, {(2u << 16u) | spv::OpCapability, static_cast(spv::CapabilityMinLod)}); + const std::array shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, 0}}}; + const VkPhysicalDeviceSubgroupProperties subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES}; + expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false); }, "unsupported device capability 42"); + AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, false, false, false, false, false, false, false, false, false, false, true, true); + } { ShaderRecompiler::RecompileResult vertex; vertex.spirv = makeModule({.layerOutput = true}); @@ -12711,6 +12924,18 @@ void pipelineJournalTests(bool longPath = false, bool extended = false) { mappedState.colors[0].componentMapping = 0x24; BuildPipelineStoreKey(mapped, context, mappedState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL); Require(mapped != original, "the component mapping did not enter the key"); + auto legacyState = state; + legacyState.stages.legacyVertex = true; + auto legacyVertex = module(4); + legacyVertex.bindings = vertex.bindings; + const std::array legacyShaders{{{Stage::Vertex, &legacyVertex, 0}, {Stage::Fragment, &fragment, PipelinePushSlotBytes}}}; + GraphicsPipelineRecipe legacyDecoded; + Require(DecodeGraphicsPipelineRecipe(EncodeGraphicsPipelineRecipe(MakeGraphicsPipelineRecipe(legacyState, input, layouts.View(), legacyShaders, VK_IMAGE_LAYOUT_GENERAL)), legacyDecoded), "the legacy vertex recipe does not decode"); + std::vector legacyFlag, legacyKey, legacyReplayed; + BuildPipelineStoreKey(legacyFlag, context, legacyState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL); + BuildPipelineStoreKey(legacyKey, context, legacyState, input, layouts.View(), legacyShaders, VK_IMAGE_LAYOUT_GENERAL); + BuildPipelineStoreKey(legacyReplayed, context, legacyDecoded.state, legacyDecoded.vertexInput, layouts.View(), legacyShaders, legacyDecoded.attachmentLayout); + Require(legacyFlag == original && legacyKey != original && legacyKey == legacyReplayed, "the legacy vertex selection keyed a pipeline by the state flag instead of by its vertex module"); std::vector unclamped, clamped, biased; auto clampedState = state; clampedState.depthBiasClamp = 1.0f; @@ -13278,6 +13503,7 @@ int main(int argc, char** argv) { descriptorPoolTests(); pushConstantTests(); resourceTests(); + unsetFormatStorageImageTests(); oversizedDescriptorTests(); viewAtRangeLimitTests(); guestBufferViewBytesTests(); @@ -13294,6 +13520,7 @@ int main(int argc, char** argv) { imageTableTemplateRefreshTests(); debugBranchTests(); hostImportBudgetTests(); + unifiedHeapImportBudgetTests(); textureCacheBudgetTests(); sampledTextureBudgetTests(); storageTextureBudgetTests(); diff --git a/core/libs/prx/libSceAgcDriver/tests/GuardedMemoryLoads.cpp b/core/libs/prx/libSceAgcDriver/tests/GuardedMemoryLoads.cpp index 1aa3987714..057323c8c7 100644 --- a/core/libs/prx/libSceAgcDriver/tests/GuardedMemoryLoads.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/GuardedMemoryLoads.cpp @@ -6,6 +6,7 @@ #include "SpirvBackend/SpirvEmitter.hpp" #include "SpirvBackend/SpirvAnalysis.hpp" #include "PipelineSpecialization.hpp" +#include #include #include #include @@ -19,6 +20,9 @@ namespace { +constexpr std::uint32_t PoolDwords = 514u; +constexpr std::uint32_t PoolHeader = 2u; + using namespace ShaderRecompiler; void require(bool condition, const std::string& message) { @@ -145,6 +149,15 @@ public: std::vector reads; std::uint32_t previous = 0; std::uint32_t current = 0; + const bool pooled = std::ranges::any_of(names, [](const auto& entry) { return entry.second == "scratch_pool"; }); + if (pooled) { + std::size_t scan = cursor; + for (; scan < words.size() && (words[scan] & 0xffff) != spv::OpControlBarrier; scan += words[scan] >> 16) { + if ((words[scan] & 0xffff) == spv::OpLabel) current = words[scan + 1]; + } + require(scan < words.size(), "scratch pool slot acquisition has no workgroup barrier"); + cursor = scan + (words[scan] >> 16); + } for (std::size_t steps = 0; steps < 4096; ++steps) { const auto instruction = std::span(words).subspan(cursor, words[cursor] >> 16); const auto opcode = static_cast(instruction[0] & 0xffff); @@ -161,6 +174,7 @@ public: require((pointer[0] & 0xffff) == spv::OpAccessChain, "guest read is not an indexed access"); reads.push_back(value(pointer.back())); if (rootName(instruction[3]) == "scratch_dwords") require(pointerStorage(instruction[3]) == spv::StorageClassFunction, "scratch selection accessed non-private storage"); + if (rootName(instruction[3]) == "scratch_pool") require(pointerStorage(instruction[3]) == spv::StorageClassStorageBuffer, "scratch pool accessed outside a storage buffer"); if (rootName(instruction[3]).starts_with("buffers")) require(instruction.size() == 5 && (instruction[4] & spv::MemoryAccessVolatileMask), "coherent buffer read lost its volatile access"); } if (opcode == spv::OpBranch || opcode == spv::OpBranchConditional) { @@ -200,7 +214,7 @@ private: bool isGuestPointer(std::uint32_t id) const { const auto name = rootName(id); - return name == "lds_dwords" || name == "gds" || name == "scratch_dwords" || name.starts_with("buffers"); + return name == "lds_dwords" || name == "gds" || name == "scratch_dwords" || name == "scratch_pool" || name.starts_with("buffers"); } std::uint32_t value(std::uint32_t id) const { @@ -247,10 +261,12 @@ private: case spv::OpLogicalNot: return !argument(3); case spv::OpArrayLength: require(isGuestPointer(instruction[3]), "unknown array length in access fixture"); - return 16; + return rootName(instruction[3]) == "scratch_pool" ? PoolDwords : 16; + case spv::OpUDiv: return argument(3) / argument(4); + case spv::OpAtomicIDecrement: return 0; case spv::OpLoad: require(!isGuestPointer(instruction[3]), "guest data unexpectedly controls a fixture access"); - require(rootName(instruction[3]) == "vsharp" || rootName(instruction[3]).find("subgroup") != std::string::npos, "unknown input to access condition"); + require(rootName(instruction[3]) == "vsharp" || rootName(instruction[3]) == "scratch_pool_slot" || rootName(instruction[3]) == "gl_LocalInvocationIndex" || rootName(instruction[3]).find("subgroup") != std::string::npos, "unknown input to access condition"); return 0; default: throw std::runtime_error("unsupported opcode in access condition: " + std::to_string(opcode)); } @@ -461,9 +477,9 @@ int main() { run("private scratch selection", [] { const LoadCase test{ResourceKind::Scratch, IrOpcode::LoadAddressU32, IrType::U32}; for (const bool robust : {false, true}) { - expectReads(test, robust, false, 8, {0}); - expectReads(test, robust, true, 64, {0}); - expectReads(test, robust, true, 8, {2}); + expectReads(test, robust, false, 8, {PoolHeader}); + expectReads(test, robust, true, 64, {PoolHeader}); + expectReads(test, robust, true, 8, {PoolHeader + 2u}); } }); if (failures != 0) { diff --git a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp index 54799df27a..2854acd563 100644 --- a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp @@ -158,6 +158,23 @@ void RunGuestTextureResourceTests() { const std::array reportedStorage{0x04efd000u, 0x00040011u, 0x00017a00u, 0x00005204u, 0x0af00000u, 0u, 0x00000612u, 0x00fff000u}; Require(IsNullTextureResource(reportedStorage), "the type-0 words of a written storage slot were not classified as null"); reject([&] { static_cast(DecodeTextureResource(reportedStorage)); }, "guest texture descriptor"); + Fields unsetFormat = base; + unsetFormat.format = 0u; + unsetFormat.typeRaw = 12u; + Require(!IsNullTextureResource(pack(unsetFormat)), "a zero-format texture was classified as null for every consumer"); + Require(IsNullSampledTextureResource(pack(unsetFormat)), "a zero-format image-type texture was not classified as null for a sampled consumer"); + reject([] { ResolveTextureFormat(0u); }, "unsupported guest texture format"); + unsetFormat.format = 0x1ffu; + Require(!IsNullTextureResource(pack(unsetFormat)), "an all-ones-format texture was classified as null for every consumer"); + Require(IsNullSampledTextureResource(pack(unsetFormat)), "an all-ones-format image-type texture was not classified as null for a sampled consumer"); + reject([] { ResolveTextureFormat(0x1ffu); }, "unsupported guest texture format"); + unsetFormat.typeRaw = 2u; + unsetFormat.format = 0u; + Require(!IsNullSampledTextureResource(pack(unsetFormat)), "a reserved type-2 texture with a zero format was classified as null for a sampled consumer"); + unsetFormat.format = 0x1ffu; + Require(!IsNullSampledTextureResource(pack(unsetFormat)), "a reserved type-2 texture with an all-ones format was classified as null for a sampled consumer"); + unsetFormat.typeRaw = 15u; + Require(!IsNullSampledTextureResource(pack(unsetFormat)), "a type-15 texture with an unset format was classified as null for a sampled consumer"); auto result = DecodeTextureResource(pack(base)); Require(result.baseAddress == (base.base40 << 8u), "decoded base address changed"); Require(result.width == 16 && result.height == 16, "decoded width or height changed"); diff --git a/core/libs/prx/libSceAgcDriver/tests/HostImportLifetime.cpp b/core/libs/prx/libSceAgcDriver/tests/HostImportLifetime.cpp index 603d4bb0a2..cdd0c44f52 100644 --- a/core/libs/prx/libSceAgcDriver/tests/HostImportLifetime.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/HostImportLifetime.cpp @@ -206,11 +206,16 @@ public: tracking = nullptr; } - std::shared_ptr Import(std::uint64_t address, std::size_t bytes) { + std::shared_ptr TryImport(std::uint64_t address, std::size_t bytes) { std::lock_guard gpu(GpuMutex()); const auto* imported = HostImportFor(context, address, bytes); + return imported != nullptr ? calls.Add(*imported) : nullptr; + } + + std::shared_ptr Import(std::uint64_t address, std::size_t bytes) { + auto imported = TryImport(address, bytes); Require(imported != nullptr, "host pointer import was refused for the lifetime test"); - return calls.Add(*imported); + return imported; } void Cleanup() { @@ -701,6 +706,7 @@ struct MutationRace { Allocation* allocation; std::shared_ptr replacement; unsigned calls = 0; + bool refused = false; }; MutationRace* mutationRace = nullptr; @@ -710,7 +716,10 @@ bool raceWaiter(std::uintptr_t address, std::size_t bytes) { Require(address == race.allocation->Address() && bytes == race.allocation->bytes, "pin waiter received the wrong mutation extent"); std::lock_guard gpu(GpuMutex()); race.fixture->Cleanup(); - if (++race.calls == 1) race.replacement = race.fixture->Import(address, bytes); + if (++race.calls == 1) { + race.replacement = race.fixture->TryImport(address, bytes); + race.refused = race.replacement == nullptr; + } return true; } @@ -749,7 +758,7 @@ void waiterFailure(Fixture& fixture) { Require(!imported->freed.load(), "a failed pin waiter retired an import"); } -void reimportBeforeRelock(Fixture& fixture) { +void reimportRefusedWhileWaiting(Fixture& fixture) { Allocation allocation(fixture); const auto imported = fixture.Import(allocation.Address(), allocation.bytes); MutationRace race{&fixture, &allocation}; @@ -758,9 +767,8 @@ void reimportBeforeRelock(Fixture& fixture) { { GuestAllocations::Mutation mutation; mutation.Unmap(allocation.data, allocation.bytes, [&](const void*, std::size_t, const void*, bool) { - Require(race.calls == 2 && race.replacement != nullptr, "guest mutation did not recheck an import acquired before registry relock"); + Require(race.refused && race.calls == 1 && race.replacement == nullptr, "an import of an extent a guest mutation waits on was not refused"); requireFreed(imported); - requireFreed(race.replacement); allocation.Release(); }); allocation.registered = false; @@ -801,7 +809,7 @@ int RunHostImportMutationRaceTests(const Context& context) { } Fixture fixture(context); waiterFailure(fixture); - reimportBeforeRelock(fixture); + reimportRefusedWhileWaiting(fixture); std::puts("host import mutation race tests passed"); return 0; } diff --git a/core/libs/prx/libSceAgcDriver/tests/KeyboardOverlay.cpp b/core/libs/prx/libSceAgcDriver/tests/KeyboardOverlay.cpp index ab213f5f9b..c698247b25 100644 --- a/core/libs/prx/libSceAgcDriver/tests/KeyboardOverlay.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/KeyboardOverlay.cpp @@ -296,6 +296,95 @@ void caretTests() { std::printf("caret: one bar per field, after its character, the text scrolled to keep it in the 44-character field\n"); } +ImeKeyboardView composing(const std::u16string& text, std::size_t caret, const std::u16string& preedit, std::size_t cursor, std::size_t selection) { + ImeKeyboardView view = viewOf(ImeDialog::PadKeyboardOpen({}), text, caret); + view.preedit = preedit; + view.preeditCursor = cursor; + view.preeditSelection = selection; + return view; +} + +bool sameRects(const std::vector& left, const std::vector& right) { + if (left.size() != right.size()) return false; + for (std::size_t i = 0; i < left.size(); ++i) { + if (left[i].x != right[i].x || left[i].y != right[i].y || left[i].width != right[i].width || left[i].height != right[i].height || left[i].color != right[i].color) return false; + } + return true; +} + +OverlayRect underline(const OverlayLayout& layout, std::size_t column, std::int32_t rows, OverlayColor color) { + const auto scale = layout.scale; + const auto x = KeyboardMetrics::Padding + KeyboardMetrics::FieldInset + static_cast(column) * KeyboardMetrics::Advance; + const auto y = KeyboardMetrics::Padding + KeyboardMetrics::FieldInset + KeyboardMetrics::GlyphHeight; + return {layout.bounds.x + x * scale, layout.bounds.y + y * scale, KeyboardMetrics::Advance * scale, rows * scale, colorOf(color)}; +} + +std::vector without(const std::vector& rects, const std::vector& removed) { + std::vector kept; + for (const auto& rect : rects) { + bool drop = false; + for (const auto& gone : removed) { + if (rect.x == gone.x && rect.y == gone.y && rect.width == gone.width && rect.height == gone.height && rect.color == gone.color) drop = true; + } + if (!drop) kept.push_back(rect); + } + return kept; +} + +void compositionTests() { + const VkExtent2D extent{1280, 720}; + const auto plain = AgcDriver::LayOutKeyboard(composing(u"ab", 1, u"", 0, 0), extent); + const auto ignored = AgcDriver::LayOutKeyboard(composing(u"ab", 1, u"", 3, 2), extent); + require(sameRects(plain.rects, ignored.rects), "a cursor and a selection with no composition changed the layout"); + const auto baseline = AgcDriver::LayOutKeyboard(viewOf(ImeDialog::PadKeyboardOpen({}), u"ab", 1), extent); + require(sameRects(plain.rects, baseline.rects), "a view with an empty composition does not lay out as before"); + + const auto inserted = AgcDriver::LayOutKeyboard(composing(u"ab", 1, u"X", 1, 0), extent); + const auto flat = AgcDriver::LayOutKeyboard(viewOf(ImeDialog::PadKeyboardOpen({}), u"aXb", 2), extent); + const auto extra = std::vector{underline(inserted, 1, 1, OverlayColor::Accent)}; + require(inserted.rects.size() == flat.rects.size() + 1, "a one-character composition adds " + std::to_string(inserted.rects.size() - flat.rects.size()) + " rectangles, not one underline"); + require(sameRects(without(inserted.rects, extra), flat.rects), "the composition is not drawn between the text before and after the caret, or its caret is not after it"); + + const auto selected = AgcDriver::LayOutKeyboard(composing(u"", 0, u"abc", 1, 1), extent); + const auto flatSelected = AgcDriver::LayOutKeyboard(viewOf(ImeDialog::PadKeyboardOpen({}), u"abc", 1), extent); + const std::vector marks{underline(selected, 0, 1, OverlayColor::Accent), underline(selected, 1, 2, OverlayColor::Text), underline(selected, 2, 1, OverlayColor::Accent)}; + require(sameRects(without(selected.rects, marks), flatSelected.rects), "the selected segment is not the second character underlined twice in the text colour"); + require(selected.rects.size() == flatSelected.rects.size() + 3, "a three-character composition does not add three underlines"); + + const auto pair = AgcDriver::LayOutKeyboard(composing(u"", 0, u"a\U0001F600b", 3, 0), extent); + const auto flatPair = AgcDriver::LayOutKeyboard(viewOf(ImeDialog::PadKeyboardOpen({}), u"a\U0001F600b", 3), extent); + const std::vector pairMarks{underline(pair, 0, 1, OverlayColor::Accent), underline(pair, 1, 1, OverlayColor::Accent), underline(pair, 2, 1, OverlayColor::Accent)}; + require(sameRects(without(pair.rects, pairMarks), flatPair.rects), "a surrogate pair in the composition is not one character"); + + require(!AgcDriver::KeyboardHasGlyph(0x3053), "the overlay font gained a hiragana glyph; the composition test below assumes none"); + const auto cjk = AgcDriver::LayOutKeyboard(composing(u"", 0, u"\u3053", 1, 0), extent); + const auto box = AgcDriver::LayOutKeyboard(viewOf(ImeDialog::PadKeyboardOpen({}), u"\uFFFD", 1), extent); + require(cjk.rects.size() == box.rects.size() + 1, "a character the font lacks does not draw as the replacement box"); + + std::u16string text(60, u'a'); + const auto scrolled = AgcDriver::LayOutKeyboard(composing(text, 60, u"xyz", 3, 0), extent); + const auto scale = scrolled.scale; + const OverlayRect field{scrolled.bounds.x + KeyboardMetrics::Padding * scale, scrolled.bounds.y + KeyboardMetrics::Padding * scale, (scrolled.bounds.width / scale - 2 * KeyboardMetrics::Padding) * scale, KeyboardMetrics::FieldHeight * scale, 0}; + for (const auto& rect : scrolled.rects) { + if (rect.color == colorOf(OverlayColor::Accent) && rect.width == scale && contains(field, rect)) { + const auto column = (rect.x + scale - field.x) / scale - KeyboardMetrics::FieldInset; + require(column == 44 * KeyboardMetrics::Advance, "a composition past the field does not keep the caret at the right edge"); + } + } + require(inside(scrolled, field, OverlayColor::Accent) == 1 + 3, "a long composition does not draw one caret and three underlines"); + + for (const auto& bad : {composing(u"ab", 1, u"x", 2, 0), composing(u"ab", 1, u"xy", 1, 2), composing(u"ab", 1, u"x", 0, 5)}) { + bool threw = false; + try { + static_cast(AgcDriver::LayOutKeyboard(bad, extent)); + } catch (const std::exception&) { + threw = true; + } + require(threw, "a composition cursor or selection past its text is laid out"); + } + std::printf("composition: laid out between the text around the caret, underlined, selection marked, cursor kept in view, bad offsets rejected\n"); +} + double linear(double encoded) { return encoded <= 0.04045 ? encoded / 12.92 : std::pow((encoded + 0.055) / 1.055, 2.4); } @@ -482,6 +571,7 @@ int main() { labelTests(); fallbackTests(); caretTests(); + compositionTests(); paletteTests(); systemOverlayTests(); dialogWrapTests(); diff --git a/core/libs/prx/libSceAgcDriver/tests/LdsLockBudget.cpp b/core/libs/prx/libSceAgcDriver/tests/LdsLockBudget.cpp index 55c6fc91b1..ed44ad0080 100644 --- a/core/libs/prx/libSceAgcDriver/tests/LdsLockBudget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/LdsLockBudget.cpp @@ -117,10 +117,55 @@ void verify(std::uint32_t waveSize, std::uint32_t limit, bool int64Atomics) { require(result.bdaAbiVersion == 1u, "a 64-bit LDS atomic did not get the fault buffer for a misaligned address at limit " + std::to_string(limit)); } +alignas(256) std::array Texture{}; + +void verifySpecializedImageWithoutBuffers() { + alignas(256) static constexpr std::array code{ + 0x7e020280u, 0x7e080218u, 0x343c0885u, 0x4a3c3d00u, 0x34063c84u, 0x2c3e3c87u, 0x363c3cffu, 0x0000007fu, + 0x7e140280u, 0x7e160280u, 0x7e180280u, 0x7e1a0280u, 0xf0001f08u, 0x00010a1eu, + 0xbf8c3f70u, 0xd8340000u, 0x00000a01u, 0xbf810000u + }; + static constexpr std::array capabilities{spv::CapabilityShader, spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle}; + const auto textureAddress = reinterpret_cast(Texture.data()); + std::array userData{}; + userData[4] = static_cast(textureAddress >> 8u); + userData[5] = static_cast((textureAddress >> 40u) & 0xffu) | (6u << 20u) | (3u << 30u); + userData[6] = 31u | (127u << 14u); + userData[7] = 0x24cu | (9u << 28u); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(std::span(code))}}}; + RecompileRequest request{}; + request.shader = {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}; + request.context.waveSize = 32u; + request.context.userData = userData; + request.context.compute = ShaderComputeStageInfo{{32u, 1u, 1u}, 16u, {true, false, false}, false, 1u}; + request.context.memory = memory; + request.target.vulkanVersion = 0x00403000u; + request.target.spirvVersion = 0x00010600u; + request.target.subgroupSize = 32; + request.target.supportedCapabilities = capabilities; + request.target.maxWorkgroupSize = {1024u, 1024u, 64u}; + request.target.maxWorkgroupInvocations = 1024; + request.target.maxWorkgroupSharedMemoryBytes = 64u; + request.layout = {0, 0, 0, 128}; + request.useCache = false; + const auto result = Recompile(request); + require(!result.spirv.empty(), "image program with device-memory LDS did not compile"); + require(result.workgroupMemoryDwords == 18u, "image program did not place its LDS in device memory"); + require(result.specializationId != 0u, "image program was not specialized"); +} + } int main() { unsigned failures = 0; + try { + verifySpecializedImageWithoutBuffers(); + std::cout << "LDS lock budget: specialized image program without buffers passed\n"; + } catch (const std::exception& error) { + ++failures; + const std::string message(error.what()); + std::cerr << "LDS lock budget: specialized image program without buffers: " << message.substr(0, message.find("RecompileRequest:")) << '\n'; + } for (const auto waveSize : {32u, 64u}) { for (const auto limit : {64u, 68u, 72u}) { for (const bool int64Atomics : {false, true}) { diff --git a/core/libs/prx/libSceAgcDriver/tests/OracleCodeObject.cpp b/core/libs/prx/libSceAgcDriver/tests/OracleCodeObject.cpp new file mode 100644 index 0000000000..d0e0d05bb7 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/OracleCodeObject.cpp @@ -0,0 +1,206 @@ +#include "../tools/OracleCodeObject.hpp" +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr std::array Text{ + 0xf4080100, 0xfa000000, 0x34020084, 0x34040086, 0xbf8cc07f, 0xdc388000, 0x04040001, 0xbf8c3f70, + 0x7e140280, 0x7e160280, 0x7e180280, 0x7e1a0280, 0x7e1c0280, 0x7e1e0280, 0x7e200280, 0x7e220280, + 0x7e240280, 0x7e260280, 0x7e280280, 0x7e2a0280, 0x7e2c0280, 0x7e2e0280, 0x7e300280, 0x7e320280, + 0x64140b04, 0xdc788000, 0x00060a02, 0xdc788010, 0x00060e02, 0xdc788020, 0x00061202, 0xdc788030, + 0x00061602, 0xbf810000, +}; + +struct Spec { + std::uint16_t machine = 224; + bool probe = true; + bool descriptor = true; + std::uint64_t descriptorSize = 64; + std::uint64_t probeSize = Text.size() * 4; + std::uint64_t textSectionSize = Text.size() * 4; + std::uint32_t group = 4096; + std::uint32_t privateBytes = 0; + std::uint32_t kernarg = 16; + std::int64_t entry = 4288; + std::uint32_t rsrc1 = 0x642ed00f; + std::uint32_t rsrc2 = 0x84; + std::uint16_t properties = 0x0408; +}; + +constexpr std::uint64_t RodataAddress = 0x1000; +constexpr std::uint64_t TextAddress = RodataAddress + 4288; + +template +void Put(std::vector& bytes, std::size_t offset, T value) { + if (bytes.size() < offset + sizeof(T)) bytes.resize(offset + sizeof(T)); + std::memcpy(bytes.data() + offset, &value, sizeof(T)); +} + +std::size_t AddString(std::vector& table, const std::string& text) { + const auto offset = table.size(); + table.insert(table.end(), text.begin(), text.end()); + table.push_back('\0'); + return offset; +} + +std::vector Build(const Spec& spec) { + std::vector bytes(64); + const auto append = [&](const void* data, std::size_t size) { + const auto offset = bytes.size(); + bytes.resize(offset + size); + if (size != 0) std::memcpy(bytes.data() + offset, data, size); + return offset; + }; + const auto textOffset = append(Text.data(), Text.size() * 4); + std::vector descriptor(64); + Put(descriptor, 0, spec.group); + Put(descriptor, 4, spec.privateBytes); + Put(descriptor, 8, spec.kernarg); + Put(descriptor, 16, spec.entry); + Put(descriptor, 48, spec.rsrc1); + Put(descriptor, 52, spec.rsrc2); + Put(descriptor, 56, spec.properties); + const auto rodataOffset = append(descriptor.data(), descriptor.size()); + + std::vector strings{'\0'}; + std::vector symbols(24); + const auto addSymbol = [&](const std::string& name, std::uint8_t type, std::uint16_t section, std::uint64_t value, std::uint64_t size) { + const auto at = symbols.size(); + symbols.resize(at + 24); + Put(symbols, at, static_cast(AddString(strings, name))); + Put(symbols, at + 4, static_cast(0x10u | type)); + Put(symbols, at + 6, section); + Put(symbols, at + 8, value); + Put(symbols, at + 16, size); + }; + if (spec.probe) addSymbol("probe", 2, 1, TextAddress, spec.probeSize); + if (spec.descriptor) addSymbol("probe.kd", 1, 2, RodataAddress, spec.descriptorSize); + const auto symtabOffset = append(symbols.data(), symbols.size()); + const auto strtabOffset = append(strings.data(), strings.size()); + + std::vector names{'\0'}; + const auto textName = AddString(names, ".text"); + const auto rodataName = AddString(names, ".rodata"); + const auto symtabName = AddString(names, ".symtab"); + const auto strtabName = AddString(names, ".strtab"); + const auto shstrtabName = AddString(names, ".shstrtab"); + const auto namesOffset = append(names.data(), names.size()); + while (bytes.size() % 8 != 0) bytes.push_back(std::byte{0}); + const auto headersOffset = bytes.size(); + bytes.resize(headersOffset + 6 * 64); + const auto header = [&](std::size_t index, std::size_t name, std::uint32_t type, std::uint64_t address, std::uint64_t offset, std::uint64_t size, std::uint32_t link) { + const auto at = headersOffset + index * 64; + Put(bytes, at, static_cast(name)); + Put(bytes, at + 4, type); + Put(bytes, at + 16, address); + Put(bytes, at + 24, offset); + Put(bytes, at + 32, size); + Put(bytes, at + 40, link); + }; + header(1, textName, 1, TextAddress, textOffset, spec.textSectionSize, 0); + header(2, rodataName, 1, RodataAddress, rodataOffset, 64, 0); + header(3, symtabName, 2, 0, symtabOffset, symbols.size(), 4); + header(4, strtabName, 3, 0, strtabOffset, strings.size(), 0); + header(5, shstrtabName, 3, 0, namesOffset, names.size(), 0); + + std::memcpy(bytes.data(), "\x7f" "ELF", 4); + Put(bytes, 4, 2); + Put(bytes, 5, 1); + Put(bytes, 6, 1); + Put(bytes, 16, 3); + Put(bytes, 18, spec.machine); + Put(bytes, 20, 1); + Put(bytes, 40, headersOffset); + Put(bytes, 52, 64); + Put(bytes, 58, 64); + Put(bytes, 60, 6); + Put(bytes, 62, 5); + return bytes; +} + +void Require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +void Rejects(const std::string& label, const std::vector& bytes, const std::string& expected) { + try { + ParseOracleCodeObject(bytes); + } catch (const std::runtime_error& error) { + Require(std::string(error.what()).find(expected) != std::string::npos, label + ": message \"" + error.what() + "\" does not name \"" + expected + "\""); + return; + } + throw std::runtime_error(label + ": accepted"); +} + +void Rejects(const std::string& label, const std::function& change, const std::string& expected) { + Spec spec; + change(spec); + Rejects(label, Build(spec), expected); +} + +} + +int main() { + try { + const auto kernel = ParseOracleCodeObject(Build(Spec{})); + Require(kernel.text.size() == Text.size(), "text word count differs"); + for (std::size_t index = 0; index < Text.size(); ++index) Require(kernel.text[index] == Text[index], "text word " + std::to_string(index) + " differs"); + Require(kernel.ldsBytes == 4096, "ldsBytes is not 4096"); + Require(kernel.waveSize == 32, "waveSize is not 32"); + Require(kernel.floatMode.floatMode == 237, "floatMode is not 237"); + Require(kernel.floatMode.dx10Clamp && !kernel.floatMode.ieeeMode && kernel.floatMode.fp16Overflow, "dx10Clamp, ieeeMode or fp16Overflow differs"); + Require(kernel.groupIdEnable == (std::array{true, false, false}), "groupIdEnable differs"); + Require(!kernel.tgSizeEnable, "tgSizeEnable is set"); + Require(kernel.threadIdComponentCount == 1, "threadIdComponentCount is not 1"); + + Spec wide; + wide.properties = 0x0008; + wide.rsrc2 = 0x84u | (1u << 8) | (1u << 9) | (1u << 10) | (2u << 11); + const auto wave64 = ParseOracleCodeObject(Build(wide)); + Require(wave64.waveSize == 64, "waveSize is not 64"); + Require(wave64.groupIdEnable == (std::array{true, true, true}), "groupIdEnable differs for XYZ"); + Require(wave64.tgSizeEnable && wave64.threadIdComponentCount == 3, "tgSizeEnable or threadIdComponentCount differs"); + + Rejects("machine", [](Spec& spec) { spec.machine = 62; }, "e_machine"); + Rejects("probe", [](Spec& spec) { spec.probe = false; }, "probe"); + Rejects("probe.kd", [](Spec& spec) { spec.descriptor = false; }, "probe.kd"); + Rejects("descriptor size", [](Spec& spec) { spec.descriptorSize = 63; }, "probe.kd size"); + Rejects("kernarg_size", [](Spec& spec) { spec.kernarg = 24; }, "kernarg_size"); + Rejects("private segment", [](Spec& spec) { spec.privateBytes = 16; }, "private_segment_fixed_size"); + Rejects("group segment", [](Spec& spec) { spec.group = 65540; }, "group_segment_fixed_size"); + Rejects("private segment enable", [](Spec& spec) { spec.rsrc2 |= 1u; }, "ENABLE_PRIVATE_SEGMENT"); + Rejects("user sgpr count", [](Spec& spec) { spec.rsrc2 = 0x86; }, "USER_SGPR_COUNT"); + Rejects("kernarg pointer", [](Spec& spec) { spec.properties = 0x0400; }, "ENABLE_SGPR_KERNARG_SEGMENT_PTR"); + Rejects("dispatch pointer", [](Spec& spec) { spec.properties = 0x040c; }, "user SGPR"); + Rejects("entry offset", [](Spec& spec) { spec.entry = 4292; }, "kernel_code_entry_byte_offset"); + Rejects("text size", [](Spec& spec) { spec.probeSize = 135; }, "multiple of 4"); + Rejects("zero text size", [](Spec& spec) { spec.probeSize = 0; }, "size 0"); + Rejects("symbol outside section", [](Spec& spec) { spec.textSectionSize = 64; }, "outside section"); + const auto whole = Build(Spec{}); + Rejects("truncated", std::vector(whole.begin(), whole.end() - 100), "truncated"); + Rejects("truncated header", std::vector(whole.begin(), whole.begin() + 30), "truncated"); + + for (const auto& [text, expected] : std::initializer_list>{{"0", 0u}, {"1", 1u}, {"16", 16u}, {"1024", 1024u}, {"007", 7u}, {"4294967295", 0xffffffffu}}) { + Require(ParseOracleCount(text, "threads") == expected, std::string("count \"") + text + "\" was not accepted as " + std::to_string(expected)); + } + for (const auto& [text, field] : std::initializer_list>{{"-18446744073709551615", "threads"}, {"-18446744073709551600", "out_dwords"}, {"-1", "threads"}, {"-0", "out_dwords"}, {"+1", "threads"}, {"", "out_dwords"}, {" 1", "threads"}, {"1 ", "out_dwords"}, {"1a", "threads"}, {"0x10", "out_dwords"}, {"4294967296", "threads"}, {"18446744073709551616", "out_dwords"}, {"99999999999999999999999", "threads"}}) { + try { + ParseOracleCount(text, field); + } catch (const std::runtime_error& error) { + Require(std::string(error.what()).find(field) != std::string::npos, std::string("count \"") + text + "\": message \"" + error.what() + "\" does not name " + field); + continue; + } + throw std::runtime_error(std::string("count \"") + text + "\" was accepted for " + field); + } + std::printf("oracle code object tests passed\n"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp index f3a472f47c..adb2819f08 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp @@ -1,4 +1,5 @@ #include "prx/libkernel/include/Api.hpp" +#include "prx/libSceAgcDriver/include/AgcDriver.hpp" #include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver.hpp" @@ -707,6 +708,40 @@ void testTagMarkers() { expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x79, {0x343u, 0xc6000005u}), 0x20u); }, "graphics packet in compute queue"); } +void testUserDataWriters() { + constexpr std::uint32_t untouched = 0xa5a5a5a5u; + const std::uint8_t bytes[] = {1, 2, 3, 4, 5, 6, 7, 8, 9}; + std::uint32_t commands[16]; + const auto run = [&](std::uint32_t written, std::initializer_list expected, const char* reason) { + check(written == expected.size() && std::equal(expected.begin(), expected.end(), commands) && commands[written] == untouched, reason); + AgcDriver::QueueState state; + for (std::uint32_t offset = 0; offset < written;) { + const auto length = ((commands[offset] >> 16u) & 0x3fffu) + 2u; + execute(state, std::vector(commands + offset, commands + offset + length)); + offset += length; + } + if (written > 4) check(state.userConfig.at(0x343u) == commands[written - 1], "user data payload did not reach the last user data register"); + }; + std::fill(std::begin(commands), std::end(commands), untouched); + run(sceAgcDriverUserDataWritePopMarker(commands, 0), {0xc0017904u, 0x342u, 0xc2000000u}, "draw queue pop marker"); + run(sceAgcDriverUserDataWritePopMarker(commands, 1), {0xc0017900u, 0x342u, 0xc2000000u}, "compute queue pop marker"); + run(sceAgcDriverUserDataWritePacket(commands, 1, 0x12345678u, bytes, 0), {0xc0017900u, 0x342u, 0x12345678u}, "user data packet without bytes"); + run(sceAgcDriverUserDataWritePacket(commands, 1, 0x12345678u, nullptr, 9), {0xc0017900u, 0x342u, 0x12345678u}, "user data packet without a source"); + run(sceAgcDriverUserDataWritePacket(commands, 1, 0x12345678u, bytes, 2), {0xc0027900u, 0x342u, 0x12345678u, 0x00000201u}, "user data packet with an inline dword"); + std::fill(std::begin(commands), std::end(commands), untouched); + run(sceAgcDriverUserDataWritePacket(commands, 1, 0x12345678u, bytes, 9), + {0xc0027900u, 0x342u, 0x12345678u, 0x04030201u, 0xc0043700u, 0x06010000u, 0xc343u, 0u, 0x08070605u, 0x00000009u}, "user data packet with a payload"); + std::fill(std::begin(commands), std::end(commands), untouched); + run(sceAgcDriverUserDataWritePushMarker(commands, 0, "frame", 6, 0x11223344u), + {0xc0027904u, 0x342u, 0xc1011882u, 0x6d617266u, 0xc0033704u, 0x06010000u, 0xc343u, 0u, 0x00000065u}, "push marker"); + std::fill(std::begin(commands), std::end(commands), untouched); + run(sceAgcDriverUserDataWriteSetMarker(commands, 1, "frame", 6, 0xffffffffu), + {0xc0027900u, 0x342u, 0xc0017fffu, 0x6d617266u, 0xc0033700u, 0x06010000u, 0xc343u, 0u, 0x00000065u}, "set marker"); + std::uint32_t target = 0; + AgcDriver::QueueState state; + expectFailure([&] { execute(state, makePacket(0x37, {0x500u, low(&target), high(&target), 1u}, 4u)); }, "header flags 0x4"); +} + void testMemoryCopyDecode() { using AgcDriver::Pm4::DecodeMemoryCopy; constexpr std::uint64_t source = 0x1120000000ull, destination = 0x403d7b400ull; @@ -857,6 +892,29 @@ void testWriteChangedStoresExactlyTheChangedRuns() { } } +void testWriteChangedKeepingGuestKeepsCpuWrites() { + alignas(256) static std::uint8_t guest[1024 + 13]; + std::vector original(sizeof(guest)), current(sizeof(guest)); + std::uint32_t seed = 777; + const auto next = [&] { return seed = seed * 1103515245u + 12345u, seed >> 16u; }; + std::vector cpu(sizeof(guest)); + for (std::size_t i = 0; i < sizeof(guest); ++i) { + original[i] = static_cast(next()); + const auto kind = next() % 4; + current[i] = kind >= 2 ? static_cast(static_cast(original[i]) ^ (1u + next() % 255u)) : original[i]; + cpu[i] = kind % 2 == 1 ? static_cast(static_cast(original[i]) ^ (1u + next() % 255u)) : static_cast(original[i]); + guest[i] = cpu[i]; + } + AgcDriver::GuestMemory::WriteChangedKeepingGuest(reinterpret_cast(guest), current, original); + for (std::size_t i = 0; i < sizeof(guest); ++i) { + const auto expected = cpu[i] != static_cast(original[i]) ? cpu[i] : static_cast(current[i]); + check(guest[i] == expected, "write-back overwrote a byte the CPU changed since the upload, or skipped one it did not"); + } + for (std::size_t i = 0; i < sizeof(guest); ++i) guest[i] = static_cast(original[i]); + AgcDriver::GuestMemory::WriteChangedKeepingGuest(reinterpret_cast(guest), current, original); + for (std::size_t i = 0; i < sizeof(guest); ++i) check(guest[i] == static_cast(current[i]), "write-back skipped a changed byte although the guest still held the upload"); +} + void testWriteChangedChunksOfEveryDensity() { alignas(64) static std::uint8_t guest[128]; constexpr std::size_t Tail = 5; @@ -2148,6 +2206,7 @@ int main(int argc, char** argv) { testWriteChangedKeepsUntouchedBytes(); testWriteChangedChunksOfEveryDensity(); testWriteChangedStoresExactlyTheChangedRuns(); + testWriteChangedKeepingGuestKeepsCpuWrites(); testRegisters(); testRegisterFile(); testContextAndBases(); @@ -2159,6 +2218,7 @@ int main(int argc, char** argv) { testMemory(); testTraceRegisterWrites(); testTagMarkers(); + testUserDataWriters(); testCopies(); testMemoryCopyDecode(); testMemorySynchronization(); diff --git a/core/libs/prx/libSceAgcDriver/tests/PreparationInputs.cpp b/core/libs/prx/libSceAgcDriver/tests/PreparationInputs.cpp new file mode 100644 index 0000000000..723cec94c3 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/PreparationInputs.cpp @@ -0,0 +1,219 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" +#include "ControlFlow/RequestSerializer.hpp" +#include "Optimization/ResourceProgram.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#else +#include +#endif + +namespace { + +std::string mode; +std::atomic entered{0}; + +void Require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +void SetEnvironment(const char* name, const char* value) { +#ifdef _WIN32 + Require(_putenv_s(name, value) == 0, "cannot set test environment"); +#else + Require(value != nullptr ? setenv(name, value, 1) == 0 : unsetenv(name) == 0, "cannot set test environment"); +#endif +} + +std::vector Inputs(std::uint64_t address) { + std::ostringstream prefix; + prefix << "shader_" << std::hex << address << ".input-"; + std::vector result; + for (const auto& entry : std::filesystem::directory_iterator(".")) { + const auto name = entry.path().filename().string(); + if (entry.is_regular_file() && name.starts_with(prefix.str()) && name.ends_with(".prepare.req")) result.push_back(entry.path()); + } + return result; +} + +ShaderRecompiler::DeserializedRequest Read(const std::filesystem::path& path) { + std::ifstream file(path, std::ios::binary); + Require(file.is_open(), "capture cannot be opened"); + const std::string text((std::istreambuf_iterator(file)), {}); + return ShaderRecompiler::RequestSerializer{}.Deserialize(text); +} + +bool Matches(const ShaderRecompiler::RecompileRequest& left, const ShaderRecompiler::RecompileRequest& right) { + if (left.shader.codeAddress != right.shader.codeAddress || left.shader.stage != right.shader.stage || + !std::ranges::equal(left.shader.code, right.shader.code) || + !std::ranges::equal(left.context.userData, right.context.userData) || + left.context.memory.size() != right.context.memory.size()) return false; + for (std::size_t i = 0; i < left.context.memory.size(); ++i) { + if (left.context.memory[i].guestAddress != right.context.memory[i].guestAddress || + !std::ranges::equal(left.context.memory[i].bytes, right.context.memory[i].bytes)) return false; + } + return true; +} + +void Invoke(unsigned index, std::uint64_t address = 0x12340000u) { + const std::array code{0xbf810000u}; + const std::array users{index, index + 100u}; + const std::array bytes{std::byte{1}, std::byte{2}, std::byte{3}, std::byte{4}}; + const std::array memory{{{0x100000u, bytes}}}; + ShaderRecompiler::RecompileRequest request{}; + request.shader = {ShaderRecompiler::ShaderStage::Compute, address, code, 0, {}}; + request.context.waveSize = 64; + request.context.userData = users; + request.context.memory = memory; + request.context.compute = ShaderRecompiler::ShaderComputeStageInfo{{1, 1, 1}, 0, {}, false, 0, {}}; + request.useCache = false; + const auto handle = AgcDriver::DriverDetail::PrepareShaderWithDiagnostics(request); + Require(handle == nullptr, "compiler return value was changed"); +} + +void WriteFailure() { + const std::array code{0xbf810000u}; + ShaderRecompiler::RecompileRequest request{}; + request.shader = {ShaderRecompiler::ShaderStage::Compute, 0x12340000u, code, 0, {}}; + request.context.waveSize = 64; + const std::string name = "shader_12340000.retry.req"; + Require(std::filesystem::create_directory(name), "cannot block capture path"); + Require(AgcDriver::DriverDetail::Driver::dumpRequest(request.shader.codeAddress, request, ".retry").empty(), "failed save reported a filename"); + Require(std::filesystem::remove(name), "cannot unblock capture path"); + Require(AgcDriver::DriverDetail::Driver::dumpRequest(request.shader.codeAddress, request, ".retry") == name, "failed save was not retried"); + Require(Matches(request, Read(name).request), "retried capture changed its input"); + Require(AgcDriver::DriverDetail::Driver::dumpRequest(request.shader.codeAddress, request, ".retry") == name, "successful capture was not retained"); +} + +struct Directory { + std::filesystem::path previous = std::filesystem::current_path(); + std::filesystem::path path; + Directory() { +#ifdef _WIN32 + const auto process = _getpid(); +#else + const auto process = getpid(); +#endif + path = std::filesystem::temp_directory_path() / ("anyps5-prepare-input-" + std::to_string(process) + "-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + Require(std::filesystem::create_directory(path), "cannot create isolated capture directory"); + std::filesystem::current_path(path); + } + ~Directory() { + std::error_code error; + std::filesystem::current_path(previous, error); + auto current = std::filesystem::directory_iterator(path, error); + const std::filesystem::directory_iterator end; + while (!error && current != end) { + std::filesystem::remove(current->path(), error); + if (!error) current.increment(error); + } + std::filesystem::remove(path, error); + } +}; + +} + +namespace ShaderRecompiler { + +std::shared_ptr PrepareShader(const RecompileRequest& request, ShaderPreparationContext*) { + ++entered; + const auto inputs = Inputs(request.shader.codeAddress); + const bool expected = mode != "disabled" && (mode != "selected" || request.shader.codeAddress == 0x12340000u); + bool found = false; + for (const auto& path : inputs) { + try { + auto saved = Read(path); + if (Matches(request, saved.request)) found = true; + } catch (const std::exception&) {} + } + const bool blocked = mode == "capture-write-failure" && request.context.userData[0] == 2u; + Require(found == (expected && !blocked), "capture state at compiler entry is wrong"); + if (mode == "failure") throw std::runtime_error("compiler sentinel"); + return {}; +} + +} + +int main(int argc, char** argv) { + try { + Require(argc == 2, "expected one test mode"); + mode = argv[1]; + Directory directory; + SetEnvironment("APS5_DUMP_PREPARE_INPUTS", mode == "disabled" ? "" : "1"); + SetEnvironment("APS5_DUMP_SHADERS", mode == "selected" ? "12340000" : mode == "failure" ? "1" : ""); +#ifndef _WIN32 + if (mode == "disabled") SetEnvironment("APS5_DUMP_PREPARE_INPUTS", nullptr); + if (mode != "selected" && mode != "failure") SetEnvironment("APS5_DUMP_SHADERS", nullptr); +#endif + unsigned expected = 0; + if (mode == "write-failure") { + WriteFailure(); + } else if (mode == "capture-write-failure") { + Invoke(1); + const auto first = Inputs(0x12340000u); + Require(first.size() == 1, "first capture missing"); + const auto name = first.front().filename().string(); + const auto separator = name.rfind('-'); + const auto ordinal = std::stoull(name.substr(separator + 1)); + const auto blocked = name.substr(0, separator + 1) + std::to_string(ordinal + 1) + ".prepare.req"; + Require(std::filesystem::create_directory(blocked), "cannot block next capture"); + Invoke(2); + Require(std::filesystem::remove(blocked), "cannot unblock next capture"); + Invoke(3); + expected = 2; + } else if (mode == "failure") { + try { + Invoke(1); + throw std::runtime_error("compiler failure was swallowed"); + } catch (const std::exception& error) { + Require(std::string(error.what()).find("compiler sentinel") != std::string::npos, "compiler error was changed"); + Require(std::string(error.what()).find("shader_12340000.prepare.req") != std::string::npos, "existing failure dump context was lost"); + Require(Read("shader_12340000.prepare.req").request.context.userData[0] == 1u, "existing failure dump changed"); + } + expected = 1; + } else if (mode == "concurrent") { + std::vector> futures; + for (unsigned i = 1; i <= 8; ++i) futures.push_back(std::async(std::launch::async, [i] { Invoke(i); })); + for (auto& future : futures) future.get(); + expected = 8; + } else if (mode == "selected") { + Invoke(1); + Invoke(2, 0x12340001u); + expected = 1; + } else { + Require(mode == "enabled" || mode == "disabled", "unknown test mode"); + Invoke(1); + Invoke(2); + expected = mode == "enabled" ? 2 : 0; + } + const auto inputs = Inputs(0x12340000u); + Require(inputs.size() == expected, "capture count or naming changed"); + std::set ids; + for (const auto& path : inputs) { + auto saved = Read(path); + Require(saved.request.context.userData.size() == 2, "saved user data missing"); + Require(ids.insert(saved.request.context.userData[0]).second, "concurrent capture input was overwritten"); + } + Require(entered == (mode == "write-failure" ? 0u : mode == "capture-write-failure" ? 3u : mode == "concurrent" ? 8u : mode == "failure" ? 1u : 2u), "compiler invocation count changed"); + std::printf("preparation input %s: passed\n", mode.c_str()); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "preparation input: %s\n", error.what()); + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/ProcessSamplerStack.cpp b/core/libs/prx/libSceAgcDriver/tests/ProcessSamplerStack.cpp new file mode 100644 index 0000000000..39cb30c634 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/ProcessSamplerStack.cpp @@ -0,0 +1,137 @@ +#include "prx/libSceAgcDriver/Execution/include/WorkerSampler.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +std::atomic ready{false}; +std::atomic done{false}; +std::atomic caller{0}; + +[[gnu::noinline]] void Busy() { + caller.store(reinterpret_cast(__builtin_return_address(0))); + ready.store(true); + while (!done.load(std::memory_order_relaxed)) std::atomic_signal_fence(std::memory_order_seq_cst); +} + +[[gnu::noinline]] void Parent() { + Busy(); + ready.store(false); +} + +std::atomic lastWord{0}; + +bool Accepted(const void* address) { + std::uint64_t low = 0; + std::uint64_t high = 0; + const auto at = reinterpret_cast(address); + if (!AgcDriver::ReadableStackRegion(at, low, high)) return false; + std::uint64_t last = 0; + std::memcpy(&last, reinterpret_cast(high - sizeof(last)), sizeof(last)); + lastWord.store(last); + return low <= at && at < high; +} + +bool Rejected(const void* address) { + std::uint64_t low = 0; + std::uint64_t high = 0; + return !AgcDriver::ReadableStackRegion(reinterpret_cast(address), low, high) && low == 0 && high == 0; +} + +bool HasProtection(const void* address, DWORD expected) { + MEMORY_BASIC_INFORMATION memory{}; + return VirtualQuery(address, &memory, sizeof(memory)) != 0 && memory.Protect == expected; +} + +bool Fail(const char* what, DWORD protection) { + std::fprintf(stderr, "%s (protection 0x%lx)\n", what, static_cast(protection)); + return false; +} + +bool CheckAllocated(DWORD allocation, DWORD protection, bool readable) { + void* page = VirtualAlloc(nullptr, 65536, allocation, protection); + if (page == nullptr) return Fail("cannot allocate the test region", protection); + const auto* inside = static_cast(page) + 4096; + const bool committed = (allocation & MEM_COMMIT) != 0; + const bool passed = (!committed || HasProtection(page, protection)) && (readable ? Accepted(inside) : Rejected(inside)); + VirtualFree(page, 0, MEM_RELEASE); + return passed || Fail(readable ? "a readable region was rejected" : "an unreadable region was accepted", protection); +} + +bool CheckMapped(HANDLE section, DWORD access, DWORD protection) { + void* view = MapViewOfFile(section, access, 0, 0, 65536); + if (view == nullptr) return Fail("cannot map the copy-on-write view", protection); + const bool passed = HasProtection(view, protection) && Accepted(view); + UnmapViewOfFile(view); + return passed || Fail("a readable mapped view was rejected", protection); +} + +bool CheckRegions() { + for (const DWORD protection : {PAGE_READONLY, PAGE_READWRITE, PAGE_EXECUTE_READ, PAGE_EXECUTE_READWRITE}) { + if (!CheckAllocated(MEM_RESERVE | MEM_COMMIT, protection, true)) return false; + } + HANDLE section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE, 0, 65536, nullptr); + if (section == nullptr) return Fail("cannot create the copy-on-write section", PAGE_EXECUTE_READWRITE); + const bool mapped = CheckMapped(section, FILE_MAP_COPY, PAGE_WRITECOPY) && CheckMapped(section, FILE_MAP_COPY | FILE_MAP_EXECUTE, PAGE_EXECUTE_WRITECOPY); + CloseHandle(section); + if (!mapped) return false; + for (const DWORD protection : {static_cast(PAGE_EXECUTE), static_cast(PAGE_NOACCESS), static_cast(PAGE_READWRITE | PAGE_GUARD)}) { + if (!CheckAllocated(MEM_RESERVE | MEM_COMMIT, protection, false)) return false; + } + if (!CheckAllocated(MEM_RESERVE, PAGE_READWRITE, false)) return false; + return Rejected(nullptr) || Fail("the null page was accepted", 0); +} + +} + +int main() { + if (!CheckRegions()) return 1; + const auto path = std::filesystem::temp_directory_path() / ("anyps5-process-stack-" + std::to_string(GetCurrentProcessId()) + ".txt"); + _putenv_s("APS5_SAMPLE_THREADS", path.string().c_str()); + _putenv_s("APS5_SAMPLE_WORKER", ""); + _putenv_s("APS5_SAMPLE_WORKER_QUEUE", "0x50"); + std::thread target(Parent); + while (!ready.load()) std::this_thread::yield(); + AgcDriver::StartWorkerSampler(0); + std::this_thread::sleep_for(std::chrono::milliseconds(750)); + AgcDriver::StopWorkerSampler(); + done.store(true); + target.join(); + const auto pc = caller.load(); + DWORD64 imageBase = 0; + if (RtlLookupFunctionEntry(pc, &imageBase, nullptr) != nullptr) { + std::filesystem::remove(path); + std::fprintf(stderr, "fixture unexpectedly has native Windows unwind metadata\n"); + return 1; + } + HMODULE module = nullptr; + if (!GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, reinterpret_cast(pc), &module)) return 1; + wchar_t imagePath[MAX_PATH]{}; + if (GetModuleFileNameW(module, imagePath, MAX_PATH) == 0) return 1; + const auto imageName = std::filesystem::path(imagePath).filename().string(); + const auto offset = pc - reinterpret_cast(module); + std::ifstream file(path); + std::string line; + bool found = false; + std::uint64_t inclusive = 0; + while (std::getline(file, line)) { + char name[1024]{}; + unsigned long long address = 0, self = 0, total = 0; + if (std::sscanf(line.c_str(), " %1023s %llx %llu %llu", name, &address, &self, &total) == 4 && name == imageName && address == offset && total != 0) { + found = true; + inclusive += total; + } + } + file.close(); + std::filesystem::remove(path); + std::printf("process sampler caller: %s (%llu inclusive samples)\n", found ? "passed" : "missing", static_cast(inclusive)); + return found ? 0 : 1; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 3b519c100b..dad07cdb60 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -20,6 +20,7 @@ #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureCache.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureSwizzleEquations.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" #include "prx/libSceAgcDriver/Graphics/include/UnitShadow.hpp" #include "prx/libSceAgcDriver/Execution/include/BdaFeatures.hpp" @@ -1033,6 +1034,166 @@ void copiedDirectOrderingTests(const Device& device, Recorder& recorder) { std::cout << "Copied producers precede fresh and reused direct consumers\n"; } +void copiedWriteBackTests(const Device& device, Recorder& recorder) { + auto context = device.GetContext(); + context.hostImportAlignment = 0; + context.dmaBufImport = false; + recorder.Sync(); + alignas(4096) static std::uint32_t block[1024]; + std::memset(block, 0, sizeof(block)); + const auto address = reinterpret_cast(block); + ShaderRecompiler::RecompileResult program; + ShaderRecompiler::DescriptorBinding binding; + binding.kind = ShaderRecompiler::DescriptorKind::StorageBuffer; + binding.role = ShaderRecompiler::DescriptorRole::GuestBuffers; + binding.descriptorSet = 0; + binding.binding = 0; + binding.count = 1; + binding.guestDescriptor = {static_cast(address), static_cast(address >> 32u) & 0xffffu, 1024, 0x31000000u}; + binding.bufferWritten = {true}; + program.bindings.push_back(binding); + const CompiledShader compute{ShaderRecompiler::ShaderStage::Compute, &program, 0}; + struct Release { + const Context& context; + Recorder& recorder; + VkPipeline pipeline = VK_NULL_HANDLE; + VkPipelineLayout layout = VK_NULL_HANDLE; + VkShaderModule module = VK_NULL_HANDLE; + std::shared_ptr layoutOwner; + ~Release() { + recorder.Sync(); + if (pipeline) context.Function("vkDestroyPipeline")(context.device, pipeline, nullptr); + if (layout) context.Function("vkDestroyPipelineLayout")(context.device, layout, nullptr); + if (module) context.Function("vkDestroyShaderModule")(context.device, module, nullptr); + } + } release{context, recorder}; + release.layoutOwner = std::make_shared(context, compute); + Require(release.layoutOwner->HasCopiedWrites(), "the written buffer did not take the copied path"); + const auto setLayout = release.layoutOwner->Layout(); + const VkPushConstantRange push{VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(std::uint32_t)}; + VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; + layoutInfo.setLayoutCount = 1; + layoutInfo.pSetLayouts = &setLayout; + layoutInfo.pushConstantRangeCount = 1; + layoutInfo.pPushConstantRanges = &push; + Check(context.Function("vkCreatePipelineLayout")(context.device, &layoutInfo, nullptr, &release.layout), "copied write-back pipeline layout"); + VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; + moduleInfo.codeSize = sizeof(BUFFER_UPDATE_SPV); + moduleInfo.pCode = BUFFER_UPDATE_SPV; + Check(context.Function("vkCreateShaderModule")(context.device, &moduleInfo, nullptr, &release.module), "copied write-back shader module"); + VkComputePipelineCreateInfo pipelineInfo{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; + pipelineInfo.stage = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, nullptr, 0, VK_SHADER_STAGE_COMPUTE_BIT, release.module, "main", nullptr}; + pipelineInfo.layout = release.layout; + Check(context.Function("vkCreateComputePipelines")(context.device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &release.pipeline), "copied write-back pipeline"); + const auto run = [&](std::uint32_t delta) { + auto copied = std::make_shared(context, compute); + Require(copied->HasCopiedWrites(), "a use of the written buffer did not take the copied path"); + const auto commands = recorder.Commands(); + recorder.Keep(copied); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_MEMORY_WRITE_BIT | VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT); + context.Function("vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, release.pipeline); + copied->Bind(commands, VK_PIPELINE_BIND_POINT_COMPUTE, release.layout); + context.Function("vkCmdPushConstants")(commands, release.layout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(delta), &delta); + context.Function("vkCmdDispatch")(commands, 1, 1, 1); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT | VK_ACCESS_HOST_READ_BIT); + copied->MarkGpuWrites(recorder); + recorder.Sync(); + return copied; + }; + block[0] = 0x10; + block[1] = 0x20; + const auto first = run(1); + block[0] = 0x70; + block[1] = 0x25; + first->WriteBackBuffers(); + Require(block[0] == 0x70, "a copied write-back stored the shader's dword over the one the CPU wrote after the upload"); + Require(block[1] == 0x25, "a copied write-back rolled back a dword the shader never touched"); + const auto second = run(2); + second->WriteBackBuffers(); + Require(block[0] == 0x72, "the next use of the copied buffer did not read the dword the CPU wrote"); + block[0] = 0x10; + const auto third = run(1); + block[0] = 0x00aa0010u; + third->WriteBackBuffers(); + Require(block[0] == 0x00aa0011u, "a copied write-back did not merge the shader's byte with the CPU's byte of the same dword"); + std::cout << "Copied write-backs keep the bytes the CPU wrote after the upload\n"; +} + +void changedCopyTests(const Device& device, Recorder& recorder) { + Require(recorder.Idle(), "the changed copy test needs an idle recorder"); + constexpr std::uint32_t words = 70000; + constexpr auto usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT; + auto result = std::make_shared(device.GetContext(), words * 4u, usage); + auto snapshot = std::make_shared(device.GetContext(), words * 4u, usage); + auto target = std::make_shared(device.GetContext(), words * 4u, usage); + if (result->DeviceAddress() == 0 || snapshot->DeviceAddress() == 0 || target->DeviceAddress() == 0) { + std::cout << "changed copy tests skipped: no buffer device addresses\n"; + return; + } + constexpr std::uint32_t byteMasks[] = {0xffffffffu, 0x000000ffu, 0x0000ff00u, 0x00ff0000u, 0xff000000u, 0x00ffff00u}; + std::vector uploaded(words), shader(words), held(words), expected(words); + std::uint32_t contested = 0; + std::uint32_t mixed = 0; + for (std::uint32_t i = 0; i < words; ++i) { + uploaded[i] = i * 2654435761u; + shader[i] = i % 7 == 0 ? uploaded[i] ^ (0x5a5a5a5au & byteMasks[(i / 7) % 6]) : uploaded[i]; + held[i] = i % 5 == 0 ? uploaded[i] ^ (0xc3c3c3c3u & byteMasks[(i / 5 + 1) % 6]) : uploaded[i]; + bool overlap = false; + for (std::uint32_t shift = 0; shift < 32; shift += 8) { + const auto mask = 0xffu << shift; + const bool byShader = (shader[i] & mask) != (uploaded[i] & mask); + const bool byCpu = (held[i] & mask) != (uploaded[i] & mask); + expected[i] |= (byShader && !byCpu ? shader[i] : held[i]) & mask; + overlap = overlap || (byShader && byCpu); + } + contested += overlap ? 1u : 0u; + mixed += expected[i] != shader[i] && expected[i] != held[i] ? 1u : 0u; + } + Require(contested != 0 && mixed != 0, "the changed copy data lacks words both sides changed or words that mix their bytes"); + auto counter = std::make_shared(device.GetContext(), 16, usage); + std::memset(counter->Bytes().data(), 0, 16); + std::memcpy(snapshot->Bytes().data(), uploaded.data(), words * 4u); + std::memcpy(result->Bytes().data(), shader.data(), words * 4u); + std::memcpy(target->Bytes().data(), held.data(), words * 4u); + Require(recorder.RecordChangedCopy(recorder.Commands(), result->DeviceAddress(), snapshot->DeviceAddress(), target->DeviceAddress(), words, counter->DeviceAddress()), "the changed copy kernel is unavailable"); + recorder.Keep(result); + recorder.Keep(snapshot); + recorder.Keep(target); + recorder.Keep(counter); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + std::vector stored(words); + std::memcpy(stored.data(), target->Bytes().data(), words * 4u); + for (std::uint32_t i = 0; i < words; ++i) { + Require(stored[i] == expected[i], "changed copy word " + std::to_string(i) + ": " + std::to_string(stored[i]) + ", expected " + std::to_string(expected[i]) + " (the shader's changed bytes over the target's own)"); + } + std::uint32_t counted = 0; + std::memcpy(&counted, counter->Bytes().data(), sizeof(counted)); + Require(counted == contested, "the changed copy counted " + std::to_string(counted) + " words with bytes both sides changed, expected " + std::to_string(contested)); + constexpr std::uint64_t lead = 0x1000; + auto imported = std::make_shared(device.GetContext(), lead + words * 4u, usage); + std::memset(imported->Bytes().data(), 0xa5, lead); + std::memcpy(imported->Bytes().data() + lead, held.data(), words * 4u); + HostImport import{}; + import.base = 0x7f0000011000ull; + import.bytes = words * 4u; + import.address = imported->DeviceAddress() + lead; + import.lead = lead; + const auto start = HostImportBufferStart(import); + Require(start.guest == import.base - lead && start.device == imported->DeviceAddress() && HostImportOffset(import, import.base) == lead, "the copy source of an import with a lead does not start at the import's buffer"); + Require(recorder.RecordChangedCopy(recorder.Commands(), result->DeviceAddress(), snapshot->DeviceAddress(), start.device + (import.base - start.guest), words), "the changed copy kernel is unavailable"); + recorder.Keep(imported); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + for (std::uint64_t i = 0; i < lead; ++i) Require(imported->Bytes()[i] == std::byte{0xa5}, "the changed copy into an import with a lead wrote byte " + std::to_string(i) + " of the lead"); + std::memcpy(stored.data(), imported->Bytes().data() + lead, words * 4u); + for (std::uint32_t i = 0; i < words; ++i) { + Require(stored[i] == expected[i], "changed copy word " + std::to_string(i) + " after a lead: " + std::to_string(stored[i]) + ", expected " + std::to_string(expected[i])); + } +} + void readTrackingTests(const Device& device, Recorder& recorder) { Require(Recorder::ReadTracking(), "read tracking is off (APS5_COPY_READ_TRACKING=0 set?)"); Require(recorder.Idle() && !recorder.PendingReadOverlaps(0x10000, 16), "a fresh recorder reports a read"); @@ -4123,6 +4284,11 @@ bool bdaStagingTests(const Device& device, Recorder& recorder) { std::uint32_t innerAdjustment = 0; Require(memory.Descriptor(element + 16, 64, innerAdjustment).buffer == view.buffer, "a read inside the staged element does not read its shadow"); recorder.Sync(); + const bool changedCopy = std::getenv("APS5_NO_CHANGED_COPY_BACK") == nullptr; + auto* contested = static_cast(words + first + 70); + auto* untouched = static_cast(words + first + 100); + *contested = 0x11111133u; + *untouched = 0x44444444u; auto commands = recorder.Commands(); fill(commands, view.buffer, view.offset + adjustment + 64u * 4u, 64, 0x22222222u); RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_TRANSFER_READ_BIT); @@ -4132,7 +4298,9 @@ bool bdaStagingTests(const Device& device, Recorder& recorder) { std::size_t wrong = 0; for (std::size_t index = 0; index < bytes / 4u; ++index) { const auto at = index >= first ? index - first : bytes; - const auto expected = at >= 64 && at < 80 ? 0x22222222u : 0x11111111u; + auto expected = at >= 64 && at < 80 ? 0x22222222u : 0x11111111u; + if (at == 70 && changedCopy) expected = 0x22222233u; + if (at == 100 && changedCopy) expected = 0x44444444u; if (words[index] != expected && wrong++ < 8) std::cout << "address-based staging copy-back dword " << index << " holds 0x" << std::hex << words[index] << ", expected 0x" << expected << std::dec << "\n"; } Require(wrong == 0, "an address-based staged copy-back left the wrong guest bytes"); @@ -9049,6 +9217,146 @@ void refreshProofTests(const Device& device, Recorder& recorder) { recorder.Sync(); } +bool pendingKeyRefreshTests(const Device& device, Recorder& recorder) { + const auto& base = device.GetContext(); + if (base.hostImportAlignment == 0) { + std::cout << "host imports unavailable: refreshes over a pending key store not tested\n"; + return false; + } + if (!KeyFastPath()) { + std::cout << "key fast path off: refreshes over a pending key store not tested\n"; + return false; + } + const auto decided = PrepareImportWatch(base); +#ifndef _WIN32 + const auto probe = ProbeImportWriteProtection(base); + if (probe.failure != nullptr || probe.writtenAfterSubmit != 0) { + std::cout << "the device reports watched host memory written after a GPU read: refreshes over a pending key store not tested\n"; + return false; + } +#endif + SetImportWatch(base, ImportWatch::Watch); + struct Restore { + const Context& context; + ImportWatch decided; + ~Restore() { SetImportWatch(context, decided); } + } restore{base, decided}; + constexpr std::uint32_t side = 256; + constexpr std::size_t surfaceBytes = side * side * 4; + constexpr std::size_t keyCount = surfaceBytes / 256; + constexpr std::size_t bytes = surfaceBytes + 65536; + constexpr std::uint64_t draws = 16; + void* block = AllocateWatched(bytes, 65536); + if (block == nullptr) { + std::cout << "no write watching: refreshes over a pending key store not tested\n"; + return false; + } + struct Release { + void* block; + ~Release() { ReleaseWatched(block, bytes); } + } release{block}; + auto* texels = static_cast(block); + auto* keys = texels + surfaceBytes; + std::memset(texels, 0x55, surfaceBytes); + std::memset(keys, 0x00, keyCount); + const auto address = reinterpret_cast(block); + { + GuestAllocations::Mutation mutation; + mutation.Add(block, bytes, true, true, true); + } + struct Unregister { + const Context& context; + void* block; + std::uint64_t address; + ~Unregister() { + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } + HostImportFor(context, address, bytes); + } + } unregister{base, block, address}; + if (HostImportFor(base, address, bytes) == nullptr || !AgcDriver::GuestMemory::Watched(address, bytes)) { + std::cout << "no watched host import: refreshes over a pending key store not tested\n"; + return false; + } + TextureDetiler detiler(base); + auto context = base; + context.detiler = &detiler; + GuestTextureResource resource{}; + resource.baseAddress = address; + resource.width = side; + resource.height = side; + resource.mipCount = 1; + resource.tileMode = TextureTileMode::kR64KBX; + resource.dimension = TextureDimension::k2D; + resource.format = 56; + resource.dstSelX = 4; + resource.dstSelY = 5; + resource.dstSelZ = 6; + resource.dstSelW = 7; + resource.dccAddress = address + surfaceBytes; + const auto proved = [] { return StorageTexture::RefreshesProved(); }; + { + auto image = StorageTexture::Create(context, detiler, resource, 0); + const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + const auto draw = [&](VkClearColorValue value) { + const auto commands = recorder.Commands(); + recorder.Keep(image); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); + context.Function("vkCmdClearColorImage")(commands, image->Image(), VK_IMAGE_LAYOUT_GENERAL, &value, 1, &range); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + image->MarkDirty(); + }; + const auto storePending = [&] { return recorder.PendingWriteOverlaps(resource.dccAddress, keyCount) && keys[0] == 0x00 && keys[keyCount - 1] == 0x00; }; + image->Refresh(); + image->Refresh(); + Require(image->UploadedKeys() == DccKeys::Clear0000, "the fast-cleared keys were not uploaded as a 0000 clear"); + draw({{1.0f, 0.0f, 0.0f, 1.0f}}); + Require(image->UploadedKeys() == DccKeys::Uncompressed && storePending(), "the first draw into the fast-cleared target left no pending uncompressed key store"); + const auto before = proved(); + for (std::uint64_t i = 0; i < draws; ++i) { + Require(image->Refresh(), "a refresh of the drawn target failed"); + draw({{0.0f, static_cast(i + 1) / static_cast(draws), 0.0f, 1.0f}}); + } + const auto full = draws - (proved() - before); + std::cout << draws << " draws into the unchanged target over its pending key store: " << full << " full refreshes, " << draws - full << " proved\n"; + Require(storePending(), "the key store did not stay pending in the batch through the draws"); + Require(full == 1, std::to_string(full) + " of " + std::to_string(draws) + " refreshes of the unchanged target over its pending key store ran in full, not 1"); + texels[0] = 0x11; + const auto written = proved(); + image->Refresh(); + Require(proved() == written, "a CPU write of the target over its pending key store was answered from the proof"); + Require(storePending(), "the key store landed before its batch was retired"); + const auto proof = image->KeyProof(); + Require(proof.pendingNote != 0, "the refresh over the pending key store took no proof of its write note"); + bool crossed = false; + bool own = false; + { + Recorder other(base); + for (std::uint64_t i = 1; i < proof.pendingNote; ++i) other.NotePendingWrite(0x1000, 16); + other.NotePendingWrite(resource.dccAddress, keyCount); + std::lock_guard gpu(GpuMutex()); + other.Activate(); + crossed = PendingKeyProofHolds(resource, surfaceBytes, proof); + recorder.Activate(); + own = PendingKeyProofHolds(resource, surfaceBytes, proof); + other.Sync(); + } + recorder.Sync(); + Require(!crossed, "the pending key proof held with another recorder active, holding the same write note over the keys"); + Require(own, "the pending key proof did not hold again with its own recorder active"); + Require(std::all_of(keys, keys + keyCount, [](std::uint8_t key) { return key == 0xff; }), "the uncompressed key store did not land"); + const auto landed = proved(); + image->Refresh(); + Require(proved() == landed, "the proof over the pending key store held after its batch retired"); + image->Refresh(); + Require(proved() == landed + 1, "the proof was not taken again over the landed keys"); + } + recorder.Sync(); + return true; +} + bool repeatedNeighbourStoreTests(const Device& device, Recorder& recorder) { const auto& base = device.GetContext(); constexpr std::uint32_t width = 1024; @@ -9284,6 +9592,230 @@ void unchangedCpuStampTests(const Device& device, Recorder& recorder) { std::cout << "unchanged CPU stamps: ok\n"; } +bool rewrittenBlockTests(const Device& device, Recorder& recorder) { + if (!AgcDriver::GuestMemory::WriteWatched()) { + std::cout << "no write watching: rewritten blocks not tested\n"; + return false; + } + const auto& base = device.GetContext(); + constexpr std::uint32_t side = 256; + constexpr std::size_t surfaceBytes = side * side * 4; + constexpr std::size_t block = 65536; + constexpr std::size_t bytes = surfaceBytes + block; + constexpr std::array red{255, 0, 0, 255}; + const auto pattern = [](std::size_t i) { return static_cast(i * 131u + 7u); }; + const auto decided = PrepareImportWatch(base); + struct Restore { + const Context& context; + ImportWatch decided; + ~Restore() { SetImportWatch(context, decided); } + } restore{base, decided}; + for (const bool after : {true, false}) { + const std::string what = after ? "a CPU store of the baseline bytes after the GPU results" : "a CPU store of the baseline bytes before the GPU results"; + void* memoryBlock = AllocateWatched(bytes, block); + Require(memoryBlock != nullptr, "no watched block although write watching is on: rewritten blocks"); + auto* memory = static_cast(memoryBlock); + for (std::size_t i = 0; i < bytes; ++i) memory[i] = pattern(i); + const auto address = reinterpret_cast(memoryBlock); + { + GuestAllocations::Mutation mutation; + mutation.Add(memoryBlock, bytes, true, true, true); + } + struct Unregister { + const Context& context; + void* block; + std::uint64_t address; + std::size_t bytes; + ~Unregister() { + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } + HostImportFor(context, address, bytes); + ReleaseWatched(block, bytes); + } + } unregister{base, memoryBlock, address, bytes}; + AgcDriver::GuestMemory::CollectWritesUncached(address, bytes); + TextureDetiler detiler(base); + auto context = base; + context.hostImportAlignment = 0; + context.detiler = &detiler; + GuestTextureResource resource{}; + resource.baseAddress = address; + resource.width = side; + resource.height = side; + resource.mipCount = 1; + resource.tileMode = TextureTileMode::kLinear; + resource.dimension = TextureDimension::k2D; + resource.format = 56; + resource.dstSelX = 4; + resource.dstSelY = 5; + resource.dstSelZ = 6; + resource.dstSelW = 7; + Require(DescribeSurface(resource).guestBytes == surfaceBytes, "(c) unexpected surface size"); + const auto rewrite = [&] { + for (std::size_t i = block; i < 2 * block; ++i) memory[i] = pattern(i); + AgcDriver::GuestMemory::CollectWritesUncached(address, bytes); + }; + { + auto image = StorageTexture::Create(context, detiler, resource, 0); + if (!after) rewrite(); + const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + const auto commands = recorder.Commands(); + recorder.Keep(image); + const VkClearColorValue value{{1.0f, 0.0f, 0.0f, 1.0f}}; + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); + context.Function("vkCmdClearColorImage")(commands, image->Image(), VK_IMAGE_LAYOUT_GENERAL, &value, 1, &range); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + image->MarkDirty(); + if (after) rewrite(); + Require(AgcDriver::GuestMemory::Watched(address, surfaceBytes) && HostImportFor(context, address, surfaceBytes) == nullptr, "(c) " + what + ": the image is not on the path under test"); + image->WriteBack(); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + std::vector read(bytes); + AgcDriver::GuestMemory::Read(address, read); + const auto texel = [&](std::size_t at) { + for (std::size_t c = 0; c < 4; ++c) { + if (std::to_integer(read[at + c]) != red[c]) return false; + } + return true; + }; + for (std::size_t at = 0; at < surfaceBytes; at += 4) { + const bool rewritten = at >= block && at < 2 * block; + if (after && rewritten) { + for (std::size_t c = 0; c < 4; ++c) Require(std::to_integer(read[at + c]) == pattern(at + c), "(c) " + what + ": the GPU results were stored over the CPU's later store at +" + std::to_string(at)); + } else { + Require(texel(at), "(c) " + what + ": GPU results were dropped at +" + std::to_string(at)); + } + } + } + recorder.Sync(); + } + std::cout << "rewritten blocks: ok\n"; + return true; +} + +bool rewrittenStaleUnitTests(const Device& device, Recorder& recorder) { + if (!AgcDriver::GuestMemory::WriteWatched()) { + std::cout << "no write watching: stale unit tests not run\n"; + return false; + } + const auto& base = device.GetContext(); + constexpr std::uint32_t width = 512; + constexpr std::uint32_t height = 256; + constexpr std::size_t surfaceBytes = std::size_t{width} * height * 4; + constexpr std::size_t unitBytes = 65536; + constexpr std::size_t units = surfaceBytes / unitBytes; + constexpr std::array red{255, 0, 0, 255}; + constexpr std::array blue{0, 0, 255, 255}; + const auto pattern = [](std::size_t i) { return static_cast(i * 131u + 7u); }; + for (const bool unwatch : {true, false}) { + const std::string what = unwatch ? "memory that left the write watch" : "watched memory"; + struct Backing { + Recorder& recorder; + void* block = nullptr; + ~Backing() { + try { + recorder.Sync(); + } catch (const std::exception& error) { + std::cerr << "(r) the recorded work did not finish, the test memory stays mapped: " << error.what() << '\n'; + return; + } + if (block == nullptr) return; + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } + ReleaseWatched(block, surfaceBytes); + } + } backing{recorder}; + backing.block = AllocateWatched(surfaceBytes, unitBytes); + Require(backing.block != nullptr, "(r) no watched block although write watching is on"); + { + GuestAllocations::Mutation mutation; + mutation.Add(backing.block, surfaceBytes, true, true, true); + } + auto* memory = static_cast(backing.block); + for (std::size_t i = 0; i < surfaceBytes; ++i) memory[i] = pattern(i); + const auto address = reinterpret_cast(backing.block); + AgcDriver::GuestMemory::CollectWritesUncached(address, surfaceBytes); + TextureDetiler detiler(base); + auto context = base; + context.hostImportAlignment = 0; + context.detiler = &detiler; + GuestTextureResource resource{}; + resource.baseAddress = address; + resource.width = width; + resource.height = height; + resource.mipCount = 1; + resource.tileMode = TextureTileMode::kR64KBX; + resource.dimension = TextureDimension::k2D; + resource.format = 56; + resource.dstSelX = 4; + resource.dstSelY = 5; + resource.dstSelZ = 6; + resource.dstSelW = 7; + Require(DescribeSurface(resource).guestBytes == surfaceBytes, "(r) unexpected surface size"); + Require(AgcDriver::GuestMemory::Watched(address, surfaceBytes) && HostImportFor(context, address, surfaceBytes) == nullptr, "(r) " + what + ": the image is not on the path under test"); + { + auto image = StorageTexture::Create(context, detiler, resource, 0); + const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + const auto clear = [&](VkClearColorValue value) { + const auto commands = recorder.Commands(); + recorder.Keep(image); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); + context.Function("vkCmdClearColorImage")(commands, image->Image(), VK_IMAGE_LAYOUT_GENERAL, &value, 1, &range); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + image->MarkDirty(); + }; + const auto store = [&] { + image->WriteBack(); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + }; + const auto unitHolds = [&](std::size_t unit, const std::array& texel) { + std::vector read(unitBytes); + AgcDriver::GuestMemory::Read(address + unit * unitBytes, read); + for (std::size_t i = 0; i < unitBytes; ++i) { + if (std::to_integer(read[i]) != texel[i % 4]) return false; + } + return true; + }; + const auto unitRewritten = [&] { + std::vector read(unitBytes); + AgcDriver::GuestMemory::Read(address + unitBytes, read); + for (std::size_t i = 0; i < unitBytes; ++i) { + if (std::to_integer(read[i]) != pattern(unitBytes + i)) return false; + } + return true; + }; + clear({{1.0f, 0.0f, 0.0f, 1.0f}}); + for (std::size_t i = unitBytes; i < 2 * unitBytes; ++i) memory[i] = pattern(i); + AgcDriver::GuestMemory::CollectWritesUncached(address, surfaceBytes); + store(); + Require(unitRewritten(), "(r) " + what + ": the first write-back stored over unit 1, which the CPU rewrote with its baseline bytes after the results"); + for (std::size_t unit = 0; unit < units; ++unit) { + if (unit != 1) Require(unitHolds(unit, red), "(r) " + what + ": the first write-back did not store unit " + std::to_string(unit)); + } + if (unwatch) { + AgcDriver::GuestMemory::Unwatch(address, surfaceBytes); + Require(!AgcDriver::GuestMemory::Watched(address, surfaceBytes), "(r) the memory is still write-watched"); + } + clear({{0.0f, 0.0f, 1.0f, 1.0f}}); + store(); + Require(unitRewritten(), "(r) " + what + ": the second write-back stored the image's stale unit 1 over the CPU's store, which nothing re-uploaded"); + for (std::size_t unit = 0; unit < units; ++unit) { + if (unit != 1) Require(unitHolds(unit, blue), "(r) " + what + ": the second write-back did not store unit " + std::to_string(unit)); + } + } + std::cout << "a unit kept for the CPU and written again before a re-upload, in " << what << ": ok\n"; + } + return true; +} + std::uint64_t publishRaceAddress = 0; void writeDuringPublish(std::uint64_t begin, std::uint64_t end) { @@ -13697,6 +14229,234 @@ void Run(Context context) { } #endif +namespace SmallElementDetileTest { + +std::uint32_t equationOffset(const TextureSwizzleEquation& equation, std::uint32_t x, std::uint32_t y) { + std::uint32_t offset = 0; + for (std::size_t bit = 0; bit < equation.bits.size(); ++bit) { + const auto mask = equation.bits[bit]; + const auto selected = (x & (mask & 0xfffu)) ^ ((y << 12) & (mask & 0xfff000u)); + offset |= static_cast(std::popcount(selected) & 1u) << bit; + } + return offset; +} + +std::uint32_t standardOffset(std::uint32_t elementBytes, std::uint32_t blockBytes, std::uint32_t x, std::uint32_t y) { + std::uint32_t offset = elementBytes == 1u ? ((y << 4) & 0x1f0u) ^ ((y << 5) & 0x400u) ^ (x & 0x00fu) ^ ((x << 5) & 0x200u) ^ ((x << 6) & 0x800u) + : ((y << 4) & 0x070u) ^ ((y << 5) & 0x100u) ^ ((y << 6) & 0x400u) ^ ((x << 1) & 0x00eu) ^ ((x << 4) & 0x080u) ^ ((x << 5) & 0x200u) ^ ((x << 6) & 0x800u); + if (blockBytes > 4096u) offset ^= elementBytes == 1u ? ((x << 7) & 0x2000u) ^ ((x << 8) & 0x8000u) ^ ((y << 6) & 0x1000u) ^ ((y << 7) & 0x4000u) + : ((x << 7) & 0x2000u) ^ ((x << 8) & 0x8000u) ^ ((y << 7) & 0x1000u) ^ ((y << 8) & 0x4000u); + return offset & (blockBytes - 1u); +} + +std::uint32_t tiledOffset(TextureTileMode mode, std::uint32_t elementBytes, const TileMipLayout& mip, std::uint32_t x, std::uint32_t y) { + if (mode == TextureTileMode::kLinear) return y * mip.pitchBytes + x * elementBytes; + const auto block = ThinBlockLayout(mode, elementBytes); + const auto sx = mip.tail ? x + mip.tailX : x; + const auto sy = mip.tail ? y + mip.tailY : y; + const auto base = mip.tail ? 0u : (y / block[2] * mip.blocksPerRow + x / block[1]) * block[0]; + if (mode == TextureTileMode::kStandard4KB || mode == TextureTileMode::kStandard64KB) return base + standardOffset(elementBytes, block[0], sx, sy); + const auto* equation = FindTextureSwizzleEquation(EquationSwizzleMode(mode), elementBytes); + Require(equation != nullptr, "small element detile test: no swizzle equation"); + return base + equationOffset(*equation, sx, sy); +} + +class StorageImage { +public: + StorageImage(const Context& context, VkFormat format, std::uint32_t width, std::uint32_t height) : context(context) { + try { + VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + info.imageType = VK_IMAGE_TYPE_2D; + info.format = format; + info.extent = {width, height, 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_STORAGE_BIT; + info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + Check(context.Function("vkCreateImage")(context.device, &info, nullptr, &image), "vkCreateImage small element detile"); + VkMemoryRequirements requirements{}; + context.Function("vkGetImageMemoryRequirements")(context.device, image, &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); + Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &memory), "vkAllocateMemory small element detile"); + Check(context.Function("vkBindImageMemory")(context.device, image, memory, 0), "vkBindImageMemory small element detile"); + VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + viewInfo.image = image; + viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; + viewInfo.format = format; + viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView small element detile"); + } catch (...) { + release(); + throw; + } + } + ~StorageImage() { release(); } + StorageImage(const StorageImage&) = delete; + StorageImage& operator=(const StorageImage&) = delete; + VkImage Image() const { return image; } + VkImageView View() const { return view; } + +private: + void release() noexcept { + if (view != VK_NULL_HANDLE) context.Function("vkDestroyImageView")(context.device, view, nullptr); + if (image != VK_NULL_HANDLE) context.Function("vkDestroyImage")(context.device, image, nullptr); + if (memory != VK_NULL_HANDLE) context.Function("vkFreeMemory")(context.device, memory, nullptr); + } + Context context; + VkImage image = VK_NULL_HANDLE; + VkDeviceMemory memory = VK_NULL_HANDLE; + VkImageView view = VK_NULL_HANDLE; +}; + +void fillPattern(std::span bytes, std::uint32_t seed) { + for (std::size_t i = 0; i < bytes.size(); ++i) bytes[i] = static_cast((i * 37u + seed) & 0xffu); +} + +void fillSentinel(std::span bytes) { + std::fill(bytes.begin(), bytes.end(), std::byte{0xa5}); +} + +void requireBytes(std::span actual, std::span expected, const std::string& what) { + Require(actual.size() >= expected.size(), what + ": readback is shorter than expected"); + for (std::size_t i = 0; i < expected.size(); ++i) { + if (actual[i] != expected[i]) Require(false, what + ": byte " + std::to_string(i) + " is " + std::to_string(static_cast(actual[i])) + ", expected " + std::to_string(static_cast(expected[i]))); + } +} + +void hostBarrier(const Context& context, VkCommandBuffer commands) { + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); +} + +} + +void smallElementDetileTests(Device& device) { + using namespace SmallElementDetileTest; + const auto& context = device.GetContext(); + TextureDetiler detiler(context); + struct Case { + TextureTileMode mode; + const char* name; + std::uint32_t width; + std::uint32_t height; + std::uint32_t mips; + bool tail; + }; + const std::array cases{{ + {TextureTileMode::kStandard64KB, "SW_64KB_S", 37, 21, 1, false}, + {TextureTileMode::kStandard4KB, "SW_4KB_S", 70, 9, 1, false}, + {TextureTileMode::kR64KBX, "SW_64KB_R_X", 37, 21, 1, false}, + {TextureTileMode::kLinear, "linear", 37, 5, 1, false}, + {TextureTileMode::kStandard64KB, "SW_64KB_S tail", 256, 256, 9, true}, + {TextureTileMode::kR64KBX, "SW_64KB_R_X tail", 256, 256, 9, true}, + }}; + for (const auto& testCase : cases) { + for (const std::uint32_t elementBytes : {1u, 2u}) { + const auto layouts = ComputeElementMipLayout(testCase.mode, elementBytes, testCase.width, testCase.height, testCase.mips); + std::size_t level = 0; + if (testCase.tail) { + while (level < layouts.size() && !layouts[level].tail) ++level; + Require(level < layouts.size(), std::string("small element detile test: ") + testCase.name + " has no tail mip"); + } + const auto& mip = layouts[level]; + const auto tiledBytes = static_cast(mip.tiledSize); + const auto linearBytes = static_cast(mip.linearSize); + struct WindowCase { + const char* name; + DetileWindow window; + }; + std::vector windows{{"whole mip", DetileWindow{}}}; + DetileWindow ranged{}; + ranged.rangeBegin = 1u; + ranged.rangeEnd = static_cast(tiledBytes / 2u + 3u); + windows.push_back({"unaligned byte range", ranged}); + if (mip.width > 9u) { + DetileWindow columns{}; + columns.columnBegin = 3u; + columns.columnEnd = mip.width - 2u; + windows.push_back({"unaligned columns", columns}); + } + for (const auto& windowCase : windows) { + const auto& window = windowCase.window; + const auto what = std::string(testCase.name) + " " + std::to_string(elementBytes) + "B " + std::to_string(mip.width) + "x" + std::to_string(mip.height) + ", " + windowCase.name; + const auto columnEnd = window.columnEnd != 0 ? std::min(window.columnEnd, mip.width) : mip.width; + const auto gridEnd = std::min(mip.width, window.columnBegin + (columnEnd - window.columnBegin + 7u) / 8u * 8u); + const auto moved = [&](std::uint32_t x, std::uint32_t offset) { return x >= window.columnBegin && x < gridEnd && offset >= window.rangeBegin && offset < window.rangeEnd; }; + Buffer linear(context, linearBytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + Buffer tiled(context, tiledBytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + Buffer retiled(context, tiledBytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + Buffer detiled(context, linearBytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + fillPattern(linear.Bytes(), 11u); + fillPattern(tiled.Bytes(), 29u); + fillSentinel(retiled.Bytes()); + fillSentinel(detiled.Bytes()); + std::vector expectedRetile(tiledBytes, std::byte{0xa5}); + std::vector expectedDetile(linearBytes, std::byte{0xa5}); + for (std::uint32_t y = 0; y < mip.height; ++y) { + for (std::uint32_t x = 0; x < mip.width; ++x) { + const auto offset = tiledOffset(testCase.mode, elementBytes, mip, x, y); + if (!moved(x, offset)) continue; + const auto linearOffset = y * mip.pitchBytes + x * elementBytes; + for (std::uint32_t byte = 0; byte < elementBytes; ++byte) { + expectedRetile[offset + byte] = linear.Bytes()[linearOffset + byte]; + expectedDetile[linearOffset + byte] = tiled.Bytes()[offset + byte]; + } + } + } + detiler.BeginBatch(); + CommandBatch batch(context); + const auto commands = batch.Handle(); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT); + detiler.Dispatch(commands, testCase.mode, elementBytes, linear.Handle(), 0, retiled.Handle(), 0, mip, true, 0, false, window); + detiler.Dispatch(commands, testCase.mode, elementBytes, tiled.Handle(), 0, detiled.Handle(), 0, mip, false, 0, false, window); + hostBarrier(context, commands); + batch.SubmitAndWait(); + requireBytes(retiled.Bytes(), expectedRetile, what + ", buffer retile"); + requireBytes(detiled.Bytes(), expectedDetile, what + ", buffer detile"); + const auto format = TextureDetiler::ImageElementFormat(elementBytes); + VkFormatProperties properties{}; + context.formatProperties(context.physical, format, &properties); + if (!context.singlePassStorage || (properties.optimalTilingFeatures & VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT) == 0) continue; + StorageImage image(context, format, mip.width, mip.height); + Buffer imageRetiled(context, tiledBytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + fillSentinel(imageRetiled.Bytes()); + std::vector expectedImage(tiledBytes, std::byte{0xa5}); + for (std::uint32_t y = 0; y < mip.height; ++y) { + for (std::uint32_t x = 0; x < mip.width; ++x) { + const auto offset = tiledOffset(testCase.mode, elementBytes, mip, x, y); + if (!moved(x, offset)) continue; + for (std::uint32_t byte = 0; byte < elementBytes; ++byte) expectedImage[offset + byte] = tiled.Bytes()[offset + byte]; + } + } + detiler.BeginBatch(); + CommandBatch imageBatch(context); + const auto imageCommands = imageBatch.Handle(); + VkImageMemoryBarrier toGeneral{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + toGeneral.dstAccessMask = VK_ACCESS_SHADER_WRITE_BIT; + toGeneral.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + toGeneral.newLayout = VK_IMAGE_LAYOUT_GENERAL; + toGeneral.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + toGeneral.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + toGeneral.image = image.Image(); + toGeneral.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + context.Function("vkCmdPipelineBarrier")(imageCommands, VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 1, &toGeneral); + RecordMemoryBarrier(context, imageCommands, VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT); + detiler.DispatchImage(imageCommands, testCase.mode, elementBytes, tiled.Handle(), 0, image.View(), mip, false); + RecordMemoryBarrier(context, imageCommands, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT); + detiler.DispatchImage(imageCommands, testCase.mode, elementBytes, imageRetiled.Handle(), 0, image.View(), mip, true, 0, window); + hostBarrier(context, imageCommands); + imageBatch.SubmitAndWait(); + requireBytes(imageRetiled.Bytes(), expectedImage, what + ", image retile"); + } + } + } + std::cout << "small element detile tests passed\n"; +} + int main(int argc, char** argv) { _ZSt13set_terminatePFvvE_nid_postfix([] { std::fflush(nullptr); @@ -13881,6 +14641,10 @@ int main(int argc, char** argv) { } if (argc == 2 && std::string_view(argv[1]) == "--map-gate-stress-only") return mapGateStressTests(device) ? 0 : 77; std::unique_lock gpu(GpuMutex()); + if (argc == 2 && std::string_view(argv[1]) == "--small-element-detile-only") { + smallElementDetileTests(device); + return 0; + } #ifdef _WIN32 if (argc == 2 && std::string_view(argv[1]) == "--storage-cpu-snapshot-only") { if (device.GetContext().hostImportAlignment == 0) { @@ -13906,6 +14670,10 @@ int main(int argc, char** argv) { copiedDirectOrderingTests(device, recorder); return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--copied-write-back-only") { + copiedWriteBackTests(device, recorder); + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--completion-wait-only") { completionWaitTests(device, recorder); std::cout << "Completion wait tests passed\n"; @@ -13955,6 +14723,8 @@ int main(int argc, char** argv) { unchangedCpuStampTests(device, recorder); return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--rewritten-blocks-only") return rewrittenBlockTests(device, recorder) ? 0 : 77; + if (argc == 2 && std::string_view(argv[1]) == "--rewritten-stale-units-only") return rewrittenStaleUnitTests(device, recorder) ? 0 : 77; if (argc == 2 && std::string_view(argv[1]) == "--idle-writeback-race-only") { idleWriteBackRaceTests(device, recorder); return 0; @@ -13964,6 +14734,14 @@ int main(int argc, char** argv) { keptUnitSettleTests(device, recorder); return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--pending-key-refresh-only") { + if (!pendingKeyRefreshTests(device, recorder)) { + std::cout << "skipped, refreshes over a pending key store are not testable here\n"; + return 77; + } + std::cout << "Refreshes over a pending DCC key store passed\n"; + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--clear-hand-over-only") { clearHandOverTests(device, recorder, false); if (!clearHandOverTests(device, recorder, true)) { @@ -14049,6 +14827,10 @@ int main(int argc, char** argv) { parkedImportUnmapTests(device); return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--changed-copy-only") { + changedCopyTests(device, recorder); + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--window-import-unmap-only") { gpu.unlock(); windowImportUnmapTests(device); @@ -14059,6 +14841,13 @@ int main(int argc, char** argv) { std::cout << "skipped, unit shadows or host imports are unavailable\n"; return 77; } +#ifndef _WIN32 + if (const auto probe = AgcDriver::Graphics::ProbeImportWriteProtection(device.GetContext()); probe.failure != nullptr || probe.writtenAfterSubmit != 0) { + if (probe.failure != nullptr) std::cout << "skipped, the host import probe failed at " << probe.failure << " (" << static_cast(probe.result) << ")\n"; + else std::cout << "skipped, the device reports " << probe.writtenAfterSubmit << " of " << probe.pages << " host-imported pages written after a GPU read, so a forced watch of the import reads its submissions as CPU stores\n"; + return 77; + } +#endif idlePublishWaitTests(device, recorder); return 0; } @@ -14224,6 +15013,7 @@ int main(int argc, char** argv) { readListTests(device, recorder); overlappedRangeTests(recorder); spaceWriteSyncTests(device, recorder); + changedCopyTests(device, recorder); writeSettledTests(device, recorder); completionCountTests(device, recorder); afterRecordedWorkTests(device, recorder); @@ -14288,6 +15078,8 @@ int main(int argc, char** argv) { singlePassTests(device, recorder, false); singlePassTests(device, recorder, true); unchangedCpuStampTests(device, recorder); + rewrittenBlockTests(device, recorder); + rewrittenStaleUnitTests(device, recorder); repeatedNeighbourStoreTests(device, recorder); keptUnitSettleTests(device, recorder); settledBaselineTests(device, recorder); @@ -14311,6 +15103,7 @@ int main(int argc, char** argv) { shadowedKeyStoreTests(device, recorder); sampleDumpTests(device, recorder); copiedDirectOrderingTests(device, recorder); + copiedWriteBackTests(device, recorder); gpu.unlock(); hostImportUnmapTests(device, recorder); drawSnapshotPatchTests(device); diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDeviceProfile.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDeviceProfile.cpp index 20112c62f3..8129d74649 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDeviceProfile.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDeviceProfile.cpp @@ -1,5 +1,6 @@ #include "prx/libSceAgcDriver/Execution/include/ShaderDeviceProfile.hpp" #include "BdaAbi.hpp" +#include "CacheKey.hpp" #include "Optimization/BindingAllocator.hpp" #include #include @@ -134,6 +135,60 @@ void CheckProfile() { Require(std::ranges::find(geometryAccepted.Target().supportedCapabilities, spv::CapabilityGeometry) != geometryAccepted.Target().supportedCapabilities.end(), "enabled geometry shaders were rejected"); } +std::vector CacheKeyFor(const ShaderRecompiler::SpirvTarget& target) { + static constexpr std::array code{0xbf810000u}; + ShaderRecompiler::RecompileRequest request{}; + request.shader = {ShaderRecompiler::ShaderStage::Compute, 0x10000u, code, 0, {}}; + request.context.waveSize = 64; + request.target = target; + std::vector key; + ShaderRecompiler::RecompileCacheKey::Build(request, key); + return key; +} + +std::vector PerKeyDigestKeyFor(ShaderRecompiler::SpirvTarget target) { + target.featureDigestSealed = false; + return CacheKeyFor(target); +} + +void CheckFeatureDigest() { + ProfileInput input; + const AgcDriver::ShaderDeviceProfile profile(input.Target(), input.device, input.limits); + const auto sealed = profile.Target(); + Require(sealed.featureDigestSealed, "the shader device profile did not seal its feature digest"); + const auto key = CacheKeyFor(sealed); + Require(key == PerKeyDigestKeyFor(sealed), "the sealed feature digest differs from the per-key digest"); + const std::vector capabilityCopy(sealed.supportedCapabilities.begin(), sealed.supportedCapabilities.end()); + const std::vector extensionStorage(sealed.supportedExtensions.begin(), sealed.supportedExtensions.end()); + const std::vector extensionCopy(extensionStorage.begin(), extensionStorage.end()); + auto copied = sealed; + copied.supportedCapabilities = capabilityCopy; + copied.supportedExtensions = extensionCopy; + Require(CacheKeyFor(copied) == key, "equal feature lists in other storage gave another key than the sealed digest"); + auto fewerCapabilities = sealed; + fewerCapabilities.supportedCapabilities = sealed.supportedCapabilities.first(sealed.supportedCapabilities.size() - 1u); + Require(CacheKeyFor(fewerCapabilities) != key && CacheKeyFor(fewerCapabilities) == PerKeyDigestKeyFor(fewerCapabilities), "a sealed digest outlived the capability list it was computed from"); + auto fewerExtensions = sealed; + fewerExtensions.supportedExtensions = sealed.supportedExtensions.first(1u); + Require(CacheKeyFor(fewerExtensions) != key && CacheKeyFor(fewerExtensions) == PerKeyDigestKeyFor(fewerExtensions), "a sealed digest outlived the extension list it was computed from"); + ProfileInput reordered; + std::ranges::reverse(reordered.capabilities); + std::ranges::reverse(reordered.extensions); + const AgcDriver::ShaderDeviceProfile reorderedProfile(reordered.Target(), reordered.device, reordered.limits); + Require(CacheKeyFor(reorderedProfile.Target()) == key, "two profiles with the same features gave different keys"); + ProfileInput geometry; + geometry.capabilities.push_back(spv::CapabilityGeometry); + geometry.core.geometryShader = VK_TRUE; + const AgcDriver::ShaderDeviceProfile geometryProfile(geometry.Target(), geometry.device, geometry.limits); + const auto geometryKey = CacheKeyFor(geometryProfile.Target()); + Require(geometryKey != key && geometryKey == PerKeyDigestKeyFor(geometryProfile.Target()), "a profile capability did not change the feature digest"); + ProfileInput indexing; + indexing.extensions.push_back("SPV_EXT_descriptor_indexing"); + const AgcDriver::ShaderDeviceProfile indexingProfile(indexing.Target(), indexing.device, indexing.limits); + const auto indexingKey = CacheKeyFor(indexingProfile.Target()); + Require(indexingKey != key && indexingKey != geometryKey && indexingKey == PerKeyDigestKeyFor(indexingProfile.Target()), "a profile extension did not change the feature digest"); +} + void CheckAbi() { using ShaderRecompiler::RuntimeAbi::Binding; using ShaderRecompiler::RuntimeAbi::BindingNumber; @@ -219,6 +274,7 @@ int main() { CheckAbi(); CheckHeaps(); CheckProfile(); + CheckFeatureDigest(); std::cout << "shader runtime ABI and device profile tests passed\n"; return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index 192babfe6c..68fe9d637c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -159,6 +159,7 @@ bool sameBindings(const std::vector& left, const std::vector< void requireSameArtifact(const CompiledShaderArtifact& left, const CompiledShaderArtifact& right, const char* what) { const std::string prefix = std::string(what) + ": "; require(left.spirv == right.spirv, prefix + "SPIR-V differs"); + require(left.meshWindowBuffer == right.meshWindowBuffer, prefix + "mesh window buffer flags differ"); require(left.bdaAbiVersion == right.bdaAbiVersion, prefix + "BDA ABI version differs"); require(left.descriptorFaults == right.descriptorFaults, prefix + "descriptor fault count differs"); require(left.windowLaneGuards == right.windowLaneGuards, prefix + "window lane guards differ"); @@ -247,7 +248,7 @@ DescriptorBinding sampleBinding(std::uint32_t seed) { binding.samplerCoordinatesIgnored = {false, seed % 2 == 0, true}; binding.imageUnnormalized = {true, false, seed % 2 == 0}; binding.imageUnnormalizedBaseLevel = {false, true, seed % 3 == 0}; - binding.imageSamplers = {0x5u, 0u, 0x80000000u}; + binding.imageSamplers = {0x5u, 0u, 0x8000000080000000u}; binding.bufferRead = {true, false, false, true, true, false}; binding.bufferExtent = {32, 0, 16, 4096, 8, 0}; return binding; @@ -255,6 +256,7 @@ DescriptorBinding sampleBinding(std::uint32_t seed) { RecompileResult sampleResult() { RecompileResult result; + result.meshWindowBuffer = true; std::vector words(1000); for (std::size_t i = 0; i < words.size(); ++i) words[i] = static_cast(i * 2654435761u); words[0] = 0x07230203u; @@ -358,6 +360,9 @@ CompiledVariant sampleVariant() { image.sampledOnly = false; image.foldTexelOffsetsOnly = true; image.remappedDepth1D = true; + image.layerRowsCompatible = false; + image.layerRows = true; + image.maxArrayLayers = 0x01020304u; image.optionalDescriptor = true; image.byElements = 4; image.byComponents = 1; @@ -447,6 +452,13 @@ void verifyResultRoundTrip() { require(ShaderDiskCache::DecodeResult(bytes, decoded), "an encoded result does not decode"); requireSameResult(result, decoded, "result round trip"); require(decoded.variantId == 0 && !decoded.cacheHit, "the per-process fields were stored"); + auto memoized = result; + memoized.drawKeyMemo.words.store(std::make_shared>(std::vector{1u, 2u, 3u})); + std::vector memoizedBytes; + ShaderDiskCache::EncodeResult(memoized, memoizedBytes); + require(memoizedBytes == bytes, "the draw key memo was stored"); + RecompileResult memoizedDecoded; + require(ShaderDiskCache::DecodeResult(memoizedBytes, memoizedDecoded) && !memoizedDecoded.drawKeyMemo.words.load(), "a decoded result has a draw key memo"); for (std::size_t size = 0; size < bytes.size(); size += size < 256 ? 1 : 97) { RecompileResult partial; require(!ShaderDiskCache::DecodeResult(std::span(bytes).first(size), partial), "a result truncated to " + std::to_string(size) + " bytes decodes"); @@ -843,6 +855,7 @@ void verifyKeySensitivity() { changes("wave LDS memory barriers", [](SampleRequest& sample) { sample.request.target.waveLdsMemoryBarriers = true; }); changes("device-scope LDS memory barriers", [](SampleRequest& sample) { sample.request.target.deviceScopeLdsMemoryBarriers = true; }); changes("LDS lock fences", [](SampleRequest& sample) { sample.request.target.ldsLockFences = true; }); + changes("mesh window buffers", [](SampleRequest& sample) { sample.request.target.meshWindowBuffer = true; }); changes("the workgroup size limit", [](SampleRequest& sample) { sample.request.target.maxWorkgroupSize[2] = 128; }); changes("the invocation limit", [](SampleRequest& sample) { sample.request.target.maxWorkgroupInvocations = 512; }); changes("the shared memory limit", [](SampleRequest& sample) { sample.request.target.maxWorkgroupSharedMemoryBytes = 32768; }); diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index d730d11de2..98df094230 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -916,6 +916,98 @@ void verifyPlanRetention() { require(copiedWalk.active == wholeWalk.active && copiedWalk.results == wholeWalk.results && copiedWalk.flat == wholeWalk.flat, "plan retention: the plan's walk differs from the program's"); } +std::uint32_t branchReadsRefused = 0; + +bool readFiveUnlessBranch(void*, std::uint64_t, std::uint32_t* value) { + if (ShaderRecompiler::SrtEvaluatingBranch()) { + ++branchReadsRefused; + return false; + } + *value = 5u; + return true; +} + +void verifyPendingBranchLeftUnpruned() { + using namespace ShaderRecompiler; + std::vector> values; + std::uint32_t ids = 0; + const auto make = [&](IrOpcode opcode, IrType type) -> IrValue& { + values.push_back(std::make_unique(opcode, type, ids++)); + return *values.back(); + }; + const auto constant = [&](std::uint32_t value) -> IrValue& { + auto& immediate = make(IrOpcode::Void, IrType::U32); + immediate.SetImmediateU32(value); + return immediate; + }; + const auto userData = [&](std::uint32_t index) -> IrValue& { + auto& reg = make(IrOpcode::Void, IrType::ScalarReg); + reg.SetRegister({RegisterBank::Scalar, index}); + auto& read = make(IrOpcode::GetUserData, IrType::U32); + read.AddArgument(®); + return read; + }; + const auto binary = [&](IrOpcode opcode, IrType type, IrValue& left, IrValue& right) -> IrValue& { + auto& value = make(opcode, type); + value.AddArgument(&left); + value.AddArgument(&right); + return value; + }; + constexpr std::uint32_t Chain = 64; + IrValue* address = &userData(4); + for (std::uint32_t step = 0; step < Chain; ++step) address = &binary(IrOpcode::IAdd32, IrType::U32, *address, constant(1)); + auto& load = make(IrOpcode::LoadBufferU32, IrType::U32); + load.AddArgument(address); + auto& low = binary(IrOpcode::CompositeExtractU32x2, IrType::U32, binary(IrOpcode::CompositeConstructU32x2, IrType::U32x2, userData(1), load), constant(0)); + auto& mask = binary(IrOpcode::IEqual32, IrType::Bool, userData(5), constant(0)); + auto& choice = make(IrOpcode::SelectU32, IrType::U32); + choice.AddArgument(&mask); + choice.AddArgument(&userData(6)); + choice.AddArgument(&load); + auto& offset = binary(IrOpcode::ReadFirstLane, IrType::U32, choice, mask); + auto& read = binary(IrOpcode::LoadAddressU32, IrType::U32, binary(IrOpcode::CompositeConstructU64, IrType::U64, userData(2), userData(3)), offset); + read.SetFlags(MemoryFlags{0, 0}); + IrProgram program; + auto& resources = program.Resources(); + resources.srtPlanComplete = true; + resources.resourceTrackingComplete = true; + MemoryInfo memory; + memory.kind = ResourceKind::ScalarAddress; + resources.memoryInfo.push_back(memory); + for (std::uint32_t source = 0; source < 2u; ++source) { + DescriptorSource descriptor; + descriptor.dwordCount = 4; + for (std::uint32_t dword = 0; dword < 4u; ++dword) descriptor.dwords[dword] = &userData(10u + source * 4u + dword); + resources.descriptorSources.push_back(descriptor); + BufferResource buffer; + buffer.source = source; + resources.info.buffers.push_back(buffer); + } + resources.controlFlow.push_back({&binary(IrOpcode::IEqual32, IrType::Bool, low, constant(7)), {1, 2}, {0}, {}}); + resources.controlFlow.push_back({&binary(IrOpcode::IEqual32, IrType::Bool, read, constant(5)), {3, 4}, {}, {}}); + resources.controlFlow.push_back({nullptr, {}, {}, {}}); + resources.controlFlow.push_back({nullptr, {}, {}, {}}); + resources.controlFlow.push_back({nullptr, {}, {1}, {}}); + const auto plan = ResourceMaterializer{}.ExtractPlan(program); + std::array userWords{}; + userWords[1] = 7u; + userWords[2] = 0x1000u; + userWords[6] = 8u; + for (std::uint32_t word = 10; word < userWords.size(); ++word) userWords[word] = 0x100u + word; + SrtRuntime runtime{userWords}; + runtime.readMemory = readFiveUnlessBranch; + runtime.readSpecializationMemory = readFiveUnlessBranch; + branchReadsRefused = 0; + const auto copiedWalk = walkPlan(plan, plan.materializationSources, plan.cleanFlatSlots, runtime); + require(branchReadsRefused != 0u && !SrtEvaluatingBranch(), "pending branch: the plan's walk read its branch condition outside a branch scope or left the scope open"); + branchReadsRefused = 0; + const std::array sources{0u, 1u}; + const auto wholeWalk = walkPlan(program.Resources(), sources, {}, runtime); + require(branchReadsRefused != 0u && !SrtEvaluatingBranch(), "pending branch: the program's walk read its branch condition outside a branch scope or left the scope open"); + require(wholeWalk.active == std::vector{1u, 1u}, "pending branch: a condition whose word could not be read pruned a successor"); + require(copiedWalk.active == wholeWalk.active && copiedWalk.results == wholeWalk.results && copiedWalk.flat == wholeWalk.flat, "pending branch: the plan's walk differs from the program's"); +} + void verifyPlanRetentionChains() { using namespace ShaderRecompiler; std::vector kept; @@ -2012,7 +2104,7 @@ void verifyTypeZeroImageMaterialization() { { auto tornT = oldT; - tornT[1] &= ~(0x1ffu << 20u); + tornT[3] = (tornT[3] & ~(0xfu << 28u)) | (2u << 28u); srt.fill(0u); std::copy(tornT.begin(), tornT.end(), srt.begin()); std::copy(outputV.begin(), outputV.end(), srt.begin() + 16u); @@ -2053,7 +2145,7 @@ void verifyTypeZeroImageMaterialization() { { auto tornT = oldT; - tornT[1] &= ~(0x1ffu << 20u); + tornT[3] = (tornT[3] & ~(0xfu << 28u)) | (2u << 28u); srt.fill(0u); std::copy(tornT.begin(), tornT.end(), srt.begin()); std::copy(outputV.begin(), outputV.end(), srt.begin() + 16u); @@ -2274,6 +2366,145 @@ void verifyTypeZeroImageMaterialization() { } } +void verifyUnsetFormatImageMaterialization() { + using namespace ShaderRecompiler; + constexpr std::array measuredImage{0xffffffffu, 0xffffffffu, 0x00000019u, 0xc0017901u, 0x00000342u, 0xc8000000u, 0xc0064900u, 0x0000062fu}; + const auto materialize = [](const std::array& words, const ImageResource& image) { + IrResourcePlan plan; + plan.srtPlanComplete = true; + plan.resourceTrackingComplete = true; + plan.userDataCount = 0u; + plan.materializationSources = {0u}; + std::vector> values; + DescriptorSource source; + source.dwordCount = static_cast(words.size()); + for (std::uint32_t i = 0u; i < words.size(); ++i) { + auto value = std::make_unique(IrOpcode::Void, IrType::U32, i); + value->SetImmediateU32(words[i]); + source.dwords[i] = value.get(); + values.push_back(std::move(value)); + } + plan.descriptorSources.push_back(source); + plan.info.images.push_back(image); + plan.info.runtimeImageModes.push_back(ResourceMaterializer::RuntimeImageModes(image)); + ResourceSnapshot snapshot; + ResourceMaterializer{}.Materialize(plan, SrtRuntime{}, snapshot); + return snapshot; + }; + ImageResource sampled; + sampled.source = 0u; + sampled.resourceClass = ImageResourceClass::Sampled; + sampled.dimension = RdnaImageDimension::Dim1DArray; + sampled.read = true; + for (const std::uint32_t format : {0u, 0x1ffu}) { + auto words = measuredImage; + words[1] = (words[1] & ~(0x1ffu << 20u)) | (format << 20u); + const auto snapshot = materialize(words, sampled); + require(snapshot.images.size() == 1u && snapshot.images[0].dwordCount == 8u && snapshot.images[0].dwords == words, + "an unset-format measured image-type T# was rejected or altered during materialization"); + for (const bool atomic : {false, true}) { + auto storage = sampled; + storage.resourceClass = ImageResourceClass::Storage; + storage.read = false; + storage.written = !atomic; + storage.atomic = atomic; + bool rejected = false; + try { + static_cast(materialize(words, storage)); + } catch (const std::runtime_error& error) { + rejected = std::string_view(error.what()).find("image descriptor") != std::string_view::npos; + } + require(rejected, atomic ? "an unset-format atomic image descriptor was accepted" : "an unset-format writable image descriptor was accepted"); + } + } + const auto expectInvalidType = [&](std::uint32_t type, const char* message) { + auto words = measuredImage; + words[1] &= ~(0x1ffu << 20u); + words[3] = (words[3] & ~(0xfu << 28u)) | (type << 28u); + expectFailure([&] { static_cast(materialize(words, sampled)); }, "runtime image descriptor is invalid", message); + }; + expectInvalidType(2u, "a reserved TYPE 2 T# with an unset format was accepted as a null image"); + expectInvalidType(7u, "a reserved TYPE 7 T# with an unset format was accepted as a null image"); + expectInvalidType(15u, "an unimplemented TYPE 15 T# with an unset format was accepted as a null image"); +} + +void CompileDescriptorWeb(const std::vector& code, std::uint32_t address, const char* message, std::ptrdiff_t optionalImages = 2) { + using namespace ShaderRecompiler; + constexpr std::uint32_t Format8888UNorm = 56; + constexpr std::uint32_t Type2D = 9; + struct alignas(256) Texture { std::array bytes{}; }; + static Texture textures[2]; + const auto imageDescriptor = [&](const Texture& texture) { + const auto base = static_cast(reinterpret_cast(texture.bytes.data())); + return std::array{static_cast(base >> 8u), static_cast((base >> 40u) & 0xffu) | (Format8888UNorm << 20u) | (3u << 30u), 3u << 14u, 0xfacu | (Type2D << 28u), 0u, 0u, 0u, 0u}; + }; + std::array output{}; + const auto outputAddress = static_cast(reinterpret_cast(output.data())); + std::array srt{}; + const auto first = imageDescriptor(textures[0]); + const auto second = imageDescriptor(textures[1]); + std::copy(first.begin(), first.end(), srt.begin()); + const std::array outputV{static_cast(outputAddress), static_cast((outputAddress >> 32u) & 0xffffu), 16u, 0x30027facu}; + std::copy(outputV.begin(), outputV.end(), srt.begin() + 16); + srt[20] = 1u; + std::copy(second.begin(), second.end(), srt.begin() + 24); + const auto srtAddress = static_cast(reinterpret_cast(srt.data())); + const std::array userData{static_cast(srtAddress), static_cast(srtAddress >> 32u)}; + const std::array capabilities{29u, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess}; + const std::array extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; + RecompileRequest request{}; + request.shader = {ShaderStage::Compute, address, code, 0, {}}; + request.context.waveSize = 32u; + request.context.userDataBaseRegister = 0; + request.context.userData = userData; + request.context.compute = ShaderComputeStageInfo{{32u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = 32; + request.target.supportedCapabilities = capabilities; + request.target.supportedExtensions = extensions; + request.target.bdaAbiVersion = BdaAbi::Version; + request.target.fragmentShaderBarycentricEnabled = false; + request.layout.pushConstantSizeBytes = 128; + const auto plan = GetResourcePlan(request); + require(plan->info.usesFaultBuffer && plan->info.images.size() >= 2u && std::ranges::count_if(plan->info.images, [](const ImageResource& image) { return image.optionalDescriptor; }) >= optionalImages, message); + AgcDriver::ShaderMemory memory({}); + const auto capture = memory.Capture(request); + const auto memoryRegions = memory.Regions(); + request.context.memory = memoryRegions; + static_cast(Recompile(request, *capture)); +} + + +void verifyDescriptorWebLaneIndexFold() { + const std::vector code{ + 0xf4000400u, 0xfa000050u, 0xf4080500u, 0xfa000020u, 0xf4080600u, 0xfa000040u, 0xf40c0200u, 0xfa000000u, 0xbf8cc07fu, 0xbf068010u, 0xbf850005u, 0xbf068110u, + 0xbf85000cu, 0xf40c0200u, 0xfa000060u, 0xbf8cc07fu, 0x7e040208u, 0x7e060209u, 0x7e08020au, 0x7e0a020bu, 0x7e0c020cu, 0x7e0e020du, 0x7e10020eu, 0x7e12020fu, + 0xbf820009u, 0x7e040208u, 0x7e060209u, 0x7e08020au, 0x7e0a020bu, 0x7e0c020cu, 0x7e0e020du, 0x7e10020eu, 0xd7650009u, 0x000100c1u, 0x7e100502u, 0x7e120503u, + 0x7e140504u, 0x7e160505u, 0x7e180506u, 0x7e1a0507u, 0x7e1c0508u, 0x7e1e0509u, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf810000u}; + CompileDescriptorWeb(code, 0x24000u, "descriptor web lane index: the path whose T# holds the lane index was not folded beside the two T#s into faulting arms"); +} + +void verifyDescriptorWebTexelFold() { + const std::vector code{ + 0xf4000400u, 0xfa000050u, 0xf4080500u, 0xfa000020u, 0xf4080600u, 0xfa000040u, 0xf40c0200u, 0xfa000000u, 0xbf8cc07fu, 0xf0000108u, 0x00020900u, 0xbf8c3f70u, + 0xbf068010u, 0xbf850005u, 0xbf068110u, 0xbf85000cu, 0xf40c0200u, 0xfa000060u, 0xbf8cc07fu, 0x7e040208u, 0x7e060209u, 0x7e08020au, 0x7e0a020bu, 0x7e0c020cu, + 0x7e0e020du, 0x7e10020eu, 0x7e12020fu, 0xbf820007u, 0x7e040208u, 0x7e060209u, 0x7e08020au, 0x7e0a020bu, 0x7e0c020cu, 0x7e0e020du, 0x7e10020eu, 0x7e100502u, + 0x7e120503u, 0x7e140504u, 0x7e160505u, 0x7e180506u, 0x7e1a0507u, 0x7e1c0508u, 0x7e1e0509u, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, + 0xbf810000u}; + CompileDescriptorWeb(code, 0x26000u, "descriptor web texel: the path whose T# holds a texel from an image read was not folded beside the read's T# into a faulting arm", 1); +} + +void verifyDescriptorWebSelectOrder() { + const std::vector code{ + 0xf4000400u, 0xfa000050u, 0xf4080500u, 0xfa000020u, 0xf4080600u, 0xfa000040u, 0xf40c0200u, 0xfa000000u, 0xf40c0700u, 0xfa000060u, 0xbf8cc07fu, 0x7d880090u, + 0x7e14021cu, 0x02041408u, 0x7e16021du, 0x02061609u, 0x7e18021eu, 0x0208180au, 0x7e1a021fu, 0x020a1a0bu, 0xbf068010u, 0xbf85000au, 0x7d880088u, 0x7e1c0220u, + 0x020c1c0cu, 0x7e1e0221u, 0x020e1e0du, 0x7e200222u, 0x0210200eu, 0x7e220223u, 0x0212220fu, 0xbf820004u, 0x7e0c020cu, 0x7e0e020du, 0x7e10020eu, 0x7e12020fu, + 0x7e100502u, 0x7e120503u, 0x7e140504u, 0x7e160505u, 0x7e180506u, 0x7e1a0507u, 0x7e1c0508u, 0x7e1e0509u, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, + 0x80060000u, 0xbf810000u}; + CompileDescriptorWeb(code, 0x25000u, "descriptor web select order: per-lane T# selects in two blocks were not split into arms"); +} + void verifyOutOfRangeConstantReads() { std::vector code{0xf40c0200u, 0xfa000000u, 0xf4080500u, 0xfa000020u, 0xf4080600u, 0xfa000040u, 0xf4000400u, 0xfa000050u, 0xf4080a00u, 0xfa000080u, 0xbf8cc07fu, 0xf42c0814u, 0xfa000000u, 0xbf8cc07fu, 0xbf068010u, 0xbf850008u}; for (std::uint32_t dword = 0; dword < 8u; dword++) { @@ -2544,6 +2775,89 @@ void verifyExecGuardedDescriptorSources() { } } +void verifyPendingBranchCapture() { + using namespace ShaderRecompiler; + constexpr std::uint32_t Format8888UNorm = 56; + constexpr std::uint32_t Type2D = 9; + struct alignas(256) Texture { std::array bytes{}; }; + static Texture texture; + const auto base = static_cast(reinterpret_cast(texture.bytes.data())); + const std::array valid{static_cast(base >> 8u), static_cast((base >> 40u) & 0xffu) | (Format8888UNorm << 20u) | (3u << 30u), 3u << 14u, 0xfacu | (Type2D << 28u), 0u, 0u, 0u, 0u}; + const std::array invalid{0x12345678u, 0xffffffffu, 0xffffffffu, 2u << 28u, 0u, 0u, 0u, 0u}; + const std::array sampler{0u, 0u, 0u, 0u}; + static std::array output{}; + const auto outputAddress = static_cast(reinterpret_cast(output.data())); + const std::array outputV{static_cast(outputAddress), static_cast((outputAddress >> 32u) & 0xffffu), 16u, 0xfacu}; + alignas(4096) static std::array srt{}; + alignas(4096) static std::array flags{}; + const auto flagsAddress = static_cast(reinterpret_cast(flags.data())); + const std::array flagsV{static_cast(flagsAddress), static_cast((flagsAddress >> 32u) & 0xffffu), 16u, 0xfacu}; + static std::uint64_t conditionAddress = 0; + static std::uint64_t waits = 0; + const auto fill = [&](const std::array& image, std::uint32_t mode) { + srt.fill(0u); + std::copy(image.begin(), image.end(), srt.begin()); + std::copy(sampler.begin(), sampler.end(), srt.begin() + 8); + std::copy(outputV.begin(), outputV.end(), srt.begin() + 16); + std::copy(flagsV.begin(), flagsV.end(), srt.begin() + 24); + flags[0] = mode; + }; + const auto srtAddress = static_cast(reinterpret_cast(srt.data())); + conditionAddress = flagsAddress; + const std::array userData{static_cast(srtAddress), static_cast(srtAddress >> 32u)}; + const std::array capabilities{29u, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess}; + const std::array extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; + RecompileRequest request{}; + const std::vector code{0xf4080700u, 0xfa000060u, 0xf4080500u, 0xfa000020u, 0xf4080600u, 0xfa000040u, 0xbf8cc07fu, 0xf420040eu, 0xfa000000u, 0xbf8cc07fu, 0xbf068010u, 0xbf850006u, 0xf40c0200u, 0xfa000000u, 0xbf8cc07fu, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf810000u}; + request.shader = {ShaderStage::Compute, 0x2a000u, code, 0, {}}; + request.context.waveSize = 32; + request.context.userDataBaseRegister = 0; + request.context.userData = userData; + request.context.compute = ShaderComputeStageInfo{{32u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = 32; + request.target.supportedCapabilities = capabilities; + request.target.supportedExtensions = extensions; + request.target.bdaAbiVersion = BdaAbi::Version; + request.target.fragmentShaderBarycentricEnabled = false; + request.layout.pushConstantSizeBytes = 128; + + struct FlushReset { + ~FlushReset() { AgcDriver::GuestMemory::SetFlushHook(nullptr); } + } flushReset; + AgcDriver::GuestMemory::SetFlushHook([](std::uint64_t address, std::size_t bytes) { + if (address < conditionAddress + sizeof(std::uint32_t) && conditionAddress < address + bytes) ++waits; + }); + const auto pending = +[](std::uint64_t address, std::size_t bytes, std::span) -> AgcDriver::ShaderMemory::PendingWrite { + using Pending = AgcDriver::ShaderMemory::PendingWrite; + return address < conditionAddress + sizeof(std::uint32_t) && conditionAddress < address + bytes ? Pending::Sync : Pending::None; + }; + const auto captured = [&](const std::array& image, std::uint32_t mode) { + fill(image, mode); + waits = 0; + AgcDriver::ShaderMemory memory({}, pending); + return memory.Capture(request); + }; + const auto holds = [](const std::shared_ptr& capture, const std::array& image) { + return capture->snapshot.images.size() == 1u && std::equal(image.begin(), image.end(), capture->snapshot.images[0].dwords.begin()); + }; + const std::array null{}; + + const auto unpruned = captured(valid, 0u); + require(waits == 0u, ("pending branch capture: the branch condition over a pending GPU write waited " + std::to_string(waits) + " times").c_str()); + require(holds(unpruned, valid), "pending branch capture: the branch over a pending GPU write was pruned instead of keeping both successors"); + const auto redone = captured(invalid, 0u); + require(waits != 0u, "pending branch capture: a capture that failed behind an unpruned branch was not redone waiting for the condition"); + require(holds(redone, null), "pending branch capture: the capture redone waiting did not prune the branch its condition skips"); + fill(invalid, 1u); + { + AgcDriver::ShaderMemory memory({}, pending); + expectFailure([&] { static_cast(memory.Capture(request)); }, "runtime image descriptor is invalid", "pending branch capture: an invalid T# behind a taken branch was accepted after the capture was redone waiting"); + } + std::cout << "pending branch capture passed\n"; +} + void verifyGuardedDescriptorSources() { using namespace ShaderRecompiler; constexpr std::uint32_t Format8888UNorm = 56; @@ -2598,6 +2912,22 @@ void verifyGuardedDescriptorSources() { AgcDriver::ShaderMemory memory({}); expectFailure([&] { static_cast(memory.Capture(request)); }, "runtime image descriptor is invalid", "guarded sources: an invalid T# behind a taken branch was accepted"); } + auto unsetFormat = valid; + unsetFormat[1] = valid[1] & ~(0x1ffu << 20u); + fill(unsetFormat, 1u); + { + AgcDriver::ShaderMemory memory({}); + const auto capture = memory.Capture(request); + require(capture->snapshot.images.size() == 1u && capture->snapshot.images[0].dwords == unsetFormat, "guarded sources: a T# with an unset format behind a taken branch was rejected or altered"); + } + auto filledFormat = valid; + filledFormat[1] = valid[1] | (0x1ffu << 20u); + fill(filledFormat, 1u); + { + AgcDriver::ShaderMemory memory({}); + const auto capture = memory.Capture(request); + require(capture->snapshot.images.size() == 1u && capture->snapshot.images[0].dwords == filledFormat, "guarded sources: a T# with an all-ones format behind a taken branch was rejected or altered"); + } auto typeOne = valid; typeOne[3] = 0xfacu | (1u << 28u); fill(typeOne, 1u); @@ -2923,6 +3253,11 @@ TableCompiled CompileTable(ShaderRecompiler::RecompileRequest request) { return out; } +std::shared_ptr CaptureTable(const ShaderRecompiler::RecompileRequest& request) { + AgcDriver::ShaderMemory memory({}); + return memory.Capture(request); +} + std::array TableSelected(const ShaderRecompiler::ImageTableSnapshot& tables, std::uint32_t table, const char* message) { namespace Abi = ShaderRecompiler::ImageTableAbi; const auto& column = tables.tables.at(table); @@ -4395,24 +4730,26 @@ void verifyImageTableUnkeyedAccess() { struct TableManyGuest { std::array, 128> heap{}; std::array output{}; - std::array srt{}; + std::array srt{}; }; constexpr std::uint32_t TableManyDirectWord = 96u; constexpr std::uint32_t TableManyExtraSamplerWord = 112u; +constexpr std::uint32_t TableManyMoreSamplerWord = 116u; +constexpr std::array TableManySamplerSgprs{8u, 12u, 64u, 68u, 72u, 76u, 80u, 84u}; -std::vector TableManyKeyCode(std::uint32_t first, std::uint32_t second, bool twoSamplers, std::uint32_t direct = 0u, bool extraSampler = false) { +std::vector TableManyKeyCode(std::uint32_t first, std::uint32_t second, std::uint32_t samplers, std::uint32_t direct = 0u, bool extraSampler = false) { std::vector code{0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u, 0xf4080700u, 0xfa000030u, 0xf4080a00u, 0xfa000040u, 0x7e100280u}; for (std::uint32_t image = 0; image < direct; image++) code.insert(code.end(), {0xf40c0000u | ((44u + image * 8u) << 6u), 0xfa000000u | ((TableManyDirectWord + image * 8u) * 4u)}); if (extraSampler) code.insert(code.end(), {0xf4080f00u, 0xfa000000u | (TableManyExtraSamplerWord * 4u)}); + for (std::uint32_t sampler = 2u; sampler < samplers; sampler++) code.insert(code.end(), {0xf4080000u | (TableManySamplerSgprs[sampler] << 6u), 0xfa000000u | ((TableManyMoreSamplerWord + (sampler - 2u) * 4u) * 4u)}); for (std::uint32_t read = 0; read < first + second; read++) { - code.insert(code.end(), {0xf4000400u, 0xfa000050u + read * 4u, 0x8f108510u, read < first ? 0xf42c0502u : 0xf42c0514u, 0x20000000u, 0xf09c0f08u, 0x00450400u, 0x06100908u}); - if (twoSamplers) code.insert(code.end(), {0xf09c0f08u, 0x00650400u, 0x06100908u}); + code.insert(code.end(), {0xf4000400u, 0xfa000050u + read * 4u, 0x8f108510u, read < first ? 0xf42c0502u : 0xf42c0514u, 0x20000000u}); + for (std::uint32_t sampler = 0; sampler < samplers; sampler++) code.insert(code.end(), {0xf09c0f08u, ((TableManySamplerSgprs[sampler] / 4u) << 21u) | 0x00050400u, 0x06100908u}); } for (std::uint32_t image = 0; image < direct; image++) { const auto resource = ((44u + image * 8u) / 4u) << 16u; - code.insert(code.end(), {0xf09c0f08u, 0x00400400u | resource, 0x06100908u}); - if (twoSamplers) code.insert(code.end(), {0xf09c0f08u, 0x00600400u | resource, 0x06100908u}); + for (std::uint32_t sampler = 0; sampler < samplers; sampler++) code.insert(code.end(), {0xf09c0f08u, ((TableManySamplerSgprs[sampler] / 4u) << 21u) | 0x00000400u | resource, 0x06100908u}); if (extraSampler && image + 1u == direct) code.insert(code.end(), {0xf09c0f08u, 0x01e00400u | resource, 0x06100908u}); } code.insert(code.end(), {0xe0700000u, 0x80070800u, 0xbf810000u}); @@ -4439,6 +4776,8 @@ void verifySelectedRecordsWithinLimits(bool pairs, bool direct) { std::copy(direct.begin(), direct.end(), guest.srt.begin() + TableManyDirectWord + image * 8u); } guest.srt[TableManyExtraSamplerWord] = 0x91u; + std::fill(guest.srt.begin() + TableManyMoreSamplerWord, guest.srt.end(), 0u); + for (std::uint32_t sampler = 2u; sampler < TableManySamplerSgprs.size(); sampler++) guest.srt[TableManyMoreSamplerWord + (sampler - 2u) * 4u] = 0x92u; const auto address = static_cast(reinterpret_cast(guest.srt.data())); const std::array userData{static_cast(address), static_cast(address >> 32u)}; const auto columnOf = [](const IrResourcePlan& plan, const ImageResource& image) { return *plan.descriptorSources.at(image.source).tableColumn; }; @@ -4446,49 +4785,50 @@ void verifySelectedRecordsWithinLimits(bool pairs, bool direct) { return static_cast(std::count_if(plan.info.images.begin(), plan.info.images.end(), [&](const ImageResource& image) { return image.table != NoTable && columnOf(plan, image).selected; })); }; const std::string name = pairs ? "selected records within the pair limit" : "selected records within the image limit"; - const std::uint32_t limit = pairs ? ShaderInfo::MaxSampledPairs / 2u : ShaderInfo::MaxImages; + const std::uint32_t samplers = pairs ? static_cast(TableManySamplerSgprs.size()) : 1u; + const std::uint32_t limit = pairs ? ShaderInfo::MaxSampledPairs / samplers : ShaderInfo::MaxImages; const auto outside = Abi::PoisonCode(static_cast(Abi::PoisonReason::OutsideDomain)); if (direct) { const auto directImages = [&](const IrResourcePlan& plan) { return static_cast(std::count_if(plan.info.images.begin(), plan.info.images.end(), [](const ImageResource& image) { return image.table == NoTable; })); }; const auto reads = limit - 1u; - const auto fittingDirectCode = TableManyKeyCode(reads, 0u, pairs, 1u); + const auto fittingDirectCode = TableManyKeyCode(reads, 0u, samplers, 1u); const auto fittingDirect = GetResourcePlan(TableRequest(fittingDirectCode, userData)); const auto describe = [&](const IrResourcePlan& plan) { return std::to_string(plan.info.images.size()) + " images (" + std::to_string(selectedTables(plan)) + " selected tables, " + std::to_string(directImages(plan)) + " direct), " + std::to_string(plan.info.sampledPairs.size()) + " pairs"; }; - require(fittingDirect->info.images.size() == limit && selectedTables(*fittingDirect) == reads && directImages(*fittingDirect) == 1u && fittingDirect->info.sampledPairs.size() == (pairs ? 2u : 1u) * limit, (name + ": selected reads and a direct image that fit lost the per-key split").c_str()); - std::cout << name << ": " << reads << " selected reads and 1 direct image" << (pairs ? ", each with 2 samplers" : "") << ": " << describe(*fittingDirect) << '\n'; - const auto pastDirectCode = TableManyKeyCode(reads, 0u, pairs, pairs ? 1u : 2u, pairs); + require(fittingDirect->info.images.size() == limit && selectedTables(*fittingDirect) == reads && directImages(*fittingDirect) == 1u && fittingDirect->info.sampledPairs.size() == samplers * limit, (name + ": selected reads and a direct image that fit lost the per-key split").c_str()); + std::cout << name << ": " << reads << " selected reads and 1 direct image" << (pairs ? ", each with " + std::to_string(samplers) + " samplers" : std::string()) << ": " << describe(*fittingDirect) << '\n'; + const auto pastDirectCode = TableManyKeyCode(reads, 0u, samplers, pairs ? 1u : 2u, pairs); const auto pastDirectRequest = TableRequest(pastDirectCode, userData); const auto pastDirect = GetResourcePlan(pastDirectRequest); const auto mergedDirect = std::find_if(pastDirect->info.images.begin(), pastDirect->info.images.end(), [](const ImageResource& image) { return image.table != NoTable; }); - require(pastDirect->info.images.size() == (pairs ? 2u : 3u) && selectedTables(*pastDirect) == 0u && directImages(*pastDirect) == (pairs ? 1u : 2u) && pastDirect->info.sampledPairs.size() == (pairs ? 5u : 3u) && mergedDirect != pastDirect->info.images.end(), (name + ": selected reads that the direct " + (pairs ? "sampler" : "images") + " push past the limit were not merged into one table").c_str()); + require(pastDirect->info.images.size() == (pairs ? 2u : 3u) && selectedTables(*pastDirect) == 0u && directImages(*pastDirect) == (pairs ? 1u : 2u) && pastDirect->info.sampledPairs.size() == (pairs ? 2u * samplers + 1u : 3u) && mergedDirect != pastDirect->info.images.end(), (name + ": selected reads that the direct " + (pairs ? "sampler" : "images") + " push past the limit were not merged into one table").c_str()); const auto mergedColumn = columnOf(*pastDirect, *mergedDirect); require(!mergedColumn.selected && mergedColumn.selection == 0u && !mergedColumn.unkeyed && mergedColumn.keyDomains.size() == reads, (name + ": the table merged beside the direct " + (pairs ? "sampler" : "images") + " is not the union of the keys").c_str()); - const auto compiledDirect = CompileTable(pastDirectRequest); - const auto& directTables = compiledDirect.capture->snapshot.tables; + const auto capturedDirect = pairs ? CaptureTable(pastDirectRequest) : CompileTable(pastDirectRequest).capture; + const auto& directTables = capturedDirect->snapshot.tables; require(directTables.tables.size() == 1u, (name + ": the snapshot beside the direct " + (pairs ? "sampler" : "images") + " does not hold one table").c_str()); const auto& directSnapshot = directTables.tables[mergedDirect->table]; for (const auto key : {2u, 2u + reads / 2u, 1u + reads}) { require(key < directSnapshot.codes.size() && (directSnapshot.codes[key] & Abi::PoisonFlag) == 0u && directSnapshot.words.at(directSnapshot.codes[key]).dwords == heap[key], (name + ": a key beside the direct " + (pairs ? "sampler" : "images") + " does not bind its record").c_str()); } require(directSnapshot.codes.at(1) == outside && directSnapshot.codes.at(reads + 2u) == outside, (name + ": a key no read selects was bound beside the direct " + (pairs ? "sampler" : "images")).c_str()); - std::cout << name << ": " << reads << " selected reads and " << (pairs ? "1 direct image with a third sampler" : "2 direct images") << ": " << describe(*pastDirect) << ", the reads merged into one table with " << mergedColumn.keyDomains.size() << " key domains\n"; + std::cout << name << ": " << reads << " selected reads and " << (pairs ? "1 direct image with one more sampler" : "2 direct images") << ": " << describe(*pastDirect) << ", the reads merged into one table with " << mergedColumn.keyDomains.size() << " key domains\n"; return; } - const auto fitting = TableManyKeyCode(limit, 0u, pairs); + const auto fitting = TableManyKeyCode(limit, 0u, samplers); const auto atLimit = GetResourcePlan(TableRequest(fitting, userData)); - require(atLimit->info.images.size() == limit && selectedTables(*atLimit) == limit && atLimit->info.sampledPairs.size() == (pairs ? 2u : 1u) * limit, (name + ": reads that fit lost the per-key split").c_str()); - const auto past = TableManyKeyCode(limit + 1u, 0u, pairs); + require(atLimit->info.images.size() == limit && selectedTables(*atLimit) == limit && atLimit->info.sampledPairs.size() == samplers * limit, (name + ": reads that fit lost the per-key split").c_str()); + const auto past = TableManyKeyCode(limit + 1u, 0u, samplers); const auto request = TableRequest(past, userData); const auto plan = GetResourcePlan(request); - require(plan->info.images.size() == 1u && plan->info.images[0].table == 0u && plan->info.sampledPairs.size() == (pairs ? 2u : 1u), (name + ": reads past the limit were not merged into one table").c_str()); + require(plan->info.images.size() == 1u && plan->info.images[0].table == 0u && plan->info.sampledPairs.size() == samplers, (name + ": reads past the limit were not merged into one table").c_str()); const auto merged = columnOf(*plan, plan->info.images[0]); require(!merged.selected && merged.selection == 0u && !merged.unkeyed && merged.keyDomains.size() == limit + 1u, (name + ": the merged table is not the union of the keys").c_str()); - const auto compiled = CompileTable(request); - const auto& tables = compiled.capture->snapshot.tables; + const auto captured = pairs ? CaptureTable(request) : CompileTable(request).capture; + const auto& tables = captured->snapshot.tables; require(tables.tables.size() == 1u, (name + ": the snapshot does not hold one table").c_str()); const auto& snapshot = tables.tables[0]; for (const auto key : {2u, 2u + limit / 2u, 2u + limit}) { @@ -4496,12 +4836,151 @@ void verifySelectedRecordsWithinLimits(bool pairs, bool direct) { } require(snapshot.codes.at(1) == outside && snapshot.codes.at(limit + 3u) == outside, (name + ": a key no read selects was bound").c_str()); if (pairs) return; - const auto split = TableManyKeyCode(40u, 30u, false); + const auto split = TableManyKeyCode(40u, 30u, 1u); const auto twoHeaps = GetResourcePlan(TableRequest(split, userData)); const auto unselected = std::find_if(twoHeaps->info.images.begin(), twoHeaps->info.images.end(), [&](const ImageResource& image) { return !columnOf(*twoHeaps, image).selected; }); require(twoHeaps->info.images.size() == 31u && selectedTables(*twoHeaps) == 30u && unselected != twoHeaps->info.images.end() && columnOf(*twoHeaps, *unselected).keyDomains.size() == 40u, (name + ": the larger heap was not merged first, or the smaller one lost its split").c_str()); } +struct SamplerLimitGuest { + std::array record{}; + std::array output{}; + std::array srt{}; +}; + +constexpr std::uint32_t SamplerLimitTableSamplerByte = 2048u; +constexpr std::uint32_t SamplerLimitDirectSamplerWord = 8u; +constexpr std::uint32_t SamplerLimitDirectImageWord = 160u; + +std::vector SamplerLimitCode(std::uint32_t tableSamplers, bool directImages, const std::vector>& pairs) { + std::vector code{0xf4080100u, 0xfa000000u, 0xf4080700u, 0xfa000010u, 0x7e200500u, 0x8f108c10u, 0x7e100280u}; + for (const auto& [image, sampler] : pairs) { + if (directImages) code.insert(code.end(), {0xf40c0500u, 0xfa000000u | ((SamplerLimitDirectImageWord + image * 8u) * 4u)}); + else code.insert(code.end(), {0xf42c0502u, 0x20000000u | (image * 32u)}); + if (sampler < tableSamplers) code.insert(code.end(), {0xf4280202u, 0x20000000u | (SamplerLimitTableSamplerByte + sampler * 16u)}); + else code.insert(code.end(), {0xf4080200u, 0xfa000000u | ((SamplerLimitDirectSamplerWord + (sampler - tableSamplers) * 4u) * 4u)}); + code.insert(code.end(), {0xf09c0f08u, 0x00450400u, 0x06100908u}); + } + code.insert(code.end(), {0xe0700000u, 0x80070800u, 0xbf810000u}); + return code; +} + +std::uint32_t SamplerLimitImageOf(const std::uint32_t* words) { + return (words[2] >> 14u) - 1u; +} + +void verifySamplerLimits() { + using namespace ShaderRecompiler; + static TableTexture textures[4]; + alignas(4096) static SamplerLimitGuest guest; + constexpr std::uint32_t Images = 51u; + constexpr std::uint32_t TableSamplers = 60u; + guest.record = {}; + for (std::uint32_t image = 0; image < Images; image++) { + const auto words = TableT(textures[image % 4u].bytes.data(), TableFormat8888UNorm, TableType2D, image + 1u); + std::copy(words.begin(), words.end(), guest.record.begin() + image * 8u); + } + for (std::uint32_t sampler = 0; sampler < 64u; sampler++) { + const std::array words{0x92u, (sampler + 1u) << 12u, 0u, 0u}; + std::copy(words.begin(), words.end(), guest.record.begin() + SamplerLimitTableSamplerByte / 4u + sampler * 4u); + } + guest.srt = {}; + const auto heapV = TableV(guest.record.data(), 4096u, 1u); + const auto outputV = TableV(guest.output.data(), 0u, 16u); + std::copy(heapV.begin(), heapV.end(), guest.srt.begin()); + std::copy(outputV.begin(), outputV.end(), guest.srt.begin() + 4); + for (std::uint32_t sampler = 0; sampler < 36u; sampler++) { + const std::array words{0x92u, (sampler + 101u) << 12u, 0u, 0u}; + std::copy(words.begin(), words.end(), guest.srt.begin() + SamplerLimitDirectSamplerWord + sampler * 4u); + } + for (std::uint32_t image = 0; image < 3u; image++) { + const auto words = TableT(textures[image].bytes.data(), image == 1u ? TableFormat32UInt : TableFormat8888UNorm, TableType2D, image + 1u); + std::copy(words.begin(), words.end(), guest.srt.begin() + SamplerLimitDirectImageWord + image * 8u); + } + const auto address = static_cast(reinterpret_cast(guest.srt.data())); + const std::array userData{static_cast(address), static_cast(address >> 32u)}; + + const auto directOf = [](std::uint32_t image) { return image < 19u ? 0u : image < 36u ? 1u : 2u; }; + const auto riftPairs = [&](std::uint32_t tableSamplers) { + std::vector> pairs; + for (std::uint32_t image = 0; image < Images; image++) pairs.emplace_back(image, image); + for (std::uint32_t sampler = Images; sampler < tableSamplers; sampler++) pairs.emplace_back(sampler - Images, sampler); + for (std::uint32_t image = 0; image < Images; image++) pairs.emplace_back(image, tableSamplers + directOf(image)); + pairs.emplace_back(0u, tableSamplers + 2u); + return pairs; + }; + const auto riftCode = SamplerLimitCode(TableSamplers, false, riftPairs(TableSamplers)); + const auto riftRequest = TableRequest(riftCode, userData); + const auto rift = GetResourcePlan(riftRequest); + const auto& samplers = rift->info.samplers; + const auto tableCount = static_cast(std::count_if(samplers.begin(), samplers.end(), [](const SamplerResource& sampler) { return sampler.table != NoTable; })); + require(samplers.size() == TableSamplers + 3u && tableCount == TableSamplers && rift->info.images.size() == Images && rift->info.sampledPairs.size() == 112u, ("sampler limits: the program planned " + std::to_string(samplers.size()) + " samplers (" + std::to_string(tableCount) + " through the table), " + std::to_string(rift->info.images.size()) + " images and " + std::to_string(rift->info.sampledPairs.size()) + " pairs, expected 63 (60), 51 and 112").c_str()); + std::vector directResources; + for (std::uint32_t resource = 0; resource < samplers.size(); resource++) { + if (samplers[resource].table == NoTable) directResources.push_back(resource); + } + require(directResources.size() == 3u && directResources.front() >= 32u, "sampler limits: the direct samplers were not planned after the table samplers, at resource indices past 31"); + const auto compiled = CompileTable(riftRequest); + const auto* guestSamplers = TableBinding(*compiled.result, DescriptorRole::GuestSamplers); + require(guestSamplers != nullptr && guestSamplers->guestDescriptor.size() == 6u * 4u, "sampler limits: the three direct samplers are not bound as six sampler elements"); + for (std::uint32_t element = 0; element < 6u; element++) { + require(guestSamplers->guestDescriptor[element * 4u + 1u] == (element / 2u + 101u) << 12u, "sampler limits: a direct sampler element does not hold its S#"); + } + const auto* table = TableBinding(*compiled.result, DescriptorRole::ImageTable); + require(table != nullptr && table->count == Images && table->imageSamplers.size() == Images, "sampler limits: the image table does not bind the 51 images with their sampler masks"); + for (std::uint32_t element = 0; element < Images; element++) { + const auto image = SamplerLimitImageOf(table->guestDescriptor.data() + element * 8u); + require(image < Images, "sampler limits: an image table element does not hold one of the images"); + auto expected = std::uint64_t{3} << (directOf(image) * 2u); + if (image == 0u) expected |= std::uint64_t{3} << 4u; + require(table->imageSamplers[element] == expected, ("sampler limits: image " + std::to_string(image) + " is paired with sampler elements " + std::to_string(table->imageSamplers[element]) + ", expected " + std::to_string(expected)).c_str()); + } + std::cout << "sampler limits: 60 table samplers and 3 direct samplers (resources " << directResources[0] << " to " << directResources[2] << "), 51 images and 112 pairs planned; each image table element pairs with its direct sampler elements\n"; + + constexpr std::uint32_t Direct = 20u; + std::vector> directPairs; + for (std::uint32_t sampler = 0; sampler < Direct; sampler++) { + directPairs.emplace_back(0u, sampler); + directPairs.emplace_back(1u, sampler); + if (sampler >= 16u) directPairs.emplace_back(2u, sampler); + } + const auto directCode = SamplerLimitCode(0u, true, directPairs); + const auto direct = CompileTable(TableRequest(directCode, userData)); + const auto* directSamplers = TableBinding(*direct.result, DescriptorRole::GuestSamplers); + require(directSamplers != nullptr && directSamplers->guestDescriptor.size() == Direct * 2u * 4u, "sampler limits: 20 direct samplers, each paired with a filtered and an integer image, are not bound as 40 sampler elements"); + const auto all = (std::uint64_t{1} << (Direct * 2u)) - 1u; + std::uint32_t pairedImages = 0u; + for (const auto& binding : direct.result->bindings) { + if (binding.kind != DescriptorKind::SampledImage || binding.role != DescriptorRole::GuestImages) continue; + for (std::uint32_t element = 0; element < binding.count; element++) { + const auto image = SamplerLimitImageOf(binding.guestDescriptor.data() + element * 8u); + const auto expected = image == 2u ? all & ~((std::uint64_t{1} << 32u) - 1u) : all; + require(image < 3u && binding.imageSamplers.at(element) == expected, ("sampler limits: direct image " + std::to_string(image) + " is paired with sampler elements " + std::to_string(binding.imageSamplers.at(element)) + ", expected " + std::to_string(expected)).c_str()); + pairedImages++; + } + } + require(pairedImages == 3u, "sampler limits: the three direct images are not bound"); + std::cout << "sampler limits: 20 direct samplers bound as 40 sampler elements; the image sampled only through samplers 16 to 19 pairs with elements 32 to 39\n"; + + constexpr std::uint32_t TableDirect = 18u; + std::vector> tablePairs; + for (std::uint32_t sampler = 0; sampler < TableDirect; sampler++) tablePairs.emplace_back(sampler % 2u, sampler); + const auto tableDirectCode = SamplerLimitCode(0u, false, tablePairs); + const auto tableDirect = CompileTable(TableRequest(tableDirectCode, userData)); + const auto* tableDirectSamplers = TableBinding(*tableDirect.result, DescriptorRole::GuestSamplers); + require(tableDirectSamplers != nullptr && tableDirectSamplers->guestDescriptor.size() == TableDirect * 2u * 4u, "sampler limits: 18 direct samplers paired with image table images are not bound as 36 sampler elements"); + const auto* tableImages = TableBinding(*tableDirect.result, DescriptorRole::ImageTable); + require(tableImages != nullptr && tableImages->count == 2u && tableImages->imageSamplers.size() == 2u, "sampler limits: the image table does not bind the two images beside 18 direct samplers"); + for (std::uint32_t element = 0; element < 2u; element++) { + const auto image = SamplerLimitImageOf(tableImages->guestDescriptor.data() + element * 8u); + std::uint64_t expected = 0u; + for (std::uint32_t sampler = image; sampler < TableDirect; sampler += 2u) expected |= std::uint64_t{3} << (sampler * 2u); + require(image < 2u && tableImages->imageSamplers[element] == expected, ("sampler limits: image table image " + std::to_string(image) + " is paired with sampler elements " + std::to_string(tableImages->imageSamplers[element]) + ", expected " + std::to_string(expected)).c_str()); + } + std::cout << "sampler limits: 18 direct samplers beside an image table bound as 36 sampler elements; each table image pairs with its samplers' elements up to 35\n"; + +} + void verifyImageTableSrtKey() { using namespace ShaderRecompiler; namespace Abi = ImageTableAbi; @@ -7201,7 +7680,7 @@ void verifyPixelRequestSerialization() { minimal.context.pixel->layerExported = true; minimal.context.pixel->ditheredAlphaToCoverage = true; const auto encoded = serializer.Serialize(minimal); - require(requestPrefix(encoded, 8u) == "NVNQQTMAAAA=", "new requests did not use serialization version 51"); + require(requestPrefix(encoded, 8u) == "NVNQQTUAAAA=", "new requests did not use serialization version 53"); auto withBarycentric = minimal; withBarycentric.target.fragmentBarycentricBuiltins = true; require(serializer.Deserialize(serializer.Serialize(withBarycentric)).request.target.fragmentBarycentricBuiltins && !serializer.Deserialize(encoded).request.target.fragmentBarycentricBuiltins, "barycentric built-ins were lost in serialization"); @@ -7322,7 +7801,7 @@ void verifyPixelRequestSerialization() { for (std::size_t bytes = 0; bytes < 45u; ++bytes) { expectFailure([&] { static_cast(serializer.Deserialize(requestPrefix(encoded, mappingOffset + bytes))); }, "truncated data", "a truncated pixel mapping, packing, dual-source, alpha-to-coverage, stencil reference export flag, primitive class, layer export, dither or front-face flag, quad pixel mask or quad sample mask was accepted"); } - for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQTIAAAA=", "NVNQQTQAAAA="}) { + for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQTIAAAA=", "NVNQQTYAAAA="}) { expectFailure([&] { static_cast(serializer.Deserialize(unsupported)); }, "serialization version", "an unsupported request version was accepted"); } static constexpr std::string_view version18Request{ @@ -7441,15 +7920,17 @@ void verifyDepthExportClamp() { const auto both = RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(tail)); require(both.request.target.largePixelFunctionLdsBuffer && both.request.target.pixelFunctionStorageDwords == 2048u && both.request.context.pixel->depthExportMin == 0.25f && both.request.context.pixel->depthExportMax == 0.75f, - "depth export: a version 49 request lost the bounds or the version 45 target tail"); + "depth export: a version 53 request lost the bounds or the version 45 target tail"); auto bytes = decodeBase64(RequestSerializer{}.Serialize(tail)); - require(bytes.size() > 8u && bytes[4] == 51u, "depth export: a depth exporting request did not use serialization version 51"); + require(bytes.size() > 8u && bytes[4] == 53u, "depth export: a depth exporting request did not use serialization version 53"); static constexpr std::array boundBytes{0x00u, 0x00u, 0x80u, 0x3eu, 0x00u, 0x00u, 0x40u, 0x3fu}; const auto found = std::search(bytes.begin(), bytes.end(), boundBytes.begin(), boundBytes.end()); require(found != bytes.end() && std::search(found + 1, bytes.end(), boundBytes.begin(), boundBytes.end()) == bytes.end(), "depth export: the clamp bounds were not written exactly once"); bytes.erase(found, found + boundBytes.size()); bytes.pop_back(); + bytes.pop_back(); + bytes.pop_back(); for (const std::uint8_t legacyVersion : {48u, 47u, 46u}) { if (legacyVersion == 47u) bytes.pop_back(); bytes[4] = legacyVersion; @@ -7469,13 +7950,15 @@ void verifyDepthExportClamp() { ditheredPixel.ditheredAlphaSamples = 2u; dithered.context.pixel = ditheredPixel; auto ditheredBytes = decodeBase64(RequestSerializer{}.Serialize(dithered)); - require(ditheredBytes.size() > 8u && ditheredBytes[4] == 51u, "a two-sample dithered request did not use serialization version 51"); + require(ditheredBytes.size() > 8u && ditheredBytes[4] == 53u, "a two-sample dithered request did not use serialization version 53"); if (depthExport) { const auto bounds = std::search(ditheredBytes.begin(), ditheredBytes.end(), boundBytes.begin(), boundBytes.end()); require(bounds != ditheredBytes.end(), "a two-sample dithered depth exporting request lacks the clamp bounds"); ditheredBytes.erase(bounds, bounds + boundBytes.size()); } ditheredBytes.pop_back(); + ditheredBytes.pop_back(); + ditheredBytes.pop_back(); ditheredBytes[4] = 48u; const auto main48 = RequestSerializer{}.Deserialize(encodeBase64(ditheredBytes)); require(main48.request.context.pixel->ditheredAlphaToCoverage && main48.request.context.pixel->ditheredAlphaSamples == 2u && main48.request.context.pixel->depthExportEnable == depthExport && @@ -7541,6 +8024,8 @@ void verifyVertexInfoVersion19() { require(offset + 4u <= plain.size() && flagged[offset] == 0xefu && flagged[offset + 1u] == 0xcdu && flagged[offset + 2u] == 0xabu && flagged[offset + 3u] == 0x89u, "PA_CL_VS_OUT_CNTL is not the four bytes the version 20 vertex info adds"); require(std::equal(plain.begin() + offset + 4u, plain.end(), flagged.begin() + offset + 4u), "PA_CL_VS_OUT_CNTL moved the fields after it"); auto version20Vertex = flagged; + version20Vertex.pop_back(); + version20Vertex.pop_back(); version20Vertex.resize(version20Vertex.size() - sizeof(std::uint32_t)); version20Vertex.pop_back(); version20Vertex.pop_back(); @@ -7555,6 +8040,8 @@ void verifyVertexInfoVersion19() { const auto layered = serializer.Deserialize(serializer.Serialize(vertex)); require(layered.request.context.vertex.has_value() && layered.request.context.vertex->paClVsOutCntl == vertex.context.vertex->paClVsOutCntl, "a layered vertex info lost its clip distances or render target index"); auto legacy = plain; + legacy.pop_back(); + legacy.pop_back(); legacy.resize(legacy.size() - sizeof(std::uint32_t)); legacy.pop_back(); legacy.pop_back(); @@ -7628,11 +8115,11 @@ void verifyVertexInfoVersion19() { auto disabledBlend = merging; disabledBlend.context.pixel->targetMerges.at(0).blend = {false, true, 6u, 7u, 1u, 1u, 0u, 4u, 0xbu, {1u, 2u, 3u, 4u}}; require(serializer.Serialize(disabledBlend) == serializer.Serialize(merging), "a disabled color target blend changed the serialized request"); - // A version 43 request: no color target merge, and no fused arithmetic flag, pixel function LDS flag, storage size or barycentric built-ins flag at the end. + // A version 43 request: no color target merge, and no fused arithmetic flag, pixel function LDS flag, storage size, barycentric built-ins flag, host interpolation first flag or shader image minimum LOD flag at the end. const auto version43Bytes = [&](std::vector bytes) { require(bytes.end() - bytes.begin() > mergeOffset + static_cast(mergeBytes) && std::equal(plainPixel.begin() + mergeOffset, plainPixel.begin() + mergeOffset + static_cast(mergeBytes), bytes.begin() + mergeOffset), "a replay fixture does not hold the default color target merge after the sample order"); bytes.erase(bytes.begin() + mergeOffset, bytes.begin() + mergeOffset + static_cast(mergeBytes)); - bytes.resize(bytes.size() - sizeof(std::uint32_t) - 4u); + bytes.resize(bytes.size() - sizeof(std::uint32_t) - 6u); return bytes; }; auto version42 = version43Bytes(plainPixel); @@ -7794,9 +8281,11 @@ void verifyDepthRemapRequests() { const auto plainBytes = decodeBase64(plain); const auto remappedBytes = decodeBase64(remapped); constexpr std::size_t pointTailBytes = 13u; - require(plainBytes.size() == remappedBytes.size() && plainBytes.size() > 15u + pointTailBytes && std::equal(plainBytes.begin(), plainBytes.end() - 15 - pointTailBytes, remappedBytes.begin()) && std::equal(plainBytes.end() - 8 - pointTailBytes, plainBytes.end(), remappedBytes.end() - 8 - pointTailBytes), "the depth remap flag and mode are not the 6 bytes before the f32 flush and denormal preserve flags, the point tail, the fused arithmetic flag, the pixel function LDS flag, the pixel function storage size, the barycentric built-in flag and the tessellation evaluation subgroup flag of a vertex request"); + require(plainBytes.size() == remappedBytes.size() && plainBytes.size() > 17u + pointTailBytes && std::equal(plainBytes.begin(), plainBytes.end() - 17 - pointTailBytes, remappedBytes.begin()) && std::equal(plainBytes.end() - 10 - pointTailBytes, plainBytes.end(), remappedBytes.end() - 10 - pointTailBytes), "the depth remap flag and mode are not the 6 bytes before the f32 flush and denormal preserve flags, the point tail, the fused arithmetic flag, the pixel function LDS flag, the pixel function storage size, the barycentric built-in flag, the tessellation evaluation subgroup flag, the host interpolation first flag and the shader image minimum LOD flag of a vertex request"); for (const std::uint8_t legacyVersion : {32u, 34u}) { auto legacyBytes = remappedBytes; + legacyBytes.pop_back(); + legacyBytes.pop_back(); legacyBytes.resize(legacyBytes.size() - sizeof(std::uint32_t)); legacyBytes.pop_back(); legacyBytes.pop_back(); @@ -7810,6 +8299,8 @@ void verifyDepthRemapRequests() { require(serializer.Serialize(legacy.request) == plain, ("a version " + std::to_string(legacyVersion) + " vertex request misread the fields before the depth remap").c_str()); } auto version35 = remappedBytes; + version35.pop_back(); + version35.pop_back(); version35.resize(version35.size() - sizeof(std::uint32_t)); version35.pop_back(); version35.pop_back(); @@ -7863,8 +8354,8 @@ void verifyMergedHullConfiguration() { require(!serializer.Deserialize(separate).request.graphics->tessellation->mergedHull, "a separate hull configuration was read as merged"); constexpr std::size_t pointTailBytes = 13u; auto legacy = decodeBase64(merged); - require(legacy.size() > 56u + pointTailBytes && legacy[4] == 51u, "a merged hull request did not use serialization version 51"); - legacy.resize(legacy.size() - sizeof(std::uint32_t) - 3u - pointTailBytes); + require(legacy.size() > 58u + pointTailBytes && legacy[4] == 53u, "a merged hull request did not use serialization version 53"); + legacy.resize(legacy.size() - sizeof(std::uint32_t) - 5u - pointTailBytes); legacy.pop_back(); legacy.pop_back(); require(legacy[legacy.size() - 52u] == 1u, "the merged hull flag is not the last byte of the tessellation configuration"); @@ -8086,7 +8577,7 @@ void verifyPixelInputs() { } } -ShaderRecompiler::RecompileResult recompileSlots(std::initializer_list controls, std::span code) { +ShaderRecompiler::RecompileRequest slotRequest(std::initializer_list controls, std::span code, bool hostInterpolationFirst) { using namespace ShaderRecompiler; ShaderPixelStageInfo pixel{}; pixel.interpolatorCount = static_cast(controls.size()); @@ -8104,15 +8595,21 @@ ShaderRecompiler::RecompileResult recompileSlots(std::initializer_list capabilities{spv::CapabilityFloat64}; request.target.supportedCapabilities = capabilities; request.layout.pushConstantSizeBytes = 128; request.useCache = false; - return Recompile(request); + return request; } -std::vector> slotInputs(std::initializer_list controls, std::span code) { - const auto result = recompileSlots(controls, code); +ShaderRecompiler::RecompileResult recompileSlots(std::initializer_list controls, std::span code, bool hostInterpolationFirst = false) { + return ShaderRecompiler::Recompile(slotRequest(controls, code, hostInterpolationFirst)); +} + +std::vector> slotInputs(std::initializer_list controls, std::span code, bool hostInterpolationFirst = false) { + const auto result = recompileSlots(controls, code, hostInterpolationFirst); const auto& words = result.spirv.Words(); std::map locations; std::map perVertex; @@ -8158,6 +8655,43 @@ void verifyPixelParameterSlots() { expectFailure([] { static_cast(recompileSlots({0x423u, 0x3u}, shared)); }, "passes its vertices through unchanged", "an interpolated pass-through input was accepted"); } +void verifyHostInterpolationFirst() { + static constexpr std::array shared{0xc8100000u, 0xc8110001u, 0xc8140500u, 0xc8150501u, 0xf800180fu, 0x05040504u, 0xbf810000u}; + auto inputs = slotInputs({0x3u, 0x3u}, shared, true); + require(inputs.size() == 1u && inputs[0].first == 3u && !inputs[0].second, "host interpolation first still read a plain v_interp input per vertex"); + static constexpr std::array mixed{0xc8100000u, 0xc8110001u, 0xc8160402u, 0xc81a0802u, 0xc81e0f02u, 0xf800180fu, 0x07060504u, 0xbf810000u}; + inputs = slotInputs({0x0u, 0x400u, 0x22u, 0x320u}, mixed, true); + require(inputs.size() == 1u && inputs[0].first == 0u && inputs[0].second, "host interpolation first did not keep KHR barycentrics for a slot read flat and interpolated"); + require(!recompileSlots({0x0u, 0x400u, 0x22u, 0x320u}, mixed, true).barycentricEmulation.active, "host interpolation first emulated barycentrics the device provides"); + using namespace ShaderRecompiler; + const RequestSerializer serializer; + const auto replayFixture = [&](std::initializer_list controls, std::span code) { + std::vector keys[2]; + for (const bool hostFirst : {false, true}) { + auto request = slotRequest(controls, code, hostFirst); + const auto encoded = serializer.Serialize(request); + const auto replay = serializer.Deserialize(encoded); + require(replay.request.target.hostInterpolationFirst == hostFirst && serializer.Serialize(replay.request) == encoded, "replay lost host interpolation first"); + std::vector replayKey; + RecompileCacheKey::Build(request, keys[hostFirst]); + RecompileCacheKey::Build(replay.request, replayKey); + require(replayKey == keys[hostFirst], "replay changed the cache key of a host interpolation first request"); + const auto original = Recompile(request); + const auto replayed = Recompile(replay.request); + require(replayed.barycentricEmulation.active == original.barycentricEmulation.active, "replay changed the barycentric path of a host interpolation first request"); + verifyResult(original, replayed); + request.useCache = true; + const auto cached = Recompile(request); + const auto hit = Recompile(serializer.Deserialize(serializer.Serialize(request)).request); + require(hit.cacheHit && hit.variantId == cached.variantId, "host interpolation first replay did not reuse the original shader variant"); + verifyResult(cached, hit); + } + require(keys[0] != keys[1], "the cache key ignores host interpolation first"); + }; + replayFixture({0x3u, 0x3u}, shared); + replayFixture({0x0u, 0x400u, 0x22u, 0x320u}, mixed); +} + void verifyF16PixelParameterSlots() { static constexpr std::array high{0xd7420002u, 0x00020100u, 0xd75a0003u, 0x040a0300u, 0xf800180fu, 0x03030303u, 0xbf810000u}; static constexpr std::array low{0xd7420002u, 0x00020000u, 0xd75a0003u, 0x040a0200u, 0xf800180fu, 0x03030303u, 0xbf810000u}; @@ -10647,6 +11181,11 @@ int main(int argc, char** argv) { std::cout << "Direct samplers count toward the sampled pair limit beside selected image-table records\n"; return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--sampler-limits") { + verifySamplerLimits(); + std::cout << "Sampler limits passed\n"; + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--srt-read-patching") { verifyUniformVectorComponents(); verifyWaterfallRuntimeValues(); @@ -10670,16 +11209,22 @@ int main(int argc, char** argv) { verifyLayoutPair(); verifySignedSrtComparison(); verifyTypeZeroImageMaterialization(); + verifyUnsetFormatImageMaterialization(); verifyOutOfRangeConstantReads(); + verifyDescriptorWebLaneIndexFold(); + verifyDescriptorWebTexelFold(); + verifyDescriptorWebSelectOrder(); verifySignedMinimum(); verifyMayEvaluate(); verifyMayDecideSharedConditions(); verifyPureFlatSlots(); verifyPlanConditionPruning(); verifyPlanRetention(); + verifyPendingBranchLeftUnpruned(); verifyPlanRetentionChains(); verifyPlanCopyAnswers(); verifyGuardedDescriptorSources(); + verifyPendingBranchCapture(); verifyLaneMaskGuardedDescriptorSources(); verifyExecGuardedDescriptorSources(); verifySrtTape(); @@ -10720,6 +11265,7 @@ int main(int argc, char** argv) { verifyLegacyPixelRequests(); verifyPixelExportReplay(); verifyPixelParameterSlots(); + verifyHostInterpolationFirst(); verifyF16PixelParameterSlots(); verifyComputedTexelOffsets(); verifyShaderClockScopes(); diff --git a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp index 93251a5641..5c0440e4f8 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp @@ -220,6 +220,9 @@ void testEndOfPipeInterrupts() { check(sceAgcDriverAddEqEvent(eq, 0, &graphicsTag) == 0, "graphics event registration failed"); check(sceAgcDriverAddEqEvent(eq, 0x20, &computeTag) == 0, "compute event registration failed"); expectFailure([] { sceAgcDriverAddEqEvent(0, 0, nullptr); }); + expectFailure([&] { sceAgcDriverAddEqEvent(eq, -1, &graphicsTag); }); + expectFailure([] { sceAgcDriverDeleteEqEvent(0, 0); }); + expectFailure([&] { sceAgcDriverDeleteEqEvent(eq, -1); }); struct Output final : AgcDriver::IVideoOutput, AgcDriver::IFlipRequest, std::enable_shared_from_this { std::atomic ready{0}; std::shared_ptr Reserve(const AgcDriver::FlipInfo&) override { return shared_from_this(); } @@ -312,6 +315,7 @@ void testEndOfPipeInterrupts() { check(sceAgcDriverDeleteEqEvent(eq, 0x20) == 0, "compute event deletion failed"); owner.reset(); check(sceKernelDeleteEqueue(eq) == 0, "event queue deletion failed"); + expectFailure([&] { sceAgcDriverDeleteEqEvent(eq, 0); }); } std::array writeData(volatile std::uint32_t* address, std::uint32_t value) { diff --git a/core/libs/prx/libSceAgcDriver/tests/UnsupportedDiagnostics.cpp b/core/libs/prx/libSceAgcDriver/tests/UnsupportedDiagnostics.cpp index f1546a3e4b..d570e6c831 100644 --- a/core/libs/prx/libSceAgcDriver/tests/UnsupportedDiagnostics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/UnsupportedDiagnostics.cpp @@ -1,7 +1,8 @@ #include "prx/libSceAgcDriver/include/AgcDriver.hpp" #include "SceTypes.hpp" +#include "tests/ExportLookup.hpp" -int main() { +int main(int argc, char** argv) { if (sceAgcDriverGetCoredumpAddress() != nullptr) return 1; if (sceAgcDriverSetTargetRingForDiag() != static_cast(0x8A6D0001)) return 2; auto* volatile linked = &sceAgcDriverSetFlip; @@ -13,5 +14,35 @@ int main() { if (sceAgcDriverSubmitToRazorACQ() != static_cast(0x8A6D0001)) return 8; if (sceAgcDriverSuspendPointSubmitDirect() != static_cast(0x8A6D0001)) return 9; if (sceAgcDriverModuleRegistration() != static_cast(0x8A6D0003)) return 10; + struct Export { + int (APS5_VABI* function)(void); + const char* name; + }; + const Export exports[] = { + {sceAgcDriverCreateQueue, "sceAgcDriverCreateQueue"}, + {sceAgcDriverCwsrResumeAcq, "sceAgcDriverCwsrResumeAcq"}, + {sceAgcDriverCwsrSuspendAcq, "sceAgcDriverCwsrSuspendAcq"}, + {sceAgcDriverDebugHardwareStatus, "sceAgcDriverDebugHardwareStatus"}, + {sceAgcDriverDestroyQueue, "sceAgcDriverDestroyQueue"}, + {sceAgcDriverGetRegShadowInfo, "sceAgcDriverGetRegShadowInfo"}, + {sceAgcDriverGetRegShadowInfoAgr, "sceAgcDriverGetRegShadowInfoAgr"}, + {sceAgcDriverGetReservedDmemForAgc, "sceAgcDriverGetReservedDmemForAgc"}, + {sceAgcDriverGetWorkloadStreamInfo, "sceAgcDriverGetWorkloadStreamInfo"}, + {sceAgcDriverIDHSSubmit, "sceAgcDriverIDHSSubmit"}, + {sceAgcDriverNotifyDefaultStates, "sceAgcDriverNotifyDefaultStates"}, + {sceAgcDriverPassInfoDownward, "sceAgcDriverPassInfoDownward"}, + {sceAgcDriverPatchClearState, "sceAgcDriverPatchClearState"}, + {sceAgcDriverSetWorkloadComplete, "sceAgcDriverSetWorkloadComplete"}, + {sceAgcDriverSetWorkloadsActive, "sceAgcDriverSetWorkloadsActive"}, + {sceAgcDriverSetupAsyncGraphics, "sceAgcDriverSetupAsyncGraphics"}, + {sceAgcDriverSetupRegisterShadow, "sceAgcDriverSetupRegisterShadow"}, + {sceAgcDriverSubmitToHDRScopesACQ, "sceAgcDriverSubmitToHDRScopesACQ"}, + {sceAgcDriverSuspendPointSubmit, "sceAgcDriverSuspendPointSubmit"}, + {sceAgcDriverSysEnableSubmitDone45Exception, "sceAgcDriverSysEnableSubmitDone45Exception"}, + {sceAgcDriverSysGetClientNumber, "sceAgcDriverSysGetClientNumber"}, + {sceAgcDriverSysIsGameClosed, "sceAgcDriverSysIsGameClosed"}, + {sceAgcDriverTmpInitIdhs, "sceAgcDriverTmpInitIdhs"}, + }; + if (!ExportsResolveByName(argc > 1 ? argv[1] : nullptr, exports, "sceAgcDriverNotExported")) return 11; return 0; } diff --git a/core/libs/prx/libSceAgcDriver/tests/VertexInput.cpp b/core/libs/prx/libSceAgcDriver/tests/VertexInput.cpp index 48d26f71df..4da90d4dc9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/VertexInput.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/VertexInput.cpp @@ -58,6 +58,131 @@ template void fails(TAction&& fn) { Require(failed, "invalid vertex input was accepted"); } +std::vector randomInput(std::mt19937& random) { + std::vector input; + const auto size = random() % 9; + for (unsigned n = 0; n < size; ++n) { + auto a = attribute(n); + a.components = 1 + random() % 4; + a.fetchIndex = random() % 2; + a.resource.fields[0] += (random() % 4096) * 16; + a.resource.fields[1] = (random() % 33) * 16u << 16u; + a.resource.fields[2] = random(); + const std::array formats{13u, 22u, 36u, 50u, 56u, 64u, 71u, 74u, 77u}; + a.resource.fields[3] = formats[random() % formats.size()] << 12u; + input.push_back(a); + } + return input; +} + +void rememberedFormats(const Context& ctx) { + VertexBufferFormats formats; + auto remembered = ctx; + remembered.vertexFormats = &formats; + for (unsigned pass = 0; pass < 2; ++pass) { + std::mt19937 random(4099); + for (unsigned i = 0; i < 2000; ++i) { + const auto input = randomInput(random); + const auto expected = BuildVertexInputLayout(ctx, input); + const auto count = queries.load(); + same(BuildVertexInputLayout(remembered, input), expected); + Require(pass == 0 || queries == count, "a remembered vertex format was queried again"); + } + } + auto unsupported = attribute(); + unsupported.components = 1; + unsupported.resource.fields[3] = 5u << 12u; + fails([&] { BuildVertexInputLayout(ctx, std::span(&unsupported, 1)); }); + fails([&] { BuildVertexInputLayout(remembered, std::span(&unsupported, 1)); }); + const auto count = queries.load(); + fails([&] { BuildVertexInputLayout(remembered, std::span(&unsupported, 1)); }); + Require(queries == count, "a remembered unsupported vertex format was queried again"); + auto duplicate = std::array{attribute(3), attribute(3)}; + fails([&] { BuildVertexInputLayout(remembered, duplicate); }); +} + +std::array lacking{}; + +std::string outcome(const Context& ctx, const VertexAttribute& input) { + try { + const auto layout = BuildVertexInputLayout(ctx, std::span(&input, 1)); + const auto host = layout.attributes[0].format; + return std::string(host == DecodeVertexFormat(input).format ? "narrow " : "widened ") + std::to_string(host) + " " + std::to_string(layout.bindings[0].stride); + } catch (const std::runtime_error& error) { + return error.what(); + } +} + +std::vector everyFormat() { + std::vector inputs; + for (std::uint32_t code = 0; code < 128u; ++code) { + for (std::uint32_t components = 1; components <= 4u; ++components) { + for (std::uint32_t formatComponents = 0; formatComponents <= 4u; ++formatComponents) { + auto input = attribute(); + input.components = components; + input.formatComponents = formatComponents; + input.resource.fields[3] = code << 12u; + inputs.push_back(input); + } + } + } + for (std::uint32_t components = 1; components <= 4u; ++components) { + auto input = attribute(); + input.components = components; + input.resource.fields = {}; + inputs.push_back(input); + } + return inputs; +} + +void rememberedFormatSets() { + auto ctx = context(); + ctx.formatProperties = [](VkPhysicalDevice, VkFormat format, VkFormatProperties* properties) { + ++queries; + *properties = {}; + const auto index = static_cast(format); + if (index >= lacking.size() || !lacking[index]) properties->bufferFeatures = VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT; + }; + const auto inputs = everyFormat(); + std::mt19937 random(8191); + std::vector expected; + unsigned widened = 0; + unsigned refused = 0; + for (unsigned device = 0; device < 9; ++device) { + for (auto& format : lacking) format = random() % 8 < (device < 8 ? device : 4u); + expected.clear(); + for (const auto& input : inputs) { + expected.push_back(outcome(ctx, input)); + widened += expected.back().starts_with("widened"); + refused += expected.back().find("device does not support vertex format") != std::string::npos; + } + VertexBufferFormats formats; + auto remembered = ctx; + remembered.vertexFormats = &formats; + if (device < 8) { + for (unsigned pass = 0; pass < 2; ++pass) { + const auto count = queries.load(); + for (std::size_t i = 0; i < inputs.size(); ++i) Require(outcome(remembered, inputs[i]) == expected[i], "a remembered vertex format answered unlike the device"); + Require(pass == 0 || queries == count, "a remembered vertex format was queried again"); + } + continue; + } + std::atomic mismatches{0}; + std::barrier ready(8); + std::vector threads; + for (unsigned t = 0; t < 8; ++t) threads.emplace_back([&, t] { + ready.arrive_and_wait(); + for (std::size_t n = 0; n < inputs.size(); ++n) { + const auto i = (n + t * 331u) % inputs.size(); + if (outcome(remembered, inputs[i]) != expected[i]) ++mismatches; + } + }); + for (auto& thread : threads) thread.join(); + Require(mismatches == 0, "concurrent layout builds disagreed with the device's vertex formats"); + } + Require(widened != 0 && refused != 0, "the device format sets left vertex format widening or refusal untested"); +} + void descriptorDiagnostics(const Context& ctx) { auto input = attribute(7); input.resource.fields = {0x89abcde0u, 0x0010abcdu, 0x12345678u, 0x0104dfacu}; @@ -88,6 +213,8 @@ void descriptorDiagnostics(const Context& ctx) { void run() { auto ctx = context(); descriptorDiagnostics(ctx); + rememberedFormats(ctx); + rememberedFormatSets(); VertexInputCache cache(ctx, 2); std::array attributes{attribute(0), attribute(1)}; auto first = get(cache, ctx, 1, attributes); @@ -137,19 +264,7 @@ void run() { VertexInputCache generated(ctx, 16); std::mt19937 random(719); for (unsigned i = 0; i < 10000; ++i) { - std::vector input; - const auto size = random() % 9; - for (unsigned n = 0; n < size; ++n) { - auto a = attribute(n); - a.components = 1 + random() % 4; - a.fetchIndex = random() % 2; - a.resource.fields[0] += (random() % 4096) * 16; - a.resource.fields[1] = (random() % 33) * 16u << 16u; - a.resource.fields[2] = random(); - const std::array formats{13u, 22u, 36u, 50u, 56u, 64u, 71u, 74u, 77u}; - a.resource.fields[3] = formats[random() % formats.size()] << 12u; - input.push_back(a); - } + auto input = randomInput(random); const auto expected = BuildVertexInputLayout(ctx, input); same(*get(generated, ctx, 1 + i, input), expected); for (auto& a : input) a.resource.fields[0] += 256; diff --git a/core/libs/prx/libSceAgcDriver/tests/WaitShutdown.cpp b/core/libs/prx/libSceAgcDriver/tests/WaitShutdown.cpp index ee2a3fb221..dc04843754 100644 --- a/core/libs/prx/libSceAgcDriver/tests/WaitShutdown.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/WaitShutdown.cpp @@ -74,6 +74,11 @@ void CheckCommandBufferSubmission() { Require(sceAgcDriverSubmitMultiCommandBuffers(&graphicsCtx, cmdBufs, cmdSizes, 1) == 0, "graphics context multi submit failed"); Require(sceAgcDriverSubmitMultiCommandBuffers(&computeCtx, cmdBufs, cmdSizes, 1) == 0, "compute context multi submit failed"); + alignas(alignof(std::uint32_t)) char unalignedBuffer[sizeof(MockQueueContext) + 4]{}; + auto* unalignedCtx = reinterpret_cast(unalignedBuffer + 1); + Require(sceAgcDriverSubmitCommandBuffer(unalignedCtx, &emptyPacket) == 0, "unaligned context submit must succeed"); + Require(sceAgcDriverSubmitMultiCommandBuffers(unalignedCtx, cmdBufs, cmdSizes, 1) == 0, "unaligned context multi submit must succeed"); + bool submitRejected = false; try { sceAgcDriverSubmitCommandBuffer(&invalidCtx, &emptyPacket); diff --git a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp index 07c6129c27..2cfdd40fe7 100644 --- a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp @@ -1684,6 +1684,35 @@ void CheckCollectBoundaries() { } } +#ifdef _WIN32 +void CheckWidenedCollectBesideUncommittedPages() { + constexpr std::size_t reserved = std::size_t{1} << 30; + constexpr std::size_t committed = Block - 4 * 4096; + void* range = GuestArena::GuestArenaAllocate_nid_postfix(reserved, Block); + struct Cleanup { + void* range; + ~Cleanup() { + GuestArena::GuestArenaReset_nid_postfix(range, reserved); + GuestArena::GuestArenaRelease_nid_postfix(range, reserved); + } + } cleanup{range}; + void* memory = static_cast(range) + reserved - Block; + MEMORY_BASIC_INFORMATION tail{}; + Require(VirtualQuery(static_cast(memory) + committed, &tail, sizeof(tail)) == sizeof(tail) && tail.State == MEM_RESERVE, "the test block's tail is not a placeholder"); + GuestArena::GuestArenaCommit_nid_postfix(memory, committed, PAGE_READWRITE, committed); + Require(VirtualQuery(static_cast(memory) + committed, &tail, sizeof(tail)) == sizeof(tail) && tail.State == MEM_RESERVE, "committing the test block's head committed its tail"); + const auto base = reinterpret_cast(memory); + auto* bytes = static_cast(memory); + std::memset(memory, 17, committed); + BumpCollectEpoch(); + const auto before = CollectWrites(base + 4096, 64); + Require(before != 0, "a collect beside uncommitted pages of its block failed"); + bytes[4096 + 8] = 19; + BumpCollectEpoch(); + Require(CollectWrites(base + 4096, 64) > before && !UnchangedSince(base + 4096, 64, before), "a collect beside uncommitted pages missed a CPU write"); +} +#endif + void CheckImportWindow() { void* memory = AllocateWatched(3 * Block); const auto base = reinterpret_cast(memory); @@ -2276,7 +2305,45 @@ void CheckAdoptLatestEpoch() { Require(keptOwn, "adopting the newest collect epoch moved a thread off its own"); } -int main() { +void CheckGenerationWrap() { + constexpr std::uint64_t Wrap = std::uint64_t{1} << 32u; + constexpr std::size_t bytes = 192 * Block; + void* memory = AllocateWatched(bytes); + void* scratch = AllocateWatched(Block); + const auto base = reinterpret_cast(memory); + const auto other = reinterpret_cast(scratch); + std::memset(memory, 0x11, bytes); + std::memset(scratch, 0x11, Block); + BumpCollectEpoch(); + Require(CollectWrites(base, bytes) != 0 && CollectWrites(other, Block) != 0, "the wrap fixture was not collected"); + AdvanceTrackerGenerationForTest(Wrap - 4); + BumpCollectEpoch(); + const auto before = CollectWritesUncached(base, bytes); + Require(before >= Wrap - 4 && before < Wrap, "the stamp before the crossing was not taken just below 2^32"); + Require(UnchangedSince(base, bytes, before), "a range nothing wrote after its collect reports a change"); + for (int store = 0; store < 8; ++store) MarkWritten(other, 4); + Require(TrackerGeneration() > Wrap, "the tracker generation did not cross 2^32"); + const auto middle = base + bytes / 2; + const auto stored = MarkWritten(middle, 4); + Require(stored > Wrap && stored > before, "a driver store after 2^32 returned a stamp not newer than one taken before"); + Require(!UnchangedSince(base, bytes, before), "a driver store after 2^32 was reported unchanged over whole coarse runs"); + Require(!UnchangedSince(middle, Block, before), "a driver store after 2^32 was reported unchanged in its block"); + Require(WrittenSince(base, bytes, before) && StoredOver(middle, 4, before), "a driver store after 2^32 was not seen as written since the earlier stamp"); + Require(NewestStamp(middle, Block) > before, "the newest stamp after 2^32 is not newer than the earlier stamp"); + Require(!StoredAllSince_nid_no_patch(middle, 4, stored) && StoredAllSince_nid_no_patch(middle, 4, before), "the driver piece of a store after 2^32 lost its generation"); + Require(UnchangedSince(base, bytes / 2, before) && UnchangedSince(base, bytes, stored), "a store after 2^32 changed other blocks or a later stamp"); + static_cast(memory)[bytes - 1] = 0x22; + const auto last = base + bytes - Block; + Require(CollectWritesUncached(last, Block) > Wrap, "a collect after 2^32 returned a wrapped generation"); + Require(!UnchangedSince(last, Block, before) && !UnchangedSinceCollected(last, Block, before), "a CPU store after 2^32 was reported unchanged"); + std::array generations{before, before, before}; + std::array changed{}; + std::array cpu{}; + Require(ChangedBlocks(base + bytes - 3 * Block, 3 * Block, generations, changed, cpu), "the changed-block query over the wrap fixture failed"); + Require(changed[0] == BlockUnchanged && changed[2] == BlockWritten && cpu[0] == 0 && cpu[2] == 1, "per-block changes after 2^32 were misreported"); +} + +int main(int argc, char** argv) { try { #ifdef _WIN32 if (std::wstring_view(GetCommandLineW()).ends_with(MetadataCommitMode)) { @@ -2287,6 +2354,11 @@ int main() { std::cout << "no write watching: skipped\n"; return 77; } + if (argc == 2 && std::string_view(argv[1]) == "--generation-wrap") { + CheckGenerationWrap(); + std::cout << "generation wrap test passed\n"; + return 0; + } #ifdef _WIN32 if (std::wstring_view(GetCommandLineW()).ends_with(HostHoleMode)) { CheckHostHole(); @@ -2345,6 +2417,9 @@ int main() { #endif CheckVersionRanges(); CheckCollectBoundaries(); +#ifdef _WIN32 + CheckWidenedCollectBesideUncommittedPages(); +#endif CheckImportWindow(); CheckImportWindowZeroPages(); CheckImportWindowOtherThread(); diff --git a/core/libs/prx/libSceAgcDriver/tests/cmake/agc_driver_alpha_to_coverage_tests.cmake b/core/libs/prx/libSceAgcDriver/tests/cmake/agc_driver_alpha_to_coverage_tests.cmake index ac0bbbe423..d8914f68f9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/cmake/agc_driver_alpha_to_coverage_tests.cmake +++ b/core/libs/prx/libSceAgcDriver/tests/cmake/agc_driver_alpha_to_coverage_tests.cmake @@ -15,6 +15,8 @@ if(BUILD_TESTING) add_test(NAME ${testName} COMMAND agc_driver_alpha_to_coverage_tests --dcc-compared-import ${samples}) list(APPEND multisampledDccClearTests ${testName}) endforeach() - set_tests_properties(agc_driver_dithered_alpha_to_coverage agc_driver_two_sample_dithered_alpha_to_coverage PROPERTIES + add_test(NAME agc_driver_alpha_to_coverage_sample_mask_export COMMAND agc_driver_alpha_to_coverage_tests --sample-mask8) + add_test(NAME agc_driver_two_sample_alpha_to_coverage_sample_mask_export COMMAND agc_driver_alpha_to_coverage_tests --sample-mask2) + set_tests_properties(agc_driver_dithered_alpha_to_coverage agc_driver_two_sample_dithered_alpha_to_coverage agc_driver_alpha_to_coverage_sample_mask_export agc_driver_two_sample_alpha_to_coverage_sample_mask_export PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") endif() diff --git a/core/libs/prx/libSceAgcDriver/tests/cmake/agc_driver_depth_plane_partial_store_tests.cmake b/core/libs/prx/libSceAgcDriver/tests/cmake/agc_driver_depth_plane_partial_store_tests.cmake index 31e1d85526..aedf433478 100644 --- a/core/libs/prx/libSceAgcDriver/tests/cmake/agc_driver_depth_plane_partial_store_tests.cmake +++ b/core/libs/prx/libSceAgcDriver/tests/cmake/agc_driver_depth_plane_partial_store_tests.cmake @@ -21,5 +21,6 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() add_test(NAME agc_driver_depth_plane_partial_store_unimported COMMAND agc_driver_depth_plane_partial_store_tests) + add_test(NAME agc_driver_depth_plane_partial_store_image_copy COMMAND agc_driver_depth_plane_partial_store_tests --image-copy) add_test(NAME agc_driver_depth_plane_partial_store_buffer_copy COMMAND agc_driver_depth_plane_partial_store_tests --buffer-copy) endif() diff --git a/core/libs/prx/libSceAgcDriver/tests/cmake/agc_driver_depth_storage_tests.cmake b/core/libs/prx/libSceAgcDriver/tests/cmake/agc_driver_depth_storage_tests.cmake new file mode 100644 index 0000000000..e5f7e8312a --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/cmake/agc_driver_depth_storage_tests.cmake @@ -0,0 +1,13 @@ + +if(BUILD_TESTING) + add_test(NAME agc_driver_depth_storage_copies COMMAND agc_driver_depth_storage_tests) + add_test(NAME agc_driver_depth_storage_restricted_depth COMMAND agc_driver_depth_storage_tests --restricted-depth) + set_tests_properties(agc_driver_depth_storage_copies PROPERTIES + ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_DEPTH_TRANSFER_PASS=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + set_tests_properties(agc_driver_depth_storage_restricted_depth PROPERTIES + ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(agc_driver_depth_storage_copies agc_driver_depth_storage_restricted_depth PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceAgcDriver/tests/cmake/agc_driver_draw_offset_indices_tests.cmake b/core/libs/prx/libSceAgcDriver/tests/cmake/agc_driver_draw_offset_indices_tests.cmake new file mode 100644 index 0000000000..da26ba76db --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/cmake/agc_driver_draw_offset_indices_tests.cmake @@ -0,0 +1,19 @@ +if(BUILD_TESTING) + foreach(runtimeFetchIndex IN ITEMS 65535 65536 65537) + foreach(runtimeFetchPath IN ITEMS indexed compact-restart) + set(runtimeFetchArguments runtime-fetch ${runtimeFetchIndex}) + if(runtimeFetchPath STREQUAL "compact-restart") + list(APPEND runtimeFetchArguments compact-restart) + endif() + set(runtimeFetchTest agc_driver_runtime_fetch_${runtimeFetchPath}_${runtimeFetchIndex}) + add_test(NAME ${runtimeFetchTest} COMMAND agc_driver_draw_offset_indices_tests ${runtimeFetchArguments}) + set_tests_properties(${runtimeFetchTest} PROPERTIES + ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" TIMEOUT 45 SKIP_RETURN_CODE 77 + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(${runtimeFetchTest} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() + endforeach() +endif() diff --git a/core/libs/prx/libSceAgcDriver/tests/cmake/agc_driver_gds_dma_fill_tests.cmake b/core/libs/prx/libSceAgcDriver/tests/cmake/agc_driver_gds_dma_fill_tests.cmake new file mode 100644 index 0000000000..7a6d61800b --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/cmake/agc_driver_gds_dma_fill_tests.cmake @@ -0,0 +1,8 @@ +if(BUILD_TESTING) + add_test(NAME agc_driver_gds_dma_fill_private COMMAND agc_driver_gds_dma_fill_tests) + set_tests_properties(agc_driver_gds_dma_fill_private PROPERTIES TIMEOUT 30 ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_SHARED_GDS=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(agc_driver_gds_dma_fill_private PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceAgcDriver/tests/cmake/agc_driver_image_sample_lod_clamp_tests.cmake b/core/libs/prx/libSceAgcDriver/tests/cmake/agc_driver_image_sample_lod_clamp_tests.cmake new file mode 100644 index 0000000000..4cf0a20695 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/cmake/agc_driver_image_sample_lod_clamp_tests.cmake @@ -0,0 +1,8 @@ +if(BUILD_TESTING) + add_test(NAME agc_driver_image_sample_lod_clamp_shader_lane COMMAND agc_driver_image_sample_lod_clamp_tests --shader-lane) + set_tests_properties(agc_driver_image_sample_lod_clamp_shader_lane PROPERTIES ENVIRONMENT "APS5_NO_IMAGE_VIEW_MIN_LOD=1;APS5_NO_GPL=1;ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 60) + if(WIN32) + set_tests_properties(agc_driver_image_sample_lod_clamp_shader_lane PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceAgcDriver/tests/cmake/agc_driver_native_multisample_depth_tests.cmake b/core/libs/prx/libSceAgcDriver/tests/cmake/agc_driver_native_multisample_depth_tests.cmake index aa54fc5196..2e8dd64629 100644 --- a/core/libs/prx/libSceAgcDriver/tests/cmake/agc_driver_native_multisample_depth_tests.cmake +++ b/core/libs/prx/libSceAgcDriver/tests/cmake/agc_driver_native_multisample_depth_tests.cmake @@ -12,10 +12,11 @@ if(BUILD_TESTING) add_test(NAME agc_driver_native_depth_expand_gate COMMAND agc_driver_native_multisample_depth_tests --expand-gate) add_test(NAME agc_driver_native_depth_stencil_view COMMAND agc_driver_native_multisample_depth_tests --stencil-view) add_test(NAME agc_driver_native_depth_buffer_clear COMMAND agc_driver_native_multisample_depth_tests --buffer-clear) - set_tests_properties(agc_driver_native_depth_clears agc_driver_native_depth_clear_refusals agc_driver_native_depth_stencil agc_driver_native_depth_separate_stencil agc_driver_native_depth_aliased_stencil agc_driver_native_depth_expand_gate agc_driver_native_depth_stencil_view agc_driver_native_depth_buffer_clear PROPERTIES TIMEOUT 30 ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + add_test(NAME agc_driver_native_depth_uniform_planes COMMAND agc_driver_native_multisample_depth_tests --uniform-planes) + set_tests_properties(agc_driver_native_depth_clears agc_driver_native_depth_clear_refusals agc_driver_native_depth_stencil agc_driver_native_depth_separate_stencil agc_driver_native_depth_aliased_stencil agc_driver_native_depth_expand_gate agc_driver_native_depth_stencil_view agc_driver_native_depth_buffer_clear agc_driver_native_depth_uniform_planes 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_depth_retirement PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") - set_tests_properties(agc_driver_native_depth_clears agc_driver_native_depth_clear_refusals agc_driver_native_depth_stencil agc_driver_native_depth_separate_stencil agc_driver_native_depth_aliased_stencil agc_driver_native_depth_expand_gate agc_driver_native_depth_stencil_view agc_driver_native_depth_buffer_clear PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(agc_driver_native_depth_clears agc_driver_native_depth_clear_refusals agc_driver_native_depth_stencil agc_driver_native_depth_separate_stencil agc_driver_native_depth_aliased_stencil agc_driver_native_depth_expand_gate agc_driver_native_depth_stencil_view agc_driver_native_depth_buffer_clear agc_driver_native_depth_uniform_planes PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test(NAME agc_driver_native_depth_driver_path COMMAND agc_driver_native_multisample_depth_tests --driver-path) add_test(NAME agc_driver_native_depth_driver_queued_fill COMMAND agc_driver_native_multisample_depth_tests --driver-queued-fill) diff --git a/core/libs/prx/libSceAgcDriver/tests/cmake/agc_driver_oracle_runner_tests.cmake b/core/libs/prx/libSceAgcDriver/tests/cmake/agc_driver_oracle_runner_tests.cmake new file mode 100644 index 0000000000..e583cf5791 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/cmake/agc_driver_oracle_runner_tests.cmake @@ -0,0 +1 @@ +target_link_libraries(agc_driver_oracle_runner_tests PRIVATE agc_oracle_support) diff --git a/core/libs/prx/libSceAgcDriver/tests/cmake/agc_driver_recorder_tests.cmake b/core/libs/prx/libSceAgcDriver/tests/cmake/agc_driver_recorder_tests.cmake index 1a60ec98e7..d1b60a1eb9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/cmake/agc_driver_recorder_tests.cmake +++ b/core/libs/prx/libSceAgcDriver/tests/cmake/agc_driver_recorder_tests.cmake @@ -79,6 +79,12 @@ if(BUILD_TESTING) set_tests_properties(agc_pipeline_cache agc_pipeline_cache_file PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test(NAME agc_detile_small_elements COMMAND agc_driver_recorder_tests --small-element-detile-only) + set_tests_properties(agc_detile_small_elements PROPERTIES SKIP_RETURN_CODE 77) + if(WIN32) + set_tests_properties(agc_detile_small_elements PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test(NAME agc_import_watch COMMAND agc_driver_recorder_tests --import-watch-only) set_tests_properties(agc_import_watch PROPERTIES SKIP_RETURN_CODE 77) add_test(NAME agc_import_settle_initial_dcc COMMAND agc_driver_recorder_tests --settle-initial-dcc-only) @@ -116,6 +122,18 @@ if(BUILD_TESTING) set_tests_properties(agc_unchanged_cpu_stamps PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test(NAME agc_storage_rewritten_blocks COMMAND agc_driver_recorder_tests --rewritten-blocks-only) + set_tests_properties(agc_storage_rewritten_blocks PROPERTIES SKIP_RETURN_CODE 77) + if(WIN32) + set_tests_properties(agc_storage_rewritten_blocks PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test(NAME agc_storage_rewritten_stale_units COMMAND agc_driver_recorder_tests --rewritten-stale-units-only) + set_tests_properties(agc_storage_rewritten_stale_units PROPERTIES SKIP_RETURN_CODE 77) + if(WIN32) + set_tests_properties(agc_storage_rewritten_stale_units PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() foreach(mode IN ITEMS map ring shared uncached) add_test(NAME agc_collect_mapping_history_${mode} COMMAND agc_driver_recorder_tests --collect-mapping-history-only) set_tests_properties(agc_collect_mapping_history_${mode} PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) @@ -171,6 +189,12 @@ if(BUILD_TESTING) set_tests_properties(agc_storage_idle_writeback_race agc_storage_kept_unit_settle PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test(NAME agc_storage_pending_key_refresh COMMAND agc_driver_recorder_tests --pending-key-refresh-only) + set_tests_properties(agc_storage_pending_key_refresh PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_storage_pending_key_refresh PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test(NAME agc_storage_pending_after_unwatch COMMAND agc_driver_recorder_tests --pending-after-unwatch-only) set_tests_properties(agc_storage_pending_after_unwatch PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) if(WIN32) @@ -288,6 +312,12 @@ if(BUILD_TESTING) set_tests_properties(agc_copied_direct_ordering PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test(NAME agc_copied_write_back COMMAND agc_driver_recorder_tests --copied-write-back-only) + set_tests_properties(agc_copied_write_back PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_copied_write_back PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test(NAME agc_storage_writeback_blocks COMMAND agc_driver_recorder_tests --writeback-blocks-only) set_tests_properties(agc_storage_writeback_blocks PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30 ENVIRONMENT "APS5_PIN_WAIT_MS=3000") if(WIN32) @@ -332,6 +362,8 @@ if(BUILD_TESTING) endif() add_test(NAME agc_sparse_run_import_copied COMMAND agc_driver_recorder_tests --sparse-run-copied-only) set_tests_properties(agc_sparse_run_import_copied PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) + add_test(NAME agc_changed_copy COMMAND agc_driver_recorder_tests --changed-copy-only) + set_tests_properties(agc_changed_copy PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) add_test(NAME agc_unit_shadow COMMAND agc_driver_recorder_tests --unit-shadow-only) set_tests_properties(agc_unit_shadow PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30 ENVIRONMENT "APS5_PIN_WAIT_MS=3000") add_test(NAME agc_idle_publish_wait COMMAND agc_driver_recorder_tests --idle-publish-wait-only) @@ -389,7 +421,7 @@ if(BUILD_TESTING) add_test(NAME agc_metadata_pass COMMAND agc_driver_recorder_tests --metadata-pass-only) set_tests_properties(agc_metadata_pass PROPERTIES SKIP_RETURN_CODE 77) if(WIN32) - set_tests_properties(agc_recorder_read_list agc_host_import_unmap agc_parked_import_unmap agc_window_import_unmap agc_host_import_retirement agc_sparse_run_import agc_sparse_run_import_copied agc_unit_shadow agc_driver_recorder agc_idle_publish_wait agc_recorder_command_buffer_release agc_descriptor_pool_recycling agc_draw_snapshot_cap agc_draw_snapshot_epoch agc_multisample_guest_access agc_repeat_storage_guard agc_draw_inputs_in_place agc_draw_inputs_copied agc_resident_present agc_slice_dirty agc_queued_write_groups agc_metadata_pass agc_tiled_resolve PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_recorder_read_list agc_host_import_unmap agc_parked_import_unmap agc_window_import_unmap agc_host_import_retirement agc_sparse_run_import agc_sparse_run_import_copied agc_unit_shadow agc_driver_recorder agc_idle_publish_wait agc_recorder_command_buffer_release agc_descriptor_pool_recycling agc_draw_snapshot_cap agc_draw_snapshot_epoch agc_multisample_guest_access agc_repeat_storage_guard agc_draw_inputs_in_place agc_draw_inputs_copied agc_resident_present agc_slice_dirty agc_queued_write_groups agc_metadata_pass agc_tiled_resolve agc_changed_copy PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") add_test(NAME agc_host_import_second_mapping COMMAND agc_driver_recorder_tests --host-import-second-mapping-only) set_tests_properties(agc_host_import_second_mapping PROPERTIES SKIP_RETURN_CODE 77 ENVIRONMENT_MODIFICATION diff --git a/core/libs/prx/libSceAgcDriver/tests/cmake/agc_driver_sampled_cache_budget_tests.cmake b/core/libs/prx/libSceAgcDriver/tests/cmake/agc_driver_sampled_cache_budget_tests.cmake index 454037a68c..ed0f578b29 100644 --- a/core/libs/prx/libSceAgcDriver/tests/cmake/agc_driver_sampled_cache_budget_tests.cmake +++ b/core/libs/prx/libSceAgcDriver/tests/cmake/agc_driver_sampled_cache_budget_tests.cmake @@ -19,6 +19,14 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endforeach() + add_test(NAME agc_driver_sampled_cache_budget_report_age COMMAND agc_driver_sampled_cache_budget_tests budget-report-age) + set_tests_properties(agc_driver_sampled_cache_budget_report_age PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_TEXTURE_CACHE_MIB=" SKIP_RETURN_CODE 77 TIMEOUT 30 + PASS_REGULAR_EXPRESSION "active storage image cache budget 4096 MiB.*storage_limit_age_ms=[1-9][0-9]*" + FAIL_REGULAR_EXPRESSION "FAIL:|VUID-") + if(WIN32) + set_tests_properties(agc_driver_sampled_cache_budget_report_age PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() set_property(TEST agc_driver_recover_srgb_allocation APPEND PROPERTY ENVIRONMENT_MODIFICATION "APS5_SRGB_SHADER_DECODE=set:copy") add_test(NAME agc_driver_resource_cache_storage_admission COMMAND agc_driver_sampled_cache_budget_tests resource-admission) add_test(NAME agc_driver_resource_cache_oversized_storage_admission COMMAND agc_driver_sampled_cache_budget_tests resource-oversized) diff --git a/core/libs/prx/libSceAgcDriver/tests/cmake/agc_driver_unsupported_diagnostics_tests.cmake b/core/libs/prx/libSceAgcDriver/tests/cmake/agc_driver_unsupported_diagnostics_tests.cmake index a9c3c84ac0..b2b8a800d8 100644 --- a/core/libs/prx/libSceAgcDriver/tests/cmake/agc_driver_unsupported_diagnostics_tests.cmake +++ b/core/libs/prx/libSceAgcDriver/tests/cmake/agc_driver_unsupported_diagnostics_tests.cmake @@ -3,7 +3,7 @@ if(BUILD_TESTING) 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) + 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:$") diff --git a/core/libs/prx/libSceAgcDriver/tests/cmake/agc_oracle_code_object_tests.cmake b/core/libs/prx/libSceAgcDriver/tests/cmake/agc_oracle_code_object_tests.cmake new file mode 100644 index 0000000000..f17785d12a --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/cmake/agc_oracle_code_object_tests.cmake @@ -0,0 +1,18 @@ +if(BUILD_TESTING) + add_test_executable(agc_oracle_code_object_tests tests/OracleCodeObject.cpp tools/OracleCodeObject.cpp) + target_include_directories(agc_oracle_code_object_tests PRIVATE + $ + ${CMAKE_SOURCE_DIR}/core/shader/recompiler + ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR} + ${ANYPS5_SPIRV_HEADERS_INCLUDE_DIR} + ) + target_link_libraries(agc_oracle_code_object_tests PRIVATE libc) + set_target_properties(agc_oracle_code_object_tests PROPERTIES CXX_EXTENSIONS OFF) + configure_windows_unwind(agc_oracle_code_object_tests) + add_test(NAME agc_oracle_code_object COMMAND agc_oracle_code_object_tests) + set_tests_properties(agc_oracle_code_object PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_oracle_code_object PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceAgcDriver/tests/cmake/agc_shader_memory_tests.cmake b/core/libs/prx/libSceAgcDriver/tests/cmake/agc_shader_memory_tests.cmake index 8fa9ac4eb0..e86ed31bbe 100644 --- a/core/libs/prx/libSceAgcDriver/tests/cmake/agc_shader_memory_tests.cmake +++ b/core/libs/prx/libSceAgcDriver/tests/cmake/agc_shader_memory_tests.cmake @@ -3,8 +3,11 @@ add_test_executable(agc_shader_memory_tests EXCLUDE_FROM_ALL Execution/src/ShaderMemory.cpp Execution/src/GuestMemory.cpp Execution/src/PerformanceTraceThread.cpp + Graphics/src/GuestTextureResource.cpp + Graphics/src/TextureTiling.cpp + Graphics/src/TextureFormat.cpp ) -target_include_directories(agc_shader_memory_tests PRIVATE ${LIBS_INCLUDE_DIR}) +target_include_directories(agc_shader_memory_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) target_link_libraries(agc_shader_memory_tests PRIVATE shader_recompiler_agc_driver libc) set_target_properties(agc_shader_memory_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_shader_memory_tests) @@ -18,7 +21,8 @@ if(BUILD_TESTING) add_test(NAME agc_shader_selected_records_pair_limit COMMAND agc_shader_memory_tests --selected-records-pair-limit) add_test(NAME agc_shader_selected_records_direct_image_limit COMMAND agc_shader_memory_tests --selected-records-direct-image-limit) add_test(NAME agc_shader_selected_records_direct_pair_limit COMMAND agc_shader_memory_tests --selected-records-direct-pair-limit) - set_tests_properties(agc_shader_selected_records_image_limit agc_shader_selected_records_pair_limit agc_shader_selected_records_direct_image_limit agc_shader_selected_records_direct_pair_limit PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + add_test(NAME agc_shader_sampler_limits COMMAND agc_shader_memory_tests --sampler-limits) + set_tests_properties(agc_shader_selected_records_image_limit agc_shader_selected_records_pair_limit agc_shader_selected_records_direct_image_limit agc_shader_selected_records_direct_pair_limit agc_shader_sampler_limits PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") add_test(NAME agc_shader_srt_read_patching COMMAND agc_shader_memory_tests --srt-read-patching) set_tests_properties(agc_shader_srt_read_patching PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") add_test(NAME agc_shader_descriptor_phis COMMAND agc_shader_memory_tests --descriptor-phis) @@ -66,7 +70,7 @@ if(BUILD_TESTING) add_test(NAME agc_shader_entry_lane_proof_budget COMMAND agc_shader_memory_tests --entry-lane-proof-budget) set_tests_properties(agc_shader_entry_lane_proof_budget PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(WIN32) - set_tests_properties(agc_shader_bindless agc_shader_selected_records_image_limit agc_shader_selected_records_pair_limit agc_shader_selected_records_direct_image_limit agc_shader_selected_records_direct_pair_limit agc_shader_srt_read_patching agc_shader_type_zero_image_materialization agc_shader_frontend_pair agc_shader_frontend_pair_off agc_shader_layout_pair agc_shader_layout_pair_off agc_shader_result_memo_budget agc_shader_result_memo_budget_threads agc_shader_result_memo_budget_masked agc_shader_entry_lane_proof_budget PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(agc_shader_bindless agc_shader_selected_records_image_limit agc_shader_selected_records_pair_limit agc_shader_selected_records_direct_image_limit agc_shader_selected_records_direct_pair_limit agc_shader_srt_read_patching agc_shader_type_zero_image_materialization agc_shader_frontend_pair agc_shader_frontend_pair_off agc_shader_layout_pair agc_shader_layout_pair_off agc_shader_result_memo_budget agc_shader_result_memo_budget_threads agc_shader_result_memo_budget_masked agc_shader_entry_lane_proof_budget agc_shader_sampler_limits PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() agc_add_default_test(agc_shader_memory_tests) endif() diff --git a/core/libs/prx/libSceAgcDriver/tests/cmake/agc_write_tracking_tests.cmake b/core/libs/prx/libSceAgcDriver/tests/cmake/agc_write_tracking_tests.cmake index c2a9e93d4e..6a37c12100 100644 --- a/core/libs/prx/libSceAgcDriver/tests/cmake/agc_write_tracking_tests.cmake +++ b/core/libs/prx/libSceAgcDriver/tests/cmake/agc_write_tracking_tests.cmake @@ -12,6 +12,8 @@ if(BUILD_TESTING) set_target_properties(agc_write_tracking_tests PROPERTIES EXCLUDE_FROM_ALL OFF) add_test(NAME agc_write_tracking COMMAND agc_write_tracking_tests) set_tests_properties(agc_write_tracking PROPERTIES SKIP_RETURN_CODE 77) + add_test(NAME agc_write_tracking_generation_wrap COMMAND agc_write_tracking_tests --generation-wrap) + set_tests_properties(agc_write_tracking_generation_wrap PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 20) if(WIN32) add_test(NAME agc_write_tracking_metadata_commit COMMAND agc_write_tracking_tests --metadata-commit) set_tests_properties(agc_write_tracking_metadata_commit PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) @@ -27,7 +29,7 @@ if(BUILD_TESTING) set_tests_properties(agc_compared_shared_generations PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 20) add_test(NAME agc_write_tracking_rewatch_import_pages COMMAND agc_write_tracking_tests --rewatch-import-pages) set_tests_properties(agc_write_tracking_rewatch_import_pages PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 20) - set_tests_properties(agc_write_tracking agc_write_tracking_metadata_commit agc_write_tracking_cpu_write_pin agc_resident_direct_remap agc_rewatched_direct_shared agc_import_alias_shared agc_compared_shared_generations agc_write_tracking_rewatch_import_pages PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_write_tracking agc_write_tracking_generation_wrap agc_write_tracking_metadata_commit agc_write_tracking_cpu_write_pin agc_resident_direct_remap agc_rewatched_direct_shared agc_import_alias_shared agc_compared_shared_generations agc_write_tracking_rewatch_import_pages PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp index 126c4ba22f..cd982b67d3 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp @@ -522,6 +522,62 @@ constexpr std::array TwoSampleDitherCases{{ {0x00007e00u, 4u, {0x00u, 0x00u, 0x00u, 0x00u}}, }}; +void SampleMaskExportCoverage(AgcDriver::VulkanDevice& device, std::uint32_t samples, std::span cases) { + const ColorTargetLayout layout(Width, Height, ColorTileMode::RenderTarget, 8); + const auto all = (1u << samples) - 1u; + std::vector undithered; + for (const std::uint32_t bits : {0x3e800000u, 0x3f000000u, 0x3f400000u}) { + const auto mask = std::bit_cast(bits) < 0.5f ? 0u : all; + undithered.push_back({bits, Float32Export, {mask, mask, mask, mask}}); + } + std::vector> blocks; + for (const bool dithered : {true, false}) { + for (const auto& test : dithered ? cases : std::span(undithered)) { + for (const std::uint32_t exported : {0x00000001u, 0xaaaaaaaau, 0xffffff0fu}) { + blocks.push_back(std::make_unique(layout.Bytes() * samples)); + auto& source = *blocks.back(); + std::memset(source.data, 0, source.bytes); + const auto multisampled = DecodeTarget(source, samples); + for (std::uint32_t sample = 0; sample < samples; ++sample) { + const Color reference{std::ldexp(1.0f, static_cast(sample) - static_cast(samples - 1u)), 0, 0, 1}; + Draw(device, multisampled, false, Float32Export, Program(reference, Float32Export, 1u << sample), false, true); + } + const auto alpha = test.format == Float32Export ? std::bit_cast(test.bits) : 0.0f; + const Color color{0, 0, 0, alpha}; + const auto half = test.format == Float16Export ? std::optional(static_cast(test.bits)) : std::nullopt; + const auto code = Program(color, test.format, exported, half); + if (dithered) { + Draw(device, multisampled, true, test.format, code, true, true, static_cast(samples)); + } else { + Draw(device, multisampled, true, test.format, code, false, true); + } + blocks.push_back(std::make_unique(layout.Bytes())); + auto& resolved = *blocks.back(); + std::memset(resolved.data, 0, resolved.bytes); + AgcDriver::Graphics::ColorMetadataPass pass{}; + pass.mode = AgcDriver::Graphics::ColorMetadataPass::Mode::Resolve; + pass.source = multisampled; + pass.targets = {DecodeTarget(resolved)}; + device.ColorMetadataPass(pass); + const auto stored = ReadBack(device, resolved); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto mask = test.masks[2u * (x & 1u) + (y & 1u)] & exported & all; + const auto expected = x < ScissorWidth ? static_cast(all - mask) / static_cast((1u << (samples - 1u)) * samples) : 0.0f; + const auto actual = FromHalf(stored[layout.Offset(x, y) / 2u]); + if (actual == expected) continue; + char detail[224]; + std::snprintf(detail, sizeof(detail), "%s coverage export %u alpha 0x%08x with exported mask 0x%08x pixel (%u,%u): resolved %.9g, expected %.9g for mask 0x%02x", dithered ? "dithered" : "undithered", test.format, test.bits, exported, x, y, actual, expected, mask); + throw std::runtime_error(detail); + } + } + programCount = 0; + } + } + } + AgcDriver::Graphics::ClearMultisampleTargets(device.Device()); +} + void DccSrgbClearTests(AgcDriver::VulkanDevice& device, std::uint32_t samples) { const ColorTargetLayout layout(Width, Height, ColorTileMode::RenderTarget, 4); const ColorTargetLayout allocation(Width, Height, ColorTileMode::RenderTarget, 8); @@ -1275,6 +1331,18 @@ int main(int argc, char** argv) { std::puts("dithered eight-sample alpha-to-coverage tests passed"); return 0; } + if (argc == 2 && (std::string_view(argv[1]) == "--sample-mask8" || std::string_view(argv[1]) == "--sample-mask2")) { + const bool eight = std::string_view(argv[1]) == "--sample-mask8"; + try { + SampleMaskExportCoverage(*device, eight ? 8u : 2u, eight ? std::span(EightSampleDitherCases) : std::span(TwoSampleDitherCases)); + } catch (const std::runtime_error& error) { + if (std::string_view(error.what()) != "AGC graphics: the device cannot render color with the target's sample count") throw; + std::printf("skipped, the device lacks %s-sample color attachments: %s\n", eight ? "eight" : "two", error.what()); + return VulkanTestSkipped; + } + std::printf("%s-sample alpha-to-coverage with sample-mask export tests passed\n", eight ? "eight" : "two"); + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--dithered2") { try { DitheredCoverage(*device, 2, TwoSampleDitherCases); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/CalibratedClock.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/CalibratedClock.cpp new file mode 100644 index 0000000000..77caebd08c --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/CalibratedClock.cpp @@ -0,0 +1,57 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Context.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; + +void EnableTraceTimestamps() { +#ifdef _WIN32 + _putenv_s("APS5_TRACE_SECONDS", "100000:1"); +#else + setenv("APS5_TRACE_SECONDS", "100000:1", 1); +#endif +} + +double Milliseconds(std::chrono::steady_clock::duration duration) { + return std::chrono::duration(duration).count(); +} + +} + +int main() { + try { + EnableTraceTimestamps(); + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + double firstGpuNs = 0.0; + std::chrono::steady_clock::time_point firstCpu; + if (!device->CalibrateClock(firstGpuNs, firstCpu)) { + std::cout << "skipped, the device calibrates its timestamps against no host clock the driver reads\n"; + return VulkanTestSkipped; + } + const double offsetMs = std::abs(Milliseconds(std::chrono::steady_clock::now() - firstCpu)); + Require(offsetMs < 50.0, "the calibrated GPU timestamp maps to a CPU time " + std::to_string(offsetMs) + " ms away from now: the host clock read is not the one the driver calibrated against"); + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + double secondGpuNs = 0.0; + std::chrono::steady_clock::time_point secondCpu; + Require(device->CalibrateClock(secondGpuNs, secondCpu), "a second calibration failed after the first succeeded"); + const double cpuMs = Milliseconds(secondCpu - firstCpu); + const double gpuMs = (secondGpuNs - firstGpuNs) / 1e6; + Require(cpuMs >= 150.0 && std::abs(gpuMs - cpuMs) < 2.0, "200 ms apart, the two calibrations differ by " + std::to_string(gpuMs) + " ms on the GPU clock and " + std::to_string(cpuMs) + " ms on the CPU clock"); + std::cout << "calibrated clock tests passed (offset " << offsetMs << " ms, GPU " << gpuMs << " ms against CPU " << cpuMs << " ms)\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DccKeyPresync.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DccKeyPresync.cpp index c39c8fa542..bb09efdd14 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DccKeyPresync.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DccKeyPresync.cpp @@ -4,7 +4,10 @@ #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" #include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" #include "Recompiler.hpp" #include "VulkanTestDevice.hpp" #ifdef _WIN32 @@ -34,7 +37,9 @@ using ShaderRecompiler::ShaderStage; constexpr std::uint32_t Threads = 32; constexpr std::uint32_t Side = 128; +constexpr std::uint32_t Format16Unorm = 7; constexpr std::uint32_t Format8888Unorm = 56; +constexpr std::uint32_t Format565Unorm = 133; constexpr std::uint32_t TileRenderTarget64KB = 0x1b; constexpr std::uint32_t Type2D = 9; constexpr std::size_t SurfaceBytes = 65536; @@ -68,9 +73,11 @@ public: #endif Require(block != nullptr, "DCC key pre-sync: cannot allocate the guest block"); GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + if (AgcDriver::GuestMemory::WriteWatched()) GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, BlockBytes); } ~GuestBlock() { + if (AgcDriver::GuestMemory::WriteWatched()) GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(block, BlockBytes); GuestAllocations::Mutation().Remove(block); #ifdef _WIN32 VirtualFree(block, 0, MEM_RELEASE); @@ -88,17 +95,17 @@ private: std::uint8_t* block = nullptr; }; -std::array TextureDescriptor(const std::uint8_t* texels, const std::uint8_t* keys) { +std::array TextureDescriptor(const std::uint8_t* texels, const std::uint8_t* keys, std::uint32_t format, bool dccAlphaOnMsb = false) { const auto address = AddressOf(texels); const auto meta = AddressOf(keys) >> 8u; return { static_cast(address >> 8u), - static_cast((address >> 40u) & 0xffu) | (Format8888Unorm << 20u) | (((Side - 1u) & 3u) << 30u), + static_cast((address >> 40u) & 0xffu) | (format << 20u) | (((Side - 1u) & 3u) << 30u), ((Side - 1u) >> 2u) | ((Side - 1u) << 14u), 0xfacu | (TileRenderTarget64KB << 20u) | (Type2D << 28u), 0u, 0u, - (1u << 20u) | (1u << 21u) | (static_cast(meta & 0xffu) << 24u), + (1u << 20u) | (1u << 21u) | (dccAlphaOnMsb ? (1u << 22u) : 0u) | (static_cast(meta & 0xffu) << 24u), static_cast(meta >> 8u), }; } @@ -130,14 +137,14 @@ struct Scenario { std::vector sampleData; }; -Scenario Prepare(std::uint8_t* block, std::uint8_t* outputBlock) { +Scenario Prepare(std::uint8_t* block, std::uint8_t* outputBlock, std::uint32_t format = Format8888Unorm, bool dccAlphaOnMsb = false) { Scenario scenario{block, block + KeysOffset, block + SourceKeysOffset, reinterpret_cast(outputBlock), std::vector(16, 0u), std::vector(24, 0u)}; const auto source = BufferDescriptor(scenario.source, KeyBytes); const auto destination = BufferDescriptor(scenario.keys, KeyBytes); std::copy(source.begin(), source.end(), scenario.copyData.begin()); std::copy(destination.begin(), destination.end(), scenario.copyData.begin() + 4); const auto results = BufferDescriptor(scenario.output, Threads * 16u); - const auto texture = TextureDescriptor(scenario.texels, scenario.keys); + const auto texture = TextureDescriptor(scenario.texels, scenario.keys, format, dccAlphaOnMsb); std::copy(results.begin(), results.end(), scenario.sampleData.begin()); std::copy(texture.begin(), texture.end(), scenario.sampleData.begin() + 4); std::copy(PointSampler.begin(), PointSampler.end(), scenario.sampleData.begin() + 12); @@ -301,6 +308,7 @@ void RunOneKeyReadPerLookup(AgcDriver::VulkanDevice& device, const Scenario& sce void RunNoWriter(AgcDriver::VulkanDevice& device, const Scenario& scenario) { const auto sample = Compile(device, SampleSecondTexelCode, scenario.sampleData); + const auto proofs = AgcDriver::Graphics::SampledLookupKeyProofs(); const auto before = AgcDriver::VulkanDevice::PresyncCounts(); Require(!AgcDriver::Graphics::DccKeyWritersPending(AddressOf(scenario.keys), KeyBytes), "the idle device still reports a pending key writer"); auto prepared = device.PrepareDispatch(sample, {}, Threads, reinterpret_cast(SampleSecondTexelCode.data())); @@ -310,6 +318,50 @@ void RunNoWriter(AgcDriver::VulkanDevice& device, const Scenario& scenario) { std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); device.Dispatch(sample, 1, 1, 1, {}, reinterpret_cast(SampleSecondTexelCode.data()), std::move(prepared)); device.WaitIdle(); + if (!AgcDriver::GuestMemory::Watched(AddressOf(scenario.keys), KeyBytes)) { + std::puts("skipped, the sampled keys are outside the write watch: a proof cannot survive the lookup"); + return; + } + Require(AgcDriver::Graphics::SampledLookupKeyProofs() > proofs, "a repeated lookup with nothing written rescanned its sampled DCC keys"); +} + +void RunView(AgcDriver::VulkanDevice& device, const Scenario& scenario) { + const auto sample = Compile(device, SampleSecondTexelCode, scenario.sampleData); + auto prepared = device.PrepareDispatch(sample, {}, Threads, reinterpret_cast(SampleSecondTexelCode.data())); + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + device.Dispatch(sample, 1, 1, 1, {}, reinterpret_cast(SampleSecondTexelCode.data()), std::move(prepared)); + device.WaitIdle(); +} + +std::uint64_t GuestBytes(const Scenario& scenario) { + const std::span texture(scenario.sampleData.data() + 4, 8); + return AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(texture)).guestBytes; +} + +void RunTwoFormatViews(AgcDriver::VulkanDevice& device, const Scenario& clear, const Scenario& stored) { + if (!AgcDriver::GuestMemory::Watched(AddressOf(clear.keys), KeyBytes)) { + std::puts("skipped, the two-format keys are outside the write watch: a format-keyed proof cannot be observed"); + return; + } + Require(AddressOf(clear.texels) == AddressOf(stored.texels) && AddressOf(clear.keys) == AddressOf(stored.keys), "the two sampled views do not share one DCC range"); + Require(GuestBytes(clear) == GuestBytes(stored), "the two sampled views do not cover the same surface bytes"); + std::array probe{}; + Require(AgcDriver::Graphics::FillDccClear(AgcDriver::Graphics::ResolveTextureFormat(Format16Unorm), AgcDriver::Graphics::DccKeys::Clear0001, false, probe), "R16_UNORM cannot encode the clear used by the two-format test"); + Require(!AgcDriver::Graphics::FillDccClear(AgcDriver::Graphics::ResolveTextureFormat(Format565Unorm), AgcDriver::Graphics::DccKeys::Clear0001, false, probe), "R5G6B5 unexpectedly encodes the clear used by the two-format test"); + + Reset(clear); + std::memset(clear.keys, 0x40, KeyBytes); + AgcDriver::GuestMemory::BumpCollectEpoch(); + const auto before = AgcDriver::Graphics::KeyProofCounts(); + RunView(device, clear); + const auto primed = AgcDriver::Graphics::KeyProofCounts(); + Require(primed.scanned > before.scanned && primed.unstable == before.unstable, "the first format did not leave a stable key proof"); + std::fill(stored.output, stored.output + Threads * 4u, 0u); + RunView(device, stored); + const auto scanned = AgcDriver::Graphics::KeyProofCounts(); + Require(scanned.proved == primed.proved && scanned.scanned > primed.scanned, "a second view of one DCC range reused the first format's key proof"); + const bool any = std::any_of(stored.output, stored.output + Threads * 4u, [](std::uint32_t value) { return value != 0u; }); + Require(any, "a second view of one DCC range with another format did not read its stored texels"); } } @@ -344,6 +396,12 @@ int main() { RunOneKeyReadPerLookup(*device, reads, "a dispatch's first lookup"); RunOneKeyReadPerLookup(*device, reads, "a dispatch's repeated lookup"); RunKnownKeys(*device, fills); + GuestBlock twoFormats; + GuestBlock clearOutput; + GuestBlock storedOutput; + const auto clearView = Prepare(twoFormats.Data(), clearOutput.Data(), Format16Unorm); + const auto storedView = Prepare(twoFormats.Data(), storedOutput.Data(), Format565Unorm); + RunTwoFormatViews(*device, clearView, storedView); RunMixedWordKeys(*device, fills); RunWordFillKeyBytes(*device, fills); std::puts("DCC key pre-sync tests passed"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlanePartialStore.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlanePartialStore.cpp index e0f079cace..12d2c8cb44 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlanePartialStore.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlanePartialStore.cpp @@ -128,8 +128,11 @@ std::vector> ReadStart(AgcDriver::Vulkan int main(int argc, char** argv) { try { const bool bufferCopy = argc > 1 && std::string(argv[1]) == "--buffer-copy"; + const bool imageCopy = argc > 1 && std::string(argv[1]) == "--image-copy"; + if (bufferCopy || imageCopy) AgcDriver::Graphics::SetDepthTransferPasses(false); if (bufferCopy) AgcDriver::Graphics::SetDirectDepthCopy(false); const bool forcedOff = bufferCopy || std::getenv("APS5_NO_DIRECT_DEPTH_COPY") != nullptr; + const bool passesOff = bufferCopy || imageCopy || std::getenv("APS5_NO_DEPTH_TRANSFER_PASS") != nullptr; const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; Require(AgcDriver::Graphics::DepthSliceBytes(PlaneExtent, 4u) == PlaneBytes, "the 384x200 depth plane is not six 64 KiB tiles"); @@ -330,11 +333,15 @@ int main(int argc, char** argv) { stored = Since(before); Require(stored.stores == 1 && stored.units == Units, "a lookup of every layer of an array did not store the depth plane over its second layer"); for (std::size_t unit = 0; unit < Units; ++unit) RequireUnit(layerPlane, unit, 0.25f, Texels(unit), "a lookup of every layer of an array, the depth plane over its second layer"); + const auto passes = AgcDriver::Graphics::DepthTransferPassCount(); + Require(!passesOff || passes == 0, "depth plane transfers ran in shader passes with the passes forced off"); + Require(passesOff || !device->graphicsContext().pushDescriptors || passes > 0, "the device enables VK_KHR_push_descriptor but no depth plane transfer ran in shader passes"); + if (!passesOff && passes == 0) std::puts("depth transfer passes not exercised: the device lacks VK_KHR_push_descriptor"); const auto direct = AgcDriver::Graphics::DirectDepthCopyCount(); Require(!forcedOff || direct == 0, "depth plane copies went directly with the buffer path forced"); - Require(forcedOff || !device->graphicsContext().maintenance8 || direct > 0, "the device enables VK_KHR_maintenance8 but no depth plane copy went directly"); - if (!forcedOff && direct == 0) std::puts("direct depth copies not exercised: the device lacks VK_KHR_maintenance8 or no storage format pairs with the depth plane"); - std::printf("depth plane partial store tests passed (%s, %llu direct depth copies)\n", forcedOff ? "buffer path" : "default path", static_cast(direct)); + Require(forcedOff || !passesOff || !device->graphicsContext().maintenance8 || direct > 0, "the device enables VK_KHR_maintenance8 but no depth plane copy went directly"); + if (!forcedOff && passesOff && direct == 0) std::puts("direct depth copies not exercised: the device lacks VK_KHR_maintenance8 or no storage format pairs with the depth plane"); + std::printf("depth plane partial store tests passed (%s, %llu shader passes, %llu direct depth copies)\n", forcedOff ? "buffer path" : passesOff ? "image copies" : "default path", static_cast(passes), static_cast(direct)); return 0; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp index 0c706f407d..03af252573 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp @@ -35,7 +35,7 @@ constexpr std::uint32_t Format8Uint = 5; class Device { public: - Device() { + explicit Device(bool unrestrictedDepth) { #ifdef _WIN32 library = SDL_LoadObject("vulkan-1.dll"); #elif defined(__APPLE__) @@ -93,6 +93,14 @@ public: enabled.shaderInt64 = VK_TRUE; address.pNext = &bytes; std::vector extensionsEnabled{VK_KHR_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME, VK_KHR_8BIT_STORAGE_EXTENSION_NAME}; + if (hasExtension(VK_KHR_PUSH_DESCRIPTOR_EXTENSION_NAME) && (families[family].queueFlags & VK_QUEUE_GRAPHICS_BIT) != 0) { + extensionsEnabled.push_back(VK_KHR_PUSH_DESCRIPTOR_EXTENSION_NAME); + context.pushDescriptors = true; + } + if (unrestrictedDepth && hasExtension(VK_EXT_DEPTH_RANGE_UNRESTRICTED_EXTENSION_NAME)) { + extensionsEnabled.push_back(VK_EXT_DEPTH_RANGE_UNRESTRICTED_EXTENSION_NAME); + context.depthRangeUnrestricted = true; + } if (hasExtension(VK_EXT_EXTERNAL_MEMORY_HOST_EXTENSION_NAME)) { extensionsEnabled.push_back(VK_EXT_EXTERNAL_MEMORY_HOST_EXTENSION_NAME); VkPhysicalDeviceExternalMemoryHostPropertiesEXT hostProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTERNAL_MEMORY_HOST_PROPERTIES_EXT}; @@ -336,13 +344,79 @@ void RunPairings(Context context, std::uint8_t* block) { ExpectFailure([&] { static_cast(DepthSurfaceStoreBack(context, *depth)); }, "through a view that another view of the plane", "a store back through a replaced view was taken by an older pairing that covers the plane"); } +constexpr std::array RoundTripWords{0x3e800000u, 0x3f800000u, 0x00000000u, 0x80000000u, 0xbf000000u, 0x40000000u, 0x00000001u, 0x007fffffu, 0x807fffffu, 0x7f7fffffu, 0x7f800000u, 0xff800000u, 0x7fc00000u, 0x7fc12345u, 0x7f800001u, 0xffc00000u}; + +std::vector RoundTrip(Context context, std::uint8_t* block, VkFormat format) { + const auto address = reinterpret_cast(block); + const auto submit = [&](Recorder& recorder) { + recorder.Submit(); + Check(context.Function("vkQueueWaitIdle")(context.queue), "vkQueueWaitIdle"); + }; + Recorder recorder(context); + recorder.Activate(); + constexpr std::uint32_t sentinel = 0x12345678u; + for (std::size_t texel = 0; texel < Side * Side; ++texel) std::memcpy(block + texel * 4, &RoundTripWords[texel % RoundTripWords.size()], 4); + const DepthTarget target{address, format == VK_FORMAT_D32_SFLOAT_S8_UINT ? address + BlockBytes / 2 : 0, {Side, Side}, format, 0.25f, 7}; + Require(DepthSurfaceView(context, target) != VK_NULL_HANDLE, "the round trip's depth surface was not created"); + const auto stored = StorageTexture::Create(context, *context.detiler, PlaneResource(address, Format32Float), 0); + submit(recorder); + Require(DepthSurfaceStoreBack(context, *stored), "the round trip's storage image was not stored into the depth surface"); + submit(recorder); + for (std::size_t texel = 0; texel < Side * Side; ++texel) std::memcpy(block + texel * 4, &sentinel, 4); + auto layered = PlaneResource(address, Format32Float); + layered.dimension = TextureDimension::k2DArray; + const auto read = DepthSurfaceStorage(context, {}, layered, 0); + Require(read != nullptr, "the round trip's depth plane has no storage image"); + submit(recorder); + read->MarkDirty(); + read->WriteBack(); + submit(recorder); + std::vector words(Side * Side); + std::memcpy(words.data(), block, words.size() * 4); + ClearDepthSurfaces(context.device); + return words; } -int main() { +void RunRoundTrips(Context context, std::uint8_t* block) { + const bool passesOff = std::getenv("APS5_NO_DEPTH_TRANSFER_PASS") != nullptr; + for (const auto format : {VK_FORMAT_D32_SFLOAT, VK_FORMAT_D32_SFLOAT_S8_UINT}) { + SetDepthTransferPasses(false); + const auto copied = RoundTrip(context, block, format); + SetDepthTransferPasses(true); + const auto before = DepthTransferPassCount(); + const auto passed = RoundTrip(context, block, format); + Require(passesOff || !context.pushDescriptors || DepthTransferPassCount() > before, "the round trip did not read the depth plane in the shader pass"); + Require(passed == copied, "the shader pass read the depth plane into the storage image differently from the copy"); + const auto kept = [&](std::uint32_t word) { + for (std::size_t texel = 0; texel < copied.size(); ++texel) { + if (RoundTripWords[texel % RoundTripWords.size()] == word && copied[texel] != word) return false; + } + return true; + }; + for (const auto word : {0x3e800000u, 0x3f800000u, 0x00000000u}) Require(kept(word), "a depth value in [0, 1] did not come back from the depth plane"); + if (context.depthRangeUnrestricted) { + for (const auto word : {0xbf000000u, 0x40000000u}) Require(kept(word), "-0.5 or 2.0 did not come back from a depth plane with the depth range unrestricted"); + } + std::string changed; + for (const auto word : RoundTripWords) { + if (kept(word)) continue; + char text[16]; + std::snprintf(text, sizeof(text), " 0x%08x", word); + changed += text; + } + std::printf("depth round trip, vk format %d, depth range %s: the shader pass matches the copy; words the copy changed:%s\n", static_cast(format), context.depthRangeUnrestricted ? "unrestricted" : "[0, 1]", changed.empty() ? " none" : changed.c_str()); + } + SetDepthTransferPasses(true); +} + +} + +int main(int argc, char** argv) { try { + const bool restrictedDepth = argc > 1 && std::string(argv[1]) == "--restricted-depth"; std::unique_ptr device; try { - device = std::make_unique(); + device = std::make_unique(!restrictedDepth); } catch (const std::exception& error) { if (std::getenv("ANYPS5_REQUIRE_VULKAN") != nullptr) throw; std::printf("skipped, no usable Vulkan device: %s\n", error.what()); @@ -359,7 +433,10 @@ int main() { ClearDepthSurfaces(context.device); RunPairings(context, block); ClearDepthSurfaces(context.device); + RunRoundTrips(context, block); } + const bool passesOff = std::getenv("APS5_NO_DEPTH_TRANSFER_PASS") != nullptr; + Require(passesOff ? DepthTransferPassCount() == 0 : !context.pushDescriptors || DepthTransferPassCount() > 0, passesOff ? "a depth plane transfer ran in shader passes with APS5_NO_DEPTH_TRANSFER_PASS set" : "the device enables VK_KHR_push_descriptor but no depth plane transfer ran in shader passes"); GuestAllocations::Mutation().Remove(block); std::cout << "depth surface storage access tests passed\n"; return 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthSurfaceFromStorage.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthSurfaceFromStorage.cpp index 8c7267c2ba..5893c70a25 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthSurfaceFromStorage.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthSurfaceFromStorage.cpp @@ -51,7 +51,7 @@ constexpr std::uint32_t TileDepth64KB = 0x18; constexpr std::uint32_t Type2D = 9; constexpr std::uint32_t Type2DArray = 13; constexpr std::size_t PlaneBytes = 65536; -constexpr std::size_t BlockBytes = 11 * PlaneBytes; +constexpr std::size_t BlockBytes = 15 * PlaneBytes; constexpr std::uint32_t RowPasses = 15; alignas(256) constexpr std::array StoreRowCode{ @@ -241,6 +241,20 @@ void RetireStoresDepthCase(AgcDriver::VulkanDevice& device, const DepthFastClear Require(half == stored.level0.size() / sizeof(float), "the retire left " + std::to_string(stored.level0.size() / sizeof(float) - half) + " of the drawn depth plane's texels without the depth drawn on the GPU"); } +void RetiredSurfaceCase(AgcDriver::VulkanDevice& device, const DepthFastClearHarness::Shaders& shaders, std::uint8_t* plane, std::uint8_t* metadata, bool clearFirst, const std::string& what) { + Fill(plane, VK_FORMAT_D32_SFLOAT); + std::memset(metadata, 0xff, DepthFastClearHarness::HtileBytes); + const auto htile = Address(metadata); + static_cast(DepthFastClearHarness::Draw(device, shaders, Plane(plane, VK_FORMAT_D32_SFLOAT, htile), {})); + AgcDriver::Graphics::RetireDepthSurfaces(AgcDriver::VulkanDeviceTestAccess::ContextOf(device).device, Address(plane), PlaneBytes); + Require(!AgcDriver::Graphics::DepthSurfaceAt(Address(plane)), what + ": the depth surface was not retired"); + if (clearFirst) AgcDriver::Graphics::NoteHtileDepthClear(htile, DepthFastClearHarness::HtileBytes); + StoreRow(device, plane, VK_FORMAT_D32_SFLOAT, 1); + Require(AgcDriver::Graphics::StorageTexture::FindPending(Address(plane), PlaneBytes) != nullptr, what + ": the stored row is not pending in a storage image"); + if (!clearFirst) AgcDriver::Graphics::NoteHtileDepthClear(htile, DepthFastClearHarness::HtileBytes); + RequireCount(DepthFastClearHarness::Draw(device, shaders, Plane(plane, VK_FORMAT_D32_SFLOAT, htile), {}), clearFirst ? RowPasses : Covered, what); +} + } int main() { @@ -270,6 +284,8 @@ int main() { RequireCount(DepthFastClearHarness::Draw(*device, shaders, depthStencil, {.depthTest = false, .stencilTest = true, .stencil = equalZero}), Covered, "the stencil plane of a new D32S8 depth surface that took a storage write, tested EQUAL to its clear value 0"); PendingChainCase(*device, shaders, block.Plane(8)); RetireStoresDepthCase(*device, shaders, block.Plane(9), block.Plane(10)); + RetiredSurfaceCase(*device, shaders, block.Plane(11), block.Plane(12), true, "a retired D32 surface with HTILE, a depth clear noted on it, then a storage write over its whole plane, bound again"); + RetiredSurfaceCase(*device, shaders, block.Plane(13), block.Plane(14), false, "a retired D32 surface with HTILE, a storage write over its whole plane, then a depth clear noted on it, bound again"); if (!failures.empty()) { std::string text; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DescriptorWebFaults.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DescriptorWebFaults.cpp index dc0524d6a8..abb28b0b68 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DescriptorWebFaults.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DescriptorWebFaults.cpp @@ -82,6 +82,25 @@ alignas(256) constexpr std::array TableNoLaneWebCode{ 0xbf810000u, }; +alignas(256) constexpr std::array LaneIndexWebCode{ + 0xf4000400u, 0xfa000050u, 0xf4080500u, 0xfa000020u, 0xf4080600u, 0xfa000040u, 0xf40c0200u, 0xfa000000u, + 0xbf8cc07fu, 0xbf068010u, 0xbf850005u, 0xbf068110u, 0xbf85000cu, 0xf40c0200u, 0xfa000060u, 0xbf8cc07fu, + 0x7e040208u, 0x7e060209u, 0x7e08020au, 0x7e0a020bu, 0x7e0c020cu, 0x7e0e020du, 0x7e10020eu, 0x7e12020fu, + 0xbf820009u, 0x7e040208u, 0x7e060209u, 0x7e08020au, 0x7e0a020bu, 0x7e0c020cu, 0x7e0e020du, 0x7e10020eu, + 0xd7650009u, 0x000100c1u, 0x7e100502u, 0x7e120503u, 0x7e140504u, 0x7e160505u, 0x7e180506u, 0x7e1a0507u, + 0x7e1c0508u, 0x7e1e0509u, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf810000u, +}; + +alignas(256) constexpr std::array NoLaneLaneIndexWebCode{ + 0xf4000400u, 0xfa000050u, 0xf4080500u, 0xfa000020u, 0xf4080600u, 0xfa000040u, 0xf40c0200u, 0xfa000000u, + 0xbf8cc07fu, 0xbf068010u, 0xbf850005u, 0xbf068110u, 0xbf85000cu, 0xf40c0200u, 0xfa000060u, 0xbf8cc07fu, + 0x7e040208u, 0x7e060209u, 0x7e08020au, 0x7e0a020bu, 0x7e0c020cu, 0x7e0e020du, 0x7e10020eu, 0x7e12020fu, + 0xbf820009u, 0x7e040208u, 0x7e060209u, 0x7e08020au, 0x7e0a020bu, 0x7e0c020cu, 0x7e0e020du, 0x7e10020eu, + 0xd7650009u, 0x000100c1u, 0x7e100502u, 0x7e120503u, 0x7e140504u, 0x7e160505u, 0x7e180506u, 0x7e1a0507u, + 0x7e1c0508u, 0x7e1e0509u, 0xbefe0380u, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, + 0xbf810000u, +}; + class StderrCapture { public: StderrCapture() { @@ -246,6 +265,16 @@ void RunTests(AgcDriver::VulkanDevice& device) { Require(Faulted(tableSelected.log, TableSamplePc), "descriptor web faults: with an image table and no poisoned record, a lane selecting an undefined arm did not fault at the read:\n" + tableSelected.log); const auto tableNoLanes = Run(device, TableNoLaneWebCode, UndefinedFormat, SecondSelector, true); Require(Clean(tableNoLanes.log) && tableNoLanes.word == Untouched, "descriptor web faults: with an image table, an undefined arm faulted with no lane enabled:\n" + tableNoLanes.log); + + constexpr std::uint32_t LaneIndexSamplePc = 0xa8u; + const auto laneValid = Run(device, LaneIndexWebCode, Format8888UNorm, ValidSelector); + Require(Clean(laneValid.log) && Reads(laneValid.word, FirstTexel), "descriptor web faults: with a lane-index path, a lane selecting the first arm faulted or did not read its texture:\n" + laneValid.log); + const auto laneOther = Run(device, LaneIndexWebCode, Format8888UNorm, SecondSelector); + Require(Clean(laneOther.log) && Reads(laneOther.word, SecondTexel), "descriptor web faults: with a lane-index path, a lane selecting the second arm faulted or did not read its texture:\n" + laneOther.log); + const auto laneFolded = Run(device, LaneIndexWebCode, Format8888UNorm, FoldedSelector); + Require(Faulted(laneFolded.log, LaneIndexSamplePc), "descriptor web faults: a lane on the lane-index path did not fault at the read:\n" + laneFolded.log); + const auto laneFoldedNoLanes = Run(device, NoLaneLaneIndexWebCode, Format8888UNorm, FoldedSelector); + Require(Clean(laneFoldedNoLanes.log) && laneFoldedNoLanes.word == Untouched, "descriptor web faults: the lane-index path faulted or wrote with no lane enabled:\n" + laneFoldedNoLanes.log); } } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DrawOffsetIndices.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DrawOffsetIndices.cpp index 9413952247..34895d5545 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DrawOffsetIndices.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DrawOffsetIndices.cpp @@ -50,6 +50,18 @@ alignas(256) constexpr std::array VertexCode{ 0xbf810000, }; +alignas(256) constexpr auto RuntimeFetchCode = [] { + std::array code{}; + std::copy_n(VertexCode.begin(), 23, code.begin()); + const std::array stores{ + 0xe0c01000, 0x80020c04, 0xe0c01004, 0x80020d04, + 0xe0c01008, 0x80020e04, 0xe0c0100c, 0x80020f04 + }; + std::copy(stores.begin(), stores.end(), code.begin() + 23); + std::copy(VertexCode.begin() + 25, VertexCode.end(), code.begin() + 31); + return code; +}(); + alignas(256) constexpr std::array PostAddCode{ 0x7e020280, 0x7e040280, 0xd70f7d05, 0x00020a10, @@ -281,7 +293,7 @@ constexpr std::uint32_t Width = 64; constexpr std::uint32_t Height = 32; alignas(256) std::array Pixels{}; alignas(256) std::array Output{}; -alignas(256) std::array VertexSlots{}; +alignas(256) std::array VertexSlots{}; alignas(256) std::array InstanceSlots{}; alignas(256) std::array BufferTable{}; alignas(256) constexpr std::array Indices{2, 0, 1, 0}; @@ -327,7 +339,7 @@ public: return userData; } - ShaderRecompiler::RecompileResult Vertex(std::span code, const std::vector& userData) const { + ShaderRecompiler::RecompileResult Vertex(std::span code, const std::vector& userData, bool runtimeFetch = false) const { ShaderRecompiler::ShaderVertexStageInfo info{}; info.resourcesNum = 2; info.resources[0].fields = {BufferTable[0], BufferTable[1], BufferTable[2], BufferTable[3]}; @@ -336,7 +348,7 @@ public: info.resourcesDst[1] = {11, 1, 1, 1}; info.fetchAttribReg = 6; info.fetchBufferReg = 4; - info.fetchEmbedded = true; + info.fetchEmbedded = !runtimeFetch; const std::array memory{{ {reinterpret_cast(code.data()), std::as_bytes(code)}, {reinterpret_cast(BufferTable.data()), std::as_bytes(std::span(BufferTable))} @@ -349,7 +361,7 @@ public: }; request.useCache = false; auto result = ShaderRecompiler::Recompile(request); - Require(result.vertexOffsetSgpr == static_cast(BaseVertexSgpr) && result.instanceOffsetSgpr == static_cast(StartInstanceSgpr), "the fetch adds of s16 and s17 were not folded into the draw (vertex " + std::to_string(result.vertexOffsetSgpr) + ", instance " + std::to_string(result.instanceOffsetSgpr) + ")"); + if (!runtimeFetch) Require(result.vertexOffsetSgpr == static_cast(BaseVertexSgpr) && result.instanceOffsetSgpr == static_cast(StartInstanceSgpr), "the fetch adds of s16 and s17 were not folded into the draw (vertex " + std::to_string(result.vertexOffsetSgpr) + ", instance " + std::to_string(result.instanceOffsetSgpr) + ")"); return result; } @@ -371,7 +383,7 @@ public: return state; } - void Draw(const ShaderRecompiler::RecompileResult& vertex, const AgcDriver::Pm4::DrawParameters& draw) { + void Draw(const ShaderRecompiler::RecompileResult& vertex, const AgcDriver::Pm4::DrawParameters& draw, bool compactRestart = false) { const auto vertexPush = static_cast(vertex.pushConstants.size()); ShaderRecompiler::ShaderPixelStageInfo pixelInfo{}; pixelInfo.wave32 = true; @@ -382,7 +394,15 @@ public: const auto pixel = ShaderRecompiler::Recompile(pixelRequest); const std::array shaders{{{ShaderStage::Vertex, &vertex, 0u}, {ShaderStage::Fragment, &pixel, PixelPushOffset(vertexPush, device->Target())}}}; Output.fill(Unwritten); - device->Draw(State(), draw, shaders); + if (compactRestart) { + auto context = device->graphicsContext(); + context.primitiveListRestart = false; + auto state = State(); + state.primitiveRestart = true; + AgcDriver::Graphics::Draw(context, state, draw, shaders); + } else { + device->Draw(State(), draw, shaders); + } device->WaitIdle(); } @@ -480,6 +500,25 @@ std::string Analysis(Fixture& fixture) { return failures; } +std::string RuntimeFetch(Fixture& fixture, std::uint32_t index, bool compactRestart) { + const auto userData = fixture.UserData(0, 0); + const auto vertex = fixture.Vertex(RuntimeFetchCode, userData, true); + Require(vertex.vertexAttributes.empty(), "runtime fetch must not become fixed vertex attributes"); + VertexSlots.fill(0); + InstanceSlots.fill(0); + alignas(256) static std::array indices{}; + indices = {0xffffffffu, index, index, index, 0xffffffffu}; + const auto first = compactRestart ? 0u : 1u; + const auto count = compactRestart ? 5u : 3u; + const AgcDriver::Pm4::DrawParameters draw{reinterpret_cast(indices.data() + first), count, 4, 1, 0, true, 0, 0}; + fixture.Draw(vertex, draw, compactRestart); + std::cout << "runtime fetch, index " << index << ", compact restart " << compactRestart + << ", attributes " << vertex.vertexAttributes.size() << ": atomic readback " + << Output[0] << ' ' << Output[1] << ' ' << Output[2] << ' ' << Output[3] << '\n'; + const std::array expected{index, 0, index, 0}; + return std::equal(expected.begin(), expected.end(), Output.begin()) ? "" : "runtime-fetched VertexIndex was changed\n"; +} + std::string Run(Fixture& fixture) { std::string failures = Analysis(fixture); const std::array autoIndices{0, 1, 2}; @@ -587,12 +626,22 @@ std::string Run(Fixture& fixture) { } -int main() { +int main(int argc, char** argv) { try { std::shared_ptr device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; Fixture fixture(device); - const auto failures = Run(fixture); + std::string failures; + if (argc == 1) failures = Run(fixture); + else { + Require(argc == 3 || argc == 4, "expected runtime-fetch INDEX [compact-restart]"); + Require(std::string(argv[1]) == "runtime-fetch", "unknown case"); + const auto index = std::stoul(argv[2]); + Require(index < VertexSlots.size(), "index exceeds the runtime input buffer"); + const bool compactRestart = argc == 4; + if (compactRestart) Require(std::string(argv[3]) == "compact-restart", "unknown index path"); + failures = RuntimeFetch(fixture, static_cast(index), compactRestart); + } if (!failures.empty()) { std::cerr << "vertex and instance indices differ from the guest's:\n" << failures; return 1; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/F32ArithmeticNan.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/F32ArithmeticNan.cpp index 939ecca409..2ff72ba842 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/F32ArithmeticNan.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/F32ArithmeticNan.cpp @@ -185,7 +185,7 @@ bool IsNan16(std::uint32_t value) { return (value & 0x7fffu) > 0x7c00u; } -void Run(AgcDriver::VulkanDevice& device, const std::optional& floatMode) { +void Run(AgcDriver::VulkanDevice& device, const std::optional& floatMode, std::span code = Code) { for (std::uint32_t tid = 0; tid < Threads; ++tid) Fill(tid, &Input[tid * Inputs]); Output.fill(0xdeadbeefu); std::vector userData(8, 0u); @@ -193,7 +193,6 @@ void Run(AgcDriver::VulkanDevice& device, const std::optional(Output.size() * 4u)); std::copy(input.begin(), input.end(), userData.begin()); std::copy(output.begin(), output.end(), userData.begin() + 4); - const std::span code(Code); 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{ @@ -235,6 +234,27 @@ int main() { Check(ExpectedNoIeee, "IEEE=0"); Run(*device, ShaderRecompiler::ShaderFloatMode{0xc0u, true, true, false}); Check(ExpectedIeee, "IEEE=1"); + for (const std::uint32_t factor : {0x40000000u, 0x3f000000u, 0u, 0x7f800000u}) { + std::vector code(Code.begin(), Code.end()); + code[24] = (code[24] & ~0x1ffu) | 0xffu; + code.insert(code.begin() + 25, factor); + for (const bool ieee : {false, true}) { + Run(*device, ShaderRecompiler::ShaderFloatMode{0xc0u, true, ieee, false}, code); + for (std::uint32_t tid = 0u; tid < Threads; ++tid) { + const std::uint32_t source = Rows[tid][1]; + std::uint32_t expected = source; + if (IsNan32(source)) { + if (ieee) expected |= 0x00400000u; + } else { + const std::uint32_t input = (source & 0x7fffffffu) < 0x00800000u ? source & 0x80000000u : source; + const std::uint32_t product = std::bit_cast(std::bit_cast(factor) * std::bit_cast(input)); + expected = IsNan32(product) ? 0xffc00000u : product; + if ((expected & 0x7fffffffu) < 0x00800000u) expected &= 0x80000000u; + } + Expect(tid, Output[tid * Results + 3u], expected, "literal multiplication"); + } + } + } } std::puts("f32 arithmetic nan tests passed"); return 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/GcnCompareExecWrites.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/GcnCompareExecWrites.cpp new file mode 100644 index 0000000000..0b5484e984 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/GcnCompareExecWrites.cpp @@ -0,0 +1,224 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "RdnaDecoder/RdnaInstructionDecoder.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Lanes = 64; +constexpr std::uint32_t CaseDwords = 8; +constexpr std::uint32_t CaseBytes = CaseDwords * 4; + +constexpr std::uint32_t Sgpr8 = 8; +constexpr std::uint32_t VccLo = 106; +constexpr std::uint32_t VccHi = 107; +constexpr std::uint32_t ExecLo = 126; +constexpr std::uint32_t Literal = 255; +constexpr std::uint32_t Vgpr = 256; +constexpr std::uint32_t Four = 246; +constexpr std::uint32_t MinusOne = 193; +constexpr std::uint32_t Inline(std::uint32_t value) { return 128u + value; } + +constexpr std::uint32_t SMovB32 = 0x03; +constexpr std::uint32_t SMovB64 = 0x04; +constexpr std::uint32_t SNop = 0x00; +constexpr std::uint32_t SEndpgm = 0x01; +constexpr std::uint32_t SWaitcnt = 0x0c; +constexpr std::uint32_t VMovB32 = 0x01; +constexpr std::uint32_t VCvtF32U32 = 0x06; +constexpr std::uint32_t VCmpxEqF32 = 0x12; +constexpr std::uint32_t VCmpLtU32 = 0xc1; +constexpr std::uint32_t VCmpxLtU32 = 0xd1; +constexpr std::uint32_t BufferStoreDword = 0x1c; + +constexpr std::uint32_t Sop1(std::uint32_t opcode, std::uint32_t sdst, std::uint32_t ssrc0) { + return 0xbe800000u | (sdst << 16u) | (opcode << 8u) | ssrc0; +} + +constexpr std::uint32_t Sopp(std::uint32_t opcode, std::uint32_t immediate) { + return 0xbf800000u | (opcode << 16u) | immediate; +} + +constexpr std::uint32_t Vop1(std::uint32_t opcode, std::uint32_t vdst, std::uint32_t src0) { + return 0x7e000000u | (vdst << 17u) | (opcode << 9u) | src0; +} + +constexpr std::uint32_t Vopc(std::uint32_t opcode, std::uint32_t src0, std::uint32_t vsrc1) { + return 0x7c000000u | (opcode << 17u) | (vsrc1 << 9u) | src0; +} + +std::vector Vop3Compare(std::uint32_t opcode, std::uint32_t sdst, std::uint32_t src0, std::uint32_t src1) { + return {0xd0000000u | (opcode << 17u) | sdst, src0 | (src1 << 9u)}; +} + +constexpr std::array Mubuf(std::uint32_t opcode, std::uint32_t vdata, std::uint32_t vaddr, std::uint32_t offset) { + return {0xe0000000u | (opcode << 18u) | (1u << 12u) | offset, vaddr | (vdata << 8u) | (Inline(0) << 24u)}; +} + +constexpr std::uint64_t AllLanes = ~0ull; +constexpr std::uint64_t LowHalf = 0x00000000ffffffffull; +constexpr std::uint64_t Quarters = 0x0000ffff0000ffffull; +constexpr std::uint64_t Lane4 = 0x0000000000000010ull; +constexpr std::uint64_t GcnBufferBaseLimit = 1ull << 44u; +constexpr std::uintptr_t LowAddressHint = std::uintptr_t{1} << 40u; +constexpr std::uint64_t VccPreset = 0x2222222211111111ull; +constexpr std::uint64_t SdstPreset = 0x4444444433333333ull; + +struct Case { + const char* name; + std::uint64_t exec; + std::vector compare; + std::uint64_t expectedExec; + std::uint64_t expectedVcc; + std::uint64_t expectedSdst; +}; + +const std::array Cases{{ + {"v_cmpx_lt_u32_e32 vcc, v0, 32", AllLanes, {Vopc(VCmpxLtU32, Vgpr + 0, 1)}, LowHalf, LowHalf, SdstPreset}, + {"v_cmpx_lt_u32_e64 s[8:9], v0, 32", AllLanes, Vop3Compare(VCmpxLtU32, Sgpr8, Vgpr + 0, Vgpr + 1), LowHalf, VccPreset, LowHalf}, + {"v_cmpx_eq_f32_e32 vcc, 4.0, f32(v0)", AllLanes, {Vopc(VCmpxEqF32, Four, 2)}, Lane4, Lane4, SdstPreset}, + {"v_cmpx_lt_u32_e32 vcc, v0, 64 under exec 0000ffff0000ffff", Quarters, {Vopc(VCmpxLtU32, Vgpr + 0, 4)}, Quarters, Quarters, SdstPreset}, + {"v_cmpx_lt_u32_e64 s[8:9], v0, 64 under exec 0000ffff0000ffff", Quarters, Vop3Compare(VCmpxLtU32, Sgpr8, Vgpr + 0, Vgpr + 4), Quarters, VccPreset, Quarters}, + {"v_cmpx_lt_u32_e64 vcc, v0, 32", AllLanes, Vop3Compare(VCmpxLtU32, VccLo, Vgpr + 0, Vgpr + 1), LowHalf, LowHalf, SdstPreset}, + {"v_cmp_lt_u32_e64 s[8:9], v0, 32", AllLanes, Vop3Compare(VCmpLtU32, Sgpr8, Vgpr + 0, Vgpr + 1), AllLanes, VccPreset, LowHalf}, +}}; + +constexpr std::size_t OutputDwords = Cases.size() * CaseDwords; + +std::span LowOutput() { + constexpr std::size_t bytes = 65536; +#ifdef _WIN32 + void* block = VirtualAlloc(reinterpret_cast(LowAddressHint), bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); + if (block == nullptr) block = VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); +#else + void* block = mmap(reinterpret_cast(LowAddressHint), bytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (block == MAP_FAILED) block = nullptr; +#endif + Require(block != nullptr && reinterpret_cast(block) + bytes <= GcnBufferBaseLimit, "GCN compare EXEC writes: cannot allocate the output below the 44-bit GCN buffer base limit"); + return {static_cast(block), OutputDwords}; +} + +std::vector Program() { + std::vector code; + const auto emit = [&](auto... words) { (code.push_back(words), ...); }; + const auto movConstant = [&](std::uint32_t sdst, std::uint32_t value) { emit(Sop1(SMovB32, sdst, Literal), value); }; + emit(Vop1(VMovB32, 1, Inline(32))); + emit(Vop1(VMovB32, 4, Inline(64))); + emit(Vop1(VCvtF32U32, 2, Vgpr + 0)); + movConstant(16, static_cast(Quarters)); + movConstant(17, static_cast(Quarters >> 32u)); + for (std::uint32_t index = 0; index < Cases.size(); ++index) { + const auto& item = Cases[index]; + movConstant(VccLo, static_cast(VccPreset)); + movConstant(VccHi, static_cast(VccPreset >> 32u)); + movConstant(Sgpr8, static_cast(SdstPreset)); + movConstant(Sgpr8 + 1, static_cast(SdstPreset >> 32u)); + if (item.exec != AllLanes) emit(Sop1(SMovB64, ExecLo, 16)); + for (const auto word : item.compare) emit(word); + emit(Sopp(SNop, 4)); + emit(Sop1(SMovB64, 10, ExecLo)); + emit(Sop1(SMovB64, 12, VccLo)); + emit(Sop1(SMovB64, 14, Sgpr8)); + emit(Sop1(SMovB64, ExecLo, MinusOne)); + const auto base = index * CaseBytes; + if (base <= 64u) emit(Vop1(VMovB32, 11, Inline(base))); + else emit(Vop1(VMovB32, 11, Literal), base); + for (std::uint32_t dword = 0; dword < 6; ++dword) emit(Vop1(VMovB32, 12 + dword, 10 + dword)); + for (std::uint32_t dword = 0; dword < 6; ++dword) { + const auto store = Mubuf(BufferStoreDword, 12 + dword, 11, dword * 4u); + emit(store[0], store[1]); + } + } + emit(Sopp(SWaitcnt, 0)); + emit(Sopp(SEndpgm, 0)); + return code; +} + +std::array GcnBufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x27facu}; +} + +void Run(AgcDriver::VulkanDevice& device, std::span code, std::span output, const ShaderRecompiler::SpirvTarget& target) { + std::fill(output.begin(), output.end(), 0xdeadbeefu); + const auto descriptor = GcnBufferDescriptor(output.data(), static_cast(output.size_bytes())); + const std::vector userData(descriptor.begin(), descriptor.end()); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Lanes, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {Lanes, 0, userData, compute, std::nullopt, std::nullopt, memory}, + 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(); +} + +std::string Hex(std::uint64_t value) { + char text[17]; + std::snprintf(text, sizeof(text), "%016llx", static_cast(value)); + return text; +} + +void Check(std::span output, const char* run) { + for (std::uint32_t index = 0; index < Cases.size(); ++index) { + const auto& item = Cases[index]; + const auto pair = [&](std::uint32_t dword) { + return static_cast(output[index * CaseDwords + dword]) | (static_cast(output[index * CaseDwords + dword + 1]) << 32u); + }; + const auto expect = [&](const char* what, std::uint64_t actual, std::uint64_t expected) { + Require(actual == expected, std::string("GCN compare EXEC writes ") + run + ": " + item.name + " left " + what + " " + Hex(actual) + ", expected " + Hex(expected)); + }; + expect("exec", pair(0), item.expectedExec); + expect("vcc", pair(2), item.expectedVcc); + expect("s[8:9]", pair(4), item.expectedSdst); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (device->Target().subgroupSize < 32u) { + std::printf("skipped, the device's subgroups are narrower than the 32 lanes a wave64 run needs (%u lanes)\n", device->Target().subgroupSize); + return VulkanTestSkipped; + } + ShaderRecompiler::SetGcnGuestShaders(true); + const auto code = Program(); + const auto output = LowOutput(); + Run(*device, code, output, device->Target()); + Check(output, "wave64"); + Run(*device, code, output, device->ComputeTarget(32)); + Check(output, "wave64 split"); + std::puts("GCN compare EXEC writes 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/GcnPaddedDepthSampling.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/GcnPaddedDepthSampling.cpp new file mode 100644 index 0000000000..2a3a307db1 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/GcnPaddedDepthSampling.cpp @@ -0,0 +1,291 @@ +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "DepthFastClearHarness.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Format16Unorm = 7; +constexpr std::uint32_t Format32Float = 22; +constexpr std::uint32_t Type2D = 9; +constexpr std::uint32_t Swizzle64KbZX = 24; +constexpr std::uint32_t ClampEdge = 2; +constexpr VkExtent2D Padded{64, 64}; +constexpr std::uint32_t Width = 48; +constexpr std::uint32_t Height = 40; +constexpr std::size_t BlockBytes = 1u << 20u; +constexpr std::size_t InputOffset = 0; +constexpr std::size_t OutputOffset = 0x1000; +constexpr std::size_t D32Offset = 0x10000; +constexpr std::size_t D16Offset = 0x40000; + +alignas(256) constexpr std::array Code{ + 0x34020084, 0xe0381000, 0x80000201, 0xbf8c3f70, 0xf09c0f08, 0x00820602, 0xf0001f08, 0x00020a04, + 0x34020085, 0xbf8c3f70, 0xe0781000, 0x80010601, 0xe0781010, 0x80010a01, 0xbf810000, +}; + +alignas(256) std::array Pixels{}; + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(BlockBytes, 65536)); + if (block != nullptr) GuestArena::GuestArenaCommit_nid_postfix(block, BlockBytes, PAGE_READWRITE, BlockBytes); +#else + constexpr std::uintptr_t alignment = 65536; + void* mapped = mmap(nullptr, BlockBytes + alignment, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (mapped != MAP_FAILED) { + const auto begin = reinterpret_cast(mapped); + const auto aligned = (begin + alignment - 1) & ~(alignment - 1); + if (aligned != begin) munmap(mapped, aligned - begin); + if (aligned + BlockBytes != begin + BlockBytes + alignment) munmap(reinterpret_cast(aligned + BlockBytes), begin + alignment - aligned); + block = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, BlockBytes); + } +#endif + Require(block != nullptr, "GCN padded depth sampling: cannot allocate the guest block"); + std::memset(block, 0, BlockBytes); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + AgcDriver::GuestMemory::CollectWritesUncached(reinterpret_cast(block), BlockBytes); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(block, BlockBytes); + GuestArena::GuestArenaRelease_nid_postfix(block, BlockBytes); +#else + munmap(block, BlockBytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(block, BlockBytes); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint8_t* At(std::size_t offset) { return block + offset; } + +private: + std::uint8_t* block = nullptr; +}; + +struct Probe { + float u; + float v; + std::uint32_t x; + std::uint32_t y; +}; + +std::array RawBuffer(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::array TextureDescriptor(const void* data, std::uint32_t format) { + const auto address = static_cast(reinterpret_cast(data)); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (format << 20u) | (((Width - 1u) & 3u) << 30u), + ((Width - 1u) >> 2u) | ((Height - 1u) << 14u), + 0xfacu | (Swizzle64KbZX << 20u) | (Type2D << 28u), + 0u, 0u, 0u, 0u, + }; +} + +void DrawDepth(AgcDriver::VulkanDevice& device, std::uint64_t address, VkFormat format, float z, VkRect2D scissor) { + using DepthFastClearHarness::PixelCode; + using DepthFastClearHarness::VertexCode; + static std::deque>> vertexData; + vertexData.push_back({{-1.0f, -1.0f, z, 1.0f}, {3.0f, -1.0f, z, 1.0f}, {-1.0f, 3.0f, z, 1.0f}}); + const auto& vertices = vertexData.back(); + const auto target = device.Target(); + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = DepthFastClearHarness::BufferDescriptor(vertices.data(), 16u, static_cast(vertices.size())); + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertexRequest{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {32, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertexRequest.useCache = false; + const auto vertex = ShaderRecompiler::Recompile(vertexRequest); + const auto vertexPush = static_cast(vertex.pushConstants.size()); + ShaderRecompiler::ShaderPixelStageInfo pixelInfo{}; + pixelInfo.wave32 = true; + pixelInfo.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionX) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionY); + pixelInfo.posX = true; + pixelInfo.posY = true; + pixelInfo.targetOutputMode[0] = 9; + pixelInfo.targetExportMapping.fill(0xe4u); + std::vector pixelUserData(4, 0u); + const std::array pixelMemory{{{reinterpret_cast(PixelCode.data()), std::as_bytes(std::span(PixelCode))}}}; + ShaderRecompiler::RecompileRequest fragmentRequest{ + {ShaderStage::Fragment, reinterpret_cast(PixelCode.data()), PixelCode, 0, {}}, + {32, 0, pixelUserData, std::nullopt, pixelInfo, std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragmentRequest.useCache = false; + const auto pixel = ShaderRecompiler::Recompile(fragmentRequest); + const std::array shaders{{ + {ShaderStage::Vertex, &vertex, 0}, + {ShaderStage::Fragment, &pixel, PixelPushOffset(vertexPush, target)} + }}; + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 32u, 32u, std::nullopt, std::nullopt}; + state.color = {reinterpret_cast(Pixels.data()), Padded, VK_FORMAT_R8G8B8A8_UNORM, AgcDriver::Graphics::ColorTargetLayout(Padded.width, Padded.height, AgcDriver::Graphics::ColorTileMode::Linear).Bytes(), 0xe4u}; + state.colors = {state.color}; + state.hasColorTarget = true; + state.depth = AgcDriver::Graphics::DepthTarget{address, 0, Padded, format, 1.0f, 0}; + state.depth->gcn = true; + state.depthTest = true; + state.depthWrite = true; + state.depthCompare = VK_COMPARE_OP_ALWAYS; + state.renderExtent = Padded; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Padded.height), static_cast(Padded.width), -static_cast(Padded.height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = scissor; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15; + state.blends = {state.blend}; + state.blendConstants = {}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(vertices.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); +} + +std::vector Probes() { + std::vector probes; + constexpr std::array, 26> texels{{ + {0, 0}, {47, 0}, {0, 39}, {39, 31}, {40, 32}, {47, 39}, {46, 38}, {47, 32}, {40, 39}, {20, 20}, {10, 35}, {45, 5}, {41, 33}, + {44, 36}, {47, 36}, {44, 39}, {43, 35}, {43, 39}, {47, 35}, {39, 39}, {47, 31}, {40, 31}, {39, 32}, {46, 39}, {5, 30}, {30, 10}, + }}; + for (const auto& [x, y] : texels) probes.push_back({(static_cast(x) + 0.5f) / Width, (static_cast(y) + 0.5f) / Height, x, y}); + probes.push_back({1.25f, 1.25f, 47, 39}); + probes.push_back({-0.1f, -0.1f, 0, 0}); + probes.push_back({1.25f, 0.4875f, 47, 19}); + probes.push_back({0.5125f, 1.25f, 24, 39}); + probes.push_back({1.25f, 0.8125f, 47, 32}); + probes.push_back({0.8125f, 1.25f, 39, 39}); + return probes; +} + +float DepthAt(std::uint32_t x, std::uint32_t y, bool refreshed) { + if (refreshed && x >= 44 && y >= 36) return 0.625f; + return x >= 40 && y >= 32 ? 0.5f : 0.25f; +} + +std::uint32_t SampledTexel(float coordinate, std::uint32_t size) { + return static_cast(std::clamp(static_cast(std::floor(coordinate * static_cast(size))), 0, static_cast(size) - 1)); +} + +void Check(AgcDriver::VulkanDevice& device, GuestBlock& block, std::uint8_t* depth, std::uint32_t format, bool d16, bool refreshed, const char* what) { + const auto probes = Probes(); + Require(probes.size() == Threads, "GCN padded depth sampling needs one probe a thread"); + auto* input = reinterpret_cast(block.At(InputOffset)); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + input[tid * 4u + 0u] = std::bit_cast(probes[tid].u); + input[tid * 4u + 1u] = std::bit_cast(probes[tid].v); + input[tid * 4u + 2u] = probes[tid].x; + input[tid * 4u + 3u] = probes[tid].y; + } + auto* output = reinterpret_cast(block.At(OutputOffset)); + std::fill(output, output + Threads * 8u, -1.0f); + std::vector userData(20, 0u); + const auto in = RawBuffer(input, Threads * 16u); + const auto out = RawBuffer(output, Threads * 32u); + const auto texture = TextureDescriptor(depth, format); + const std::array sampler{ClampEdge | (ClampEdge << 3u) | (ClampEdge << 6u), 0u, 0u, 0u}; + std::copy(in.begin(), in.end(), userData.begin()); + std::copy(out.begin(), out.end(), userData.begin() + 4); + std::copy(texture.begin(), texture.end(), userData.begin() + 8); + std::copy(sampler.begin(), sampler.end(), userData.begin() + 16); + const std::span code(Code); + 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(); + const auto expect = [&](std::uint32_t tid, const char* op, float actual, std::uint32_t x, std::uint32_t y) { + const float expected = DepthAt(x, y, refreshed); + const bool match = d16 ? std::fabs(actual - expected) <= 2.0f / 65535.0f : std::bit_cast(actual) == std::bit_cast(expected); + char text[224]; + std::snprintf(text, sizeof(text), "%s, %s thread %u at texel (%u,%u) of the %ux%u view: read %.8f, expected %.8f", what, op, tid, x, y, Width, Height, actual, expected); + Require(match, text); + }; + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + expect(tid, "image_sample_lz", output[tid * 8u], SampledTexel(probes[tid].u, Width), SampledTexel(probes[tid].v, Height)); + expect(tid, "image_load", output[tid * 8u + 4u], probes[tid].x, probes[tid].y); + } +} + +void Case(AgcDriver::VulkanDevice& device, GuestBlock& block, std::size_t offset, VkFormat depthFormat, std::uint32_t format, const char* what) { + auto* depth = block.At(offset); + const auto address = reinterpret_cast(depth); + const bool d16 = depthFormat == VK_FORMAT_D16_UNORM; + DrawDepth(device, address, depthFormat, 0.75f, {{0, 0}, Padded}); + DrawDepth(device, address, depthFormat, 0.25f, {{0, 0}, {Width, Height}}); + DrawDepth(device, address, depthFormat, 0.5f, {{40, 32}, {8, 8}}); + Check(device, block, depth, format, d16, false, what); + DrawDepth(device, address, depthFormat, 0.625f, {{44, 36}, {4, 4}}); + Check(device, block, depth, format, d16, true, (std::string(what) + " after a redraw").c_str()); +} + +} + +int main() { + try { + GuestBlock block; + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Case(*device, block, D32Offset, VK_FORMAT_D32_SFLOAT, Format32Float, "D32 64x64 PS4 depth through a 48x40 R32F view"); + Case(*device, block, D16Offset, VK_FORMAT_D16_UNORM, Format16Unorm, "D16 64x64 PS4 depth through a 48x40 R16 view"); + std::puts("GCN padded depth sampling 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/GdsDmaFill.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/GdsDmaFill.cpp index 2acc0815bb..9d55a5dedc 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/GdsDmaFill.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/GdsDmaFill.cpp @@ -73,6 +73,13 @@ std::uint64_t Address(const void* pointer) { return static_cast(reinterpret_cast(pointer)); } +constexpr std::array StoreProgram{ + 0x34060082, + 0x7e0c02ff, 0x5eed0001u, + 0xe0702000, 0x80000603, + 0xbf810000, +}; + std::array FillGds(std::uint32_t value, std::uint32_t offset = GdsOffset, std::uint32_t bytes = FillBytes) { return {0xc0055000u, (2u << 29u) | (1u << 20u), value, 0u, offset, 0u, bytes}; } @@ -85,6 +92,10 @@ std::array CopyGdsTo(std::uint64_t destination, std::uint32_t return {0xc0055000u, 1u << 29u, offset, 0u, static_cast(destination), static_cast(destination >> 32u), bytes}; } +std::array CopyToGds(std::uint64_t source, std::uint32_t offset, std::uint32_t bytes) { + return {0xc0055000u, 1u << 20u, static_cast(source), static_cast(source >> 32u), offset, 0u, bytes}; +} + template void Append(std::vector& words, const std::array& packet) { words.insert(words.end(), packet.begin(), packet.end()); @@ -136,6 +147,11 @@ int main() { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; using namespace AgcDriver::DriverDetail; + const bool sharedMapping = AgcDriver::Pm4::GdsShared(); + check(sharedMapping == (AgcDriver::Pm4::ImportableGdsRequested() && std::getenv("APS5_NO_SHARED_GDS") == nullptr), sharedMapping ? "the GDS is in shared memory although that was not requested or is switched off" : "the GDS kept its private mapping although shared memory was requested"); + const bool shared = AgcDriver::Pm4::GdsImportable(); + check(!shared || sharedMapping, "the GDS counts as importable without a shared mapping"); + if (sharedMapping && !shared) std::printf("the device refused the host import of the shared GDS: the copy path is checked\n"); auto* readBack = GuestBlock(); auto* filled = GuestBlock(); auto* held = GuestBlock(); @@ -272,6 +288,8 @@ int main() { Submit(std::move(words)); AgcDriverWaitIdle_nid_postfix(); const auto drainsBefore = storesDrained.load(); + const auto gpuStoresBefore = storesOnGpu.load() + storesBehindCompletions.load(); + const auto gpuCopiesBefore = copiesBehindPendingSources.load(); words.clear(); AppendDispatch(words, code, output); Append(words, FillGds(3000u, CounterOffset, 16)); @@ -288,7 +306,43 @@ int main() { check(first[0] == 500u, "the dispatch before the fill read " + std::to_string(first[0]) + ", not the earlier fill's 500"); check(second[0] == 3000u, "the dispatch after the fill, in the same submission, read " + std::to_string(second[0]) + ", not the fill's 3000"); check(counters[0] == 3032u && counters[1] == 2968u && counters[2] == 3032u, "the GDS counters after a dispatch, a fill and a dispatch are " + std::to_string(counters[0]) + ", " + std::to_string(counters[1]) + " and " + std::to_string(counters[2]) + ", not 3032, 2968 and 3032: the first dispatch's atomics landed after the fill"); - check(storesDrained.load() == drainsBefore + 1, "a fill behind a dispatch still using the GDS did not drain the device as main does"); + if (shared) { + check(storesDrained.load() == drainsBefore, "a fill of the shared GDS behind a dispatch still using it drained the device"); + check(storesOnGpu.load() + storesBehindCompletions.load() == gpuStoresBefore + 1, "a fill of the shared GDS behind a dispatch still using it was not stored on the GPU"); + check(copiesBehindPendingSources.load() == gpuCopiesBefore + 1, "the copy out of the shared GDS behind the dispatches using it was not recorded on the GPU"); + } else { + check(storesDrained.load() == drainsBefore + 1, "a fill behind a dispatch still using the GDS did not drain the device as main does"); + } + } + + { + constexpr std::uint32_t OrderOffset = 0x400; + constexpr std::uint32_t OrderValue = 0x5151a1a1u; + auto* storeCode = GuestBlock(); + auto* orderOutput = GuestBlock(); + auto* orderReadBack = GuestBlock(); + std::memcpy(storeCode, StoreProgram.data(), sizeof(StoreProgram)); + const auto drainsBefore = storesDrained.load(); + const auto gpuStoresBefore = storesOnGpu.load() + storesBehindCompletions.load(); + const auto gpuCopiesBefore = copiesBehindPendingSources.load(); + words.clear(); + AppendDispatch(words, storeCode, orderOutput); + Append(words, CopyToGds(Address(orderOutput), OrderOffset, 4)); + Append(words, FillGds(OrderValue, OrderOffset, 4)); + Append(words, CopyGdsTo(Address(orderReadBack), OrderOffset, 4)); + Submit(std::move(words)); + AgcDriverWaitIdle_nid_postfix(); + std::array value{}; + AgcDriver::GuestMemory::Read(Address(orderReadBack), std::as_writable_bytes(std::span(value)), 4); + std::array stored{}; + AgcDriver::GuestMemory::Read(Address(orderOutput), std::as_writable_bytes(std::span(stored)), 4); + check(stored[0] == 0x5eed0001u, "the dispatch that writes the copy's source stored " + std::to_string(stored[0]) + ", not 0x5eed0001"); + check(value[0] == OrderValue, "a GDS fill after a copy into the GDS from the output of a dispatch that does not use the GDS read back as " + std::to_string(value[0]) + ", not the fill's value: the copy landed after the fill"); + if (shared) { + check(copiesBehindPendingSources.load() > gpuCopiesBefore, "the copy into the shared GDS behind the dispatch writing its source was not recorded on the GPU"); + check(storesOnGpu.load() + storesBehindCompletions.load() > gpuStoresBefore, "the fill of the shared GDS behind the GPU copy into it was not stored on the GPU"); + check(storesDrained.load() == drainsBefore, "a DMA_DATA into or out of the shared GDS behind a dispatch drained the device"); + } } LibcRunShutdown_nid_postfix(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/IdleStoreYields.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/IdleStoreYields.cpp index e2d59fd5ca..e8c21ebf7c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/IdleStoreYields.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/IdleStoreYields.cpp @@ -6,6 +6,7 @@ #include "VulkanTestDevice.hpp" #include #include +#include #include #include #include @@ -25,7 +26,8 @@ constexpr std::uint32_t Untouched = 0xdeadbeefu; constexpr std::uint32_t Stored = 0x12345678u; constexpr std::uint32_t FarRow = 1000; constexpr std::uint64_t Piece = 4u << 20u; -constexpr std::uint32_t Attempts = 10; +alignas(4) std::atomic betweenImagesHold{0}; +alignas(4) std::atomic withinAnImageHold{0}; alignas(256) constexpr std::array StoreRowZeroCode{ 0x7e040300, 0x7e060280, 0x7e0202ff, Stored, 0xf0200108, 0x00020102, 0xbf810000, 0xbf810000, @@ -108,13 +110,38 @@ bool Contains(std::uint64_t begin, std::uint64_t end) { return false; } -void SubmitAndWaitForPending(std::vector& words) { +class QueueHold { +public: + explicit QueueHold(std::atomic& label) : label(label) {} + ~QueueHold() { Release(); } + QueueHold(const QueueHold&) = delete; + QueueHold& operator=(const QueueHold&) = delete; + + void Append(std::vector& words) const { + const auto address = AddressOf(&label); + words.insert(words.end(), {0xc0053c00u, 0x13u, static_cast(address), static_cast(address >> 32u), 1u, 0xffffffffu, 0x19u}); + } + + bool Reached() const { + std::array waits{}; + const auto count = AgcDriverAwaitedWaits_nid_postfix(waits.data(), waits.size()); + return std::ranges::any_of(std::span(waits.data(), count), [&](const AgcDriver::AwaitedWait& wait) { return wait.address == AddressOf(&label); }); + } + + void Release() { label.store(1, std::memory_order_release); } + +private: + std::atomic& label; +}; + +void SubmitAndWaitHeld(std::vector& words, const QueueHold& hold) { + hold.Append(words); Packet packet{words.data(), static_cast(words.size()), 0, {}}; AgcDriver::Submit(&packet, 0); const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); - while (!AgcDriver::Graphics::StorageTexture::AnyPending() && std::chrono::steady_clock::now() < deadline) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + while (!hold.Reached() && std::chrono::steady_clock::now() < deadline) std::this_thread::sleep_for(std::chrono::microseconds(200)); + Require(hold.Reached(), "idle yield fixture: the queue did not reach the wait after the dispatches"); Require(AgcDriver::Graphics::StorageTexture::AnyPending(), "idle yield fixture did not observe pending storage results"); - std::this_thread::sleep_for(std::chrono::milliseconds(5)); } struct Registered { @@ -133,17 +160,6 @@ struct Registered { } }; -void Repeat(void (*scenario)()) { - for (std::uint32_t attempt = 1;; ++attempt) { - try { - scenario(); - return; - } catch (const std::exception&) { - if (attempt == Attempts) throw; - } - } -} - void StopsBetweenImages() { Surface first(256, 256); Surface second(256, 256); @@ -151,10 +167,12 @@ void StopsBetweenImages() { std::vector words; AppendStore(words, first, StoreRowZeroCode); AppendStore(words, second, StoreRowZeroCode); - SubmitAndWaitForPending(words); + QueueHold hold(betweenImagesHold); + SubmitAndWaitHeld(words, hold); const auto pending = [](const Surface& surface) { return Pending(surface.Address(), surface.Address() + surface.bytes); }; { std::lock_guard gpuLock(AgcDriver::GuestMemory::GpuMutex()); + hold.Release(); Require(pending(first) && pending(second), "the queue-idle store ran before the fixture could take the GPU mutex"); std::size_t checks = 0; const bool finished = AgcDriver::Graphics::StorageTexture::FlushAllPendingUntil("idle", [&] { return checks++ != 0; }); @@ -173,12 +191,14 @@ void StopsWithinAnImage() { std::vector words; AppendStore(words, surface, StoreRowZeroCode); AppendStore(words, surface, StoreFarRowCode); - SubmitAndWaitForPending(words); + QueueHold hold(withinAnImageHold); + SubmitAndWaitHeld(words, hold); const auto begin = surface.Address(); const auto split = begin + Piece; const auto end = begin + surface.bytes; { std::lock_guard gpuLock(AgcDriver::GuestMemory::GpuMutex()); + hold.Release(); Require(Pending(begin, end), "the large image has no pending results"); std::size_t checks = 0; const bool finished = AgcDriver::Graphics::StorageTexture::FlushAllPendingUntil("idle", [&] { return checks++ != 0; }); @@ -201,8 +221,8 @@ int main() { struct ShutdownGuard { ~ShutdownGuard() { try { AgcDriverShutdown_nid_postfix(); } catch (...) {} } } shutdown; - Repeat(StopsBetweenImages); - Repeat(StopsWithinAnImage); + StopsBetweenImages(); + StopsWithinAnImage(); AgcDriverShutdown_nid_postfix(); std::puts("idle store yields tests passed"); return 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageArrayRowsPastLayerLimit.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageArrayRowsPastLayerLimit.cpp new file mode 100644 index 0000000000..e76dbca6a0 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageArrayRowsPastLayerLimit.cpp @@ -0,0 +1,614 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "Recompiler.hpp" +#include "prx/libSceAgcDriver/Execution/include/ShaderMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" +#include "Optimization/ResourceProgram.hpp" +#include "Optimization/ResourceMaterializer.hpp" +#include "IntermediateRepresentation/IrMetadata/ResourcePlan.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Width = 8; +constexpr std::uint32_t MaxGuestLayers = 8192; +constexpr std::uint32_t Format8888UNorm = 56; +constexpr std::uint32_t Type1DArray = 12; +constexpr std::uint32_t Wrap = 0; +constexpr std::uint32_t ClampEdge = 2; +constexpr std::uint32_t Bilinear = (1u << 20u) | (1u << 22u); + +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; +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 SampleLevelZero{ + 0x34020084, 0xe0381000, 0x80000201, 0xbf8c3f70, 0xf09c0f20, 0x00820602, 0x34020085, 0xbf8c3f70, + 0xe0781000, 0x80010601, 0xbf810000, +}; + +alignas(256) constexpr std::array SampleLevel{ + 0x34020084, 0xe0381000, 0x80000201, 0xbf8c3f70, 0xf0900f20, 0x00820602, 0x34020085, 0xbf8c3f70, + 0xe0781000, 0x80010601, 0xbf810000, +}; + +std::uint32_t Layers = 0; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::array TextureDescriptor(std::uint32_t baseArray) { + const auto address = static_cast(reinterpret_cast(Texels.data())); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format8888UNorm << 20u) | (((Width - 1u) & 3u) << 30u), + (Width - 1u) >> 2u, + 0xfacu | (Type1DArray << 28u), + (Layers - 1u) | (baseArray << 16u), 0u, 0u, 0u, + }; +} + +std::array Texel(std::uint32_t x, std::uint32_t layer) { + return {static_cast(x * 30u + layer % 5u), static_cast((layer & 1u) != 0u ? 255u : 0u), static_cast(layer), 0xff}; +} + +void FillTexels() { + std::ranges::fill(Texels, std::uint8_t{0}); + const auto surface = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(TextureDescriptor(0))); + const auto& mip = surface.mips.at(0); + Require(surface.layers == Layers && surface.guestBytes <= Texels.size(), "the test array does not fit its storage"); + for (std::uint32_t layer = 0; layer < Layers; ++layer) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto texel = Texel(x, layer); + std::copy(texel.begin(), texel.end(), Texels.begin() + static_cast(surface.GuestLayerOffset(layer) + mip.tiledOffset + x * 4u)); + } + } +} + +void FillInput(std::uint32_t limit) { + const auto last = static_cast(Layers - 1u); + const std::array layers{0.0f, 1.0f, 2.4f, 1.6f, -1.0f, -0.4f, 0.4f, last, last + 0.4f, last + 1.0f, last + 7.0f, 1.0e6f, -1.0e6f, static_cast(limit - 1u), static_cast(limit), static_cast(limit) + 0.6f}; + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + Input[tid * 4u + 0u] = std::bit_cast(-0.2f + 0.045f * static_cast(tid)); + Input[tid * 4u + 1u] = std::bit_cast(layers[tid % layers.size()]); + Input[tid * 4u + 2u] = std::bit_cast(static_cast(tid % 3u) * 1.7f); + Input[tid * 4u + 3u] = 0u; + } +} + +float Unorm(std::uint8_t value) { + return static_cast(value); +} + +class RegisteredTextureRange { +public: + RegisteredTextureRange(void* address, std::size_t bytes) : address(address) { + GuestAllocations::Mutation mutation; + mutation.Add(address, bytes, true, false, true); + } + + ~RegisteredTextureRange() { + Unregister(); + } + + void Resize(std::size_t bytes) { + GuestAllocations::Mutation mutation; + if (registered) mutation.Remove(address); + mutation.Add(address, bytes, true, false, true); + registered = true; + } + + void Unregister() { + if (!registered) return; + GuestAllocations::Mutation mutation; + mutation.Remove(address); + registered = false; + } + + RegisteredTextureRange(const RegisteredTextureRange&) = delete; + RegisteredTextureRange& operator=(const RegisteredTextureRange&) = delete; + +private: + void* address; + bool registered = true; +}; + +int CaptureStillRowsRegisteredWindow(AgcDriver::VulkanDevice& device, std::uint32_t limit) { + constexpr std::uint32_t baseArray = 1u; + if (limit > MaxGuestLayers - 3u) { + std::puts("still-rows: skipped because the device layer limit leaves no guest descriptor headroom"); + return VulkanTestSkipped; + } + if (2u * (limit + 1u) > device.graphicsContext().limits.maxImageDimension2D) { + std::puts("still-rows: skipped because the registered row geometry exceeds maxImageDimension2D"); + return VulkanTestSkipped; + } + Layers = limit + 3u; + const auto texture = TextureDescriptor(baseArray); + const auto rawResource = AgcDriver::Graphics::DecodeTextureResource(texture); + const auto rawGeometry = AgcDriver::Graphics::DescribeSurface(rawResource); + const auto rawViewLayers = rawGeometry.imageLayers - rawResource.baseArray; + Require(rawResource.baseArray == baseArray && rawViewLayers == limit + 2u, "still-rows: the raw descriptor does not cover L+2 slices from baseArray 1"); + Require(rawGeometry.layerBytes != 0u, "still-rows: the descriptor has no complete layer size"); + if (rawGeometry.guestBytes > Texels.size()) { + std::puts("still-rows: skipped because the full descriptor geometry exceeds the 4 MiB test backing"); + return VulkanTestSkipped; + } + const auto registeredBytes = static_cast(rawGeometry.layerBytes * (limit + 2u)); + if (registeredBytes > Texels.size()) { + std::puts("still-rows: skipped because the exact registered complete-slice range exceeds the 4 MiB test backing"); + return VulkanTestSkipped; + } + Require(reinterpret_cast(Texels.data()) % TexelAlignment == 0u, "still-rows: the descriptor base is not 64 KiB aligned"); + FillTexels(); + FillInput(limit + 1u); + RegisteredTextureRange registered(Texels.data(), registeredBytes); + const auto range = GuestAllocations::GuestAllocationsLookup_nid_postfix(reinterpret_cast(Texels.data()), false); + Require(range != nullptr && range->address == reinterpret_cast(Texels.data()) && range->bytes == registeredBytes && range->readable && !range->writable && range->gpu, "still-rows: lookup did not return the exact readable registered extent"); + const auto completeRegisteredSlices = registeredBytes / rawGeometry.layerBytes; + Require(completeRegisteredSlices == limit + 2u, "still-rows: the registered extent does not contain the expected complete slices"); + const auto expectedLastArray = static_cast(completeRegisteredSlices - 1u); + const auto boundedRows = expectedLastArray - baseArray + 1u; + Require(expectedLastArray == limit + 1u && boundedRows == limit + 1u, "still-rows: the independent complete-range oracle did not produce the expected L+1 view"); + std::vector userData(20, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(sizeof(Input))); + const auto output = BufferDescriptor(Output.data(), static_cast(sizeof(Output))); + const std::array sampler{ClampEdge | (ClampEdge << 3u) | (ClampEdge << 6u), 0u, 0u, 0u}; + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + std::copy(texture.begin(), texture.end(), userData.begin() + 8); + std::copy(sampler.begin(), sampler.end(), userData.begin() + 16); + const std::array memory{{{reinterpret_cast(SampleLevelZero.data()), std::as_bytes(std::span(SampleLevelZero))}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(SampleLevelZero.data()), SampleLevelZero, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + AgcDriver::DriverDetail::ShaderSnapshot snapshot{reinterpret_cast(SampleLevelZero.data()), 0, 0, {SampleLevelZero.begin(), SampleLevelZero.end()}, {}}; + request.shader.code = snapshot.code; + auto invocation = AgcDriver::DriverDetail::InvocationFor(snapshot, 0, request); + AgcDriver::ShaderMemory shaderMemory(request.context.memory); + const auto capture = AgcDriver::DriverDetail::CaptureInvocation(shaderMemory, snapshot, 0, request, invocation); + Require(capture->snapshot.images.size() == 1u && capture->plan->info.images.size() == 1u && capture->plan->info.runtimeImageModes.size() == 1u, "still-rows: capture did not resolve exactly one sampled image"); + const auto& capturedDescriptor = capture->snapshot.images.front(); + const auto capturedResource = AgcDriver::Graphics::DecodeTextureResource(std::span(capturedDescriptor.dwords).first(capturedDescriptor.dwordCount)); + const auto capturedGeometry = AgcDriver::Graphics::DescribeSurface(capturedResource); + const auto& image = capture->plan->info.images.front(); + const auto& modes = capture->plan->info.runtimeImageModes.front(); + const auto modeIndex = ShaderRecompiler::ResourceMaterializer::RuntimeImageMode(image, capturedDescriptor, modes); + const auto& selectedMode = modes.at(modeIndex); + Require(selectedMode.layerRows, "still-rows: the over-limit effective view did not select layerRows mode"); + Require(capture->snapshot.registeredImages.size() == 1u && capture->snapshot.registeredImages.front().provenance.dwords == texture && capture->snapshot.registeredImages.front().effective == capturedDescriptor && capture->snapshot.registeredImages.front().rangeBytes == registeredBytes, "still-rows: capture identity lost original provenance, effective geometry, or the registered extent"); + Require(capturedResource.baseArray == baseArray && capturedResource.depthOrLastArray == expectedLastArray && capturedGeometry.imageLayers - capturedResource.baseArray == boundedRows, "still-rows: captured descriptor geometry differs from the independent complete registered window before dispatch"); + const auto expectedRowHeight = boundedRows * 2u; + Require(expectedRowHeight <= device.graphicsContext().limits.maxImageDimension2D, "still-rows: the independent bound row height exceeds maxImageDimension2D"); + const auto result = invocation.Materialize(*capture); + auto prepared = device.PrepareDispatch(*result, {}, Threads, reinterpret_cast(SampleLevelZero.data()), std::nullopt, &capture->snapshot.registeredImages); + device.Dispatch(*result, 1, 1, 1, {}, reinterpret_cast(SampleLevelZero.data()), std::move(prepared), nullptr, std::nullopt, &capture->snapshot.registeredImages); + device.WaitIdle(); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto logicalLayer = static_cast(std::clamp(std::nearbyint(std::bit_cast(Input[tid * 4u + 1u])), 0.0f, static_cast(limit))); + const auto layer = baseArray + logicalLayer; + const auto x = static_cast(std::clamp(static_cast(std::floor(std::bit_cast(Input[tid * 4u + 0u]) * static_cast(Width))), 0, static_cast(Width) - 1)); + const auto expected = Texel(x, layer); + for (std::uint32_t component = 0; component < expected.size(); ++component) Require(std::fabs(Output[tid * 8u + component] * 255.0f - Unorm(expected[component])) <= 1.0f, "still-rows: first/last/beyond-limit ClampEdge samples disagree with the existing Texel oracle"); + } + return 0; +} + +void CaptureRegisteredWindow(AgcDriver::VulkanDevice& device, std::uint32_t limit) { + constexpr std::uint32_t baseArray = 1u; + Layers = limit + 2u; + const auto texture = TextureDescriptor(baseArray); + const auto rawResource = AgcDriver::Graphics::DecodeTextureResource(texture); + const auto rawGeometry = AgcDriver::Graphics::DescribeSurface(rawResource); + const auto rawViewLayers = rawGeometry.imageLayers - rawResource.baseArray; + Require(rawResource.baseAddress == reinterpret_cast(Texels.data()) && rawResource.baseArray == baseArray && rawResource.format == Format8888UNorm, "registered window: the decoded descriptor lost its aligned sampled-image base, baseArray, or format"); + Require(rawViewLayers == limit + 1u, "registered window: the raw descriptor is not maxImageArrayLayers + 1 from baseArray 1"); + Require(rawGeometry.layerBytes != 0u && rawGeometry.guestBytes <= Texels.size() && rawGeometry.layerBytes * (baseArray + limit) <= Texels.size(), "registered window: the complete raw geometry or bounded complete-slice range does not fit the real backing"); + Require(reinterpret_cast(Texels.data()) % TexelAlignment == 0u, "registered window: the descriptor base is not 64 KiB aligned"); + + const auto registeredBytes = static_cast(rawGeometry.layerBytes * (baseArray + limit)); + const auto generationBefore = GuestAllocations::GuestAllocationsGeneration_nid_postfix(); + RegisteredTextureRange registered(Texels.data(), registeredBytes); + const auto generationAfter = GuestAllocations::GuestAllocationsGeneration_nid_postfix(); + Require(generationAfter > generationBefore, "registered window: adding the slice range did not advance the allocation generation"); + { + const auto range = GuestAllocations::GuestAllocationsLookup_nid_postfix(reinterpret_cast(Texels.data()), false); + Require(range != nullptr && range->address == reinterpret_cast(Texels.data()) && range->bytes == registeredBytes && range->readable && !range->writable && range->gpu, "registered window: lookup did not return the one readable sampled-image extent"); + Require(range->allocationAddress == range->address && range->allocationBytes == registeredBytes, "registered window: lookup allocation extent differs from the registered slice range"); + const auto registeredViewLayers = range->bytes / rawGeometry.layerBytes - rawResource.baseArray; + Require(registeredViewLayers == limit, "registered window: the actual registered extent does not bound the descriptor view to the device limit"); + } + + std::vector userData(20, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(sizeof(Input))); + const auto output = BufferDescriptor(Output.data(), static_cast(sizeof(Output))); + const std::array sampler{ClampEdge | (ClampEdge << 3u) | (ClampEdge << 6u), 0u, 0u, 0u}; + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + std::copy(texture.begin(), texture.end(), userData.begin() + 8); + std::copy(sampler.begin(), sampler.end(), userData.begin() + 16); + const std::array memory{{{reinterpret_cast(SampleLevelZero.data()), std::as_bytes(std::span(SampleLevelZero))}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(SampleLevelZero.data()), SampleLevelZero, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + AgcDriver::DriverDetail::ShaderSnapshot snapshot{reinterpret_cast(SampleLevelZero.data()), 0, 0, {SampleLevelZero.begin(), SampleLevelZero.end()}, {}}; + request.shader.code = snapshot.code; + auto invocation = AgcDriver::DriverDetail::InvocationFor(snapshot, 0, request); + Require(!invocation.Request().context.layerRowSamples, "registered window: the fresh invocation already has layer-row sampling enabled"); + const auto beforeCapture = snapshot.prepared->entries.size(); + Require(beforeCapture == 1u, "registered window: the fresh snapshot did not prepare exactly one initial native invocation"); + AgcDriver::ShaderMemory shaderMemory(request.context.memory); + const auto capture = AgcDriver::DriverDetail::CaptureInvocation(shaderMemory, snapshot, 0, request, invocation); + Require(!invocation.Request().context.layerRowSamples, "registered window: capture promoted the sampled array to layer rows before dispatch"); + Require(snapshot.prepared->entries.size() == beforeCapture, "registered window: capture added a prepared mode while selecting the effective native view"); + Require(capture->snapshot.images.size() == 1u && capture->plan->info.images.size() == 1u && capture->plan->info.runtimeImageModes.size() == 1u, "registered window: capture did not resolve exactly one sampled image"); + + const auto& capturedDescriptor = capture->snapshot.images.front(); + Require(capturedDescriptor.dwordCount == 8u, "registered window: capture did not retain a complete image descriptor"); + const auto capturedResource = AgcDriver::Graphics::DecodeTextureResource(std::span(capturedDescriptor.dwords).first(capturedDescriptor.dwordCount)); + const auto capturedGeometry = AgcDriver::Graphics::DescribeSurface(capturedResource); + Require(capturedResource.baseArray == baseArray && capturedGeometry.imageLayers - capturedResource.baseArray == limit, "registered window: captured descriptor window was not normalized to the complete registered slice extent"); + const auto& image = capture->plan->info.images.front(); + const auto& modes = capture->plan->info.runtimeImageModes.front(); + const auto modeIndex = ShaderRecompiler::ResourceMaterializer::RuntimeImageMode(image, capturedDescriptor, modes); + const auto& selectedMode = modes.at(modeIndex); + Require(selectedMode.dimension == ShaderRecompiler::RdnaImageDimension::Dim1DArray && !selectedMode.layerRows, "registered window: the normalized capture did not select the native 1D-array mode"); + Require(capture->snapshot.registeredImages.size() == 1u && capture->snapshot.registeredImages.front().provenance.dwords == texture, "registered window: capture did not retain the original descriptor provenance"); + Require(capture->snapshot.registeredImages.front().effective == capturedDescriptor && capture->snapshot.registeredImages.front().generation == generationAfter && capture->snapshot.registeredImages.front().rangeBytes == registeredBytes, "registered window: capture identity does not match the effective view's registered range"); + Require(capture->snapshot.imageAllocationGeneration == generationAfter, "registered window: capture did not bind the stable allocation generation"); + FillTexels(); + FillInput(limit); + auto cachedRequest = request; + cachedRequest.useCache = true; + auto cachedInvocation = AgcDriver::DriverDetail::InvocationFor(snapshot, 0, cachedRequest); + AgcDriver::ShaderMemory cachedMemory(cachedRequest.context.memory); + const auto cachedCapture = AgcDriver::DriverDetail::CaptureInvocation(cachedMemory, snapshot, 0, cachedRequest, cachedInvocation); + const auto cachedResult = cachedInvocation.Materialize(*cachedCapture); + Require(cachedResult == cachedInvocation.Materialize(*cachedCapture), "registered window: identical registered captures did not reuse the cached materialization"); + const auto dispatchCaptured = [&](const auto& result, const auto& currentCapture) { + auto prepared = device.PrepareDispatch(*result, {}, Threads, reinterpret_cast(SampleLevelZero.data()), std::nullopt, ¤tCapture->snapshot.registeredImages); + device.Dispatch(*result, 1, 1, 1, {}, reinterpret_cast(SampleLevelZero.data()), std::move(prepared), nullptr, std::nullopt, ¤tCapture->snapshot.registeredImages); + device.WaitIdle(); + }; + dispatchCaptured(cachedResult, cachedCapture); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto layer = baseArray + static_cast(std::clamp(std::nearbyint(std::bit_cast(Input[tid * 4u + 1u])), 0.0f, static_cast(limit - 1u))); + const auto x = static_cast(std::clamp(static_cast(std::floor(std::bit_cast(Input[tid * 4u + 0u]) * static_cast(Width))), 0, static_cast(Width) - 1)); + const auto expected = Texel(x, layer); + for (std::uint32_t component = 0; component < expected.size(); ++component) Require(std::fabs(Output[tid * 8u + component] * 255.0f - Unorm(expected[component])) <= 1.0f, "registered window: bound native image geometry sampled outside its registered extent"); + } + const auto stableView = cachedCapture->snapshot.registeredImages.front(); + std::array unrelatedStorage{}; + const auto stableGeneration = GuestAllocations::GuestAllocationsGeneration_nid_postfix(); + std::shared_ptr unchangedResult; + std::shared_ptr unchangedCapture; + { + RegisteredTextureRange unrelated(unrelatedStorage.data(), sizeof(unrelatedStorage)); + const auto unrelatedGeneration = GuestAllocations::GuestAllocationsGeneration_nid_postfix(); + Require(unrelatedGeneration > stableGeneration, "registered window: the unrelated mapping did not advance the allocation generation"); + auto unchangedInvocation = AgcDriver::DriverDetail::InvocationFor(snapshot, 0, cachedRequest); + AgcDriver::ShaderMemory unchangedMemory(cachedRequest.context.memory); + unchangedCapture = AgcDriver::DriverDetail::CaptureInvocation(unchangedMemory, snapshot, 0, cachedRequest, unchangedInvocation); + const auto& unchangedView = unchangedCapture->snapshot.registeredImages.front(); + Require(unchangedView.provenance == stableView.provenance && unchangedView.effective == stableView.effective && unchangedView.address == stableView.address && unchangedView.rangeAddress == stableView.rangeAddress && unchangedView.rangeBytes == stableView.rangeBytes && unchangedView.allocationAddress == stableView.allocationAddress && unchangedView.allocationBytes == stableView.allocationBytes && unchangedView.backingSerial == stableView.backingSerial && unchangedView.hasRange == stableView.hasRange && unchangedView.readable == stableView.readable && unchangedView.writable == stableView.writable && unchangedView.gpu == stableView.gpu, "registered window: an unrelated mapping changed the unchanged sampled-image range facts"); + Require(!unchangedInvocation.Request().context.layerRowSamples && snapshot.prepared->entries.size() == beforeCapture, "registered window: an unrelated generation bump promoted or prepared a row-mode artifact for the unchanged range"); + const auto unchangedMode = ShaderRecompiler::ResourceMaterializer::RuntimeImageMode(unchangedCapture->plan->info.images.front(), unchangedCapture->snapshot.images.front(), unchangedCapture->plan->info.runtimeImageModes.front()); + Require(unchangedCapture->plan->info.runtimeImageModes.front().at(unchangedMode).dimension == ShaderRecompiler::RdnaImageDimension::Dim1DArray && !unchangedCapture->plan->info.runtimeImageModes.front().at(unchangedMode).layerRows, "registered window: an unrelated generation bump changed the unchanged native image mode"); + unchangedResult = unchangedInvocation.Materialize(*unchangedCapture); + Require(unchangedResult == cachedResult, "registered window: an unrelated generation bump missed the warm cached materialization for unchanged range identity"); + } + const auto removedUnrelatedGeneration = GuestAllocations::GuestAllocationsGeneration_nid_postfix(); + Require(removedUnrelatedGeneration > stableGeneration, "registered window: removing the unrelated mapping did not advance the allocation generation"); + auto removedUnrelatedInvocation = AgcDriver::DriverDetail::InvocationFor(snapshot, 0, cachedRequest); + AgcDriver::ShaderMemory removedUnrelatedMemory(cachedRequest.context.memory); + const auto removedUnrelatedCapture = AgcDriver::DriverDetail::CaptureInvocation(removedUnrelatedMemory, snapshot, 0, cachedRequest, removedUnrelatedInvocation); + const auto& removedUnrelatedView = removedUnrelatedCapture->snapshot.registeredImages.front(); + Require(removedUnrelatedView.effective == stableView.effective && removedUnrelatedView.rangeAddress == stableView.rangeAddress && removedUnrelatedView.rangeBytes == stableView.rangeBytes && removedUnrelatedView.backingSerial == stableView.backingSerial, "registered window: removing an unrelated mapping changed the sampled-image range identity"); + Require(!removedUnrelatedInvocation.Request().context.layerRowSamples && snapshot.prepared->entries.size() == beforeCapture, "registered window: removing an unrelated mapping promoted or prepared a row-mode artifact for the unchanged range"); + const auto removedUnrelatedResult = removedUnrelatedInvocation.Materialize(*removedUnrelatedCapture); + Require(removedUnrelatedResult == cachedResult, "registered window: removing an unrelated mapping missed the warm cached materialization for unchanged range identity"); + dispatchCaptured(unchangedResult, unchangedCapture); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto layer = baseArray + static_cast(std::clamp(std::nearbyint(std::bit_cast(Input[tid * 4u + 1u])), 0.0f, static_cast(limit - 1u))); + const auto x = static_cast(std::clamp(static_cast(std::floor(std::bit_cast(Input[tid * 4u + 0u]) * static_cast(Width))), 0, static_cast(Width) - 1)); + const auto expected = Texel(x, layer); + for (std::uint32_t component = 0; component < expected.size(); ++component) Require(std::fabs(Output[tid * 8u + component] * 255.0f - Unorm(expected[component])) <= 1.0f, "registered window: an unrelated registration altered the bound native image result"); + } + dispatchCaptured(removedUnrelatedResult, removedUnrelatedCapture); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto layer = baseArray + static_cast(std::clamp(std::nearbyint(std::bit_cast(Input[tid * 4u + 1u])), 0.0f, static_cast(limit - 1u))); + const auto x = static_cast(std::clamp(static_cast(std::floor(std::bit_cast(Input[tid * 4u + 0u]) * static_cast(Width))), 0, static_cast(Width) - 1)); + const auto expected = Texel(x, layer); + for (std::uint32_t component = 0; component < expected.size(); ++component) Require(std::fabs(Output[tid * 8u + component] * 255.0f - Unorm(expected[component])) <= 1.0f, "registered window: an unrelated generation change altered the bound native image result"); + } + if (limit > 1u) { + std::uint64_t previousSerial = 0; + { + const auto range = GuestAllocations::GuestAllocationsLookup_nid_postfix(reinterpret_cast(Texels.data()), false); + previousSerial = range->backingSerial; + } + registered.Resize(registeredBytes); + const auto sameExtentGeneration = GuestAllocations::GuestAllocationsGeneration_nid_postfix(); + { + const auto sameExtentRange = GuestAllocations::GuestAllocationsLookup_nid_postfix(reinterpret_cast(Texels.data()), false); + Require(sameExtentGeneration != generationAfter && sameExtentRange != nullptr && sameExtentRange->backingSerial != previousSerial, "registered window: same-extent registration replacement did not change allocation identity"); + } + auto sameExtentInvocation = AgcDriver::DriverDetail::InvocationFor(snapshot, 0, cachedRequest); + AgcDriver::ShaderMemory sameExtentMemory(cachedRequest.context.memory); + const auto sameExtentCapture = AgcDriver::DriverDetail::CaptureInvocation(sameExtentMemory, snapshot, 0, cachedRequest, sameExtentInvocation); + const auto sameExtentResult = sameExtentInvocation.Materialize(*sameExtentCapture); + Require(sameExtentCapture->snapshot.registeredImages.front().effective == cachedCapture->snapshot.registeredImages.front().effective && sameExtentCapture->snapshot.registeredImages.front().backingSerial != cachedCapture->snapshot.registeredImages.front().backingSerial, "registered window: same-extent replacement lost descriptor equality or allocation identity"); + Require(sameExtentResult != cachedResult, "registered window: allocation-only change reused a stale cached materialization"); + dispatchCaptured(sameExtentResult, sameExtentCapture); + registered.Resize(static_cast(rawGeometry.layerBytes * (baseArray + limit - 1u))); + const auto resizedGeneration = GuestAllocations::GuestAllocationsGeneration_nid_postfix(); + std::size_t resizedBytes = 0; + { + const auto resizedRange = GuestAllocations::GuestAllocationsLookup_nid_postfix(reinterpret_cast(Texels.data()), false); + Require(resizedGeneration != generationAfter && resizedRange != nullptr && resizedRange->bytes < registeredBytes, "registered window: resizing the registration did not change its identity"); + resizedBytes = resizedRange->bytes; + } + auto resizedInvocation = AgcDriver::DriverDetail::InvocationFor(snapshot, 0, cachedRequest); + AgcDriver::ShaderMemory resizedMemory(cachedRequest.context.memory); + const auto resizedCapture = AgcDriver::DriverDetail::CaptureInvocation(resizedMemory, snapshot, 0, cachedRequest, resizedInvocation); + Require(!resizedInvocation.Request().context.layerRowSamples && snapshot.prepared->entries.size() == beforeCapture, "registered window: allocation-only change promoted or added a row-mode artifact"); + Require(resizedCapture->snapshot.imageAllocationGeneration == resizedGeneration, "registered window: recapture reused a stale allocation generation"); + Require(resizedCapture->snapshot.registeredImages.size() == 1u && resizedCapture->snapshot.registeredImages.front().generation == resizedGeneration && resizedCapture->snapshot.registeredImages.front().rangeBytes == resizedBytes, "registered window: recapture reused stale registered-view identity"); + Require(resizedCapture->snapshot.registeredImages.front().provenance == capture->snapshot.registeredImages.front().provenance, "registered window: allocation-only change altered original descriptor provenance"); + Require(resizedCapture->snapshot.registeredImages.front().effective != capture->snapshot.registeredImages.front().effective, "registered window: allocation extent change did not change effective descriptor identity"); + const auto resizedResource = AgcDriver::Graphics::DecodeTextureResource(resizedCapture->snapshot.images.front().dwords); + const auto resizedGeometry = AgcDriver::Graphics::DescribeSurface(resizedResource); + Require(resizedResource.baseArray == baseArray && resizedGeometry.imageLayers - resizedResource.baseArray == limit - 1u, "registered window: recaptured physical geometry disagrees with the effective registered extent"); + const auto resizedMode = ShaderRecompiler::ResourceMaterializer::RuntimeImageMode(resizedCapture->plan->info.images.front(), resizedCapture->snapshot.images.front(), resizedCapture->plan->info.runtimeImageModes.front()); + Require(resizedCapture->plan->info.runtimeImageModes.front().at(resizedMode).dimension == ShaderRecompiler::RdnaImageDimension::Dim1DArray && !resizedCapture->plan->info.runtimeImageModes.front().at(resizedMode).layerRows, "registered window: resized native view changed to a layer-row mode"); + const auto resizedResult = resizedInvocation.Materialize(*resizedCapture); + Require(resizedResult != sameExtentResult, "registered window: changed complete-slice geometry reused a stale cached materialization"); + dispatchCaptured(resizedResult, resizedCapture); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto layer = baseArray + static_cast(std::clamp(std::nearbyint(std::bit_cast(Input[tid * 4u + 1u])), 0.0f, static_cast(limit - 2u))); + const auto x = static_cast(std::clamp(static_cast(std::floor(std::bit_cast(Input[tid * 4u + 0u]) * static_cast(Width))), 0, static_cast(Width) - 1)); + const auto expected = Texel(x, layer); + for (std::uint32_t component = 0; component < expected.size(); ++component) Require(std::fabs(Output[tid * 8u + component] * 255.0f - Unorm(expected[component])) <= 1.0f, "registered window: resized bound geometry disagrees with the current registered slice extent"); + } + registered.Resize(rawGeometry.guestBytes); + const auto fullRangeGeneration = GuestAllocations::GuestAllocationsGeneration_nid_postfix(); + auto fullRowInvocation = AgcDriver::DriverDetail::InvocationFor(snapshot, 0, cachedRequest); + AgcDriver::ShaderMemory fullRowMemory(cachedRequest.context.memory); + const auto fullRowCapture = AgcDriver::DriverDetail::CaptureInvocation(fullRowMemory, snapshot, 0, cachedRequest, fullRowInvocation); + Require(fullRowInvocation.Request().context.layerRowSamples, "registered window: complete over-limit registration did not produce the full-row mode"); + Require(fullRowCapture->snapshot.imageAllocationGeneration == fullRangeGeneration && fullRowCapture->snapshot.registeredImages.size() == 1u && fullRowCapture->snapshot.registeredImages.front().provenance.dwords == texture, "registered window: full-row capture lost the unchanged descriptor or current allocation generation"); + const auto fullRowResource = AgcDriver::Graphics::DecodeTextureResource(fullRowCapture->snapshot.images.front().dwords); + const auto fullRowGeometry = AgcDriver::Graphics::DescribeSurface(fullRowResource); + Require(fullRowResource.baseArray == baseArray && fullRowGeometry.imageLayers - fullRowResource.baseArray == limit + 1u, "registered window: full-row capture did not retain the complete over-limit guest view"); + const auto fullRowMode = ShaderRecompiler::ResourceMaterializer::RuntimeImageMode(fullRowCapture->plan->info.images.front(), fullRowCapture->snapshot.images.front(), fullRowCapture->plan->info.runtimeImageModes.front()); + Require(fullRowCapture->plan->info.runtimeImageModes.front().at(fullRowMode).layerRows, "registered window: complete over-limit capture did not select layer-row mode"); + const auto fullRowResult = fullRowInvocation.Materialize(*fullRowCapture); + Require(fullRowResult == fullRowInvocation.Materialize(*fullRowCapture), "registered window: identical full-row captures did not reuse the cached materialization"); + Require(fullRowResult != resizedResult, "registered window: full-row transition reused the bounded-native cached materialization"); + const auto fullRowPreparedEntries = snapshot.prepared->entries.size(); + registered.Resize(registeredBytes); + const auto reboundGeneration = GuestAllocations::GuestAllocationsGeneration_nid_postfix(); + auto reboundInvocation = AgcDriver::DriverDetail::InvocationFor(snapshot, 0, cachedRequest); + AgcDriver::ShaderMemory reboundMemory(cachedRequest.context.memory); + const auto reboundCapture = AgcDriver::DriverDetail::CaptureInvocation(reboundMemory, snapshot, 0, cachedRequest, reboundInvocation); + Require(reboundCapture->snapshot.imageAllocationGeneration == reboundGeneration && reboundCapture->snapshot.registeredImages.size() == 1u, "registered window: native recapture lost the current bounded allocation generation"); + const auto& reboundView = reboundCapture->snapshot.registeredImages.front(); + Require(reboundView.provenance.dwords == fullRowCapture->snapshot.registeredImages.front().provenance.dwords && reboundView.provenance.dwords == texture && reboundView.effective != fullRowCapture->snapshot.registeredImages.front().effective && reboundView.backingSerial != fullRowCapture->snapshot.registeredImages.front().backingSerial && reboundView.rangeBytes == registeredBytes, "registered window: bounded native recapture reused full-row range identity or changed raw descriptor provenance"); + const auto reboundResource = AgcDriver::Graphics::DecodeTextureResource(reboundCapture->snapshot.images.front().dwords); + const auto reboundGeometry = AgcDriver::Graphics::DescribeSurface(reboundResource); + Require(reboundResource.baseArray == baseArray && reboundGeometry.imageLayers - reboundResource.baseArray == limit, "registered window: bounded native recapture did not restore the device-limit view geometry"); + const auto reboundMode = ShaderRecompiler::ResourceMaterializer::RuntimeImageMode(reboundCapture->plan->info.images.front(), reboundCapture->snapshot.images.front(), reboundCapture->plan->info.runtimeImageModes.front()); + Require(reboundCapture->plan->info.runtimeImageModes.front().at(reboundMode).dimension == ShaderRecompiler::RdnaImageDimension::Dim1DArray && !reboundCapture->plan->info.runtimeImageModes.front().at(reboundMode).layerRows, "registered window: bounded native recapture did not select native 1D-array mode"); + Require(snapshot.prepared->entries.size() == fullRowPreparedEntries, "registered window: bounded native recovery added an unnecessary prepared mode"); + const auto reboundResult = reboundInvocation.Materialize(*reboundCapture); + Require(reboundResult != fullRowResult, "registered window: bounded native recapture reused the full-row materialization"); + dispatchCaptured(reboundResult, reboundCapture); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto layer = baseArray + static_cast(std::clamp(std::nearbyint(std::bit_cast(Input[tid * 4u + 1u])), 0.0f, static_cast(limit - 1u))); + const auto x = static_cast(std::clamp(static_cast(std::floor(std::bit_cast(Input[tid * 4u + 0u]) * static_cast(Width))), 0, static_cast(Width) - 1)); + const auto expected = Texel(x, layer); + for (std::uint32_t component = 0; component < expected.size(); ++component) Require(std::fabs(Output[tid * 8u + component] * 255.0f - Unorm(expected[component])) <= 1.0f, "registered window: bounded native recovery after full-row capture produced incorrect sampled output"); + } + registered.Unregister(); + const auto missingGeneration = GuestAllocations::GuestAllocationsGeneration_nid_postfix(); + auto missingInvocation = AgcDriver::DriverDetail::InvocationFor(snapshot, 0, cachedRequest); + AgcDriver::ShaderMemory missingMemory(cachedRequest.context.memory); + const auto missingCapture = AgcDriver::DriverDetail::CaptureInvocation(missingMemory, snapshot, 0, cachedRequest, missingInvocation); + Require(missingInvocation.Request().context.layerRowSamples && missingCapture->snapshot.imageAllocationGeneration == missingGeneration && missingCapture->snapshot.registeredImages.size() == 1u && !missingCapture->snapshot.registeredImages.front().hasRange && missingCapture->snapshot.registeredImages.front().provenance.dwords == texture, "registered window: missing-range capture did not retain the raw descriptor and current absent-range identity"); + const auto missingResult = missingInvocation.Materialize(*missingCapture); + Require(missingResult != reboundResult, "registered window: missing-range materialization reused the bounded-native cached result"); + registered.Resize(registeredBytes); + const auto restoredGeneration = GuestAllocations::GuestAllocationsGeneration_nid_postfix(); + auto restoredInvocation = AgcDriver::DriverDetail::InvocationFor(snapshot, 0, cachedRequest); + AgcDriver::ShaderMemory restoredMemory(cachedRequest.context.memory); + const auto restoredCapture = AgcDriver::DriverDetail::CaptureInvocation(restoredMemory, snapshot, 0, cachedRequest, restoredInvocation); + Require(restoredCapture->snapshot.imageAllocationGeneration == restoredGeneration && restoredCapture->snapshot.registeredImages.size() == 1u, "registered window: complete registration did not restore native mode after missing-range capture"); + const auto& restoredView = restoredCapture->snapshot.registeredImages.front(); + Require(restoredView.provenance.dwords == texture && restoredView.effective != missingCapture->snapshot.registeredImages.front().effective && restoredView.backingSerial != missingCapture->snapshot.registeredImages.front().backingSerial && restoredView.rangeBytes == registeredBytes && restoredView.hasRange, "registered window: restored native capture reused missing-range identity"); + const auto restoredResource = AgcDriver::Graphics::DecodeTextureResource(restoredCapture->snapshot.images.front().dwords); + const auto restoredGeometry = AgcDriver::Graphics::DescribeSurface(restoredResource); + Require(restoredResource.baseArray == baseArray && restoredGeometry.imageLayers - restoredResource.baseArray == limit, "registered window: restored native capture did not recover bounded view geometry"); + const auto restoredMode = ShaderRecompiler::ResourceMaterializer::RuntimeImageMode(restoredCapture->plan->info.images.front(), restoredCapture->snapshot.images.front(), restoredCapture->plan->info.runtimeImageModes.front()); + Require(restoredCapture->plan->info.runtimeImageModes.front().at(restoredMode).dimension == ShaderRecompiler::RdnaImageDimension::Dim1DArray && !restoredCapture->plan->info.runtimeImageModes.front().at(restoredMode).layerRows, "registered window: restored native capture did not select native 1D-array mode"); + Require(snapshot.prepared->entries.size() == fullRowPreparedEntries, "registered window: missing-range recovery added an unnecessary prepared mode"); + const auto restoredResult = restoredInvocation.Materialize(*restoredCapture); + Require(restoredResult != missingResult && restoredResult != reboundResult, "registered window: restored registration reused stale missing-range or prior-backing materialization"); + dispatchCaptured(restoredResult, restoredCapture); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto layer = baseArray + static_cast(std::clamp(std::nearbyint(std::bit_cast(Input[tid * 4u + 1u])), 0.0f, static_cast(limit - 1u))); + const auto x = static_cast(std::clamp(static_cast(std::floor(std::bit_cast(Input[tid * 4u + 0u]) * static_cast(Width))), 0, static_cast(Width) - 1)); + const auto expected = Texel(x, layer); + for (std::uint32_t component = 0; component < expected.size(); ++component) Require(std::fabs(Output[tid * 8u + component] * 255.0f - Unorm(expected[component])) <= 1.0f, "registered window: native recovery after missing-range capture produced incorrect sampled output"); + } + } +} + +void Expect(std::uint32_t tid, const std::string& name, std::uint32_t component, float scaled, float expected, float tolerance) { + Require(std::fabs(scaled - expected) <= tolerance, name + ": thread " + std::to_string(tid) + " component " + std::to_string(component) + " is " + std::to_string(scaled) + "/255, expected " + std::to_string(expected) + "/255"); +} + +struct Compiled { + std::size_t bindings = 0; + std::uint32_t elements = 0; + ShaderRecompiler::SpirvContentHash spirv; + std::uint64_t registeredVariant = 0; + std::uint32_t registeredElements = 0; + std::uint32_t preparedModes = 0; + std::size_t preparedEntries = 0; + bool promotedAtCapture = false; +}; + +Compiled Run(AgcDriver::VulkanDevice& device, std::uint32_t baseArray, std::uint32_t addressX, std::uint32_t filter, const char* name, const std::array& code = SampleLevelZero) { + Output.fill(-1.0f); + std::vector userData(20, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(sizeof(Input))); + const auto output = BufferDescriptor(Output.data(), static_cast(sizeof(Output))); + const auto texture = TextureDescriptor(baseArray); + const std::array sampler{addressX | (ClampEdge << 3u) | (ClampEdge << 6u), 0u, filter, 0u}; + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + std::copy(texture.begin(), texture.end(), userData.begin() + 8); + std::copy(sampler.begin(), sampler.end(), userData.begin() + 16); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(std::span(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); + static AgcDriver::DriverDetail::ShaderSnapshot zeroSnapshot{reinterpret_cast(SampleLevelZero.data()), 0, 0, {SampleLevelZero.begin(), SampleLevelZero.end()}, {}}; + static AgcDriver::DriverDetail::ShaderSnapshot levelSnapshot{reinterpret_cast(SampleLevel.data()), 0, 0, {SampleLevel.begin(), SampleLevel.end()}, {}}; + auto& snapshot = &code == &SampleLevel ? levelSnapshot : zeroSnapshot; + request.shader.code = snapshot.code; + auto invocation = AgcDriver::DriverDetail::InvocationFor(snapshot, 0, request); + const auto beforeCapture = snapshot.prepared->entries.size(); + AgcDriver::ShaderMemory shaderMemory(request.context.memory); + const auto capture = AgcDriver::DriverDetail::CaptureInvocation(shaderMemory, snapshot, 0, request, invocation); + const auto registered = invocation.Materialize(*capture); + auto prepared = device.PrepareDispatch(*registered, {}, Threads, reinterpret_cast(code.data()), std::nullopt, &capture->snapshot.registeredImages); + device.Dispatch(*registered, 1, 1, 1, {}, reinterpret_cast(code.data()), std::move(prepared), nullptr, std::nullopt, &capture->snapshot.registeredImages); + device.WaitIdle(); + const auto viewed = Layers - baseArray; + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const float u = std::bit_cast(Input[tid * 4u + 0u]); + const auto layer = baseArray + static_cast(std::clamp(std::nearbyint(std::bit_cast(Input[tid * 4u + 1u])), 0.0f, static_cast(viewed - 1u))); + const float* actual = &Output[tid * 8u]; + std::array expected{}; + std::array tolerance{0.01f, 0.01f, 0.01f, 0.01f}; + if (filter == 0u) { + const auto scaledU = static_cast(std::floor(u * static_cast(Width))); + const auto x = addressX == Wrap ? static_cast(((scaledU % static_cast(Width)) + static_cast(Width)) % static_cast(Width)) : static_cast(std::clamp(scaledU, 0, static_cast(Width) - 1)); + const auto texel = Texel(x, layer); + for (std::uint32_t component = 0; component < 4u; ++component) expected[component] = Unorm(texel[component]); + } else { + const float position = u * static_cast(Width) - 0.5f; + const auto left = static_cast(std::floor(position)); + const float weight = position - std::floor(position); + const auto clampX = [](int x) { return static_cast(std::clamp(x, 0, static_cast(Width) - 1)); }; + const auto a = Texel(clampX(left), layer); + const auto b = Texel(clampX(left + 1), layer); + for (std::uint32_t component = 0; component < 4u; ++component) expected[component] = Unorm(a[component]) * (1.0f - weight) + Unorm(b[component]) * weight; + tolerance[0] = 1.0f; + } + for (std::uint32_t component = 0; component < 4u; ++component) Expect(tid, name, component, actual[component] * 255.0f, expected[component], tolerance[component]); + } + Compiled compiled{result.bindings.size(), 0u, result.spirv.ContentHash()}; + for (const auto& binding : result.bindings) compiled.elements += binding.count; + compiled.registeredVariant = registered->variantId; + for (const auto& binding : registered->bindings) compiled.registeredElements += binding.count; + for (const auto& image : capture->plan->info.images) compiled.preparedModes += static_cast(ShaderRecompiler::ResourceMaterializer::RuntimeImageModes(image).size()); + compiled.preparedEntries = snapshot.prepared->entries.size(); + compiled.promotedAtCapture = compiled.preparedEntries > beforeCapture; + std::printf("%s: %zu bindings, %u descriptor elements, SPIR-V %016llx%016llx\n", name, compiled.bindings, compiled.elements, static_cast(compiled.spirv.high), static_cast(compiled.spirv.low)); + std::printf("registered: %u descriptor elements, prepared: %u image modes, variant %llu, capture promotion %u\n", compiled.registeredElements, compiled.preparedModes, static_cast(compiled.registeredVariant), compiled.promotedAtCapture ? 1u : 0u); + return compiled; +} + +} + +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; + const auto limit = device->graphicsContext().limits.maxImageArrayLayers; + Require(limit != 0u, "the device reports no maxImageArrayLayers"); + if (limit >= MaxGuestLayers) { + std::puts("skipped: the device allows every array a T# can describe"); + return VulkanTestSkipped; + } + if (limit > MaxGuestLayers - 2u) { + std::puts("skipped: maxImageArrayLayers + 1 from baseArray 1 exceeds the guest descriptor's 13-bit last-array field"); + return VulkanTestSkipped; + } + const auto stillRowsResult = CaptureStillRowsRegisteredWindow(*device, limit); + if (stillRowsResult == VulkanTestSkipped) return stillRowsResult; + CaptureRegisteredWindow(*device, limit); + Layers = std::min(limit + 3u, MaxGuestLayers); + const auto baseWithin = Layers - limit; + const auto basePast = baseWithin - 1u; + FillTexels(); + FillInput(limit); + const auto atLimit = Run(*device, baseWithin, ClampEdge, 0, "point image_sample_lz on a view of exactly maxImageArrayLayers slices"); + const auto atLimitLevel = Run(*device, baseWithin, ClampEdge, Bilinear, "bilinear image_sample_l at levels 0 to 3.4 on a view of exactly maxImageArrayLayers slices", SampleLevel); + const auto pastLimit = Run(*device, basePast, ClampEdge, 0, "point image_sample_lz on a view of maxImageArrayLayers + 1 slices"); + const auto pastLimitLevel = Run(*device, basePast, ClampEdge, Bilinear, "bilinear image_sample_l at levels 0 to 3.4 on a view of maxImageArrayLayers + 1 slices", SampleLevel); + Require(atLimit.preparedModes < pastLimit.preparedModes && atLimitLevel.preparedModes < pastLimitLevel.preparedModes, "a shader that never saw a view past maxImageArrayLayers was given the layer-row mode"); + Require(!(atLimit.spirv == pastLimit.spirv) && !(atLimitLevel.spirv == pastLimitLevel.spirv), "a view past maxImageArrayLayers was sampled through the shader prepared for views within the limit"); + Require(pastLimit.promotedAtCapture && pastLimitLevel.promotedAtCapture, "capturing the first oversized view did not promote its registered shader"); + Require(atLimit.registeredVariant != pastLimit.registeredVariant && atLimitLevel.registeredVariant != pastLimitLevel.registeredVariant, "registered shaders used the same artifact before and after layer-row promotion"); + const auto repeated = Run(*device, basePast, ClampEdge, 0, "repeated oversized view through its promoted registered shader"); + const auto returned = Run(*device, baseWithin, ClampEdge, 0, "within-limit view through its promoted registered shader"); + const auto repeatedLevel = Run(*device, basePast, ClampEdge, Bilinear, "repeated explicit-LOD oversized view through its promoted registered shader", SampleLevel); + const auto returnedLevel = Run(*device, baseWithin, ClampEdge, Bilinear, "within-limit explicit-LOD view through its promoted registered shader", SampleLevel); + for (const auto* reused : {&repeated, &returned}) { + Require(reused->registeredVariant == pastLimit.registeredVariant && reused->preparedEntries == pastLimit.preparedEntries && !reused->promotedAtCapture, "registered layer-row shader was prepared again instead of reused"); + } + for (const auto* reused : {&repeatedLevel, &returnedLevel}) { + Require(reused->registeredVariant == pastLimitLevel.registeredVariant && reused->preparedEntries == pastLimitLevel.preparedEntries && !reused->promotedAtCapture, "registered explicit-LOD layer-row shader was prepared again instead of reused"); + } + Run(*device, 0, ClampEdge, 0, "point image_sample_lz on a 1D array past maxImageArrayLayers"); + Run(*device, 0, Wrap, 0, "wrapped point image_sample_lz on a 1D array past maxImageArrayLayers"); + Run(*device, 0, ClampEdge, Bilinear, "bilinear image_sample_lz on a 1D array past maxImageArrayLayers"); + Run(*device, 0, ClampEdge, Bilinear, "bilinear image_sample_l at levels 0 to 3.4 on a single-level 1D array past maxImageArrayLayers", SampleLevel); + std::puts("a 1D array past maxImageArrayLayers was sampled as rows of a 2D image"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageArrayWindowPastLayerLimit.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageArrayWindowPastLayerLimit.cpp index d6a5e73941..3185cd2e62 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageArrayWindowPastLayerLimit.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageArrayWindowPastLayerLimit.cpp @@ -1,4 +1,6 @@ #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" #include "Recompiler.hpp" @@ -9,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -16,6 +19,7 @@ namespace { +using AgcDriver::Graphics::DccKeys; using AgcDriver::Graphics::Require; using ShaderRecompiler::ShaderStage; @@ -24,6 +28,7 @@ constexpr std::uint32_t Width = 8; constexpr std::uint32_t Layers = 3; constexpr std::uint32_t BaseArray = 8100; constexpr std::uint32_t LastArray = BaseArray + Layers - 1u; +constexpr std::uint32_t MaxGuestLayers = 8192; constexpr std::uint32_t Format8888UNorm = 56; constexpr std::uint32_t Type1DArray = 12; constexpr std::uint32_t ClampEdge = 2; @@ -35,24 +40,35 @@ constexpr std::size_t TexelAlignment = 65536; std::array TexelStorage{}; const std::span Texels{reinterpret_cast((reinterpret_cast(TexelStorage.data()) + TexelAlignment - 1) & ~(TexelAlignment - 1)), TexelBytes}; +constexpr std::size_t ClearKeyBytes = TexelBytes / 256u; +alignas(256) std::array ClearKeys{}; + alignas(256) constexpr std::array Code{ 0x34020084, 0xe0381000, 0x80000201, 0xbf8c3f70, 0xf09c0f20, 0x00820602, 0xf0001f20, 0x00020a04, 0x34020085, 0xbf8c3f70, 0xe0781000, 0x80010601, 0xe0781010, 0x80010a01, 0xbf810000, }; +alignas(256) constexpr std::array SampleOnlyCode{ + 0x34020084, 0xe0381000, 0x80000201, 0xbf8c3f70, 0xf09c0f20, 0x00820602, 0x34020085, 0xbf8c3f70, + 0xe0781000, 0x80010601, 0xbf810000, +}; + std::array BufferDescriptor(const void* data, std::uint32_t bytes) { const auto address = reinterpret_cast(data); return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; } -std::array TextureDescriptor(std::uint32_t baseArray = BaseArray) { +std::array TextureDescriptor(std::uint32_t baseArray = BaseArray, std::uint32_t lastArray = LastArray, bool dccClear = false) { const auto address = static_cast(reinterpret_cast(Texels.data())); + const auto meta = dccClear ? static_cast(reinterpret_cast(ClearKeys.data())) >> 8u : 0u; return { static_cast(address >> 8u), static_cast((address >> 40u) & 0xffu) | (Format8888UNorm << 20u) | (((Width - 1u) & 3u) << 30u), (Width - 1u) >> 2u, 0xfacu | (Type1DArray << 28u), - LastArray | (baseArray << 16u), 0u, 0u, 0u, + lastArray | (baseArray << 16u), 0u, + dccClear ? (1u << 20u) | (1u << 21u) | (static_cast(meta & 0xffu) << 24u) : 0u, + static_cast(meta >> 8u), }; } @@ -94,18 +110,18 @@ void Expect(std::uint32_t tid, const char* name, const float* actual, const std: } } -void Run(AgcDriver::VulkanDevice& device, std::uint32_t baseArray = BaseArray) { +void Run(AgcDriver::VulkanDevice& device, std::uint32_t baseArray = BaseArray, std::uint32_t lastArray = LastArray, bool dccClear = false) { Output.fill(-1.0f); std::vector userData(20, 0u); const auto input = BufferDescriptor(Input.data(), static_cast(sizeof(Input))); const auto output = BufferDescriptor(Output.data(), static_cast(sizeof(Output))); - const auto texture = TextureDescriptor(baseArray); + const auto texture = TextureDescriptor(baseArray, lastArray, dccClear); const auto sampler = SamplerDescriptor(); std::copy(input.begin(), input.end(), userData.begin()); std::copy(output.begin(), output.end(), userData.begin() + 4); std::copy(texture.begin(), texture.end(), userData.begin() + 8); std::copy(sampler.begin(), sampler.end(), userData.begin() + 16); - const std::span code(Code); + const std::span code = dccClear ? std::span(SampleOnlyCode) : std::span(Code); 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{ @@ -116,14 +132,24 @@ void Run(AgcDriver::VulkanDevice& device, std::uint32_t baseArray = BaseArray) { }; request.useCache = false; const auto result = ShaderRecompiler::Recompile(request); + const auto clearsBefore = AgcDriver::Graphics::SampledTexturesCleared(); device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); device.WaitIdle(); + if (dccClear) { + const auto resource = AgcDriver::Graphics::DecodeTextureResource(texture); + Require(AgcDriver::Graphics::TextureClearKeys(resource, AgcDriver::Graphics::DescribeSurface(resource).guestBytes) == DccKeys::Clear0000, "the 1D array fixture does not expose a DCC zero clear"); + if (std::getenv("APS5_NO_TEXTURE_CACHE") == nullptr) Require(AgcDriver::Graphics::SampledTexturesCleared() == clearsBefore + 1u, "the over-limit DCC row window was not materialized by a GPU clear"); + } 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(LastArray - baseArray)); + 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])); + if (dccClear) { + Expect(tid, "image_sample_lz of a DCC-cleared row window", &Output[tid * 8u], {0, 0, 0, 0}); + } else { + 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])); + } } } @@ -137,6 +163,21 @@ int main() { FillTexels(); FillInput(); Run(*device); + const auto& limits = device->graphicsContext().limits; + const auto limit = limits.maxImageArrayLayers; + Require(limit != 0u, "the device reports no maxImageArrayLayers"); + if (limit >= MaxGuestLayers - 1u) { + std::fprintf(stderr, "skipped: maxImageArrayLayers %u leaves no encodable over-limit array index below %u\n", limit, MaxGuestLayers); + return VulkanTestSkipped; + } + if (limit + 1u > limits.maxImageDimension2D / 2u) { + std::fprintf(stderr, "skipped: maxImageArrayLayers %u needs %u physical rows but maxImageDimension2D %u permits %u\n", limit, 2u * (limit + 1u), limits.maxImageDimension2D, limits.maxImageDimension2D / 2u); + return VulkanTestSkipped; + } + const auto clearResource = AgcDriver::Graphics::DecodeTextureResource(TextureDescriptor(1u, limit + 1u, true)); + const auto clearBytes = AgcDriver::Graphics::DescribeSurface(clearResource).guestBytes; + Require(clearBytes <= Texels.size() && AgcDriver::Graphics::DccKeyBytes(clearBytes) <= ClearKeys.size(), "the over-limit clear fixture exceeds its guest texel or DCC-key storage"); + Run(*device, 1u, limit + 1u, true); try { Run(*device, 0); } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleLodClamp.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleLodClamp.cpp index 1a1fcffbc6..789fac1041 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleLodClamp.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleLodClamp.cpp @@ -1,6 +1,9 @@ #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "IntermediateRepresentation/IrMetadata/ShaderInfo.hpp" +#include "Optimization/ResourceProgram.hpp" +#include "PipelineSpecialization.hpp" #include "Recompiler.hpp" #include "VulkanTestDevice.hpp" #include @@ -9,10 +12,22 @@ #include #include #include +#include #include +#include +#include +#include #include #include +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static bool ImageViewMinLod(const VulkanDevice& device) { return device.graphicsContext().imageViewMinLod; } +}; + +} + namespace { using AgcDriver::Graphics::Require; @@ -30,15 +45,45 @@ constexpr std::uint32_t Type2D = 9; alignas(256) std::array Buffer{}; alignas(256) std::array Texels{}; -alignas(256) constexpr std::array Code{ +alignas(256) constexpr std::array Code{ 0x34020087, 0xe0301000, 0x80000501, 0xe0301004, 0x80000601, 0xe0301008, 0x80000701, 0xe030100c, - 0x80000801, 0xe0301010, 0x80000901, 0xe0301014, 0x80000a01, 0xe0301018, 0x80000b01, 0xbf8c3f70, + 0x80000801, 0xe0301010, 0x80000901, 0xe0301014, 0x80000a01, 0xe0301018, 0x80000b01, 0xe0301010, + 0x80001001, 0xe0301014, 0x80001101, 0xe030101c, 0x80001201, 0xbf8c3f70, 0xf08c0f08, 0x00610c05, 0xe0701020, 0x80000c01, 0xe0701024, 0x80000d01, 0xe0701028, 0x80000e01, 0xe070102c, 0x80000f01, 0xf0840f08, 0x00610c09, 0xe0701030, 0x80000c01, 0xe0701034, 0x80000d01, 0xe0701038, 0x80000e01, 0xe070103c, 0x80000f01, 0x7e0802ff, 0x00000201, 0xf0cc0f08, 0x00610c04, 0xe0701040, 0x80000c01, 0xe0701044, 0x80000d01, 0xe0701048, 0x80000e01, 0xe070104c, 0x80000f01, 0x7e0802ff, 0x00003e3f, 0xf0cc0f08, 0x00610c04, 0xe0701050, 0x80000c01, 0xe0701054, 0x80000d01, - 0xe0701058, 0x80000e01, 0xe070105c, 0x80000f01, 0xbf810000, + 0xe0701058, 0x80000e01, 0xe070105c, 0x80000f01, 0xf0900f08, 0x00610c10, 0xe0701060, 0x80000c01, + 0xe0701064, 0x80000d01, 0xe0701068, 0x80000e01, 0xe070106c, 0x80000f01, 0xf09c0f08, 0x00610c09, + 0xe0701070, 0x80000c01, 0xe0701074, 0x80000d01, 0xe0701078, 0x80000e01, 0xe070107c, 0x80000f01, + 0xbf810000, +}; + +alignas(256) constexpr std::array GradientCode{ + 0x34020087, 0xe0301000, 0x80000501, 0xe0301004, 0x80000601, 0xe0301008, 0x80000701, 0xe030100c, + 0x80000801, 0xe0301010, 0x80000901, 0xe0301014, 0x80000a01, 0xe0301018, 0x80000b01, 0xbf8c3f70, + 0xf08c0f08, 0x00610c05, 0xe0701020, 0x80000c01, 0xe0701024, 0x80000d01, 0xe0701028, 0x80000e01, + 0xe070102c, 0x80000f01, 0xbf810000, +}; + +alignas(256) constexpr std::array PairedCode{ + 0x34020087u, 0xe0301000u, 0x80000501u, 0xe0301004u, 0x80000601u, 0xe0301008u, 0x80000701u, 0xe030100cu, + 0x80000801u, 0xe0301010u, 0x80000901u, 0xe0301014u, 0x80000a01u, 0xbf8c3f70u, 0xf0880f08u, 0x00610c05u, + 0xe0701020u, 0x80000c01u, 0xe0701024u, 0x80000d01u, 0xe0701028u, 0x80000e01u, 0xe070102cu, 0x80000f01u, + 0xf09c0f08u, 0x00810c09u, 0xe0701030u, 0x80000c01u, 0xe0701034u, 0x80000d01u, 0xe0701038u, 0x80000e01u, + 0xe070103cu, 0x80000f01u, 0xf0880f08u, 0x00810c05u, 0xe0701040u, 0x80000c01u, 0xe0701044u, 0x80000d01u, + 0xe0701048u, 0x80000e01u, 0xe070104cu, 0x80000f01u, 0xf09c0f08u, 0x00850c09u, 0xe0701050u, 0x80000c01u, + 0xe0701054u, 0x80000d01u, 0xe0701058u, 0x80000e01u, 0xe070105cu, 0x80000f01u, 0xbf810000u, +}; + +struct Case { + std::uint32_t minLod; + std::uint32_t baseLevel; + std::uint32_t maxLod = 0xfffu; + std::uint32_t mipFilter = 1u; + std::int32_t bias = 0; + std::uint32_t filters = 0; }; struct Sample { @@ -47,10 +92,11 @@ struct Sample { std::uint32_t clamp; std::uint32_t x; std::uint32_t y; + std::int32_t lod; }; Sample SampleOf(std::uint32_t tid) { - return {tid % Levels, (tid / Levels) % Gradients, (tid / 3u) % Clamps, (tid * 7u + 3u) % Size, (tid * 5u + 1u) % Size}; + return {tid % Levels, (tid / Levels) % Gradients, (tid / 3u) % Clamps, (tid * 7u + 3u) % Size, (tid * 5u + 1u) % Size, static_cast((tid / 4u) % Levels) - 1}; } std::uint32_t Bits(float value) { @@ -68,6 +114,7 @@ void FillInput() { words[4] = Bits((static_cast(sample.x) + 0.5f) / static_cast(Size)); words[5] = Bits((static_cast(sample.y) + 0.5f) / static_cast(Size)); words[6] = Bits(static_cast(sample.clamp)); + words[7] = Bits(static_cast(sample.lod)); } } @@ -94,13 +141,13 @@ std::array BufferDescriptor(const void* data, std::uint32_t by return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; } -std::array TextureDescriptor(const void* data) { +std::array TextureDescriptor(const void* data, std::uint32_t minLod, std::uint32_t baseLevel, std::uint32_t lastLevel = Levels - 1u) { const auto address = static_cast(reinterpret_cast(data)); return { static_cast(address >> 8u), - static_cast((address >> 40u) & 0xffu) | (Format8888UNorm << 20u) | (((Size - 1u) & 3u) << 30u), + static_cast((address >> 40u) & 0xffu) | (minLod << 8u) | (Format8888UNorm << 20u) | (((Size - 1u) & 3u) << 30u), ((Size - 1u) >> 2u) | ((Size - 1u) << 14u), - 0xfacu | ((Levels - 1u) << 16u) | (Type2D << 28u), + 0xfacu | (baseLevel << 12u) | (lastLevel << 16u) | (Type2D << 28u), 0u, (Levels - 1u) << 4u, 0u, @@ -108,29 +155,37 @@ std::array TextureDescriptor(const void* data) { }; } -std::array SamplerDescriptor() { - return {0u, 0xfffu << 12u, 1u << 26u, 0u}; +std::array SamplerDescriptor(const Case& item) { + return {0u, item.maxLod << 12u, (static_cast(item.bias) & 0x3fffu) | item.filters | (item.mipFilter << 26u), 0u}; } -void Run(AgcDriver::VulkanDevice& device) { - std::vector userData(16, 0u); - const auto buffer = BufferDescriptor(Buffer.data(), static_cast(Buffer.size() * 4u)); - const auto texture = TextureDescriptor(Texels.data()); - const auto sampler = SamplerDescriptor(); - std::copy(buffer.begin(), buffer.end(), userData.begin()); - std::copy(texture.begin(), texture.end(), userData.begin() + 4); - std::copy(sampler.begin(), sampler.end(), userData.begin() + 12); - const std::span code(Code); - const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; +ShaderRecompiler::RecompileRequest Request(std::span code, const ShaderRecompiler::SpirvTarget& target, const std::vector& userData, std::span memory, bool cached) { 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(), + target, {0, 0, 0, 128} }; - request.useCache = false; - const auto result = ShaderRecompiler::Recompile(request); + request.useCache = cached; + return request; +} + +std::vector UserData(const Case& item) { + std::vector userData(16, 0u); + const auto buffer = BufferDescriptor(Buffer.data(), static_cast(Buffer.size() * 4u)); + const auto texture = TextureDescriptor(Texels.data(), item.minLod, item.baseLevel); + const auto sampler = SamplerDescriptor(item); + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + std::copy(sampler.begin(), sampler.end(), userData.begin() + 12); + return userData; +} + +void Run(AgcDriver::VulkanDevice& device, std::span code, const Case& item, bool cached) { + const auto userData = UserData(item); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const auto result = ShaderRecompiler::Recompile(Request(code, device.Target(), userData, memory, cached)); device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); device.WaitIdle(); } @@ -140,40 +195,226 @@ std::uint32_t Wrap(std::uint32_t coordinate, std::int32_t offset, std::uint32_t return static_cast(((static_cast(coordinate) + offset) % extent + extent) % extent); } -void Check() { - constexpr std::array names{"image_sample_d_cl", "image_sample_cl", "image_sample_d_cl_o (1, 2)", "image_sample_d_cl_o (-1, -2)"}; - constexpr std::array, 4> offsets{{{0, 0}, {0, 0}, {1, 2}, {-1, -2}}}; +std::uint32_t Level(float lambda, float floor, float maxLod, std::uint32_t last, std::uint32_t baseLevel) { + const auto lod = std::min(std::max({lambda, floor, 0.0f}), maxLod); + Require(std::abs(lod - std::floor(lod) - 0.5f) > 1.0f / 512.0f, "image sample LOD clamp: an expected LOD of " + std::to_string(lod) + " sits on a nearest-mip rounding tie, which Vulkan leaves to the implementation"); + return std::min(static_cast(std::ceil(lod + 0.5f)) - 1u, last) + baseLevel; +} + +std::string Describe(const Case& item) { + return "T# MIN_LOD " + std::to_string(item.minLod) + " over BASE_LEVEL " + std::to_string(item.baseLevel) + " with S# MAX_LOD " + std::to_string(item.maxLod) + ", mip filter " + std::to_string(item.mipFilter) + " and LOD bias " + std::to_string(item.bias); +} + +void Check(const Case& item, std::size_t results) { + constexpr std::array names{"image_sample_d_cl", "image_sample_cl", "image_sample_d_cl_o (1, 2)", "image_sample_d_cl_o (-1, -2)", "image_sample_l", "image_sample_lz"}; + constexpr std::array, 6> offsets{{{0, 0}, {0, 0}, {1, 2}, {-1, -2}, {0, 0}, {0, 0}}}; + const std::uint32_t last = Levels - 1u - item.baseLevel; + const auto maxLod = static_cast(item.maxLod) / 256.0f; + const auto view = item.minLod > item.baseLevel * 256u ? static_cast(std::min(item.minLod - item.baseLevel * 256u, last * 256u)) / 256.0f : 0.0f; for (std::uint32_t tid = 0; tid < Threads; ++tid) { const auto sample = SampleOf(tid); - const std::uint32_t clamped = std::max(sample.level, sample.clamp); - const std::array levels{clamped, sample.clamp, clamped, clamped}; - for (std::uint32_t index = 0; index < levels.size(); ++index) { - const std::uint32_t level = levels[index]; + const auto gradient = static_cast(sample.level) - 0.25f - static_cast(item.baseLevel) + static_cast(item.bias) / 256.0f; + const auto floor = std::max(static_cast(sample.clamp), view); + const auto sampled = Level(gradient, floor, maxLod, last, item.baseLevel); + const auto explicitLevel = [&](float lod, float clamp) { + const auto clamped = view > 0.0f ? std::max({lod, clamp, view}) : std::max(lod, clamp); + return Level(clamped + static_cast(item.bias) / 256.0f, 0.0f, maxLod, last, item.baseLevel); + }; + const auto none = -std::numeric_limits::infinity(); + const auto levelOf = [&](std::size_t index) { + switch (index) { + case 1: return explicitLevel(0.0f, static_cast(sample.clamp)); + case 4: return explicitLevel(static_cast(sample.lod), none); + case 5: return explicitLevel(0.0f, none); + default: return sampled; + } + }; + for (std::size_t index = 0; index < results; ++index) { + const std::uint32_t level = levelOf(index); const std::uint32_t size = Size >> level; const std::array expected{level, Wrap(sample.x >> level, offsets[index][0], size), Wrap(sample.y >> level, offsets[index][1], size), 255u}; for (std::uint32_t component = 0; component < 4u; ++component) { const float value = std::bit_cast(Buffer[tid * Words + ResultWord + index * 4u + component]) * 255.0f; - Require(std::lround(value) == static_cast(expected[component]), std::string(names[index]) + ": thread " + std::to_string(tid) + " component " + std::to_string(component) + " is " + std::to_string(value) + ", expected " + std::to_string(expected[component])); + Require(std::lround(value) == static_cast(expected[component]), std::string(names[index]) + " with " + Describe(item) + ": thread " + std::to_string(tid) + " component " + std::to_string(component) + " is " + std::to_string(value) + ", expected " + std::to_string(expected[component])); } } } } +void RunAndCheck(AgcDriver::VulkanDevice& device, std::span code, std::size_t results, const Case& item, bool cached) { + FillInput(); + Run(device, code, item, cached); + Check(item, results); } -int main() { +struct Pairing { + std::uint32_t minLod; + std::uint32_t baseLevel; + std::array held; + std::uint32_t secondMinLod = 0; + std::uint32_t secondLastLevel = 0; +}; + +std::string Describe(const Pairing& item) { + return "T# MIN_LOD " + std::to_string(item.minLod) + " over BASE_LEVEL " + std::to_string(item.baseLevel) + " with a second S# of words 1 " + std::to_string(item.held[1]) + " and 2 " + std::to_string(item.held[2]) + ", and a second T# of MIN_LOD " + std::to_string(item.secondMinLod) + " and LAST_LEVEL " + std::to_string(item.secondLastLevel); +} + +void RunPairing(AgcDriver::VulkanDevice& device, const Pairing& item) { + FillInput(); + std::vector userData(28, 0u); + const auto buffer = BufferDescriptor(Buffer.data(), static_cast(Buffer.size() * 4u)); + const auto texture = TextureDescriptor(Texels.data(), item.minLod, item.baseLevel); + const auto sampler = SamplerDescriptor(Case{0u, 0u}); + const auto second = TextureDescriptor(Texels.data(), item.secondMinLod, 0u, item.secondLastLevel); + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + std::copy(sampler.begin(), sampler.end(), userData.begin() + 12); + std::copy(item.held.begin(), item.held.end(), userData.begin() + 16); + std::copy(second.begin(), second.end(), userData.begin() + 20); + const std::span code(PairedCode); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const auto result = ShaderRecompiler::Recompile(Request(code, device.Target(), userData, memory, false)); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +float Bilinear(std::uint32_t coordinate, std::uint32_t size) { + const auto texel = (static_cast(coordinate) + 0.5f) / static_cast(Size) * static_cast(size) - 0.5f; + const auto low = std::floor(texel); + const auto weight = texel - low; + const auto first = Wrap(0u, static_cast(low), size); + const auto next = Wrap(0u, static_cast(low) + 1, size); + return (1.0f - weight) * static_cast(first) + weight * static_cast(next); +} + +void CheckPairing(const Pairing& item) { + const std::uint32_t last = Levels - 1u - item.baseLevel; + const auto view = item.minLod > item.baseLevel * 256u ? static_cast(std::min(item.minLod - item.baseLevel * 256u, last * 256u)) / 256.0f : 0.0f; + const auto secondView = item.secondLastLevel == 0u ? 0.0f : static_cast(std::min(item.secondMinLod, item.secondLastLevel * 256u)) / 256.0f; + const bool magnifyLinear = ((item.held[2] >> 20u) & 1u) != 0u; + const bool mipLinear = ((item.held[2] >> 26u) & 3u) == 2u; + constexpr std::array names{"image_sample_d through the mipmapped S#", "image_sample_lz through the S# that holds the LOD at 0", "image_sample_d through the S# that holds the LOD at 0", "image_sample_lz of the second T# through the S# that holds the LOD at 0"}; + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto sample = SampleOf(tid); + const auto gradient = static_cast(sample.level) - 0.25f - static_cast(item.baseLevel); + const std::array levels{Level(gradient, view, 4095.0f / 256.0f, last, item.baseLevel), Level(view, 0.0f, static_cast(last), last, item.baseLevel), Level(view, 0.0f, static_cast(last), last, item.baseLevel), Level(secondView, 0.0f, static_cast(item.secondLastLevel), item.secondLastLevel, 0u)}; + for (std::size_t index = 0; index < levels.size(); ++index) { + const auto level = levels[index]; + const auto size = Size >> level; + const auto read = [&](std::uint32_t component) { return std::bit_cast(Buffer[tid * Words + ResultWord + index * 4u + component]) * 255.0f; }; + const auto fail = [&](std::uint32_t component, float expected) { return std::string(names[index]) + " with " + Describe(item) + ": thread " + std::to_string(tid) + " component " + std::to_string(component) + " is " + std::to_string(read(component)) + ", expected " + std::to_string(expected); }; + if (index != 0u && index != 3u && mipLinear) { + const auto blended = static_cast(item.baseLevel) + view; + const auto lower = static_cast(std::floor(blended)); + const auto weight = blended - static_cast(lower); + const std::array expected{blended, (1.0f - weight) * static_cast(sample.x >> lower) + weight * static_cast(sample.x >> (lower + 1u)), (1.0f - weight) * static_cast(sample.y >> lower) + weight * static_cast(sample.y >> (lower + 1u))}; + for (std::uint32_t component = 0; component < expected.size(); ++component) Require(std::abs(read(component) - expected[component]) < 1.0f, fail(component, expected[component])); + continue; + } + const bool filtered = index != 0u && magnifyLinear; + const std::array expected{static_cast(level), filtered ? Bilinear(sample.x, size) : static_cast(sample.x >> level), filtered ? Bilinear(sample.y, size) : static_cast(sample.y >> level), 255.0f}; + for (std::uint32_t component = 0; component < 4u; ++component) Require(std::abs(read(component) - expected[component]) < (filtered ? 1.0f : 0.5f), fail(component, expected[component])); + } + } +} + +void RequireRejected(AgcDriver::VulkanDevice& device, const Pairing& item, const std::string& reason) { try { + RunPairing(device, item); + } catch (const std::exception& error) { + Require(std::string(error.what()).find(reason) != std::string::npos, Describe(item) + " was rejected with \"" + error.what() + "\", expected \"" + reason + "\""); + return; + } + Require(false, Describe(item) + " ran in the shader-clamp lane, which cannot apply its clamp as the view clamp does"); +} + +void RequireRejected(AgcDriver::VulkanDevice& device, const Case& item, const std::string& reason, std::span code = Code) { + FillInput(); + try { + Run(device, code, item, false); + } catch (const std::exception& error) { + Require(std::string(error.what()).find(reason) != std::string::npos, Describe(item) + " was rejected with \"" + error.what() + "\", expected \"" + reason + "\""); + return; + } + Require(false, Describe(item) + " ran in the shader-clamp lane, which cannot apply its clamp as the view clamp does"); +} + +bool MinLodSpecId(std::uint32_t id) { + return id >= ShaderRecompiler::PipelineSpecialization::MinLodBase && id < ShaderRecompiler::PipelineSpecialization::MinLodBase + ShaderRecompiler::ShaderInfo::MaxImages; +} + +std::set MinLodSpecIds(std::span spirv) { + std::set ids; + for (std::size_t word = 5; word < spirv.size();) { + const auto count = spirv[word] >> 16u; + if (count == 0u) break; + if ((spirv[word] & 0xffffu) == spv::OpDecorate && count >= 4u && spirv[word + 2u] == spv::DecorationSpecId) { + if (MinLodSpecId(spirv[word + 3u])) ids.insert(spirv[word + 3u]); + } + word += count; + } + return ids; +} + +void CheckSpirvLanes(const ShaderRecompiler::SpirvTarget& deviceTarget) { + const Case item{0x200u, 0u}; + const auto userData = UserData(item); + const std::span code(Code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + auto target = deviceTarget; + target.shaderImageMinLod = false; + const auto view = ShaderRecompiler::PrepareShader(Request(code, target, userData, memory, true)); + Require(MinLodSpecIds(ShaderRecompiler::GetPreparedArtifact(*view).spirv.Words()).empty(), "a target without the shader clamp declares minimum LOD specialization constants"); + target.shaderImageMinLod = true; + const auto shader = ShaderRecompiler::PrepareShader(Request(code, target, userData, memory, true)); + Require(MinLodSpecIds(ShaderRecompiler::GetPreparedArtifact(*shader).spirv.Words()) == std::set{ShaderRecompiler::PipelineSpecialization::MinLodBase}, "a target with the shader clamp does not declare exactly the image's minimum LOD specialization constant"); +} + +} + +int main(int argc, char** argv) { + try { + const bool shaderLane = argc > 1 && std::strcmp(argv[1], "--shader-lane") == 0; const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; if (!TargetHasCapability(device->Target(), spv::CapabilityMinLod)) { std::puts("skipped, the device has no shaderResourceMinLod"); return VulkanTestSkipped; } - FillInput(); + const bool shaderClamp = device->Target().shaderImageMinLod; + if (shaderLane && !shaderClamp) { + std::puts("skipped, the device does not apply T# minimum LODs in the shader under APS5_NO_IMAGE_VIEW_MIN_LOD and APS5_NO_GPL"); + return VulkanTestSkipped; + } + if (!shaderClamp && !AgcDriver::VulkanDeviceTestAccess::ImageViewMinLod(*device)) { + std::puts("skipped, the device has no VK_EXT_image_view_min_lod, and graphics pipeline libraries keep T# minimum LODs out of the shader"); + return VulkanTestSkipped; + } + CheckSpirvLanes(device->Target()); FillTexture(); - Run(*device); - Check(); - std::puts("image sample LOD clamp tests passed"); + for (const auto& item : {Case{0u, 0u}, Case{0x200u, 0u}, Case{0x200u, 1u}, Case{0x200u, 0u, 0x300u}}) RunAndCheck(*device, Code, 6u, item, false); + if (!shaderClamp) RunAndCheck(*device, GradientCode, 1u, Case{0x100u, 1u, 0xfffu, 1u, -128}, false); + if (!shaderClamp) RunAndCheck(*device, GradientCode, 1u, Case{0x200u, 0u, 0xfffu, 1u, -256}, false); + if (!shaderClamp) RunAndCheck(*device, Code, 6u, Case{0u, 0u, 0xfffu, 1u, 256}, false); + RunAndCheck(*device, Code, 6u, Case{0x100u, 1u, 0xfffu, 1u, -96}, false); + for (const auto& item : {Case{0x200u, 0u}, Case{0u, 0u}, Case{0x300u, 0u}, Case{0x200u, 1u}, Case{0x100u, 0u}}) RunAndCheck(*device, Code, 6u, item, true); + for (const auto& item : {Pairing{0x300u, 0u, {0u, 0u, 0u, 0u}}, Pairing{0x300u, 1u, {0u, 0u, 1u << 26u, 0u}}, Pairing{0x300u, 0u, {0u, 0u, (1u << 20u) | (1u << 22u), 0u}}, Pairing{0x300u, 0u, {0u, 0u, 0x3f00u, 0u}}, Pairing{0x300u, 0u, {0u, 0u, 0u, 0u}, 0x300u, 4u}}) { + RunPairing(*device, item); + CheckPairing(item); + } + if (shaderClamp) { + for (const auto& item : {Case{0x1c0u, 0u}, Case{0x2c0u, 1u}}) RunAndCheck(*device, Code, 6u, item, false); + RunAndCheck(*device, GradientCode, 1u, Case{0x1c0u, 0u}, false); + for (const auto& item : {Case{0x1c0u, 0u}, Case{0u, 0u}, Case{0x2c0u, 1u}}) RunAndCheck(*device, Code, 6u, item, true); + RequireRejected(*device, Case{0x200u, 0u, 0x100u}, "above the maximum LOD of 256/256 of S# 0, which is not implemented"); + RunPairing(*device, Pairing{0x140u, 0u, {0u, 0u, 2u << 26u, 0u}}); + CheckPairing(Pairing{0x140u, 0u, {0u, 0u, 2u << 26u, 0u}}); + RequireRejected(*device, Pairing{0x300u, 0u, {0u, 0u, 0u, 0u}, 0x100u, 4u}, "which holds the LOD at 0, also samples image 1 with another minimum LOD, which is not implemented"); + RequireRejected(*device, Case{0x200u, 0u, 0xfffu, 1u, -256}, "to an explicit LOD that S# 0 biases by -256/256, which is not implemented"); + RequireRejected(*device, Case{0x200u, 0u, 0xfffu, 1u, 64}, "to an explicit LOD that S# 0 biases by 64/256, which is not implemented"); + RequireRejected(*device, Case{0x200u, 0u, 0xfffu, 1u, 0, 1u << 20u}, "and S# 0 has different magnification and minification filters"); + } + std::puts(shaderClamp ? "image sample LOD clamp tests passed in the shader-clamp lane" : "image sample LOD clamp tests passed in the image-view lane"); return 0; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleReduction.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleReduction.cpp index e2f8d2dc66..c5fa541b6d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleReduction.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleReduction.cpp @@ -236,7 +236,7 @@ void Gather(AgcDriver::VulkanDevice& device, const Sampler& sampler, const char* } } -std::uint32_t PairedSamplers(const ShaderRecompiler::RecompileResult& result, std::uint32_t format) { +std::uint64_t PairedSamplers(const ShaderRecompiler::RecompileResult& result, std::uint32_t format) { for (const auto& binding : result.bindings) { if (binding.kind != ShaderRecompiler::DescriptorKind::SampledImage) continue; for (std::uint32_t element = 0; element < binding.count; ++element) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTableCacheBounds.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTableCacheBounds.cpp index 104fb882b6..322b5b2fdd 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTableCacheBounds.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTableCacheBounds.cpp @@ -20,6 +20,13 @@ #else #include #endif +#ifdef __linux__ +#include +#include +#include +#include +#include +#endif #include #include #include @@ -31,7 +38,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -124,6 +133,93 @@ std::vector Pm4(std::uint32_t opcode, std::initializer_list dispatchesSubmitted{0}; +std::atomic dispatchesIdle{0}; + +#ifdef __linux__ +void PrintThreadStack(int) { + void* frames[48]; + const int count = backtrace(frames, 48); + backtrace_symbols_fd(frames, count, STDERR_FILENO); +} + +std::string FirstLine(const std::string& path) { + std::ifstream file(path); + std::string line; + std::getline(file, line); + return line; +} +#endif + +void PrintStalledThreads() { +#ifdef __linux__ + void* preload[1]; + backtrace(preload, 1); + struct sigaction action {}; + action.sa_handler = PrintThreadStack; + sigemptyset(&action.sa_mask); + sigaction(SIGUSR2, &action, nullptr); + const auto self = static_cast(syscall(SYS_gettid)); + DIR* tasks = opendir("/proc/self/task"); + if (!tasks) return; + while (const dirent* entry = readdir(tasks)) { + const int thread = std::atoi(entry->d_name); + if (thread <= 0) continue; + const auto base = std::string("/proc/self/task/") + entry->d_name; + const auto stat = FirstLine(base + "/stat"); + const auto state = stat.rfind(") ") == std::string::npos ? std::string("?") : stat.substr(stat.rfind(") ") + 2, 1); + std::fprintf(stderr, "thread %d %s state=%s syscall=%s\n", thread, FirstLine(base + "/comm").c_str(), state.c_str(), FirstLine(base + "/syscall").c_str()); + if (thread == self) continue; + syscall(SYS_tgkill, getpid(), thread, SIGUSR2); + usleep(50000); + } + closedir(tasks); +#endif +} + +struct StallWatchdog { + std::atomic finished{false}; + std::atomic reporterExited{false}; + std::thread reporter; + std::thread hardStop; + + StallWatchdog(int seconds, int graceSeconds) { + const auto start = std::chrono::steady_clock::now(); + reporter = std::thread([this, seconds, start] { + const auto deadline = start + std::chrono::seconds(seconds); + while (!finished.load(std::memory_order_acquire)) { + if (std::chrono::steady_clock::now() > deadline) { + std::fprintf(stderr, "image table vouched lookups: no result after %d s, %u dispatches submitted and %u idle\n", seconds, dispatchesSubmitted.load(), dispatchesIdle.load()); + PrintStalledThreads(); + std::fflush(stderr); + std::_Exit(1); + } + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + reporterExited.store(true, std::memory_order_release); + }); + hardStop = std::thread([this, seconds, graceSeconds, start] { +#ifdef __linux__ + sigset_t blocked; + sigemptyset(&blocked); + sigaddset(&blocked, SIGUSR2); + pthread_sigmask(SIG_BLOCK, &blocked, nullptr); +#endif + const auto deadline = start + std::chrono::seconds(seconds + graceSeconds); + while (!reporterExited.load(std::memory_order_acquire)) { + if (std::chrono::steady_clock::now() > deadline) std::_Exit(1); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + }); + } + + ~StallWatchdog() { + finished.store(true, std::memory_order_release); + reporter.join(); + hardStop.join(); + } +}; + void Dispatch(const GuestBlock& block, std::size_t slot, std::size_t kernelOffset = 0, std::vector words = {}) { const auto kernel = block.At(kernelOffset); const auto srt = block.At(SrtOffset + slot * SrtBytes); @@ -131,8 +227,10 @@ void Dispatch(const GuestBlock& block, std::size_t slot, std::size_t kernelOffse words.insert(words.end(), packet.begin(), packet.end()); } Packet dcb{words.data(), static_cast(words.size()), 0, {}}; + ++dispatchesSubmitted; Require(sceAgcDriverSubmitDcb(&dcb) == 0, "image table cache bounds: the dispatch submission failed"); AgcDriverWaitIdle_nid_postfix(); + ++dispatchesIdle; } void Prepare(const GuestBlock& block) { @@ -473,6 +571,8 @@ bool ImportsStayWatchedThroughReads(const AgcDriver::Graphics::Context& context) } int main(int argc, char** argv) { + std::unique_ptr watchdog; + if (argc == 2 && std::string(argv[1]) == "--vouched-lookups") watchdog = std::make_unique(30, 15); SetBudgets(); try { { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LabelAfterImportCopy.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LabelAfterImportCopy.cpp new file mode 100644 index 0000000000..09e8d2f17d --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LabelAfterImportCopy.cpp @@ -0,0 +1,97 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; + +constexpr std::size_t CopyBytes = 0x40000; +constexpr std::size_t LabelOffset = 2 * CopyBytes; +constexpr std::size_t BlockBytes = LabelOffset + 0x10000; +constexpr int Rounds = 32; + +std::uint64_t AddressOf(const volatile void* data) { + return static_cast(reinterpret_cast(data)); +} + +std::uint8_t Pattern(int round, std::size_t at) { + return static_cast(0x31u + (at + static_cast(round) * 7u) % 67u); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + std::vector storage(BlockBytes + 0x10000u); + auto* block = reinterpret_cast((reinterpret_cast(storage.data()) + 0xffffu) & ~std::uintptr_t{0xffffu}); + auto* source = block; + auto* destination = block + CopyBytes; + auto* label = reinterpret_cast(block + LabelOffset); + std::memset(block, 0, BlockBytes); + { + GuestAllocations::Mutation mutation; + mutation.Add(block, BlockBytes, true, true, true); + } + std::vector seen(CopyBytes); + for (int round = 0; round < Rounds; ++round) { + for (std::size_t at = 0; at < CopyBytes; ++at) source[at] = Pattern(round, at); + std::memset(destination, 0xa5, CopyBytes); + *label = 0; + const std::uint32_t value = 0x600d0001u + static_cast(round); + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + const auto outcome = device->CopyBuffer(AddressOf(destination), AddressOf(source), CopyBytes, 0, std::numeric_limits::max(), 0, 0, 0, [](std::span, std::uint64_t) {}); + if (outcome.path != 1) { + std::printf("skipped, the copy took path %d instead of the GPU transfer\n", outcome.path); + return VulkanTestSkipped; + } + const int reason = device->WriteLabelOnGpu(AddressOf(label), std::as_bytes(std::span(&value, 1)), static_cast(round) + 1u, 0); + if (reason != 0) { + std::printf("skipped, the label was not recorded on the GPU (reason %d)\n", reason); + return VulkanTestSkipped; + } + device->SubmitRecorded(true); + } + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + while (*label != value) { + Require(std::chrono::steady_clock::now() < deadline, "the label after the copy never landed"); + std::this_thread::yield(); + } + std::memcpy(seen.data(), destination, CopyBytes); + std::size_t stale = 0; + for (std::size_t at = 0; at < CopyBytes; ++at) stale += seen[at] != Pattern(round, at); + Require(stale == 0, "round " + std::to_string(round) + ": a CPU read after the label that followed the copy found " + std::to_string(stale) + " of " + std::to_string(CopyBytes) + " destination bytes not yet copied"); + device->WaitIdle(); + } + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + AgcDriver::Graphics::FlushCachedTextures(device->Device()); + } + device->WaitIdle(); + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } + std::puts("label after import copy 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/LinearReadbackLabel.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LinearReadbackLabel.cpp index 36105a9e93..7091f5ddea 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/LinearReadbackLabel.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LinearReadbackLabel.cpp @@ -37,17 +37,22 @@ constexpr std::uint32_t Type2D = 9; constexpr std::uint32_t Format8888Uint = 60; constexpr std::uint32_t TexelBytes = 4; constexpr std::uint32_t IdentitySwizzle = 0xfacu; -constexpr std::size_t BlockBytes = 0x20000; +constexpr std::size_t BlockBytes = 0x200000; constexpr std::size_t LabelOffset = 0x8000; constexpr std::uint32_t LabelValue = 0x5eedf00du; constexpr std::uint32_t TiledThreads = 8; constexpr std::uint32_t TiledSide = 16; +constexpr std::uint32_t OccasionalSide = 256; +constexpr std::size_t OccasionalOffset = 0x100000; +constexpr std::size_t OccasionalLabelOffset = 0x8200; constexpr std::uint32_t Format32Uint = 20; constexpr std::uint32_t TileR64KBX = 0x1b; constexpr std::size_t TiledOffset = 0x10000; constexpr std::size_t TiledLabelOffset = 0x8100; constexpr std::uint32_t Untouched = 0xdeadbeefu; constexpr std::uint32_t Written = 0x12345678u; +constexpr std::uint32_t Rewritten = 0x0badcafeu; +constexpr std::uint32_t WrittenAgain = 0x600dd00du; alignas(256) constexpr std::array StoreCode{ 0x4a500081, 0xd5690014, 0x0201ff28, 0x9e3779b1, 0x4a2a28ff, 0x7f4a7c15, 0x4a2c28ff, 0xfe94f82a, @@ -59,6 +64,14 @@ alignas(256) constexpr std::array TiledStoreCode{ 0x7e040300, 0x7e060280, 0x7e0202ff, Written, 0xf0200108, 0x00020102, 0xbf810000, }; +alignas(256) constexpr std::array RewriteStoreCode{ + 0x7e040300, 0x7e060280, 0x7e0202ff, Rewritten, 0xf0200108, 0x00020102, 0xbf810000, +}; + +alignas(256) constexpr std::array AgainStoreCode{ + 0x7e040300, 0x7e060280, 0x7e0202ff, WrittenAgain, 0xf0200108, 0x00020102, 0xbf810000, +}; + class GuestBlock { public: GuestBlock() { @@ -125,12 +138,12 @@ std::array UserData(const void* texels) { return data; } -std::array TiledDescriptor(const void* texels) { +std::array TiledDescriptor(const void* texels, std::uint32_t side = TiledSide) { const auto address = static_cast(reinterpret_cast(texels)); return { static_cast(address >> 8u), - static_cast((address >> 40u) & 0xffu) | (Format32Uint << 20u) | (((TiledSide - 1u) & 3u) << 30u), - ((TiledSide - 1u) >> 2u) | ((TiledSide - 1u) << 14u), + static_cast((address >> 40u) & 0xffu) | (Format32Uint << 20u) | (((side - 1u) & 3u) << 30u), + ((side - 1u) >> 2u) | ((side - 1u) << 14u), IdentitySwizzle | (TileR64KBX << 20u) | (Type2D << 28u), 0u, 0u, @@ -139,9 +152,9 @@ std::array TiledDescriptor(const void* texels) { }; } -std::array TiledUserData(const void* texels) { +std::array TiledUserData(const void* texels, std::uint32_t side = TiledSide) { std::array data{}; - const auto texture = TiledDescriptor(texels); + const auto texture = TiledDescriptor(texels, side); std::copy(texture.begin(), texture.end(), data.begin() + 8); return data; } @@ -215,6 +228,36 @@ void ReadTiledAfterLabel(std::uint8_t* block) { Require(written == TiledThreads, "tiled readback label: " + std::to_string(written) + " texels of a small tiled image read after the label hold the value the dispatch before it stored, expected " + std::to_string(TiledThreads)); } +std::uint32_t CountHolding(const std::uint32_t* texels, std::size_t bytes, std::uint32_t value) { + const volatile std::uint32_t* cpu = texels; + std::uint32_t count = 0; + for (std::size_t index = 0; index < bytes / 4u; ++index) count += cpu[index] == value ? 1u : 0u; + return count; +} + +void ReadOccasionalAfterLabel(std::uint8_t* block) { + using AgcDriver::Graphics::StorageTexture; + auto* texels = reinterpret_cast(block + OccasionalOffset); + auto* label = reinterpret_cast(block + OccasionalLabelOffset); + const auto bytes = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(TiledDescriptor(texels, OccasionalSide))).guestBytes; + Require(bytes > StorageTexture::SmallStoreBytes && bytes <= StorageTexture::OccasionalStoreBytes && bytes <= BlockBytes - OccasionalOffset, "occasional readback label: the test image is not an occasional store's size"); + std::fill(texels, texels + bytes / 4u, Untouched); + const auto userData = TiledUserData(texels, OccasionalSide); + const auto stored = StorageTexture::OccasionalLabelStores(); + SubmitAndWait(Commands(TiledStoreCode, TiledThreads, userData, label), label, "occasional readback label"); + Require(CountHolding(texels, bytes, Written) == TiledThreads, "occasional readback label: the first write of an image was not stored before its label"); + Require(StorageTexture::OccasionalLabelStores() == stored + 1u, "occasional readback label: the first write of an image was not chosen for its label"); + SubmitAndWait(Commands(RewriteStoreCode, TiledThreads, userData, label), label, "occasional readback label"); + Require(StorageTexture::OccasionalLabelStores() == stored + 1u, "occasional readback label: a second write with no flip between was chosen for its label"); + StorageTexture::NoteFlip(); + SubmitAndWait(Commands(RewriteStoreCode, TiledThreads, userData, label), label, "occasional readback label"); + Require(StorageTexture::OccasionalLabelStores() == stored + 1u, "occasional readback label: an image written on consecutive flips was chosen for its label"); + for (std::uint64_t flip = 0; flip <= StorageTexture::OccasionalWriteFlips; ++flip) StorageTexture::NoteFlip(); + SubmitAndWait(Commands(AgainStoreCode, TiledThreads, userData, label), label, "occasional readback label"); + Require(CountHolding(texels, bytes, WrittenAgain) == TiledThreads, "occasional readback label: a write after a long gap was not stored before its label"); + Require(StorageTexture::OccasionalLabelStores() == stored + 2u, "occasional readback label: a write after a long gap was not chosen for its label"); +} + } int main() { @@ -226,6 +269,7 @@ int main() { GuestBlock block; ReadAfterLabel(block.Data()); ReadTiledAfterLabel(block.Data()); + ReadOccasionalAfterLabel(block.Data()); AgcDriverWaitIdle_nid_postfix(); AgcDriverShutdown_nid_postfix(); std::puts("linear readback label tests passed"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ManyDirectSamplers.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ManyDirectSamplers.cpp new file mode 100644 index 0000000000..c2bdd9a4be --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ManyDirectSamplers.cpp @@ -0,0 +1,229 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t Height = 4; +constexpr std::uint32_t Lanes = 64; +constexpr std::uint32_t Samplers = 17; +constexpr std::uint32_t Last = Samplers - 1u; +constexpr std::uint32_t SamplerWord = 32; +constexpr std::uint32_t Format32UInt = 20; +constexpr std::uint32_t Format32Float = 22; +constexpr std::uint32_t Format8888UNorm = 56; +constexpr std::uint32_t Format8888Srgb = 130; +constexpr std::uint32_t Type2D = 9; +constexpr std::uint32_t ForceDegamma = 1u << 20u; +constexpr std::uint32_t Elements = Last * 2u + 1u; +constexpr std::uint64_t FloatElements = (std::uint64_t{1} << Elements) - 1u; +constexpr std::uint64_t IntegerElements = (std::uint64_t{1} << (Last * 2u)) - 1u; +constexpr std::array PointClamp{0x92u, (4u * 256u) << 12u, 0u, 0u}; +constexpr std::array OpaqueBlackBorder{0x1b6u, (4u * 256u) << 12u, 0u, 1u << 30u}; +constexpr std::array OpaqueWhiteBorder{0x1b6u, (4u * 256u) << 12u, 0u, 2u << 30u}; + +alignas(256) std::array FloatTexels{}; +alignas(256) std::array ColorTexels{}; +alignas(256) std::array UintTexels{}; +alignas(256) std::array Output{}; +alignas(256) std::array Srt{}; + +std::vector SamplerCode(bool lastOutside) { + std::vector code{0xf4080700u, 0xfa000020u, 0xf4080900u, 0xfa000040u, 0xf4080a00u, 0xfa000050u}; + code.insert(code.end(), {0x360200bfu, 0x7e020d01u, 0x060202f0u, 0x100202ffu, 0x3c800000u, 0x7e0402ffu, 0x3e000000u, 0x061402f4u, 0x7e160302u, 0x7e220280u}); + for (std::uint32_t sampler = 0; sampler < Samplers; ++sampler) { + code.insert(code.end(), {0xf4080800u, 0xfa000000u | ((SamplerWord + sampler * 4u) * 4u)}); + if (sampler == Last) { + code.insert(code.end(), {0xf09c8f08u, lastOutside ? 0x01090c0au : 0x01090c01u}); + continue; + } + code.insert(code.end(), {0xf09c8f08u, 0x01090401u, sampler == 0u ? 0x7e100304u : 0x06100908u, 0xf09c8f08u, 0x010a0401u, 0x4a222304u}); + } + code.insert(code.end(), {0x7e240308u, 0x7e120202u, 0x34201286u, 0x4a202100u, 0x34202084u, 0xe0781000u, 0x80070c10u, 0xe0741800u, 0x80071110u, 0xbf810000u}); + return code; +} + +const std::vector InsideCode = SamplerCode(false); +const std::vector OutsideCode = SamplerCode(true); + +std::array Buffer(const void* base, std::uint32_t bytes) { + const auto address = static_cast(reinterpret_cast(base)); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::array Image(const void* texels, std::uint32_t format, std::uint32_t bcSwizzle = 0u) { + const auto address = static_cast(reinterpret_cast(texels)); + return {static_cast(address >> 8u), static_cast((address >> 40u) & 0xffu) | (format << 20u) | (((Width - 1u) & 3u) << 30u), ((Width - 1u) >> 2u) | ((Height - 1u) << 14u) | (1u << 31u), 0xfacu | (bcSwizzle << 25u) | (Type2D << 28u)}; +} + +struct Outcome { + std::vector lanes; + std::vector integerSums; + std::vector floatSums; + std::vector> samplers; + std::vector imageSamplers; +}; + +Outcome Run(AgcDriver::VulkanDevice& device, const std::vector& code, const std::array& image, const std::array& last) { + Srt = {}; + const auto output = Buffer(Output.data(), static_cast(sizeof(Output))); + std::copy(output.begin(), output.end(), Srt.begin() + 8); + const auto integerImage = Image(UintTexels.data(), Format32UInt); + std::copy(image.begin(), image.end(), Srt.begin() + 16); + std::copy(integerImage.begin(), integerImage.end(), Srt.begin() + 20); + for (std::uint32_t sampler = 0; sampler < Samplers; ++sampler) { + const auto& words = sampler == Last ? last : PointClamp; + std::copy(words.begin(), words.end(), Srt.begin() + SamplerWord + sampler * 4u); + } + const auto rootAddress = reinterpret_cast(Srt.data()); + const std::array userData{static_cast(rootAddress), static_cast(rootAddress >> 32u)}; + const std::span span(code); + const std::array memory{{ + {reinterpret_cast(span.data()), std::as_bytes(span)}, + {reinterpret_cast(Srt.data()), std::as_bytes(std::span(Srt))}, + }}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Lanes, 1, 1}, 0u, {true, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(span.data()), span, 0, {}}, + {64, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + Outcome outcome; + for (const auto& binding : result.bindings) { + if (binding.role == ShaderRecompiler::DescriptorRole::GuestSamplers) { + for (std::size_t word = 0; word + 4u <= binding.guestDescriptor.size(); word += 4u) outcome.samplers.push_back({binding.guestDescriptor[word], binding.guestDescriptor[word + 1u], binding.guestDescriptor[word + 2u], binding.guestDescriptor[word + 3u]}); + } + if (binding.kind == ShaderRecompiler::DescriptorKind::SampledImage) outcome.imageSamplers.insert(outcome.imageSamplers.end(), binding.imageSamplers.begin(), binding.imageSamplers.end()); + } + Output.fill(-1.0f); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(span.data())); + device.WaitIdle(); + outcome.lanes.assign(Output.begin(), Output.begin() + Lanes * 4u); + for (std::uint32_t lane = 0; lane < Lanes; ++lane) { + outcome.integerSums.push_back(std::bit_cast(Output[512u + lane * 4u])); + outcome.floatSums.push_back(Output[513u + lane * 4u]); + } + return outcome; +} + +std::string Refusal(AgcDriver::VulkanDevice& device, const std::vector& code, const std::array& image, const std::array& last) { + try { + static_cast(Run(device, code, image, last)); + } catch (const std::exception& error) { + return error.what(); + } + return {}; +} + +float Component(const Outcome& outcome, std::uint32_t lane, std::uint32_t component) { + return outcome.lanes.at(lane * 4u + component); +} + +float FirstSamples(float texel) { + float sum = texel; + for (std::uint32_t sampler = 1; sampler < Last; ++sampler) sum += texel; + return sum; +} + +void Fill() { + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + FloatTexels[y * Width + x] = static_cast(x) + 100.0f * static_cast(y) + 0.25f; + ColorTexels[y * Width + x] = 0x1000u + x * 3u + 0x100u * y; + UintTexels[y * Width + x] = 0x2000u + x * 5u + 0x100u * y; + } + } +} + +void CheckPairing(AgcDriver::VulkanDevice& device) { + const auto outcome = Run(device, InsideCode, Image(FloatTexels.data(), Format32Float), OpaqueWhiteBorder); + Require(outcome.samplers.size() == Elements, "many direct samplers: the sampler binding holds " + std::to_string(outcome.samplers.size()) + " elements, expected " + std::to_string(Elements)); + const auto& words = outcome.samplers[Elements - 1u]; + Require(words == OpaqueWhiteBorder, "many direct samplers: sampler element " + std::to_string(Elements - 1u) + " does not hold the last S#"); + std::vector expected{FloatElements, IntegerElements}; + auto paired = outcome.imageSamplers; + std::sort(paired.begin(), paired.end()); + std::sort(expected.begin(), expected.end()); + Require(paired == expected, "many direct samplers: the float image is not paired with sampler elements 0 to " + std::to_string(Elements - 1u) + " and the integer image with 0 to " + std::to_string(Elements - 2u)); + for (std::uint32_t lane = 0; lane < Lanes; ++lane) Require(outcome.integerSums[lane] == Last * UintTexels[lane % Width], "many direct samplers: lane " + std::to_string(lane) + " summed " + std::to_string(outcome.integerSums[lane]) + " from the integer image through the 16 point copies, expected " + std::to_string(Last * UintTexels[lane % Width])); + std::printf("many direct samplers: %u samplers bound as %zu sampler elements; the float image is paired with elements 0 to %u, the last sampler's at element %u, and the integer image with 0 to %u through the point copies\n", Samplers, outcome.samplers.size(), Elements - 1u, Elements - 1u, Elements - 2u); +} + +void CheckBorder(AgcDriver::VulkanDevice& device) { + const auto image = Image(FloatTexels.data(), Format32Float); + const auto border = Run(device, OutsideCode, image, OpaqueWhiteBorder); + const auto clamped = Run(device, OutsideCode, image, PointClamp); + const auto edge = FloatTexels[Width - 1u]; + for (std::uint32_t lane = 0; lane < Lanes; ++lane) { + const auto expected = FirstSamples(FloatTexels[lane % Width]); + Require(std::bit_cast(border.floatSums[lane]) == std::bit_cast(expected), "many direct samplers: lane " + std::to_string(lane) + " summed " + std::to_string(border.floatSums[lane]) + " through samplers 0 to 15, expected " + std::to_string(expected)); + Require(Component(border, lane, 0) == 1.0f && Component(border, lane, 3) == 1.0f, "many direct samplers: lane " + std::to_string(lane) + " read " + std::to_string(Component(border, lane, 0)) + ", alpha " + std::to_string(Component(border, lane, 3)) + " outside the image through sampler 16, expected its opaque white border"); + Require(Component(clamped, lane, 0) == edge, "many direct samplers: the clamped control of lane " + std::to_string(lane) + " read " + std::to_string(Component(clamped, lane, 0)) + ", expected the edge texel " + std::to_string(edge)); + } + std::puts("many direct samplers: sampler 16, at sampler element 32, reads its opaque white border outside the image; a clamping S# there reads the edge texel"); +} + +void CheckDegamma(AgcDriver::VulkanDevice& device) { + const auto image = Image(ColorTexels.data(), Format8888UNorm); + auto forced = PointClamp; + forced[0] |= ForceDegamma; + const auto last = Run(device, InsideCode, image, forced); + const auto srgb = Run(device, InsideCode, Image(ColorTexels.data(), Format8888Srgb), PointClamp); + const auto plain = Run(device, InsideCode, image, PointClamp); + for (std::uint32_t lane = 0; lane < Lanes; ++lane) { + for (std::uint32_t component = 0; component < 4u; ++component) { + Require(std::bit_cast(Component(last, lane, component)) == std::bit_cast(Component(srgb, lane, component)), "many direct samplers: lane " + std::to_string(lane) + " component " + std::to_string(component) + " sampled " + std::to_string(Component(last, lane, component)) + " with FORCE_DEGAMMA on sampler 16, " + std::to_string(Component(srgb, lane, component)) + " from the same texels as 8_8_8_8_SRGB"); + } + Require(std::bit_cast(last.floatSums[lane]) == std::bit_cast(srgb.floatSums[lane]), "many direct samplers: lane " + std::to_string(lane) + " summed " + std::to_string(last.floatSums[lane]) + " with FORCE_DEGAMMA on sampler 16, " + std::to_string(srgb.floatSums[lane]) + " from the same texels as 8_8_8_8_SRGB"); + Require(Component(last, lane, 1) < Component(plain, lane, 1), "many direct samplers: lane " + std::to_string(lane) + " green " + std::to_string(Component(last, lane, 1)) + " with FORCE_DEGAMMA on sampler 16 is not decoded below " + std::to_string(Component(plain, lane, 1))); + } + std::puts("many direct samplers: FORCE_DEGAMMA on sampler 16, at sampler element 32, decodes the paired 8_8_8_8_UNORM image as 8_8_8_8_SRGB"); +} + +void CheckBorderSwizzle(AgcDriver::VulkanDevice& device) { + const auto alphaImage = Image(FloatTexels.data(), Format32Float, 1u); + const auto control = Refusal(device, InsideCode, alphaImage, PointClamp); + Require(control.empty(), "many direct samplers: an alpha-moving BC swizzle without an opaque black border was refused: " + control); + std::puts("many direct samplers: an alpha-moving BC swizzle paired with a point-clamp S# on sampler 16 is planned"); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const auto limits = device->graphicsContext().limits; + if (limits.maxPerStageDescriptorSamplers < Elements || limits.maxDescriptorSetSamplers < Elements) { + std::printf("skipped, the device binds fewer than %u samplers per stage or set\n", Elements); + return VulkanTestSkipped; + } + Fill(); + CheckPairing(*device); + CheckBorder(*device); + CheckDegamma(*device); + CheckBorderSwizzle(*device); + std::puts("many direct samplers 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/MeshProvokingVertex.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/MeshProvokingVertex.cpp index 1cb6bea34a..da21e4b249 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/MeshProvokingVertex.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/MeshProvokingVertex.cpp @@ -25,6 +25,7 @@ constexpr std::uint32_t Height = 64; using Image = std::array; alignas(256) Image Pixels{}; alignas(256) constexpr std::array Indices{0, 1, 2, 3}; +alignas(256) constexpr std::array FanIndices{1, 0, 2, 3}; alignas(256) constexpr std::array, 4> Vertices{{ {-0.9f, -0.8f, 0, 1, 1, 0, 0, 1}, {-0.1f, -0.8f, 0, 1, 0, 1, 0, 1}, {-0.5f, 0.8f, 0, 1, 0, 0, 1, 1}, {0.3f, 0.8f, 0, 1, 1, 1, 0, 1} }}; @@ -65,7 +66,7 @@ void Write(std::vector& commands, std::uint32_t opcode, std::uint commands.insert(commands.end(), {0xc0010000u | (opcode << 8u), offset, value}); } -Image Draw(const std::uint32_t* pixelCode, bool flat, bool last, bool strip = false) { +Image Draw(const std::uint32_t* pixelCode, bool flat, bool last, std::uint32_t primitive = 4u) { Pixels.fill(std::byte{0}); const auto color = reinterpret_cast(Pixels.data()); const auto geometry = reinterpret_cast(GeometryCode.data()); @@ -99,13 +100,13 @@ Image Draw(const std::uint32_t* pixelCode, bool flat, bool last, bool strip = fa {0x8, static_cast(pixel >> 8u)}, {0x9, static_cast(pixel >> 40u)}, {0xb, 0} }; std::vector commands; - Write(commands, 0x79, 0x242, strip ? 6u : 4u); + Write(commands, 0x79, 0x242, primitive); Write(commands, 0x79, 0x25b, (12u << 9u) | 4u); for (const auto& [offset, value] : context) Write(commands, 0x69, offset, value); for (const auto& [offset, value] : shaders) Write(commands, 0x76, offset, value); commands.insert(commands.end(), {0xc0002a00u, 0u}); - const auto address = reinterpret_cast(Indices.data()); - const std::uint32_t count = strip ? 4u : 3u; + const auto address = reinterpret_cast(primitive == 5u ? FanIndices.data() : Indices.data()); + const std::uint32_t count = primitive == 4u ? 3u : 4u; commands.insert(commands.end(), {0xc0042700u, count, static_cast(address), static_cast(address >> 32u), count, 0u}); Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; Require(sceAgcDriverSubmitDcb(&packet) == 0, "mesh submission failed"); @@ -145,12 +146,24 @@ void CheckFlat(const Image& image, std::span pipelines; }; +std::vector SingleSampleFetchShader(std::uint32_t width, std::uint32_t height) { + auto code = FetchShader(width, height, 1u, false, true); + for (std::size_t word = 5u; word < code.size();) { + const auto count = code[word] >> 16u; + const auto opcode = code[word] & 0xffffu; + if (opcode == spv::OpTypeImage && count > 6u) code[word + 6u] = 0u; + if (opcode == spv::OpImageFetch && count > 5u && code[word + 5u] == spv::ImageOperandsSampleMask) code[word + 5u] = spv::ImageOperandsLodMask; + word += count != 0u ? count : 1u; + } + return code; +} + std::array DepthSampleWords(const DepthTarget& target) { const auto base = (target.address | target.pipeBankXor) >> 8u; const auto metadata = target.htileAddress >> 8u; @@ -1194,6 +1206,25 @@ void CheckDepthTarget(Context context, VkFormat format, std::uint32_t fragments, RecordMemoryBarrier(context, arrayBatch.Handle(), VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); arrayBatch.SubmitAndWait(); check(true, offset); + const auto firstSample = DepthSurfaceFirstSample(context, words, DecodeTextureResource(words), VkComponentMapping{}); + Require(firstSample != nullptr && DepthSurfaceFirstSample(context, words, DecodeTextureResource(words), VkComponentMapping{}) == firstSample, "sample 0 of the native depth target was not read into one reused 2D image"); + recorder.Sync(); + std::memset(output.Bytes().data(), 0xa5, output.Bytes().size()); + { + const Fetch firstFetch(context, *firstSample, output, SingleSampleFetchShader(extent.width, extent.height)); + CommandBatch firstBatch(context); + firstFetch.Run(firstBatch.Handle(), extent.width * extent.height); + RecordMemoryBarrier(context, firstBatch.Handle(), VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); + firstBatch.SubmitAndWait(); + } + output.Invalidate(); + for (std::uint32_t index = 0u; index < extent.width * extent.height; ++index) { + std::array result{}; + std::memcpy(result.data(), output.Bytes().data() + (index + 1u) * 16u, 16u); + const auto expected = offset + 1.0f / 16.0f; + const auto tolerance = format == VK_FORMAT_D16_UNORM ? 1.0f / 65535.0f : 0.0f; + Require(std::abs(result[0] - expected) <= tolerance && result[1] == 0.0f && result[2] == 0.0f && result[3] == 1.0f, "a 2D read of the native depth target did not return its sample 0 at pixel " + std::to_string(index) + ": " + std::to_string(result[0]) + ", expected " + std::to_string(expected)); + } } if (clearing) { for (std::uint32_t iteration = 0u; iteration < 3u; ++iteration) { @@ -1880,8 +1911,10 @@ void CheckAliasedStencil(Context context, std::uint32_t fragments, AliasCase whi read(std::nullopt); const auto stencilWords = StencilSampleWords(target); TargetRefuses([&] { DepthSurfaceView(context, target, true, true); }, stencilAliasRefusal); + TargetRefuses([&] { DepthSurfaceViewWithStencilWriteIntent(context, target, true, true, false); }, stencilAliasRefusal); TargetRefuses([&] { CachedSampledTexture(context, stencilWords); }, stencilAliasRefusal); Require(DepthSurfaceView(context, target) == view && CachedSampledTexture(context, words) == texture, "a depth-only use of a target whose stencil plane is unproven was refused or replaced its view"); + Require(DepthSurfaceViewWithStencilWriteIntent(context, target, true, false, false) == view, "a depth-only bind of an unproven stencil plane was refused or replaced its view"); if (!filled) { recorder.Sync(); std::memset(output.Bytes().data(), 0xa5, output.Bytes().size()); @@ -2541,6 +2574,97 @@ int CheckDriverPath(bool queued) { } +void CheckUniformPlanesWithoutHtile(Context context, std::uint32_t samples) { + constexpr VkExtent2D extent{129u, 67u}; + const auto depthBytes = static_cast(MultisampledDepthBytes_nid_no_patch(extent, 4u, samples)); + const auto stencilBytes = static_cast(MultisampledDepthBytes_nid_no_patch(extent, 1u, samples)); + TargetMemory memory(depthBytes), stencil(stencilBytes); + const auto fill = [&](float depth, std::uint8_t value) { + const auto word = std::bit_cast(depth); + for (std::size_t offset = 0u; offset < depthBytes; offset += 4u) std::memcpy(memory.Data() + offset, &word, 4u); + std::memset(stencil.Data(), value, stencilBytes); + }; + DepthTarget target{memory.Address(), stencil.Address(), extent, VK_FORMAT_D32_SFLOAT_S8_UINT, 1.0f, 0u, 0u, false, samples}; + target.depthSwizzle = 24u; + Recorder recorder(context); + recorder.Activate(); + auto words = DepthSampleWords(target); + words[6] &= ~((1u << 19u) | (1u << 21u)); + auto stencilTarget = target; + stencilTarget.address = target.stencilAddress; + auto stencilWords = DepthSampleWords(stencilTarget); + stencilWords[1] = (stencilWords[1] & ~(0x1ffu << 20u)) | (5u << 20u); + stencilWords[6] &= ~((1u << 19u) | (1u << 21u)); + const auto lookup = [&] { return DepthSurfaceTexture(context, words, DecodeTextureResource(words), VkComponentMapping{}); }; + const auto clear = [&] { + recorder.Sync(); + ClearCachedTextures(context.device); + ClearDepthSurfaces(context.device); + }; + struct Cleanup { + decltype(clear)& action; + ~Cleanup() { action(); } + } cleanup{clear}; + for (std::uint32_t field = 0u; field < 5u; ++field) { + fill(0.25f, 0x5au); + auto invalid = target; + std::string_view reason; + switch (field) { + case 0u: memory.Data()[depthBytes - 1u] = std::byte{0x3f}; reason = "does not hold one depth value"; break; + case 1u: fill(2.0f, 0x5au); reason = "does not hold one depth value"; break; + case 2u: stencil.Data()[stencilBytes - 1u] = std::byte{0x5b}; reason = "does not hold one stencil value"; break; + case 3u: invalid.htileStencil = true; reason = "stencil import"; break; + case 4u: invalid.stencilAddress = memory.Address() + 65536u; reason = "overlap"; break; + } + Require(DepthSurfaceView(context, invalid) != VK_NULL_HANDLE, "a refused uniform-plane import must preserve attachment rendering"); + TargetRefuses([&] { DepthSurfaceTexture(context, words, DecodeTextureResource(words), VkComponentMapping{}); }, reason); + clear(); + } + for (const auto& [depth, value] : std::array{std::pair{0.0f, std::uint8_t{0xc3u}}, std::pair{0.25f, std::uint8_t{0x5au}}}) { + fill(depth, value); + Require(DepthSurfaceView(context, target) != VK_NULL_HANDLE, "a uniform-plane D32S8 target without HTILE has no attachment view"); + auto texture = lookup(); + Require(texture != nullptr && texture->View() != VK_NULL_HANDLE, "a uniform-plane D32S8 target without HTILE was not sampled natively"); + Buffer output(context, (extent.width * extent.height * samples + 2u) * 16u, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + std::memset(output.Bytes().data(), 0xa5, output.Bytes().size()); + { + const Fetch fetch(context, *texture, output, FetchShader(extent.width, extent.height, samples, false, true)); + CommandBatch batch(context); + fetch.Run(batch.Handle(), extent.width * extent.height * samples); + RecordMemoryBarrier(context, batch.Handle(), VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); + batch.SubmitAndWait(); + output.Invalidate(); + for (std::uint32_t i = 0u; i < extent.width * extent.height * samples; ++i) { + std::array sampled{}; + std::memcpy(sampled.data(), output.Bytes().data() + (i + 1u) * 16u, 16u); + Require(sampled == std::array{depth, 0.0f, 0.0f, 1.0f}, "a uniform depth plane without HTILE was not imported into every sample"); + } + } + auto stencilTexture = DepthSurfaceTexture(context, stencilWords, DecodeTextureResource(stencilWords), VkComponentMapping{}); + Require(stencilTexture != nullptr && stencilTexture->View() != VK_NULL_HANDLE, "the stencil plane of a uniform-plane D32S8 target without HTILE was not sampled natively"); + { + std::memset(output.Bytes().data(), 0xa5, output.Bytes().size()); + const Fetch fetch(context, *stencilTexture, output, FetchShader(extent.width, extent.height, samples, false, false)); + CommandBatch batch(context); + fetch.Run(batch.Handle(), extent.width * extent.height * samples); + RecordMemoryBarrier(context, batch.Handle(), VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); + batch.SubmitAndWait(); + output.Invalidate(); + for (std::uint32_t i = 0u; i < extent.width * extent.height * samples; ++i) { + std::array sampled{}; + std::memcpy(sampled.data(), output.Bytes().data() + (i + 1u) * 16u, 16u); + Require(sampled[0] == value, "a uniform stencil plane without HTILE was not imported into every sample: sample " + std::to_string(i) + " holds " + std::to_string(sampled[0]) + ", the plane " + std::to_string(value)); + } + } + stencilTexture.reset(); + texture.reset(); + stencil.Data()[0] = std::byte{static_cast(value + 1u)}; + TargetRefuses([&] { lookup(); }, "guest writes"); + clear(); + } + std::printf("Native D32S8 without HTILE, samples=%u: uniform planes imported and sampled, other planes refused\n", samples); +} + int main(int argc, char** argv) { try { #ifdef __linux__ @@ -2585,6 +2709,16 @@ int main(int argc, char** argv) { } return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--uniform-planes") { + std::uint32_t checked = 0u, unavailable = 0u; + for (const auto samples : {2u, 4u, 8u}) { + if (!DepthSupported(context, HostDepthFormat(context, VK_FORMAT_D32_SFLOAT_S8_UINT), samples)) { ++unavailable; continue; } + CheckUniformPlanesWithoutHtile(context, samples); + ++checked; + } + std::printf("Native depth uniform-plane cases checked=%u unavailable=%u\n", checked, unavailable); + return checked != 0u ? 0 : VulkanTestSkipped; + } if (argc == 2 && std::string_view(argv[1]) == "--clears") { CheckClearDispatch(); std::uint32_t checked = 0u, unavailable = 0u; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/OracleRunner.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/OracleRunner.cpp new file mode 100644 index 0000000000..67f5e059a5 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/OracleRunner.cpp @@ -0,0 +1,110 @@ +#include "../../tools/OracleDispatch.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr std::uint32_t Threads = 64; +constexpr std::uint32_t ExtraBase = 0x5a000000; +constexpr std::uint32_t ExtraStep = 0x00010203; + +constexpr std::array Text{ + 0xf4080100, 0xfa000000, 0x34020084, 0x34040086, 0xbf8cc07f, 0xdc388000, 0x04040001, 0xbf8c3f70, + 0x7e140280, 0x7e160280, 0x7e180280, 0x7e1a0280, 0x7e1c0280, 0x7e1e0280, 0x7e200280, 0x7e220280, + 0x7e240280, 0x7e260280, 0x7e280280, 0x7e2a0280, 0x7e2c0280, 0x7e2e0280, 0x7e300280, 0x7e320280, + 0x34500082, 0x4a5050ff, 0x00000400, 0xdc308000, 0x0b040028, 0xbf8c3f70, 0x64140b04, 0xdc788000, + 0x00060a02, 0xdc788010, 0x00060e02, 0xdc788020, 0x00061202, 0xdc788030, 0x00061602, 0xbf810000, +}; + +struct Row { + std::uint32_t a; + std::uint32_t b; + std::uint32_t expected; + bool denormal; +}; + +constexpr std::array Rows{{ + {0x3c00, 0x3c00, 0x4000, false}, + {0x0001, 0x0001, 0x0002, true}, + {0x4200, 0x4400, 0x4700, false}, + {0x3c00, 0xbc00, 0x0000, false}, + {0x7bff, 0x3c00, 0x7bff, false}, + {0x7bff, 0x7bff, 0x7c00, false}, +}}; + +void Require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +void Rejects(const std::string& label, AgcDriver::VulkanDevice& device, const OracleKernel& kernel, std::uint32_t threads, std::span input) { + try { + RunOracleKernel(device, kernel, threads, input); + } catch (const std::runtime_error&) { + return; + } + throw std::runtime_error(label + ": accepted"); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + OracleKernel kernel{}; + kernel.text.assign(Text.begin(), Text.end()); + kernel.ldsBytes = 4096; + kernel.waveSize = 32; + kernel.floatMode = {0xc0u, true, false, false}; + kernel.groupIdEnable = {true, false, false}; + kernel.tgSizeEnable = false; + kernel.threadIdComponentCount = 1; + + constexpr std::uint32_t threads = Threads; + std::vector input(threads * 16 + threads * 4); + for (std::uint32_t lane = 0; lane < threads; ++lane) { + const auto& row = Rows[lane % Rows.size()]; + const std::array words{row.a, row.b, 0, 0}; + std::memcpy(input.data() + lane * 16, words.data(), sizeof(words)); + const std::uint32_t extra = ExtraBase + lane * ExtraStep; + std::memcpy(input.data() + threads * 16 + lane * 4, &extra, sizeof(extra)); + } + const auto output = RunOracleKernel(*device, kernel, threads, input); + Require(output.size() == threads * 64, "output size is not 64 bytes per lane"); + const bool keepsDenormals = device->Target().float16DenormPreserve; + std::uint32_t flushedDenormalRows = 0; + for (std::uint32_t lane = 0; lane < threads; ++lane) { + std::array dwords{}; + std::memcpy(dwords.data(), output.data() + lane * 64, sizeof(dwords)); + const auto& row = Rows[lane % Rows.size()]; + if (dwords[0] != row.expected && row.denormal && !keepsDenormals && dwords[0] == 0u) { + ++flushedDenormalRows; + } else { + Require(dwords[0] == row.expected, "lane " + std::to_string(lane) + " v10 is " + std::to_string(dwords[0]) + ", expected " + std::to_string(row.expected)); + } + Require(dwords[1] == ExtraBase + lane * ExtraStep, "lane " + std::to_string(lane) + " v11 is " + std::to_string(dwords[1]) + ", expected its extra dword " + std::to_string(ExtraBase + lane * ExtraStep)); + for (std::size_t index = 2; index < dwords.size(); ++index) { + Require(dwords[index] == 0, "lane " + std::to_string(lane) + " v" + std::to_string(10 + index) + " is not 0"); + } + } + + if (flushedDenormalRows != 0) std::printf("the device does not preserve f16 denormals and flushed the denormal row to 0 in %u lanes; that row is not checked there\n", flushedDenormalRows); + + Rejects("zero threads", *device, kernel, 0, input); + Rejects("too many threads", *device, kernel, 1025, input); + Rejects("threads not a wave multiple", *device, kernel, 48, input); + Rejects("short input", *device, kernel, threads, std::span(input).first(threads * 16 - 1)); + std::puts("oracle runner passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PipelineStatisticsReport.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PipelineStatisticsReport.cpp new file mode 100644 index 0000000000..aca22b8262 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PipelineStatisticsReport.cpp @@ -0,0 +1,270 @@ +#include "VulkanTestDevice.hpp" +#include "../../tools/ComputePipelineFixture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#else +#include +#endif + +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} + +namespace { + +using AgcDriver::Graphics::Require; + +class StderrCapture { +public: + StderrCapture() : path(std::filesystem::temp_directory_path() / ("aps5_pipeline_statistics_" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + ".txt")) { + std::fflush(stderr); +#ifdef _WIN32 + saved = _dup(_fileno(stderr)); + Require(saved >= 0, "cannot save stderr"); + Require(_wfreopen(path.wstring().c_str(), L"wb", stderr) != nullptr, "cannot redirect stderr"); +#else + saved = dup(fileno(stderr)); + Require(saved >= 0, "cannot save stderr"); + Require(std::freopen(path.c_str(), "wb", stderr) != nullptr, "cannot redirect stderr"); +#endif + } + + ~StderrCapture() { + restore(); + std::error_code error; + std::filesystem::remove(path, error); + } + + StderrCapture(const StderrCapture&) = delete; + StderrCapture& operator=(const StderrCapture&) = delete; + + std::string Finish() { + restore(); + std::ifstream file(path, std::ios::binary); + return {std::istreambuf_iterator(file), std::istreambuf_iterator()}; + } + +private: + void restore() { + if (saved < 0) return; + std::fflush(stderr); +#ifdef _WIN32 + _dup2(saved, _fileno(stderr)); + _close(saved); +#else + dup2(saved, fileno(stderr)); + close(saved); +#endif + saved = -1; + } + + std::filesystem::path path; + int saved = -1; +}; + +std::unique_ptr OpenDevice() { + try { + return OpenVulkanTestDevice(); + } catch (const std::exception& error) { + if (std::string_view(error.what()).find("APS5_PIPELINE_STATS requires") == std::string_view::npos) throw; + std::printf("skipped, the device reports no pipeline executable statistics: %s\n", error.what()); + return nullptr; + } +} + +struct StatisticsPipeline { + explicit StatisticsPipeline(const AgcDriver::Graphics::Context& context) : context(context) { + const auto spirv = AgcDriver::ComputePipelineFixture().spirv; + VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; + moduleInfo.codeSize = spirv.size() * sizeof(std::uint32_t); + moduleInfo.pCode = spirv.data(); + Require(context.Function("vkCreateShaderModule")(context.device, &moduleInfo, nullptr, &module) == VK_SUCCESS, "vkCreateShaderModule"); + VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; + Require(context.Function("vkCreatePipelineLayout")(context.device, &layoutInfo, nullptr, &layout) == VK_SUCCESS, "vkCreatePipelineLayout"); + VkComputePipelineCreateInfo pipelineInfo{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; + pipelineInfo.flags = VK_PIPELINE_CREATE_CAPTURE_STATISTICS_BIT_KHR; + pipelineInfo.stage = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}; + pipelineInfo.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT; + pipelineInfo.stage.module = module; + pipelineInfo.stage.pName = "main"; + pipelineInfo.layout = layout; + Require(context.Function("vkCreateComputePipelines")(context.device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &pipeline) == VK_SUCCESS, "vkCreateComputePipelines"); + } + + ~StatisticsPipeline() { + context.Function("vkDestroyPipeline")(context.device, pipeline, nullptr); + context.Function("vkDestroyPipelineLayout")(context.device, layout, nullptr); + context.Function("vkDestroyShaderModule")(context.device, module, nullptr); + } + + StatisticsPipeline(const StatisticsPipeline&) = delete; + StatisticsPipeline& operator=(const StatisticsPipeline&) = delete; + + AgcDriver::Graphics::Context context; + VkShaderModule module = VK_NULL_HANDLE; + VkPipelineLayout layout = VK_NULL_HANDLE; + VkPipeline pipeline = VK_NULL_HANDLE; +}; + +std::vector ExecutableShape(const AgcDriver::Graphics::Context& context, VkPipeline pipeline) { + const auto getProperties = context.Function("vkGetPipelineExecutablePropertiesKHR"); + const auto getStatistics = context.Function("vkGetPipelineExecutableStatisticsKHR"); + VkPipelineInfoKHR pipelineInfo{VK_STRUCTURE_TYPE_PIPELINE_INFO_KHR}; + pipelineInfo.pipeline = pipeline; + std::uint32_t executables = 0; + Require(getProperties(context.device, &pipelineInfo, &executables, nullptr) == VK_SUCCESS, "vkGetPipelineExecutablePropertiesKHR count"); + std::vector shape; + for (std::uint32_t index = 0; index < executables; ++index) { + VkPipelineExecutableInfoKHR executable{VK_STRUCTURE_TYPE_PIPELINE_EXECUTABLE_INFO_KHR}; + executable.pipeline = pipeline; + executable.executableIndex = index; + std::uint32_t statistics = 0; + Require(getStatistics(context.device, &executable, &statistics, nullptr) == VK_SUCCESS, "vkGetPipelineExecutableStatisticsKHR count"); + shape.push_back(statistics); + } + return shape; +} + +std::vector StatisticLines(const std::string& text) { + std::istringstream stream(text); + std::vector lines; + for (std::string line; std::getline(stream, line);) { + if (line.find("[pipeline-stats]") == std::string::npos) continue; + Require(line.starts_with("[pipeline-stats] executable=") && line.find("[pipeline-stats]", 1) == std::string::npos, "a [pipeline-stats] line was split by another report"); + lines.push_back(std::move(line)); + } + return lines; +} + +void CheckShape(const std::vector& report, const std::vector& shape) { + Require(!shape.empty(), "the pipeline reports no executables"); + std::size_t line = 0; + for (std::size_t executable = 0; executable < shape.size(); ++executable) { + const auto prefix = "[pipeline-stats] executable=" + std::to_string(executable) + " "; + Require(line < report.size() && report[line].starts_with(prefix + "stages=0x"), "an executable's header is missing from the reference report"); + ++line; + for (std::size_t statistic = 0; statistic < shape[executable]; ++statistic, ++line) { + Require(line < report.size() && report[line].starts_with(prefix) && !report[line].starts_with(prefix + "stages="), "an executable's statistics in the reference report differ from the queried ones"); + } + } + Require(line == report.size(), "the reference report has lines past its executables"); +} + +std::size_t CountReports(const std::vector& lines, const std::vector& reference) { + Require(!reference.empty(), "the reference report is empty"); + for (std::size_t line = 0; line < lines.size(); ++line) { + Require(lines[line] == reference[line % reference.size()], "a pipeline statistics report was interleaved with another or lost a line"); + } + Require(lines.size() % reference.size() == 0, "the last pipeline statistics report is incomplete"); + return lines.size() / reference.size(); +} + +template +bool Rejects(TCheck check) { + try { + check(); + } catch (const std::exception&) { + return true; + } + return false; +} + +void CheckParser() { + const std::vector reference = { + "[pipeline-stats] executable=0 stages=0x1 subgroup=32 name=VS", + "[pipeline-stats] executable=0 Register Count=24 (registers)", + "[pipeline-stats] executable=0 Binary Size=4096 (bytes)", + "[pipeline-stats] executable=0 Stack Size=0 (bytes)", + "[pipeline-stats] executable=1 stages=0x10 subgroup=32 name=FS", + "[pipeline-stats] executable=1 Output Colors=1 (colors)", + }; + std::string report; + for (const auto& line : reference) report += line + "\n"; + CheckShape(StatisticLines(report), {3, 1}); + Require(Rejects([&] { CheckShape(StatisticLines(report), {1, 3}); }), "the parser accepted a report against another executable shape"); + Require(CountReports(StatisticLines("[gpu] other output\n" + report + report + report), reference) == 3, "the parser did not count three intact two-executable reports"); + const auto split = report.substr(0, report.find(" (registers)")) + reference[0] + "\n (registers)\n" + report.substr(report.find(" (registers)") + 13); + Require(Rejects([&] { CountReports(StatisticLines(split + report), reference); }), "the parser accepted a split line"); + auto missing = reference; + missing.erase(missing.begin() + 2); + std::string lost; + for (const auto& line : missing) lost += line + "\n"; + Require(Rejects([&] { CountReports(StatisticLines(lost + report), reference); }), "the parser accepted a report that lost a line"); + std::string interleaved; + for (std::size_t line = 0; line < reference.size(); ++line) interleaved += reference[line] + "\n" + reference[line] + "\n"; + Require(Rejects([&] { CountReports(StatisticLines(interleaved), reference); }), "the parser accepted two interleaved reports"); + Require(Rejects([&] { CountReports(StatisticLines(report + reference[0] + "\n"), reference); }), "the parser accepted an incomplete last report"); +} + +} + +int main() { + try { + CheckParser(); + std::puts("pipeline statistics: the parser accepts intact two-executable reports and rejects split, lost, interleaved and incomplete ones"); +#ifdef _WIN32 + Require(_putenv_s("APS5_PIPELINE_STATS", "1") == 0, "cannot set APS5_PIPELINE_STATS"); +#else + Require(setenv("APS5_PIPELINE_STATS", "1", 1) == 0, "cannot set APS5_PIPELINE_STATS"); +#endif + const auto device = OpenDevice(); + if (!device) return VulkanTestSkipped; + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(*device); + Require(context.pipelineExecutableInfo, "APS5_PIPELINE_STATS=1 did not enable pipeline executable statistics"); + const StatisticsPipeline pipeline(context); + std::vector reference; + { + StderrCapture output; + AgcDriver::Graphics::LogPipelineStatistics_nid_no_patch(context, pipeline.pipeline); + reference = StatisticLines(output.Finish()); + } + const auto shape = ExecutableShape(context, pipeline.pipeline); + CheckShape(reference, shape); + constexpr unsigned threads = 8; + constexpr unsigned reportsPerThread = 64; + std::vector failures(threads); + StderrCapture output; + { + std::vector workers; + for (unsigned thread = 0; thread < threads; ++thread) { + workers.emplace_back([&, thread] { + try { + for (unsigned report = 0; report < reportsPerThread; ++report) AgcDriver::Graphics::LogPipelineStatistics_nid_no_patch(context, pipeline.pipeline); + } catch (...) { + failures[thread] = std::current_exception(); + } + }); + } + } + const auto text = output.Finish(); + for (const auto& failure : failures) { + if (failure) std::rethrow_exception(failure); + } + const auto reports = CountReports(StatisticLines(text), reference); + Require(reports == threads * reportsPerThread, "a pipeline statistics report is missing"); + std::printf("pipeline statistics: %zu reports of %zu executables and %zu lines each from %u threads, each in one piece\n", reports, shape.size(), reference.size(), threads); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "pipeline statistics report test: %s\n", error.what()); + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp index 21cbeed6e4..f02dadb119 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp @@ -300,10 +300,10 @@ void FailureCapture(AgcDriver::VulkanDevice& device) { const std::filesystem::path retryDirectory = "shader_12345cafe_retry"; std::filesystem::remove_all(retryDirectory); const auto failedPath = AgcDriver::DriverDetail::Driver::dumpRequest(request.shader.codeAddress, request, "_retry/test"); - Require(!std::ifstream(failedPath, std::ios::binary).is_open(), "uncreatable dump path was unexpectedly created"); + Require(failedPath.empty(), "uncreatable dump path reported a saved file"); std::filesystem::create_directories(retryDirectory); const auto retriedPath = AgcDriver::DriverDetail::Driver::dumpRequest(request.shader.codeAddress, request, "_retry/test"); - Require(retriedPath == failedPath, "retried dump returned a different path"); + Require(retriedPath == (retryDirectory / "test.req").generic_string(), "retried dump returned a different path"); std::ifstream retriedFile(retriedPath, std::ios::binary); Require(retriedFile.is_open(), "retried dump file was not created"); std::ostringstream retriedText; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedUnregisteredGraphics.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedUnregisteredGraphics.cpp index df68d87509..f8c2e94811 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedUnregisteredGraphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedUnregisteredGraphics.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include namespace { @@ -28,7 +29,42 @@ void Bind(AgcDriver::QueueState& queue, const void* code, std::uint32_t program, queue.shader[resources] = 16u << 1u; } +void CheckRetainedDecode() { + using namespace AgcDriver::DriverDetail; + alignas(256) static std::array code{0xbf810000u}; + alignas(256) static std::array tail{0xbf810000u}; + alignas(256) static std::array, 4097> others{}; + const auto address = reinterpret_cast(code.data()); + DrawProgram program{}; + program.snapshot = ReadRawShader(address); + program.tailContinuation = ReadRawShader(reinterpret_cast(tail.data())); + DrawDecode decode{}; + decode.programs.push_back(std::move(program)); + const ShaderRegistry registry; + Require(DecodeProgramsCurrent(decode, registry), "a newly captured raw draw decode is not current"); + for (auto& other : others) { + other.fill(0xbf810000u); + static_cast(ReadRawShader(reinterpret_cast(other.data()))); + } + const auto reread = ReadRawShader(address); + Require(reread != decode.programs.front().snapshot, "raw snapshot churn did not evict the retained snapshot"); + Require(reread->code == decode.programs.front().snapshot->code, "raw snapshot churn changed the shader contents"); + std::cout << "retained raw snapshot evicted=" << (reread != decode.programs.front().snapshot) << '\n'; + Require(DecodeProgramsCurrent(decode, registry), "an unchanged retained raw draw decode was invalidated by snapshot cache eviction"); + code[0] = 0xbf800000u; + code[1] = 0xbf810000u; + Require(!DecodeProgramsCurrent(decode, registry), "a retained raw draw decode ignored changed main code"); + code[0] = 0xbf810000u; + Require(DecodeProgramsCurrent(decode, registry), "restoring raw main code did not restore its retained decode"); + tail[0] = 0xbf800000u; + tail[1] = 0xbf810000u; + Require(!DecodeProgramsCurrent(decode, registry), "a retained raw draw decode ignored changed tail code"); + tail[0] = 0xbf810000u; + Require(DecodeProgramsCurrent(decode, registry), "restoring raw tail code did not restore its retained decode"); +} + void Check(AgcDriver::VulkanDevice& device) { + CheckRetainedDecode(); using namespace AgcDriver::DriverDetail; using namespace ShaderRecompiler; AgcDriver::QueueState queue{}; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/QueuedRawShader.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/QueuedRawShader.cpp index 9629477586..a6eae35bcf 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/QueuedRawShader.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/QueuedRawShader.cpp @@ -1,3 +1,4 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" @@ -11,6 +12,7 @@ #include #include #include +#include namespace { @@ -22,6 +24,12 @@ void QueuedCode(bool cached) { code[0] = 0x7e000200u; const auto address = reinterpret_cast(code.data()); const auto before = cached ? ReadRawShader(address) : nullptr; + DrawDecode decode{}; + if (before != nullptr) { + DrawProgram program{}; + program.snapshot = before; + decode.programs.push_back(std::move(program)); + } std::atomic finished{false}; std::atomic started{false}; struct Restore { @@ -42,6 +50,9 @@ void QueuedCode(bool cached) { }, {{address, address + sizeof(std::uint32_t)}}); while (!started.load(std::memory_order_acquire)) std::this_thread::yield(); DrawPipeline::Active() = true; + if (before != nullptr && DecodeProgramsCurrent(decode, ShaderRegistry{})) { + throw std::runtime_error("a retained raw draw decode ignored a queued write to its code"); + } const auto after = ReadRawShader(address); finished.store(true, std::memory_order_release); DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Submission); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ResidentHeapMirror.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ResidentHeapMirror.cpp index e708bf7302..8a71eab5e2 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ResidentHeapMirror.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ResidentHeapMirror.cpp @@ -287,6 +287,25 @@ void ResidentTests(AgcDriver::VulkanDevice& device) { CheckStores(written, "unchanged dispatch"); } +void StagingChunkTests(AgcDriver::VulkanDevice& device) { + constexpr std::uint64_t LargeBytes = 48u << 20u; + constexpr std::uint64_t ChunkBoundary = 32u << 20u; + constexpr std::uint64_t First = ChunkBoundary - Threads / 2 * ThreadStride; + GuestHeap read(false, 1, LargeBytes); + GuestHeap written(true); + const OutputBlock output; + for (std::uint64_t block = 0; block < LargeBytes; block += Block) read.Store(block + 8, 0x11u); + for (std::uint32_t thread = 0; thread < Threads; ++thread) read.Store(First + thread * ThreadStride, Pattern(thread)); + const ImportBudget budget(AgcDriver::Graphics::HostImportLiveBytes() + Block); + const auto before = AgcDriver::Graphics::MirrorCounters(); + Dispatch(device, read.Address() + First, written.Address(), output); + const auto made = AgcDriver::Graphics::MirrorCounters(); + Require(made.heapBytes - before.heapBytes > First + Threads * ThreadStride, "resident heap mirror: a " + std::to_string(LargeBytes >> 20u) + " MiB heap mirror does not reach past the read range (" + std::to_string((made.heapBytes - before.heapBytes) >> 10u) + " KiB held)"); + for (std::uint32_t thread = 0; thread < Threads; ++thread) { + Require(output.Data()[thread] == Pattern(thread), "resident heap mirror: an upload split into staging chunks: thread " + std::to_string(thread) + " read " + Hex(output.Data()[thread]) + " at heap offset " + Hex(First + thread * ThreadStride) + ", expected " + Hex(Pattern(thread))); + } +} + void StorageImageTests(AgcDriver::VulkanDevice& device) { GuestHeap heap(true); GuestHeap written(true); @@ -555,6 +574,7 @@ int main() { const bool sparse = context.sparseBinding && context.sparseResidencyBuffer && context.sparseResidencyAliased; if (sparse) { ResidentTests(*device); + StagingChunkTests(*device); ReusedAddressTests(*device); JoinedImportTests(*device); } else { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp index f4218f55cf..4d62f946b5 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp @@ -7,6 +7,7 @@ #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" #include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" #include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" +#include "prx/libSceAgcDriver/Graphics/include/MultisampleTextureLayout.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" #include "prx/libSceAgcDriver/Graphics/include/State.hpp" #include "prx/libc/include/GuestAllocations.hpp" @@ -518,16 +519,6 @@ void CheckWritten(const std::vector>& before, const std::ve Require(written != 0 && written != after.size(), "the stores covered every sample or none " + what); } -void RequireStoreRefused(AgcDriver::VulkanDevice& device, const std::array& texture, const std::string& expected, const std::string& what) { - std::string refusal; - try { - StoreSamples(device, texture, {{0u, 0u, 0u}}); - } catch (const std::exception& error) { - refusal = error.what(); - } - Require(refusal.find(expected) != std::string::npos, what + " was not refused: " + refusal); -} - bool DeclaresCapability(std::span words, spv::Capability capability) { for (std::size_t cursor = 5; cursor < words.size();) { const auto count = words[cursor] >> 16u; @@ -872,6 +863,33 @@ void StoreWords(const Block& block, std::uint64_t bytes, std::uint32_t word) { for (std::uint64_t offset = 0; offset < bytes; offset += sizeof(word)) std::memcpy(block.data + offset, &word, sizeof(word)); } +std::vector> StoreVaryingUnorm(const Block& block, AgcDriver::Graphics::TextureTileMode tileMode) { + using namespace AgcDriver::Graphics; + const MultisampleTextureLayout layout(tileMode, 4u, Samples, Side, Side, 1u); + Require(layout.Bytes() <= block.bytes, "the varying fixture does not fit its color block"); + StoreWords(block, block.bytes, 0u); + std::vector linear(static_cast(layout.LinearBytes())); + std::vector> expected(Side * Side * Samples); + for (std::uint32_t index = 0; index < expected.size(); ++index) { + const auto pixel = index / Samples; + const bool odd = ((pixel % Side) * 3u + (pixel / Side) * 5u + (index % Samples) * 7u) % 2u != 0u; + const std::uint32_t word = odd ? 0x00ff00ffu : 0xff00ff00u; + std::memcpy(linear.data() + index * sizeof(word), &word, sizeof(word)); + expected[index] = odd ? std::array{1.0f, 0.0f, 1.0f, 0.0f} : std::array{0.0f, 1.0f, 0.0f, 1.0f}; + } + layout.Tile(linear, std::as_writable_bytes(std::span(block.data, block.bytes))); + return expected; +} + +void CheckLoaded(const std::vector>& loaded, const std::vector>& expected, const std::string& what) { + Require(loaded.size() == expected.size(), "the loaded sample count differs " + what); + for (std::size_t index = 0; index < loaded.size(); ++index) { + char where[96]; + std::snprintf(where, sizeof(where), "sample %zu of pixel (%zu, %zu)", index % Samples, index / Samples % Side, index / Samples / Side); + Require(loaded[index] == expected[index], std::string(where) + " does not hold its guest color " + what); + } +} + ShaderRecompiler::RecompileResult CompileBdaSpan(AgcDriver::VulkanDevice& device, std::uint64_t address) { const std::span code(BdaSpanCode); const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; @@ -1437,7 +1455,17 @@ int main(int argc, char** argv) { StoreWords(mixed, colorBytes, FillOnes); const std::uint32_t odd = 0x3c000000u; std::memcpy(mixed.data + colorBytes - sizeof(odd), &odd, sizeof(odd)); - RequireRefused(*device, TargetDescriptor(mixed, SampleShift), "does not hold one repeated 32-bit value", "loading a multisampled texture no draw rendered to whose color memory is not uniform"); + CheckUniform(LoadSamples(*device, mixed), Ones, "from color memory no draw rendered to whose padding alone differs"); + for (const auto& [mode, tileMode] : {std::pair{TileRenderTarget, AgcDriver::Graphics::TextureTileMode::kR64KBX}, std::pair{TileZ64KBX, AgcDriver::Graphics::TextureTileMode::kZ64KBX}}) { + Block varying(BlockBytes); + const auto expected = StoreVaryingUnorm(varying, tileMode); + CheckLoaded(LoadSamples(*device, varying, Format8888Unorm, mode), expected, "from varying color memory of a " + std::to_string(mode) + " swizzle surface no draw rendered to"); + } + Block varyingD(BlockBytes); + StoreWords(varyingD, colorBytes, FillOnes); + std::memcpy(varyingD.data, &odd, sizeof(odd)); + RequireRefused(*device, TargetDescriptor(varyingD, SampleShift, Format16161616Float, TileD64KBX), "non-uniform multisampled guest color tiling is not implemented", "loading varying D_X color memory no draw rendered to"); + std::puts("loads of R_X and Z_X color memory no draw rendered to return each sample's guest color, and varying D_X color memory is refused"); const auto oneSample = [](std::uint32_t, std::uint32_t, std::uint32_t sample) { return sample == StoredSample; }; const auto drawnBefore = LoadSamples(*device, block); StoreSamples(*device, TargetDescriptor(block, SampleShift), SampleWrites(oneSample)); @@ -1466,8 +1494,9 @@ int main(int argc, char** argv) { Block mixedStore(BlockBytes); StoreWords(mixedStore, colorBytes, FillOnes); std::memcpy(mixedStore.data + colorBytes - sizeof(odd), &odd, sizeof(odd)); - RequireStoreRefused(*device, TargetDescriptor(mixedStore, SampleShift), "writing the 16x16 multisampled texture", "storing to a multisampled texture no draw rendered to whose color memory is not uniform"); - std::puts("stores to one sample of each pixel of a drawn 4x target and to samples of zeroed color memory no draw rendered to reach exactly those samples, and a store to non-uniform color memory no draw rendered to is refused"); + StoreSamples(*device, TargetDescriptor(mixedStore, SampleShift), SampleWrites(oneSample)); + CheckWritten(std::vector>(Side * Side * Samples, Ones), LoadSamples(*device, mixedStore), oneSample, "into one sample of each pixel of color memory no draw rendered to whose padding alone differs"); + std::puts("stores to one sample of each pixel of a drawn 4x target and to samples of zeroed or non-uniform color memory no draw rendered to reach exactly those samples"); for (const auto mode : {TileZ64KBX, TileD64KBX}) { Block swizzled(BlockBytes); StoreWords(swizzled, colorBytes, FillOnes); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp index 276bb59320..5f462aeb66 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp @@ -1050,6 +1050,42 @@ void VKAPI_PTR ReportBudget(VkPhysicalDevice, VkPhysicalDeviceMemoryProperties2* } } +struct BudgetReportAgeProbe { + std::uint64_t budget = 8ull << 30u; + unsigned queries = 0; +} budgetReportAge; + +void VKAPI_PTR ReportBudgetAge(VkPhysicalDevice, VkPhysicalDeviceMemoryProperties2* properties) { + ++budgetReportAge.queries; + auto* reported = static_cast(properties->pNext); + for (std::uint32_t heap = 0; heap < VK_MAX_MEMORY_HEAPS; ++heap) { + reported->heapBudget[heap] = budgetReportAge.budget; + reported->heapUsage[heap] = 0; + } +} + +void budgetReportAgeTests(Context context) { + constexpr std::uint64_t GiB = 1ull << 30u; + const auto heap = LargestDeviceLocalHeap(context.memory); + Require(heap.has_value(), "the budget report age fixture needs a device-local heap"); + context.memory.memoryHeaps[*heap].size = std::max(context.memory.memoryHeaps[*heap].size, 16 * GiB); + context.memoryProperties2 = ReportBudgetAge; + Require(StorageTextureCacheBudget(context) == 4 * GiB, "the initial storage limit is not 4 GiB"); + const auto queries = budgetReportAge.queries; + std::this_thread::sleep_for(std::chrono::milliseconds(25)); + budgetReportAge.budget = 2 * GiB; + Require(SampledTextureCacheBudget(context) == 2 * GiB, "sampled did not use the reduced heap budget"); + Require(budgetReportAge.queries == queries + 1, "reporting the storage limit issued an extra memory budget query"); + Require(StorageTextureCacheBudget(context) == 4 * GiB, "reporting refreshed the still-active cached storage limit"); + VkPhysicalDeviceMemoryBudgetPropertiesEXT reported{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT}; + reported.heapBudget[*heap] = budgetReportAge.budget; + Require(StorageTextureBudget(context.memory, &reported, 0, 0) == GiB, "the fresh storage calculation does not differ from the active cached limit"); + std::this_thread::sleep_for(std::chrono::milliseconds(1100)); + Require(StorageTextureCacheBudget(context) == GiB, "storage did not refresh after its own cache interval"); + Require(budgetReportAge.queries == queries + 2, "storage did not issue exactly one query when its cache interval expired"); + std::puts("sampled budget report age tests passed"); +} + std::uint64_t ExpectedBudget(const Context& context) { VkPhysicalDeviceMemoryBudgetPropertiesEXT reported{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT}; VkPhysicalDeviceMemoryProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_PROPERTIES_2, &reported}; @@ -1319,10 +1355,10 @@ int main(int argc, char** argv) { const bool disableStorageImageMultisample = mode == "without-storage-image-multisample"; const bool recover = mode == "recover-sampled" || mode == "recover-storage" || mode == "recover-srgb" || mode == "recover-repack"; #ifdef _WIN32 - if (!memory) _putenv_s("APS5_TEXTURE_CACHE_MIB", recover ? "16" : "1"); + if (!memory && mode != "budget-report-age") _putenv_s("APS5_TEXTURE_CACHE_MIB", recover ? "16" : "1"); if (std::getenv("APS5_TEMPLATE_AUDIT_MS") == nullptr) _putenv_s("APS5_TEMPLATE_AUDIT_MS", "250"); #else - if (!memory) setenv("APS5_TEXTURE_CACHE_MIB", recover ? "16" : "1", 1); + if (!memory && mode != "budget-report-age") setenv("APS5_TEXTURE_CACHE_MIB", recover ? "16" : "1", 1); setenv("APS5_TEMPLATE_AUDIT_MS", "250", 0); #endif std::unique_ptr device; @@ -1339,6 +1375,16 @@ int main(int argc, char** argv) { return 0; } auto context = device->GetContext(); + if (mode == "budget-report-age") { + try { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + budgetReportAgeTests(context); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "FAIL: %s\n", error.what()); + return 1; + } + } if (disableStorageImageMultisample) { PlaceholderImages placeholders(context); for (std::uint32_t heap = ShaderRecompiler::RuntimeAbi::FirstStorageImageBinding; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ScatteredRuns.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ScatteredRuns.cpp index ce851974a9..297f2e7a10 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ScatteredRuns.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ScatteredRuns.cpp @@ -10,7 +10,7 @@ #include "prx/libc/include/GuestAllocations.hpp" #include "VulkanTestDevice.hpp" #include "../AllocationFailure.hpp" -#ifndef _WIN32 +#ifdef __linux__ #include "prx/libc/include/GuestArena.hpp" #include #include @@ -29,7 +29,7 @@ #include #include -#ifdef _WIN32 +#ifndef __linux__ int main() { std::printf("skipped, udmabuf imports are Linux only\n"); return 77; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ScratchPool.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ScratchPool.cpp new file mode 100644 index 0000000000..0359abebcc --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ScratchPool.cpp @@ -0,0 +1,499 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#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(); } + static std::pair HeaderOf(const VulkanDevice& device, const ShaderRecompiler::RecompileResult& shader) { return device.scratchPoolHeader(shader); } +}; + +} + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; +using Words = std::vector; + +constexpr std::uint32_t PrivateHigh = 0x70000000u; +constexpr std::uint32_t WaitCnt0 = 0xbf8c0070u; +constexpr std::uint32_t EndProgram = 0xbf810000u; +constexpr std::uint32_t ShiftLaneBy5 = 0x34020085u; +constexpr std::uint32_t Barrier = 0xbf8a0000u; +constexpr std::uint32_t StoreDwordOp = 0x1cu; +constexpr std::uint32_t LoadDwordOp = 0x0cu; +constexpr std::uint32_t AtomicAddOp = 0x32u; +constexpr std::uint32_t ScratchSegment = 1u << 14u; +constexpr std::uint32_t AliasScratchDwords = 4u; +constexpr std::uint32_t ContentionScratchDwords = 1900u; +constexpr std::uint32_t ContentionGroups = 64u; +constexpr std::uint32_t DelayIterations = 100000u; + +enum class Form : std::uint8_t { Table, Literal, Computed }; +constexpr std::array FormNames{"table load", "literal", "computed"}; + +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::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +std::uint32_t FlatWord0(std::uint32_t op, bool glc, std::uint32_t offset = 0u) { + return 0xdc000000u | (op << 18u) | (glc ? 0x10000u : 0u) | offset; +} + +std::uint32_t FlatWord1(std::uint32_t vdst, std::uint32_t data, std::uint32_t address) { + return (vdst << 24u) | (0x7du << 16u) | (data << 8u) | address; +} + +void EmitFlat(Words& code, std::uint32_t op, bool glc, std::uint32_t vdst, std::uint32_t data, std::uint32_t address, std::uint32_t offset = 0u) { + code.push_back(FlatWord0(op, glc, offset)); + code.push_back(FlatWord1(vdst, data, address)); +} + +void EmitScratch(Words& code, std::uint32_t op, std::uint32_t vdst, std::uint32_t data, std::uint32_t address, std::uint32_t offset = 0u) { + code.push_back(FlatWord0(op, false, offset) | ScratchSegment); + code.push_back(FlatWord1(vdst, data, address)); +} + +void EmitTableLoad(Words& code, std::uint32_t vdst, std::uint32_t offset) { + code.push_back(0xe0381000u | offset); + code.push_back(0x80010001u | (vdst << 8u)); +} + +void EmitOutputStore(Words& code, std::uint32_t vdata, std::uint32_t offset) { + code.push_back(0xe0701000u | offset); + code.push_back(0x80000001u | (vdata << 8u)); +} + +std::uint32_t VMov(std::uint32_t vdst, std::uint32_t source) { + return 0x7e000000u | (vdst << 17u) | (1u << 9u) | source; +} + +std::uint32_t VShiftLeft(std::uint32_t vdst, std::uint32_t amount, std::uint32_t vsrc) { + return 0x34000000u | (vdst << 17u) | (vsrc << 9u) | (128u + amount); +} + +std::uint32_t VAdd(std::uint32_t vdst, std::uint32_t source0, std::uint32_t vsrc1) { + return 0x4a000000u | (vdst << 17u) | (vsrc1 << 9u) | source0; +} + +constexpr std::uint32_t Vgpr(std::uint32_t index) { return 256u + index; } +constexpr std::uint32_t Inline(std::uint32_t value) { return 128u + value; } +constexpr std::uint32_t Literal = 255u; + +void EmitDelay(Words& code) { + code.push_back(0xbe9403ffu); + code.push_back(DelayIterations); + code.push_back(0x80948114u); + code.push_back(0xbf078014u); + code.push_back(0xbf85fffdu); +} + +std::uint32_t OperandA(std::uint32_t lane) { + return 0x9e3779b1u * (lane + 1u) ^ 0x5a5a0f0fu; +} + +std::uint32_t OperandB(std::uint32_t lane) { + return OperandA(lane) ^ 0xa5a5a5a5u; +} + +std::uint32_t InitValue(std::uint32_t lane) { + return 0x3c6ef372u * (lane + 3u) + 11u; +} + +Words AliasKernel(Form form, bool atomic) { + Words code{ShiftLaneBy5}; + EmitTableLoad(code, 2u, 0u); + EmitTableLoad(code, 6u, 16u); + code.push_back(WaitCnt0); + if (form == Form::Literal) { + code.push_back(VMov(3u, Literal)); + code.push_back(PrivateHigh); + } else if (form == Form::Computed) { + code.push_back(VMov(7u, Literal)); + code.push_back(0x30000000u); + code.push_back(VAdd(3u, Vgpr(7u), 3u)); + } + if (atomic) { + EmitScratch(code, StoreDwordOp, 0u, 6u, 2u); + code.push_back(WaitCnt0); + EmitFlat(code, AtomicAddOp, true, 10u, 4u, 2u); + code.push_back(WaitCnt0); + EmitScratch(code, LoadDwordOp, 11u, 0u, 2u); + code.push_back(WaitCnt0); + } else { + EmitFlat(code, StoreDwordOp, false, 0u, 4u, 2u); + code.push_back(WaitCnt0); + EmitScratch(code, LoadDwordOp, 10u, 0u, 2u); + EmitScratch(code, StoreDwordOp, 0u, 5u, 2u, 8u); + code.push_back(WaitCnt0); + EmitFlat(code, LoadDwordOp, false, 11u, 0u, 2u, 8u); + code.push_back(WaitCnt0); + } + EmitOutputStore(code, 10u, 0u); + EmitOutputStore(code, 11u, 4u); + code.push_back(EndProgram); + return code; +} + +void EmitGlobalLaneSetup(Words& code, std::uint32_t threadShift) { + code.push_back(VMov(7u, 4u)); + code.push_back(VShiftLeft(8u, threadShift, 7u)); + code.push_back(VAdd(9u, Vgpr(8u), 0u)); + code.push_back(VShiftLeft(13u, 2u, 9u)); + code.push_back(VShiftLeft(1u, 4u, 9u)); + code.push_back(VMov(14u, Inline(0u))); + for (std::uint32_t k = 0; k < 4u; ++k) code.push_back(VAdd(15u + k, Inline(k), 13u)); +} + +void EmitScratchFill(Words& code) { + for (std::uint32_t k = 0; k < 4u; ++k) EmitScratch(code, StoreDwordOp, 0u, 15u + k, 14u, 4u * k); + code.push_back(WaitCnt0); +} + +void EmitScratchReadback(Words& code) { + for (std::uint32_t k = 0; k < 4u; ++k) EmitScratch(code, LoadDwordOp, 20u + k, 0u, 14u, 4u * k); + code.push_back(WaitCnt0); + for (std::uint32_t k = 0; k < 4u; ++k) EmitOutputStore(code, 20u + k, 4u * k); + code.push_back(EndProgram); +} + +Words ContentionKernel(std::uint32_t waveSize) { + Words code; + EmitGlobalLaneSetup(code, waveSize == 64u ? 7u : 6u); + EmitScratchFill(code); + code.push_back(Barrier); + EmitDelay(code); + EmitScratchReadback(code); + return code; +} + +Words DivergentExitKernel(std::uint32_t waveSize) { + Words code; + EmitGlobalLaneSetup(code, waveSize == 64u ? 7u : 6u); + code.push_back(0x7c000000u | (0xc1u << 17u) | Inline(waveSize - 1u)); + code.push_back(0xbf870001u); + code.push_back(EndProgram); + EmitScratchFill(code); + EmitDelay(code); + EmitScratchReadback(code); + return code; +} + +Words CalleeKernel() { + Words code{ShiftLaneBy5}; + EmitTableLoad(code, 2u, 0u); + EmitTableLoad(code, 6u, 16u); + code.push_back(WaitCnt0); + const auto counterAfterGetpc = code.size() + 1u; + code.push_back(0xbe941f00u); + code.push_back(0x8014ff14u); + const auto literalAt = code.size(); + code.push_back(0u); + code.push_back(0x82158015u); + code.push_back(0xbe9e2114u); + const auto branchAt = code.size(); + code.push_back(0xbf820000u); + code.at(literalAt) = static_cast((code.size() - counterAfterGetpc) * 4u); + EmitFlat(code, StoreDwordOp, false, 0u, 4u, 2u); + code.push_back(WaitCnt0); + code.push_back(0xbe80201eu); + code.at(branchAt) |= static_cast(code.size() - branchAt - 1u); + EmitScratch(code, LoadDwordOp, 10u, 0u, 2u); + code.push_back(WaitCnt0); + EmitOutputStore(code, 10u, 0u); + code.push_back(EndProgram); + return code; +} + +Words PlainKernel() { + Words code{ShiftLaneBy5}; + EmitTableLoad(code, 2u, 0u); + code.push_back(WaitCnt0); + EmitOutputStore(code, 2u, 0u); + code.push_back(EndProgram); + return code; +} + +struct Facts { + bool poolBinding = false; + bool functionArrayAccess = false; +}; + +Facts Inspect(std::span words) { + Facts facts; + std::set arrays, pointers, variables; + for (std::size_t cursor = 5; cursor < words.size();) { + const auto count = words[cursor] >> 16u; + const auto op = words[cursor] & 0xffffu; + Require(count != 0u, "scratch pool: malformed SPIR-V"); + if (op == spv::OpDecorate && words[cursor + 2u] == spv::DecorationDescriptorSet && words[cursor + 3u] == ShaderRecompiler::ScratchPoolDescriptorSet) facts.poolBinding = true; + if (op == spv::OpTypeArray) arrays.insert(words[cursor + 1u]); + if (op == spv::OpTypePointer && words[cursor + 2u] == spv::StorageClassFunction && arrays.contains(words[cursor + 3u])) pointers.insert(words[cursor + 1u]); + if (op == spv::OpVariable && pointers.contains(words[cursor + 1u])) variables.insert(words[cursor + 2u]); + if ((op == spv::OpAccessChain || op == spv::OpInBoundsAccessChain) && variables.contains(words[cursor + 3u])) facts.functionArrayAccess = true; + cursor += count; + } + return facts; +} + +ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, const Words& code, std::vector& userData, std::uint32_t waveSize, std::uint32_t threads, bool groupId, std::uint32_t scratchDwords) { + const std::span codeSpan(code); + const std::array memory{{{reinterpret_cast(codeSpan.data()), std::as_bytes(codeSpan)}}}; + ShaderRecompiler::ShaderComputeStageInfo compute{{threads, 1, 1}, 0, {groupId, false, false}, false, 1}; + compute.scratchDwords = scratchDwords; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(codeSpan.data()), codeSpan, 0, {}}, + {waveSize, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + return ShaderRecompiler::Recompile(request); +} + +void RunAlias(AgcDriver::VulkanDevice& device, Form form, bool atomic, std::uint32_t waveSize) { + const std::string name = std::string("scratch pool alias ") + FormNames[static_cast(form)] + (atomic ? " atomic" : " store") + " wave" + std::to_string(waveSize); + std::vector table(waveSize * 8u, 0u); + for (std::uint32_t lane = 0; lane < waveSize; ++lane) { + table[lane * 8u] = 4u; + table[lane * 8u + 1u] = form == Form::Table ? PrivateHigh : (form == Form::Computed ? PrivateHigh - 0x30000000u : 0x1234u); + table[lane * 8u + 2u] = OperandA(lane); + table[lane * 8u + 3u] = OperandB(lane); + table[lane * 8u + 4u] = InitValue(lane); + } + std::vector output(waveSize * 8u, 0xdeadbeefu); + std::vector userData(8, 0u); + const auto outputDescriptor = BufferDescriptor(output.data(), static_cast(output.size() * 4u)); + const auto tableDescriptor = BufferDescriptor(table.data(), static_cast(table.size() * 4u)); + std::copy(outputDescriptor.begin(), outputDescriptor.end(), userData.begin()); + std::copy(tableDescriptor.begin(), tableDescriptor.end(), userData.begin() + 4); + const auto code = AliasKernel(form, atomic); + const auto result = Compile(device, code, userData, waveSize, waveSize, false, AliasScratchDwords); + const auto facts = Inspect(std::span(result.spirv.data(), result.spirv.size())); + Require(facts.poolBinding && result.scratchPoolSlotDwords != 0u, name + ": the scratch program has no scratch pool binding"); + Require(!facts.functionArrayAccess, name + ": the scratch program indexes a Function array"); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); + for (std::uint32_t lane = 0; lane < waveSize; ++lane) { + const auto first = output[lane * 8u]; + const auto second = output[lane * 8u + 1u]; + const auto expectedFirst = atomic ? InitValue(lane) : OperandA(lane); + const auto expectedSecond = atomic ? InitValue(lane) + OperandA(lane) : OperandB(lane); + Require(first == expectedFirst, name + ": lane " + std::to_string(lane) + " first value " + Hex(first) + ", expected " + Hex(expectedFirst)); + Require(second == expectedSecond, name + ": lane " + std::to_string(lane) + " second value " + Hex(second) + ", expected " + Hex(expectedSecond)); + } +} + +void RunCallee(AgcDriver::VulkanDevice& device, std::uint32_t waveSize) { + const std::string name = "scratch pool callee wave" + std::to_string(waveSize); + std::vector table(waveSize * 8u, 0u); + for (std::uint32_t lane = 0; lane < waveSize; ++lane) { + table[lane * 8u] = 4u; + table[lane * 8u + 1u] = PrivateHigh; + table[lane * 8u + 2u] = OperandA(lane); + } + std::vector output(waveSize * 8u, 0xdeadbeefu); + std::vector userData(8, 0u); + const auto outputDescriptor = BufferDescriptor(output.data(), static_cast(output.size() * 4u)); + const auto tableDescriptor = BufferDescriptor(table.data(), static_cast(table.size() * 4u)); + std::copy(outputDescriptor.begin(), outputDescriptor.end(), userData.begin()); + std::copy(tableDescriptor.begin(), tableDescriptor.end(), userData.begin() + 4); + const auto code = CalleeKernel(); + const auto result = Compile(device, code, userData, waveSize, waveSize, false, AliasScratchDwords); + Require(result.scratchPoolSlotDwords != 0u, name + ": the scratch program has no scratch pool binding"); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); + for (std::uint32_t lane = 0; lane < waveSize; ++lane) { + Require(output[lane * 8u] == OperandA(lane), name + ": lane " + std::to_string(lane) + " read " + Hex(output[lane * 8u]) + " back, expected " + Hex(OperandA(lane))); + } +} + +void RunPlain(AgcDriver::VulkanDevice& device) { + std::vector table(32u * 8u, 7u); + std::vector output(32u * 8u, 0u); + std::vector userData(8, 0u); + const auto outputDescriptor = BufferDescriptor(output.data(), static_cast(output.size() * 4u)); + const auto tableDescriptor = BufferDescriptor(table.data(), static_cast(table.size() * 4u)); + std::copy(outputDescriptor.begin(), outputDescriptor.end(), userData.begin()); + std::copy(tableDescriptor.begin(), tableDescriptor.end(), userData.begin() + 4); + const auto code = PlainKernel(); + const auto result = Compile(device, code, userData, 32u, 32u, false, 0u); + const auto facts = Inspect(std::span(result.spirv.data(), result.spirv.size())); + Require(!facts.poolBinding && result.scratchPoolSlotDwords == 0u, "scratch pool: a program without scratch got a scratch pool binding"); +} + +void RunContention(AgcDriver::VulkanDevice& device, const char* label, const Words& code, std::uint32_t waveSize, bool exitsEarly) { + const auto threads = waveSize * 2u; + const auto lanes = ContentionGroups * threads; + std::vector output(static_cast(lanes) * 4u, 0xdeadbeefu); + std::vector userData(4, 0u); + const auto outputDescriptor = BufferDescriptor(output.data(), static_cast(output.size() * 4u)); + std::copy(outputDescriptor.begin(), outputDescriptor.end(), userData.begin()); + const auto result = Compile(device, code, userData, waveSize, threads, true, ContentionScratchDwords); + const std::string name = std::string("scratch pool ") + label + " wave" + std::to_string(waveSize); + Require(result.scratchPoolSlotDwords == threads * ContentionScratchDwords, name + ": unexpected slot size " + std::to_string(result.scratchPoolSlotDwords)); + for (std::uint32_t round = 0; round < 2u; ++round) { + std::fill(output.begin(), output.end(), 0xdeadbeefu); + device.Dispatch(result, ContentionGroups, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); + for (std::uint32_t lane = 0; lane < lanes; ++lane) { + const bool exited = exitsEarly && (lane % threads) < waveSize; + for (std::uint32_t k = 0; k < 4u; ++k) { + const auto value = output[static_cast(lane) * 4u + k]; + const auto expected = exited ? 0xdeadbeefu : lane * 4u + k; + Require(value == expected, name + ": round " + std::to_string(round) + " lane " + std::to_string(lane) + " dword " + std::to_string(k) + " read " + Hex(value) + ", expected " + Hex(expected)); + } + } + } +} + +void RunDiscardedInitialization(AgcDriver::VulkanDevice& device) { + const std::string name = "scratch pool initialization after a discarded batch"; + constexpr std::uint32_t waveSize = 32u; + constexpr std::uint32_t threads = waveSize * 2u; + const auto code = ContentionKernel(waveSize); + const auto lanes = ContentionGroups * threads; + std::vector output(static_cast(lanes) * 4u, 0xdeadbeefu); + std::vector userData(4, 0u); + const auto outputDescriptor = BufferDescriptor(output.data(), static_cast(output.size() * 4u)); + std::copy(outputDescriptor.begin(), outputDescriptor.end(), userData.begin()); + const auto result = Compile(device, code, userData, waveSize, threads, true, ContentionScratchDwords); + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device); + const std::uint64_t slotBytes = (static_cast(result.scratchPoolSlotDwords) + 2u) * 4u; + const std::uint64_t slots = std::clamp(std::max((1u << 20u) / slotBytes, 1u), 1u, ContentionGroups); + const std::size_t poolBytes = static_cast(slots * slotBytes); + { + AgcDriver::Graphics::DeviceBuffer dirty(context, poolBytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT); + AgcDriver::Graphics::CommandBatch batch(context); + context.Function("vkCmdFillBuffer")(batch.Handle(), dirty.Handle(), 0, poolBytes, 0xffffffffu); + batch.SubmitAndWait(); + } + AgcDriver::Graphics::Recorder::FailNextSubmitsForTest(1u); + bool discarded = false; + try { + device.Dispatch(result, ContentionGroups, 1, 1, {}, reinterpret_cast(code.data())); + device.Dispatch(result, ContentionGroups, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); + } catch (const std::exception& error) { + discarded = std::string(error.what()).find("injected failure") != std::string::npos; + } + AgcDriver::Graphics::Recorder::FailNextSubmitsForTest(0u); + Require(discarded, name + ": the first batch was not discarded"); + const auto [header, headerBytes] = AgcDriver::VulkanDeviceTestAccess::HeaderOf(device, result); + Require(header != VK_NULL_HANDLE && headerBytes != 0u, name + ": the dispatches created no scratch pool"); + AgcDriver::Graphics::Buffer readback(context, headerBytes, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + { + AgcDriver::Graphics::CommandBatch batch(context); + AgcDriver::Graphics::CopyBuffer(context, batch.Handle(), header, 0, readback.Handle(), 0, headerBytes); + AgcDriver::Graphics::RecordMemoryBarrier(context, batch.Handle(), VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); + batch.SubmitAndWait(); + } + readback.Invalidate(); + const auto* words = reinterpret_cast(readback.Bytes().data()); + for (std::uint64_t word = 0; word < headerBytes / 4u; ++word) { + Require(words[word] == 0u, name + ": header word " + std::to_string(word) + " reads " + Hex(words[word]) + " after the discarded batch"); + } + device.Dispatch(result, ContentionGroups, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); + for (std::uint32_t lane = 0; lane < lanes; ++lane) { + for (std::uint32_t k = 0; k < 4u; ++k) { + const auto value = output[static_cast(lane) * 4u + k]; + Require(value == lane * 4u + k, name + ": lane " + std::to_string(lane) + " dword " + std::to_string(k) + " read " + Hex(value) + " after the retry"); + } + } +} + +void RunEviction(AgcDriver::VulkanDevice& device) { + const std::string name = "scratch pool eviction over the total budget"; + constexpr std::uint32_t waveSize = 32u; + constexpr std::uint32_t threads = waveSize * 2u; + const auto lanes = ContentionGroups * threads; + const auto first = ContentionKernel(waveSize); + const auto second = DivergentExitKernel(waveSize); + std::vector output(static_cast(lanes) * 4u, 0xdeadbeefu); + std::vector userData(4, 0u); + const auto outputDescriptor = BufferDescriptor(output.data(), static_cast(output.size() * 4u)); + std::copy(outputDescriptor.begin(), outputDescriptor.end(), userData.begin()); + const auto firstResult = Compile(device, first, userData, waveSize, threads, true, ContentionScratchDwords); + const auto secondResult = Compile(device, second, userData, waveSize, threads, true, ContentionScratchDwords); + const auto run = [&](const ShaderRecompiler::RecompileResult& result, const Words& code, bool exitsEarly, const char* step) { + std::fill(output.begin(), output.end(), 0xdeadbeefu); + device.Dispatch(result, ContentionGroups, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); + for (std::uint32_t lane = 0; lane < lanes; ++lane) { + const bool exited = exitsEarly && (lane % threads) < waveSize; + for (std::uint32_t k = 0; k < 4u; ++k) { + const auto value = output[static_cast(lane) * 4u + k]; + const auto expected = exited ? 0xdeadbeefu : lane * 4u + k; + Require(value == expected, name + ": " + step + " lane " + std::to_string(lane) + " dword " + std::to_string(k) + " read " + Hex(value) + ", expected " + Hex(expected)); + } + } + }; + const auto held = [&](const ShaderRecompiler::RecompileResult& result) { return AgcDriver::VulkanDeviceTestAccess::HeaderOf(device, result).first != VK_NULL_HANDLE; }; + run(firstResult, first, false, "first program"); + Require(held(firstResult), name + ": the first program got no pool"); + run(secondResult, second, true, "second program"); + Require(held(secondResult) && !held(firstResult), name + ": the second pool did not evict the first"); + run(firstResult, first, false, "first program again"); + Require(held(firstResult) && !held(secondResult), name + ": the first program's pool was not recreated in place of the second"); +} + +} + +int main() { + try { +#ifdef _WIN32 + _putenv_s("APS5_SCRATCH_POOL_MIB", "1"); + _putenv_s("APS5_SCRATCH_POOL_TOTAL_MIB", "1"); +#else + setenv("APS5_SCRATCH_POOL_MIB", "1", 1); + setenv("APS5_SCRATCH_POOL_TOTAL_MIB", "1", 1); +#endif + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const bool wave64 = device->Target().subgroupSize >= 32u; + RunPlain(*device); + RunDiscardedInitialization(*device); + RunEviction(*device); + for (const std::uint32_t waveSize : {32u, 64u}) { + if (waveSize == 64u && !wave64) continue; + for (const Form form : {Form::Table, Form::Literal, Form::Computed}) { + RunAlias(*device, form, false, waveSize); + RunAlias(*device, form, true, waveSize); + } + RunCallee(*device, waveSize); + RunContention(*device, "isolation", ContentionKernel(waveSize), waveSize, false); + RunContention(*device, "divergent exit", DivergentExitKernel(waveSize), waveSize, true); + } + std::puts("scratch pool 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/SdwaFloatSelectors.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SdwaFloatSelectors.cpp index 6efd68b5d2..4ee9be5330 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SdwaFloatSelectors.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SdwaFloatSelectors.cpp @@ -121,6 +121,59 @@ constexpr const char* Names[16] = { "mul_f16 BYTE_2/DWORD -> BYTE_1", }; +alignas(256) constexpr std::array ClampCode{ + 0x34020084, 0x34060086, 0xe0381000, 0x80000401, 0xbf8c3f70, 0x64140ef9, 0x04052006, 0x66160cf9, + 0x04022505, 0x68180cf9, 0x03042607, 0x7e1a0305, 0x6a1a0ef9, 0x05043306, 0x741c0ef9, 0x06012405, + 0x721e0af9, 0x00062606, 0x64200ef9, 0x05002a05, 0x6a220ef9, 0x06252106, 0xe0781000, 0x80010a03, + 0xe0781010, 0x80010e03, 0xbf810000, +}; + +constexpr std::uint32_t ClampExpected[32][8] = { + {0x00000000u, 0x00000000u, 0x00003c00u, 0x00000000u, 0x00000000u, 0x00003c00u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x3c000000u, 0x00001db6u, 0x00000000u, 0x00000000u, 0x0000009du, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x0000003cu, 0x00004700u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00470000u, 0x00000000u, 0x00470047u, 0x00000000u, 0x00000047u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00003c00u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x3c000000u, 0x00003c00u, 0x001fba34u, 0x00000000u, 0x00000034u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x3c000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000047u, 0x00000000u, 0x00008800u}, + {0x00000000u, 0x30980000u, 0x00003c00u, 0xb3808080u, 0x00000000u, 0x00000080u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000042u, 0xc0000000u, 0x00000000u, 0x000002c0u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x1464a58eu, 0x000000a5u, 0x00002714u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000007cu, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x00000068u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00003c00u, 0x00000000u, 0x00001100u}, + {0x00000000u, 0x3c000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00003c00u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x01000000u, 0x00000000u, 0x00000100u, 0x001c0000u, 0x0000ca00u}, + {0x00000000u, 0x00800000u, 0x0000249du, 0x00800080u, 0x00000000u, 0x00000080u, 0x00000000u, 0x00000000u}, + {0x00000046u, 0x3c000000u, 0x00000000u, 0x0000bc00u, 0x000000bcu, 0x00000000u, 0xffe60000u, 0x00005100u}, + {0x00000000u, 0x00000000u, 0x00003c00u, 0x00470047u, 0x00000000u, 0x00003c00u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00800080u, 0x00000000u, 0x0000068bu, 0x00000000u, 0x0000f400u}, + {0x00000003u, 0x3c000000u, 0x00000000u, 0x00800080u, 0x00000000u, 0x00000080u, 0x00000000u, 0x0000e500u}, + {0x00000000u, 0x3c000000u, 0x00000000u, 0x00ff00ffu, 0x00000000u, 0x000000ffu, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00ff0000u, 0x00000000u, 0x00ff00ffu, 0x00000000u, 0x000000ffu, 0xfffe0000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00ff00ffu, 0x00000000u, 0x00003c00u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x3c000000u, 0x00000052u, 0x00000100u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x3c000000u, 0x00003c00u, 0x002f8e4au, 0x00000000u, 0x0000004au, 0x004a0000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x000000e7u, 0x00d8e2bcu, 0x00000000u, 0x00003c00u, 0x00000000u, 0x00000000u}, + {0x00000080u, 0x00000000u, 0x00000000u, 0xf033ad9cu, 0x00000000u, 0x0000009cu, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x0ed30000u, 0x00000000u, 0x00f70eabu, 0x0000000eu, 0x000000abu, 0xfff10000u, 0x00000000u}, + {0x00000000u, 0x00300000u, 0x00000000u, 0x00778095u, 0x00000080u, 0x00000095u, 0x00000000u, 0x00000000u}, + {0x0000004fu, 0x3c000000u, 0x00000000u, 0x00119b05u, 0x0000009bu, 0x00000005u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00003c00u, 0x00000000u, 0x00000000u} +}; +constexpr std::uint32_t ClampChecked[32] = {0xffu, 0x93u, 0xfbu, 0xddu, 0xffu, 0xdfu, 0x97u, 0xdfu, 0xd3u, 0x6eu, 0x44u, 0x37u, 0xffu, 0xd5u, 0x57u, 0xd9u, 0xefu, 0xffu, 0xffu, 0xdfu, 0xdfu, 0x1cu, 0xffu, 0xd5u, 0xfbu, 0x9fu, 0xfbu, 0xd6u, 0x0cu, 0x45u, 0xcfu, 0x22u}; +constexpr const char* ClampNames[8] = { + "add_f16 WORD_1/WORD_0 -> BYTE_0 clamp", + "sub_f16 BYTE_2/WORD_0 -> WORD_1 clamp", + "subrev_f16 WORD_0/BYTE_3 -> DWORD clamp", + "mul_f16 WORD_0/WORD_1 -> BYTE_3 preserve clamp", + "min_f16 BYTE_1/DWORD -> WORD_0 clamp", + "max_f16 DWORD/BYTE_0 -> DWORD clamp", + "add_f16 BYTE_0/WORD_1 -> BYTE_2 sext clamp", + "mul_f16 |WORD_1|/DWORD -> BYTE_1 clamp", +}; + void Fill(std::uint32_t tid, std::uint32_t* words) { std::copy(std::begin(Rows[tid]), std::end(Rows[tid]), words); } @@ -158,36 +211,14 @@ ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, std:: return ShaderRecompiler::Recompile(request); } -void Run(AgcDriver::VulkanDevice& device) { +void Run(AgcDriver::VulkanDevice& device, std::span code) { for (std::uint32_t tid = 0; tid < Threads; ++tid) Fill(tid, &Input[tid * Inputs]); Output.fill(0xdeadbeefu); - const std::span code(Code); const auto result = Compile(device, code); device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); device.WaitIdle(); } -void CheckByteSelectorClampRefused(AgcDriver::VulkanDevice& device) { - constexpr std::array, 6> forms{{ - {0x640a0cf9u, 0x04052004u}, - {0x660a0cf9u, 0x04022504u}, - {0x680a0cf9u, 0x03042604u}, - {0x6a0a0cf9u, 0x05043304u}, - {0x740a0cf9u, 0x06012404u}, - {0x720a0cf9u, 0x00062604u}, - }}; - for (const auto& form : forms) { - alignas(256) const std::array code{form[0], form[1], 0xbf810000u}; - std::string refusal; - try { - static_cast(Compile(device, code)); - } catch (const std::exception& error) { - refusal = error.what(); - } - Require(refusal.find("VOP2 SDWA clamp with byte selectors is not implemented") != std::string::npos, "f16 SDWA form " + Hex(form[0]) + " " + Hex(form[1]) + " with clamp was not refused"); - } -} - void Check() { for (std::uint32_t tid = 0; tid < Threads; ++tid) { const std::uint32_t* in = &Input[tid * Inputs]; @@ -200,15 +231,27 @@ void Check() { } } +void CheckClamp() { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const std::uint32_t* out = &Output[tid * Results]; + for (std::uint32_t i = 0; i < 8; ++i) { + if ((ClampChecked[tid] >> i) & 1u) { + Expect(tid, out[i], ClampExpected[tid][i], ClampNames[i]); + } + } + } +} + } int main() { try { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; - Run(*device); + Run(*device, Code); Check(); - CheckByteSelectorClampRefused(*device); + Run(*device, ClampCode); + CheckClamp(); std::puts("sdwa float selectors tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SdwaLdexpF16.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SdwaLdexpF16.cpp index ec46c5b46f..ac16cb2e80 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SdwaLdexpF16.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SdwaLdexpF16.cpp @@ -121,6 +121,59 @@ constexpr const char* Names[16] = { "BYTE_3/WORD_1 -> WORD_0", }; +alignas(256) constexpr std::array ClampCode{ + 0x34020084, 0x34060086, 0xe0381000, 0x80000401, 0xbf8c3f70, 0x76140af9, 0x04042004, 0x76160af9, + 0x06022504, 0x76180af9, 0x01062604, 0x7e1a0304, 0x761a0ef9, 0x00053306, 0x761c0ef9, 0x0b052906, + 0x761e0ef9, 0x0d142206, 0x76200cf9, 0x02242407, 0x762208f9, 0x08052305, 0xe0781000, 0x80010a03, + 0xe0781010, 0x80010e03, 0xbf810000, +}; + +constexpr std::uint32_t ClampExpected[32][8] = { + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00001b8au, 0x00000000u}, + {0x00000040u, 0x00080000u, 0x00003c00u, 0xf88003ffu, 0x00000000u, 0x00000000u, 0x00003c00u, 0x02000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00f0fff0u, 0x00000000u, 0x00000000u, 0x00003c00u, 0x00000000u}, + {0x00000000u, 0x3c000000u, 0x00000000u, 0x0022fe4du, 0x00000000u, 0x00000000u, 0x00001c00u, 0x00000000u}, + {0x00000000u, 0x09980000u, 0x00000000u, 0x00b3fddeu, 0x00000000u, 0x00000000u, 0x00003c00u, 0x00000000u}, + {0x00000000u, 0x3c000000u, 0x00000000u, 0x007a7ca0u, 0x00000000u, 0x00000000u, 0x00003c00u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00007d00u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00007e00u, 0x00000000u, 0x00000000u, 0x00003c00u, 0x10000000u}, + {0x00000000u, 0x3c000000u, 0x00000000u, 0x00097c3cu, 0x00000000u, 0x00000000u, 0x00003c00u, 0x00000000u}, + {0x00000000u, 0x3c000000u, 0x00000000u, 0x00edf7dbu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x3c000000u, 0x00000000u, 0x00fc7d70u, 0x00000400u, 0x00000000u, 0x00003c00u, 0x00000000u}, + {0x00000000u, 0x3c000000u, 0x00000000u, 0x0a7f7f7fu, 0x00000a00u, 0x00000000u, 0x00003c00u, 0x00000000u}, + {0x00000000u, 0x3c000000u, 0x00000000u, 0x005cfc30u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x000b0000u, 0x00000000u, 0x00b6fe11u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x0a000000u, 0x00000000u, 0x00c0ffc0u, 0x00000000u, 0x00820000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x3c000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00003c00u, 0x00000000u}, + {0x00000000u, 0x3c000000u, 0x00000000u, 0x0008fc92u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00003c00u, 0x00000000u}, + {0x00000000u, 0x00860000u, 0x00000000u, 0x00437c27u, 0x00000000u, 0x00000000u, 0x00003c00u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00207fd4u, 0x00000000u, 0x00000000u, 0x00003c00u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00957e8au, 0x00000000u, 0x00000000u, 0x00003c00u, 0x00000000u}, + {0x00000000u, 0x00050000u, 0x00000000u, 0x00057dadu, 0x00000000u, 0x00000000u, 0x00003c00u, 0x00000000u}, + {0x00000000u, 0x3c000000u, 0x00000000u, 0x00dffc34u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x3c000000u, 0x00000000u, 0x00317eabu, 0x00000000u, 0x00000000u, 0x00003c00u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00987d0au, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00a4fe29u, 0x00000000u, 0x00000000u, 0x00003c00u, 0x00000000u}, + {0x00000000u, 0x3c000000u, 0x00000000u, 0x0042ffedu, 0x00000000u, 0x00000000u, 0x00003c00u, 0x00000000u}, + {0x00000000u, 0x3c000000u, 0x00000000u, 0x001cfca9u, 0x00000000u, 0x00000000u, 0x00003c00u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x0092fd96u, 0x00000000u, 0x00000000u, 0x00003c00u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00baff3cu, 0x00000000u, 0x00000000u, 0x00003c00u, 0x00000000u}, + {0x00000000u, 0x3c000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00003c00u, 0x00000000u} +}; +constexpr std::uint32_t ClampChecked[32] = {0xe7u, 0x30u, 0xfau, 0xb8u, 0xb8u, 0xf8u, 0xbau, 0x7au, 0xb8u, 0xbdu, 0xe8u, 0xf8u, 0x98u, 0xf8u, 0xb8u, 0xe0u, 0x98u, 0xe2u, 0xf8u, 0x62u, 0xfau, 0xfau, 0xd8u, 0xb8u, 0xb8u, 0xbau, 0xfau, 0xf8u, 0xb8u, 0xdau, 0xfau, 0xe0u}; +constexpr const char* ClampNames[8] = { + "WORD_0/WORD_0 -> BYTE_0 clamp", + "BYTE_2/DWORD -> WORD_1 clamp", + "DWORD/BYTE_1 -> DWORD clamp", + "WORD_1/BYTE_0 -> BYTE_3 preserve clamp", + "WORD_1/sext BYTE_3 -> BYTE_1 sext clamp", + "-WORD_0/sext WORD_1 -> BYTE_2 clamp", + "|WORD_0|/BYTE_2 -> WORD_0 clamp", + "WORD_1/sext BYTE_0 -> BYTE_3 clamp", +}; + void Fill(std::uint32_t tid, std::uint32_t* words) { std::copy(std::begin(Rows[tid]), std::end(Rows[tid]), words); } @@ -159,10 +212,9 @@ ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, std:: return ShaderRecompiler::Recompile(request); } -void Run(AgcDriver::VulkanDevice& device) { +void Run(AgcDriver::VulkanDevice& device, std::span code) { for (std::uint32_t tid = 0; tid < Threads; ++tid) Fill(tid, &Input[tid * Inputs]); Output.fill(0xdeadbeefu); - const std::span code(Code); const auto result = Compile(device, code); device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); device.WaitIdle(); @@ -173,10 +225,7 @@ void CheckRefused(AgcDriver::VulkanDevice& device) { std::uint32_t modifier; const char* error; }; - constexpr std::array forms{{ - {0x04042004u, "VOP2 SDWA clamp with byte selectors is not implemented"}, - {0x06022504u, "VOP2 SDWA clamp with byte selectors is not implemented"}, - {0x01062604u, "VOP2 SDWA clamp with byte selectors is not implemented"}, + constexpr std::array forms{{ {0x10050404u, "VOP2 SDWA source modifiers are not supported"}, {0x26060604u, "VOP2 SDWA source modifiers are not supported"}, }}; @@ -204,14 +253,27 @@ void Check() { } } +void CheckClamp() { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const std::uint32_t* out = &Output[tid * Results]; + for (std::uint32_t i = 0; i < 8; ++i) { + if ((ClampChecked[tid] >> i) & 1u) { + Expect(tid, out[i], ClampExpected[tid][i], ClampNames[i]); + } + } + } +} + } int main() { try { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; - Run(*device); + Run(*device, Code); Check(); + Run(*device, ClampCode); + CheckClamp(); CheckRefused(*device); std::puts("sdwa ldexp f16 tests passed"); return 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/StencilPlaneMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StencilPlaneMemory.cpp index b89b0f39c8..b83fc2f0a6 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/StencilPlaneMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StencilPlaneMemory.cpp @@ -73,6 +73,51 @@ VkStencilOpState Stencil(VkCompareOp compare, VkStencilOp pass, std::uint32_t re return {VK_STENCIL_OP_KEEP, pass, VK_STENCIL_OP_KEEP, compare, 0xffu, 0xffu, reference}; } +void CheckConsumedStencilClear(AgcDriver::VulkanDevice& device, const DepthFastClearHarness::Shaders& shaders, std::uint32_t swizzle, bool sampled) { + auto* blocks = static_cast(AllocateBlocks(DepthBytes + StencilBytes + Block)); + auto* depth = reinterpret_cast(blocks); + auto* stencil = reinterpret_cast(blocks + DepthBytes); + auto* htile = reinterpret_cast(blocks + DepthBytes + StencilBytes); + const auto htileBytes = AgcDriver::Graphics::HtileMetadataBytes(PlaneExtent); + Require(htileBytes <= Block, "the clear metadata does not fit its allocation"); + std::fill(depth, depth + PlaneTexels, 1.0f); + Fill(stencil, 0u); + std::fill(htile, htile + htileBytes / sizeof(*htile), 0xffffffffu); + AgcDriver::GuestMemory::BumpCollectEpoch(); + const auto stencilBase = reinterpret_cast(stencil); + Surface surface{reinterpret_cast(depth), stencilBase, reinterpret_cast(htile), true, VK_FORMAT_D32_SFLOAT_S8_UINT, 0, 0, swizzle}; + Require(Draw(device, shaders, surface, {.depthTest = true, .depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS, .stencilTest = true, .stencil = Stencil(VK_COMPARE_OP_ALWAYS, VK_STENCIL_OP_REPLACE, 0x18u)}, PlaneExtent, Colors) == PlaneTexels, "the clear control draw did not cover the plane"); + Require(!AgcDriver::Graphics::FlushDepthPlanes(stencilBase, StencilBytes).empty(), "the clear control did not store its initial stencil"); + device.WaitIdle(); + Require(Holding(stencil, 0x18u) == StencilBytes, "the clear control stencil was not stored"); + Require(AgcDriver::Graphics::FlushDepthPlanes(stencilBase, StencilBytes).empty(), "the clear control stencil is not clean"); + std::fill(htile, htile + htileBytes / sizeof(*htile), 0u); + AgcDriver::GuestMemory::BumpCollectEpoch(); + AgcDriver::Graphics::NoteDepthMetadataFill(surface.htile, htileBytes, 0u); + if (sampled) { + AgcDriver::Graphics::GuestTextureResource resource{}; + resource.baseAddress = stencilBase; + resource.width = PlaneWidth; + resource.height = PlaneHeight; + resource.format = 5u; + resource.dimension = AgcDriver::Graphics::TextureDimension::k2D; + resource.tileMode = AgcDriver::Graphics::TextureTileMode::kZ64KBX; + resource.mipCount = resource.allocatedMipCount = 1u; + resource.dstSelX = 4u; resource.dstSelY = 5u; resource.dstSelZ = 6u; resource.dstSelW = 7u; + const std::array words{}; + Require(AgcDriver::Graphics::DepthSurfaceTexture(device.graphicsContext(), words, resource, {}) != nullptr, "the stencil clear could not be sampled"); + } else { + Require(!AgcDriver::Graphics::FlushDepthPlanes(surface.depth, DepthBytes).empty(), "the depth-only flush did not consume the pending clear"); + } + device.WaitIdle(); + surface.htile = 0u; + Require(Draw(device, shaders, surface, {.depthTest = false, .stencilTest = false}, PlaneExtent, Colors) == PlaneTexels, "the read-only rebind after a clear did not cover the plane"); + Require(!AgcDriver::Graphics::FlushDepthPlanes(stencilBase, StencilBytes).empty(), "a consumed stencil clear was lost on a read-only rebind"); + device.WaitIdle(); + Require(Holding(stencil, 0u) == StencilBytes, "the consumed stencil clear did not reach guest memory"); + Require(AgcDriver::Graphics::FlushDepthPlanes(stencilBase, StencilBytes).empty(), "the consumed stencil clear was stored twice"); +} + } int main() { @@ -98,6 +143,24 @@ int main() { Require(Holding(stencilPlane, 0x18u) == StencilBytes, "a read of the stencil plane after a draw wrote stencil 0x18 does not see it in guest memory"); Require(AgcDriver::Graphics::FlushDepthPlanes(stencilBase, StencilBytes).empty(), "a second read of the stored stencil plane stored it again"); + CheckConsumedStencilClear(*device, shaders, swizzle, true); + CheckConsumedStencilClear(*device, shaders, swizzle, false); + + std::size_t readOnlyFailures = 0; + const auto checkReadOnly = [&](const DrawOptions& options, const char* name) { + Require(draw(options) == PlaneTexels, "a read-only stencil draw did not cover the plane"); + const bool dirty = !AgcDriver::Graphics::FlushDepthPlanes(stencilBase, StencilBytes).empty(); + device->WaitIdle(); + Require(Holding(stencilPlane, 0x18u) == StencilBytes, "a read-only draw changed the stored stencil bytes"); + std::cout << name << ": " << (dirty ? "dirtied the stored stencil plane" : "kept the stored stencil plane clean") << '\n'; + if (dirty) ++readOnlyFailures; + }; + checkReadOnly({.depthTest = false, .stencilTest = false}, "stencil-off"); + checkReadOnly({.depthTest = false, .stencilTest = true, .stencil = Stencil(VK_COMPARE_OP_ALWAYS, VK_STENCIL_OP_KEEP, 0x18u)}, "all-KEEP"); + auto maskedStencil = Stencil(VK_COMPARE_OP_ALWAYS, VK_STENCIL_OP_REPLACE, 0x99u); + maskedStencil.writeMask = 0u; + checkReadOnly({.depthTest = false, .stencilTest = true, .stencil = maskedStencil}, "zero-mask"); + Require(draw({.depthTest = false, .stencilTest = true, .stencil = Stencil(VK_COMPARE_OP_ALWAYS, VK_STENCIL_OP_REPLACE, 0x2au)}) == PlaneTexels, "the draw writing stencil 0x2a did not cover the plane"); AgcDriver::Graphics::RetireDepthSurfaces(device->graphicsContext().device, reinterpret_cast(depthPlane), DepthBytes); device->WaitIdle(); @@ -111,6 +174,7 @@ int main() { Require(draw({.depthTest = false, .stencilTest = true, .stencil = Stencil(VK_COMPARE_OP_EQUAL, VK_STENCIL_OP_KEEP, 0x05u)}) == PlaneTexels, "a draw testing stencil EQUAL 0x05 after the guest wrote 0x05 into the stencil plane did not pass everywhere"); Require(draw({.depthTest = false, .stencilTest = true, .stencil = Stencil(VK_COMPARE_OP_EQUAL, VK_STENCIL_OP_KEEP, 0x18u)}) == 0u, "a draw testing stencil EQUAL 0x18 passed after the guest wrote 0x05 over the stored stencil plane"); } + Require(readOnlyFailures == 0, "read-only stencil draws dirtied the stored plane"); std::puts("stencil plane memory tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/UintColorTarget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/UintColorTarget.cpp index 9c9d7f8230..c995f30b1b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/UintColorTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/UintColorTarget.cpp @@ -20,6 +20,7 @@ #else #include #endif +#include #include #include #include @@ -58,6 +59,9 @@ constexpr TargetFormat R32{"R32_UINT", 0x410u, VK_FORMAT_R32_UINT, 1, 4}; constexpr TargetFormat R32G32{"R32G32_UINT", 0x42cu, VK_FORMAT_R32G32_UINT, 2, 4}; constexpr TargetFormat R32G32B32A32{"R32G32B32A32_UINT", 0x438u, VK_FORMAT_R32G32B32A32_UINT, 4, 4}; constexpr TargetFormat R8{"R8_UINT", 0x404u, VK_FORMAT_R8_UINT, 1, 1}; +constexpr TargetFormat R16{"R16_UINT", 0x408u, VK_FORMAT_R16_UINT, 1, 2}; +constexpr TargetFormat R8G8B8A8{"R8G8B8A8_UINT", 0x428u, VK_FORMAT_R8G8B8A8_UINT, 4, 1}; +constexpr TargetFormat R16G16B16A16{"R16G16B16A16_UINT", 0x430u, VK_FORMAT_R16G16B16A16_UINT, 4, 2}; struct ExportCase { const char* name; @@ -69,6 +73,7 @@ constexpr ExportCase Export32R{"32_R", 1, VK_COLOR_COMPONENT_R_BIT}; constexpr ExportCase Export32GR{"32_GR", 2, VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT}; constexpr ExportCase Export32AR{"32_AR", 3, VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_A_BIT}; constexpr ExportCase Export32ABGR{"32_ABGR", 9, VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT}; +constexpr ExportCase ExportFp16ABGR{"FP16_ABGR", 4, VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT}; alignas(256) constexpr std::array VertexCode{ 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, @@ -99,6 +104,10 @@ std::span WordProgram(const std::array& w return Program(words, false); } +alignas(256) constexpr std::array UserDataHalvesProgram{ + 0x7e080200u, 0x7e0a0201u, 0xf8001c0fu, 0x00000504u, 0xbf810000u, +}; + struct Block { explicit Block(std::size_t bytes) : bytes(bytes), watched(AgcDriver::GuestMemory::WriteWatched()) { #ifdef _WIN32 @@ -157,7 +166,7 @@ struct Block { std::uint8_t* data = nullptr; }; -AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block, std::size_t bytes, const TargetFormat& target, std::uint32_t attrib3) { +AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block, std::size_t bytes, const TargetFormat& target, std::uint32_t attrib3, ShaderRecompiler::ColorExportPacking packing) { AgcDriver::Registers cx; cx[0x318] = static_cast(block.Address() >> 8u); cx[0x31b] = 0; @@ -168,10 +177,11 @@ AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block, std::size_t by cx[0x390] = static_cast(block.Address() >> 40u); auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); Require(color.format == target.format && color.elementBytes == target.channels * target.channelBytes && color.componentMapping == 0xe4u && color.packing == ShaderRecompiler::ColorExportPacking::Uint && color.bytes == bytes, std::string("the UINT target did not decode as an ") + target.name + " surface with the unsigned integer packing"); + color.packing = packing; return color; } -void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, const ExportCase& exported, std::span pixelCode) { +std::uint64_t Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, const ExportCase& exported, std::span pixelCode, std::span pixelUserData = {}) { const auto exportFormat = exported.format; const auto target = device.Target(); constexpr std::uint32_t waveSize = 64; @@ -213,7 +223,7 @@ void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarge const std::array pixelMemory{{{codeAddress, std::as_bytes(pixelCode)}}}; ShaderRecompiler::RecompileRequest fragment{ {ShaderStage::Fragment, codeAddress, pixelCode, 0, {}}, - {waveSize, 0, {}, std::nullopt, AgcDriver::Graphics::DecodePixelStageInfo(context, {}), std::nullopt, pixelMemory}, + {waveSize, 0, pixelUserData, std::nullopt, AgcDriver::Graphics::DecodePixelStageInfo(context, {}), std::nullopt, pixelMemory}, target, PixelPushLayout(vertexPush, target) }; @@ -223,7 +233,7 @@ void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarge fragment.shader.code = snapshot.code; fragment.context.pixel = AgcDriver::Graphics::DecodePixelStageInfo(context, AgcDriver::Graphics::ExportMappings(state)); fragment.context.pixel->targetExportPacking = AgcDriver::Graphics::ExportPackings(state); - const ShaderRecompiler::SrtRuntime runtime{}; + const ShaderRecompiler::SrtRuntime runtime{pixelUserData}; const auto materialize = [&](const ShaderRecompiler::RecompileRequest& request) { const auto invocation = AgcDriver::DriverDetail::InvocationFor(snapshot, 0, request); return invocation.Materialize(*invocation.Capture(runtime)); @@ -234,12 +244,18 @@ void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarge const auto plain = materialize(unpacked); Require(pixelResult->variantId == ShaderRecompiler::GetPreparedArtifact(*handle).variantId && plain->variantId == pixelResult->variantId, "the draw did not use the registered pixel shader"); Require(plain->PipelineVariantId() != pixelResult->PipelineVariantId() && materialize(fragment)->PipelineVariantId() == pixelResult->PipelineVariantId(), "the unsigned integer packing did not key its own specialization"); + if (color.packing != ShaderRecompiler::ColorExportPacking::Uint) { + auto unclamped = fragment; + unclamped.context.pixel->targetExportPacking[color.exportIndex] = ShaderRecompiler::ColorExportPacking::Uint; + Require(materialize(unclamped)->PipelineVariantId() != pixelResult->PipelineVariantId(), "the clamping unsigned integer packing did not key its own specialization"); + } const std::array shaders{{ {ShaderStage::Vertex, &vertexResult, 0}, {ShaderStage::Fragment, pixelResult.get(), PixelPushOffset(vertexPush, target)} }}; const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Triangle.size()), 0, 1, 0, false}; device.Draw(state, draw, shaders); + return pixelResult->PipelineVariantId(); } std::vector ReadBack(AgcDriver::VulkanDevice& device, const Block& block) { @@ -286,24 +302,51 @@ std::array Expected(const ExportCase& exported, const std::arr return vgprs; } -void TargetTests(AgcDriver::VulkanDevice& device, const TargetFormat& target, const ExportCase& exported, std::span> values, ColorTileMode mode, const char* kind) { +std::uint32_t WidenedHalf(std::uint32_t half) { + const auto sign = (half & 0x8000u) << 16u; + const auto exponent = (half >> 10u) & 0x1fu; + auto mantissa = half & 0x3ffu; + if (exponent == 0x1fu) return sign | 0x7f800000u | (mantissa << 13u) | (mantissa != 0u ? 0x400000u : 0u); + if (exponent != 0u) return sign | ((exponent + 112u) << 23u) | (mantissa << 13u); + if (mantissa == 0u) return sign; + std::uint32_t shift = 0; + while ((mantissa & 0x400u) == 0u) { + mantissa <<= 1u; + ++shift; + } + return sign | ((113u - shift) << 23u) | ((mantissa & 0x3ffu) << 13u); +} + +std::array HalfExpected(ShaderRecompiler::ColorExportPacking packing, const std::array& vgprs) { + const auto limit = packing == ShaderRecompiler::ColorExportPacking::UintClamp8 ? 0xffu : packing == ShaderRecompiler::ColorExportPacking::UintClamp16 ? 0xffffu : 0xffffffffu; + std::array expected{}; + for (std::uint32_t channel = 0; channel < expected.size(); ++channel) expected[channel] = std::min(WidenedHalf((vgprs[channel / 2u] >> (16u * (channel % 2u))) & 0xffffu), limit); + return expected; +} + +void TargetTests(AgcDriver::VulkanDevice& device, const TargetFormat& target, const ExportCase& exported, std::span> values, ColorTileMode mode, const char* kind, ShaderRecompiler::ColorExportPacking packing) { const auto elementBytes = target.channels * target.channelBytes; const ColorTargetLayout layout(Width, Height, mode, elementBytes); Block block((layout.Bytes() + BlockBytes - 1) / BlockBytes * BlockBytes); - const auto color = DecodeTarget(block, layout.Bytes(), target, mode == ColorTileMode::Linear ? LinearAttrib3 : TiledAttrib3); + const auto color = DecodeTarget(block, layout.Bytes(), target, mode == ColorTileMode::Linear ? LinearAttrib3 : TiledAttrib3, packing); + const bool halves = exported.format == ExportFp16ABGR.format; auto before = Fill(block); + std::uint64_t variant = 0; for (const auto& vgprs : values) { - Draw(device, color, exported, WordProgram(vgprs)); - char what[160]; - std::snprintf(what, sizeof(what), "a %s export of 0x%08x 0x%08x 0x%08x 0x%08x into a %s %s target", exported.name, vgprs[0], vgprs[1], vgprs[2], vgprs[3], kind, target.name); - ExpectSurface(device, block, target, layout, before, exported, Expected(exported, vgprs), what); + const std::array userData{vgprs[0], vgprs[1]}; + const auto drawn = halves ? Draw(device, color, exported, UserDataHalvesProgram, userData) : Draw(device, color, exported, WordProgram(vgprs)); + Require(!halves || variant == 0 || drawn == variant, "the exported halves were folded into the pixel shader instead of read at run time"); + variant = drawn; + char what[192]; + std::snprintf(what, sizeof(what), "a %s export of 0x%08x 0x%08x 0x%08x 0x%08x into a %s %s target with packing %u", exported.name, vgprs[0], vgprs[1], vgprs[2], vgprs[3], kind, target.name, static_cast(packing)); + ExpectSurface(device, block, target, layout, before, exported, halves ? HalfExpected(packing, vgprs) : Expected(exported, vgprs), what); before = ReadBack(device, block); } } -void FormatTests(AgcDriver::VulkanDevice& device, const TargetFormat& target, const ExportCase& exported, std::span> values) { - TargetTests(device, target, exported, values, ColorTileMode::RenderTarget, "tiled"); - TargetTests(device, target, exported, values, ColorTileMode::Linear, "linear"); +void FormatTests(AgcDriver::VulkanDevice& device, const TargetFormat& target, const ExportCase& exported, std::span> values, ShaderRecompiler::ColorExportPacking packing = ShaderRecompiler::ColorExportPacking::Uint) { + TargetTests(device, target, exported, values, ColorTileMode::RenderTarget, "tiled", packing); + TargetTests(device, target, exported, values, ColorTileMode::Linear, "linear", packing); } } @@ -323,6 +366,21 @@ int main() { FormatTests(*device, R32G32B32A32, Export32ABGR, wide); const std::vector> narrow{{0u, 0x11111111u, 0x22222222u, 0x33333333u}, {1u, 0u, 0u, 0u}, {0x7fu, 0u, 0u, 0u}, {0xa5u, 0u, 0u, 0u}, {0xffu, 0u, 0u, 0u}}; FormatTests(*device, R8, Export32R, narrow); + constexpr std::array halves{{0x0000u, 0x8000u, 0x3c00u, 0xbc00u, 0x3800u, 0x0001u, 0x83ffu, 0x03ffu, 0x7bffu, 0x7c00u, 0xfc00u, 0x7c01u, 0xfe00u}}; + std::vector> packed; + for (std::size_t index = 0; index < halves.size(); ++index) { + const auto half = [&](std::size_t offset) { return halves[(index + offset) % halves.size()]; }; + packed.push_back({half(0) | (half(3) << 16u), half(5) | (half(7) << 16u), 0u, 0u}); + } + using ShaderRecompiler::ColorExportPacking; + FormatTests(*device, R32, ExportFp16ABGR, packed); + FormatTests(*device, R32, ExportFp16ABGR, packed, ColorExportPacking::UintClamp8); + FormatTests(*device, R32, ExportFp16ABGR, packed, ColorExportPacking::UintClamp16); + FormatTests(*device, R32G32B32A32, ExportFp16ABGR, packed); + FormatTests(*device, R16, ExportFp16ABGR, packed, ColorExportPacking::UintClamp16); + FormatTests(*device, R16G16B16A16, ExportFp16ABGR, packed, ColorExportPacking::UintClamp16); + FormatTests(*device, R8, ExportFp16ABGR, packed, ColorExportPacking::UintClamp8); + FormatTests(*device, R8G8B8A8, ExportFp16ABGR, packed, ColorExportPacking::UintClamp8); std::puts("UINT color target tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/UnclampedNormalizedBlend.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/UnclampedNormalizedBlend.cpp index a8c47fc2c4..ddcb97536c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/UnclampedNormalizedBlend.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/UnclampedNormalizedBlend.cpp @@ -5,6 +5,7 @@ #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" #include "prx/libSceAgcDriver/Graphics/include/State.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" #include "prx/libc/include/GuestAllocations.hpp" @@ -30,12 +31,22 @@ #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::ColorTargetLayout; @@ -124,6 +135,7 @@ struct DrawSetup { std::uint64_t depth = 0; bool depthWrite = false; bool withoutInterlock = false; + const AgcDriver::Graphics::Context* context = nullptr; }; struct Block { @@ -301,7 +313,8 @@ void Draw(AgcDriver::VulkanDevice& device, std::span targets, std: {ShaderStage::Fragment, pixelResult.get(), PixelPushOffset(vertexPush, target)} }}; const AgcDriver::Pm4::DrawParameters draw{0, static_cast(setup.vertices.size()), 0, 1, 0, false}; - device.Draw(state, draw, shaders); + if (setup.context != nullptr) AgcDriver::Graphics::Draw(*setup.context, state, draw, shaders); + else device.Draw(state, draw, shaders); } void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, const BlendCase& blendCase, std::uint32_t exportFormat, std::span pixelCode, const DrawSetup& setup = {}) { @@ -463,11 +476,44 @@ void RunCase(AgcDriver::VulkanDevice& device, Block& block, const ColorTargetLay Require(outcome.distinguished == testCase.distinguishesClamping, std::string("unclamped blend ") + testCase.name + (testCase.distinguishesClamping ? " does not differ from Vulkan's clamped blend, so it cannot catch a clamping approximation" : " differs from Vulkan's clamped blend, which the case declares equal")); } -void RunTwoTargets(AgcDriver::VulkanDevice& device, const std::string& name, std::array blocks, std::array layouts, std::array colors, std::array cases, std::uint32_t seed) { +struct RecoveryAtImageCreation { + static AgcDriver::Graphics::Context Install(const AgcDriver::Graphics::Context& base) { + resolver = base.deviceProc; + auto context = base; + context.deviceProc = resolve; + context.functions = nullptr; + return context; + } + + static inline bool armed = false; + static inline unsigned recoveries = 0; + static inline std::uint64_t storageBytes = 0; + +private: + static PFN_vkVoidFunction VKAPI_CALL resolve(VkDevice device, const char* name) { + if (std::string_view(name) == "vkCreateImage") return reinterpret_cast(createImage); + return resolver(device, name); + } + static VKAPI_ATTR VkResult VKAPI_CALL createImage(VkDevice device, const VkImageCreateInfo* info, const VkAllocationCallbacks* callbacks, VkImage* image) { + if (armed) { + armed = false; + ++recoveries; + const auto before = AgcDriver::Graphics::TextureCacheUsage().storageBytes; + static_cast(AgcDriver::Graphics::ReleaseTextureCaches(std::numeric_limits::max(), true)); + storageBytes = before - AgcDriver::Graphics::TextureCacheUsage().storageBytes; + } + return reinterpret_cast(resolver(device, "vkCreateImage"))(device, info, callbacks, image); + } + + static inline PFN_vkGetDeviceProcAddr resolver = nullptr; +}; + +void RunTwoTargets(AgcDriver::VulkanDevice& device, const std::string& name, std::array blocks, std::array layouts, std::array colors, std::array cases, std::uint32_t seed, const DrawSetup& setup = {}, const std::function& beforeDraw = {}) { std::array, 2> before; for (std::size_t index = 0; index < 2u; ++index) before[index] = Fill(*blocks[index], seed + static_cast(index)); + if (beforeDraw) beforeDraw(); const std::array targets{{{colors[0], cases[0].blend}, {colors[1], cases[1].blend}}}; - Draw(device, targets, Float32Export | (Float32Export << 4u), TwoTargetProgram(cases[0].rgba, cases[1].rgba)); + Draw(device, targets, Float32Export | (Float32Export << 4u), TwoTargetProgram(cases[0].rgba, cases[1].rgba), setup); for (std::size_t index = 0; index < 2u; ++index) { const auto& testCase = cases[index]; const auto what = name + ", MRT" + std::to_string(index) + " " + testCase.name; @@ -569,7 +615,18 @@ int main() { {{{"with an out-of-range source and alpha", over, {0.75f, -0.5f, 2.0f, 1.5f}, true}, {"with an out-of-range source", over, {1.5f, 0.25f, -1.0f, 0.5f}, true}}}, 501u); RunTwoTargets(*device, "an 8_8_8_8 UNORM MRT0 and an 8_8 UNORM MRT1 with different blend equations in one draw", {&block, &secondPair}, {&layout, &pairLayout}, {unorm, secondTarget}, {{unormCases[3], unormCases[4]}}, 503u); - std::printf("unclamped normalized color blend tests passed: %zu UNORM draws, %zu SNORM draws, 2 same-draw overdraws of 16 layers, 2 draws blending two targets each, 1 refusal without pixel interlock\n", unormCases.size(), snormCases.size()); + Block thirdPair(pairLayout.Bytes()); + Block fourthPair(pairLayout.Bytes()); + const auto thirdTarget = DecodeTarget(thirdPair, Unorm88Info, 0); + const auto fourthTarget = DecodeTarget(fourthPair, Unorm88Info, 1); + const auto recovering = RecoveryAtImageCreation::Install(AgcDriver::VulkanDeviceTestAccess::ContextOf(*device)); + RunTwoTargets(*device, "two 8_8 UNORM targets in one draw whose MRT1 image creation releases the texture caches as an out-of-memory allocation does, evicting MRT0's resident image", {&thirdPair, &fourthPair}, {&pairLayout, &pairLayout}, {thirdTarget, fourthTarget}, + {{{"with an out-of-range source and alpha", over, {0.75f, -0.5f, 2.0f, 1.5f}, true}, {"with an out-of-range source", over, {1.5f, 0.25f, -1.0f, 0.5f}, true}}}, 505u, {.context = &recovering}, [&] { + static_cast(AgcDriver::Graphics::CachedStorageSurface(recovering, AgcDriver::Graphics::SurfaceForTarget(thirdTarget))); + RecoveryAtImageCreation::armed = true; + }); + Require(RecoveryAtImageCreation::recoveries == 1u && RecoveryAtImageCreation::storageBytes != 0u, "the release at MRT1's image creation did not evict MRT0's resident image"); + std::printf("unclamped normalized color blend tests passed: %zu UNORM draws, %zu SNORM draws, 2 same-draw overdraws of 16 layers, 3 draws blending two targets each (1 with a texture cache release at MRT1's image creation), 1 refusal without pixel interlock\n", unormCases.size(), snormCases.size()); return 0; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; diff --git a/core/libs/prx/libSceAgcDriver/tools/OracleCodeObject.cpp b/core/libs/prx/libSceAgcDriver/tools/OracleCodeObject.cpp new file mode 100644 index 0000000000..4eecb9c80f --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tools/OracleCodeObject.cpp @@ -0,0 +1,199 @@ +#include "OracleCodeObject.hpp" +#include +#include +#include +#include + +namespace { + +constexpr std::size_t ElfHeaderBytes = 64; +constexpr std::size_t SectionHeaderBytes = 64; +constexpr std::size_t SymbolBytes = 24; +constexpr std::size_t DescriptorBytes = 64; +constexpr std::uint16_t MachineAmdGpu = 224; +constexpr std::uint16_t TypeDyn = 3; +constexpr std::uint8_t SymbolObject = 1; +constexpr std::uint8_t SymbolFunc = 2; +constexpr std::uint32_t MaxGroupBytes = 65536; + +struct Section { + std::string name; + std::uint64_t address; + std::uint64_t offset; + std::uint64_t size; + std::uint32_t link; +}; + +struct Symbol { + std::string name; + std::uint8_t type; + std::uint16_t section; + std::uint64_t value; + std::uint64_t size; +}; + +void Require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +class Reader { +public: + explicit Reader(std::span bytes) : bytes(bytes) {} + + template + T Read(std::uint64_t offset, const char* what) const { + Check(offset, sizeof(T), what); + T value; + std::memcpy(&value, bytes.data() + offset, sizeof(T)); + return value; + } + + std::span Range(std::uint64_t offset, std::uint64_t size, const char* what) const { + Check(offset, size, what); + return bytes.subspan(static_cast(offset), static_cast(size)); + } + +private: + void Check(std::uint64_t offset, std::uint64_t size, const char* what) const { + Require(offset <= bytes.size() && size <= bytes.size() - offset, std::string("code object is truncated: ") + what + " lies outside the file"); + } + + std::span bytes; +}; + +std::string NameAt(std::span table, std::uint64_t offset, const char* what) { + Require(offset < table.size(), std::string("code object ") + what + " name offset is outside its string table"); + const auto begin = reinterpret_cast(table.data()) + offset; + const auto limit = static_cast(table.size() - offset); + std::size_t length = 0; + while (length < limit && begin[length] != '\0') ++length; + Require(length < limit, std::string("code object ") + what + " name is not terminated"); + return std::string(begin, length); +} + +const Symbol& FindSymbol(const std::vector& symbols, std::string_view name) { + for (const auto& symbol : symbols) { + if (symbol.name == name) return symbol; + } + throw std::runtime_error("code object has no symbol " + std::string(name)); +} + +const Section& SectionOf(const std::vector
& sections, const Symbol& symbol, std::string_view expected) { + Require(symbol.section != 0 && symbol.section < sections.size(), "symbol " + symbol.name + " has an invalid section index"); + const auto& section = sections[symbol.section]; + Require(section.name == expected, "symbol " + symbol.name + " is in section " + section.name + ", not " + std::string(expected)); + Require(symbol.value >= section.address && symbol.value - section.address <= section.size && symbol.size <= section.size - (symbol.value - section.address), + "symbol " + symbol.name + " lies outside section " + section.name); + return section; +} + +} + +std::uint32_t ParseOracleCount(std::string_view text, const char* what) { + const auto reject = [&] { throw std::runtime_error(std::string(what) + " must be a decimal count from 0 to 4294967295, not \"" + std::string(text) + "\""); }; + if (text.empty()) reject(); + std::uint64_t value = 0; + for (const char character : text) { + if (character < '0' || character > '9') reject(); + value = value * 10u + static_cast(character - '0'); + if (value > 0xffffffffu) reject(); + } + return static_cast(value); +} + +OracleKernel ParseOracleCodeObject(std::span bytes) { + const Reader reader(bytes); + Require(bytes.size() >= ElfHeaderBytes, "code object is truncated: ELF header lies outside the file"); + Require(std::memcmp(bytes.data(), "\x7f" "ELF", 4) == 0, "code object ELF magic is wrong"); + Require(reader.Read(4, "EI_CLASS") == 2, "code object EI_CLASS is not ELF64"); + Require(reader.Read(5, "EI_DATA") == 1, "code object EI_DATA is not little-endian"); + Require(reader.Read(16, "e_type") == TypeDyn, "code object e_type is not ET_DYN"); + Require(reader.Read(18, "e_machine") == MachineAmdGpu, "code object e_machine is not EM_AMDGPU (224)"); + const auto sectionOffset = reader.Read(40, "e_shoff"); + const auto sectionEntrySize = reader.Read(58, "e_shentsize"); + const auto sectionCount = reader.Read(60, "e_shnum"); + const auto nameSection = reader.Read(62, "e_shstrndx"); + Require(sectionEntrySize == SectionHeaderBytes, "code object e_shentsize is not 64"); + Require(sectionCount != 0 && nameSection < sectionCount, "code object e_shnum or e_shstrndx is invalid"); + + struct RawSection { + std::uint32_t name; + std::uint64_t address; + std::uint64_t offset; + std::uint64_t size; + std::uint32_t link; + }; + std::vector raw; + for (std::uint16_t index = 0; index < sectionCount; ++index) { + const auto base = sectionOffset + static_cast(index) * SectionHeaderBytes; + raw.push_back({reader.Read(base, "section header"), reader.Read(base + 16, "section header"), reader.Read(base + 24, "section header"), + reader.Read(base + 32, "section header"), reader.Read(base + 40, "section header")}); + } + const auto sectionNames = reader.Range(raw[nameSection].offset, raw[nameSection].size, ".shstrtab"); + std::vector
sections; + for (const auto& item : raw) { + sections.push_back({NameAt(sectionNames, item.name, "section"), item.address, item.offset, item.size, item.link}); + } + + std::size_t symtab = sections.size(); + for (std::size_t index = 0; index < sections.size(); ++index) { + if (sections[index].name == ".symtab") symtab = index; + } + Require(symtab != sections.size(), "code object has no .symtab section"); + const auto& symbolSection = sections[symtab]; + Require(symbolSection.link < sections.size() && sections[symbolSection.link].name == ".strtab", "code object .symtab does not link to .strtab"); + Require(symbolSection.size % SymbolBytes == 0, "code object .symtab size is not a multiple of 24"); + const auto strings = reader.Range(sections[symbolSection.link].offset, sections[symbolSection.link].size, ".strtab"); + std::vector symbols; + for (std::uint64_t at = 0; at < symbolSection.size; at += SymbolBytes) { + const auto base = symbolSection.offset + at; + const auto nameOffset = reader.Read(base, "symbol"); + const auto info = reader.Read(base + 4, "symbol"); + symbols.push_back({NameAt(strings, nameOffset, "symbol"), static_cast(info & 0xfu), reader.Read(base + 6, "symbol"), + reader.Read(base + 8, "symbol"), reader.Read(base + 16, "symbol")}); + } + + const auto& probe = FindSymbol(symbols, "probe"); + const auto& descriptor = FindSymbol(symbols, "probe.kd"); + Require(probe.type == SymbolFunc, "symbol probe is not STT_FUNC"); + Require(descriptor.type == SymbolObject, "symbol probe.kd is not STT_OBJECT"); + Require(probe.size != 0, "symbol probe has size 0"); + Require(probe.size % 4 == 0, "symbol probe size is not a multiple of 4"); + Require(descriptor.size == DescriptorBytes, "symbol probe.kd size is not 64"); + const auto& textSection = SectionOf(sections, probe, ".text"); + const auto& rodataSection = SectionOf(sections, descriptor, ".rodata"); + const auto textBytes = reader.Range(textSection.offset + (probe.value - textSection.address), probe.size, "probe"); + const auto descriptorBytes = reader.Range(rodataSection.offset + (descriptor.value - rodataSection.address), descriptor.size, "probe.kd"); + + const auto field = [&](std::size_t offset, std::size_t size) { + std::uint64_t value = 0; + std::memcpy(&value, descriptorBytes.data() + offset, size); + return value; + }; + const auto group = static_cast(field(0, 4)); + const auto privateBytes = static_cast(field(4, 4)); + const auto kernargBytes = static_cast(field(8, 4)); + const auto entry = static_cast(field(16, 8)); + const auto rsrc1 = static_cast(field(48, 4)); + const auto rsrc2 = static_cast(field(52, 4)); + const auto properties = static_cast(field(56, 2)); + Require(group <= MaxGroupBytes, "kernel descriptor group_segment_fixed_size exceeds 65536"); + Require(privateBytes == 0, "kernel descriptor private_segment_fixed_size is not 0"); + Require(kernargBytes == 16, "kernel descriptor kernarg_size is not 16"); + Require(entry == static_cast(probe.value) - static_cast(descriptor.value), "kernel descriptor kernel_code_entry_byte_offset does not point at probe"); + Require((rsrc2 & 1u) == 0, "kernel descriptor compute_pgm_rsrc2 ENABLE_PRIVATE_SEGMENT is set"); + Require(((rsrc2 >> 1) & 0x1fu) == 2, "kernel descriptor compute_pgm_rsrc2 USER_SGPR_COUNT is not 2"); + Require((properties & 0x8u) != 0, "kernel descriptor kernel_code_properties ENABLE_SGPR_KERNARG_SEGMENT_PTR is not set"); + Require((properties & 0x77u) == 0, "kernel descriptor kernel_code_properties enables a user SGPR other than the kernarg pointer"); + + OracleKernel kernel{}; + kernel.text.resize(textBytes.size() / 4); + std::memcpy(kernel.text.data(), textBytes.data(), textBytes.size()); + kernel.ldsBytes = group; + kernel.waveSize = (properties & 0x400u) != 0 ? 32u : 64u; + kernel.floatMode = {(rsrc1 >> 12) & 0xffu, ((rsrc1 >> 21) & 1u) != 0, ((rsrc1 >> 23) & 1u) != 0, ((rsrc1 >> 26) & 1u) != 0}; + kernel.groupIdEnable = {((rsrc2 >> 7) & 1u) != 0, ((rsrc2 >> 8) & 1u) != 0, ((rsrc2 >> 9) & 1u) != 0}; + kernel.tgSizeEnable = ((rsrc2 >> 10) & 1u) != 0; + kernel.threadIdComponentCount = ((rsrc2 >> 11) & 3u) + 1u; + return kernel; +} diff --git a/core/libs/prx/libSceAgcDriver/tools/OracleCodeObject.hpp b/core/libs/prx/libSceAgcDriver/tools/OracleCodeObject.hpp new file mode 100644 index 0000000000..dbff31fd03 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tools/OracleCodeObject.hpp @@ -0,0 +1,26 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_TOOLS_ORACLECODEOBJECT_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_TOOLS_ORACLECODEOBJECT_HPP + +#include "Recompiler.hpp" +#include +#include +#include +#include +#include +#include + +struct OracleKernel { + std::vector text; + std::uint32_t ldsBytes; + std::uint32_t waveSize; + ShaderRecompiler::ShaderFloatMode floatMode; + std::array groupIdEnable; + bool tgSizeEnable; + std::uint32_t threadIdComponentCount; +}; + +OracleKernel ParseOracleCodeObject(std::span bytes); + +std::uint32_t ParseOracleCount(std::string_view text, const char* what); + +#endif diff --git a/core/libs/prx/libSceAgcDriver/tools/OracleDispatch.cpp b/core/libs/prx/libSceAgcDriver/tools/OracleDispatch.cpp new file mode 100644 index 0000000000..29b8e0577c --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tools/OracleDispatch.cpp @@ -0,0 +1,100 @@ +#include "OracleDispatch.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include + +namespace { + +constexpr std::size_t PageBytes = 4096; +constexpr std::size_t BlockAlignment = 65536; +constexpr std::uint32_t MaxThreads = 1024; +constexpr std::uint32_t RowBytes = 16; +constexpr std::uint32_t OutputRowBytes = 64; + +std::size_t AlignUp(std::size_t value, std::size_t alignment) { + return (value + alignment - 1) / alignment * alignment; +} + +class GuestBlock { +public: + explicit GuestBlock(std::size_t size) : size(size) { +#ifdef _WIN32 + block = static_cast(VirtualAlloc(nullptr, size, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + block = static_cast(std::aligned_alloc(BlockAlignment, size)); +#endif + if (block == nullptr) throw std::runtime_error("cannot allocate the guest block of " + std::to_string(size) + " bytes"); + GuestAllocations::Mutation().Add(block, size, 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() const { return block; } + std::size_t Size() const { return size; } + +private: + std::uint8_t* block = nullptr; + std::size_t size; +}; + +} + +std::vector RunOracleKernel(AgcDriver::VulkanDevice& device, const OracleKernel& kernel, std::uint32_t threads, std::span input) { + if (threads == 0) throw std::runtime_error("threads must be at least 1"); + if (threads > MaxThreads) throw std::runtime_error("threads must not exceed " + std::to_string(MaxThreads)); + if (kernel.waveSize == 0 || threads % kernel.waveSize != 0) throw std::runtime_error("threads must be a multiple of the wave size " + std::to_string(kernel.waveSize)); + const std::size_t rowBytes = static_cast(threads) * RowBytes; + const std::size_t outputBytes = static_cast(threads) * OutputRowBytes; + if (input.size() < rowBytes) throw std::runtime_error("input holds " + std::to_string(input.size()) + " bytes, " + std::to_string(rowBytes) + " are needed"); + if (kernel.text.empty()) throw std::runtime_error("kernel has no instructions"); + + const std::size_t inputOffset = PageBytes; + const std::size_t outputOffset = AlignUp(inputOffset + input.size(), PageBytes); + GuestBlock guest(AlignUp(outputOffset + outputBytes, BlockAlignment)); + std::memset(guest.Data(), 0, guest.Size()); + std::memcpy(guest.Data() + inputOffset, input.data(), input.size()); + std::memset(guest.Data() + outputOffset, 0xee, outputBytes); + const auto base = static_cast(reinterpret_cast(guest.Data())); + const std::array kernarg{base + inputOffset, base + outputOffset}; + std::memcpy(guest.Data(), kernarg.data(), sizeof(kernarg)); + + const std::span code(kernel.text); + const std::array regions{{ + {reinterpret_cast(code.data()), std::as_bytes(code)}, + {base, std::span(reinterpret_cast(guest.Data()), guest.Size())} + }}; + const std::vector userData{static_cast(base), static_cast(base >> 32u)}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{threads, 1, 1}, (kernel.ldsBytes + 3u) / 4u, kernel.groupIdEnable, kernel.tgSizeEnable, kernel.threadIdComponentCount}; + ShaderRecompiler::RecompileRequest request{ + {ShaderRecompiler::ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {kernel.waveSize, 0, userData, compute, std::nullopt, std::nullopt, regions, kernel.floatMode}, + 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(); + std::vector output(outputBytes); + std::memcpy(output.data(), guest.Data() + outputOffset, outputBytes); + return output; +} diff --git a/core/libs/prx/libSceAgcDriver/tools/OracleDispatch.hpp b/core/libs/prx/libSceAgcDriver/tools/OracleDispatch.hpp new file mode 100644 index 0000000000..75d81023b5 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tools/OracleDispatch.hpp @@ -0,0 +1,13 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_TOOLS_ORACLEDISPATCH_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_TOOLS_ORACLEDISPATCH_HPP + +#include "OracleCodeObject.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include +#include +#include +#include + +std::vector RunOracleKernel(AgcDriver::VulkanDevice& device, const OracleKernel& kernel, std::uint32_t threads, std::span input); + +#endif diff --git a/core/libs/prx/libSceAgcDriver/tools/OracleRunner.cpp b/core/libs/prx/libSceAgcDriver/tools/OracleRunner.cpp new file mode 100644 index 0000000000..06c3608a65 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tools/OracleRunner.cpp @@ -0,0 +1,56 @@ +#include "OracleDispatch.hpp" +#include +#include +#include +#include +#include + +namespace { + +constexpr const char* Usage = "usage: agc_oracle_runner code.co threads in.bin out.bin out_dwords\n"; + +std::vector ReadFile(const char* path) { + std::ifstream file(path, std::ios::binary | std::ios::ate); + if (!file) throw std::runtime_error(std::string("cannot open ") + path); + const auto size = file.tellg(); + if (size < 0) throw std::runtime_error(std::string("cannot size ") + path); + std::vector bytes(static_cast(size)); + file.seekg(0); + file.read(reinterpret_cast(bytes.data()), static_cast(bytes.size())); + if (!file) throw std::runtime_error(std::string("cannot read ") + path); + return bytes; +} + +} + +int main(int argc, char** argv) { + if (argc == 2 && std::string_view(argv[1]) == "--help") { + std::cout << Usage; + return 0; + } + if (argc == 2 && std::string_view(argv[1]) == "--target") { + std::cerr << "oracle runner: --target is not supported\n"; + return 2; + } + if (argc != 6) { + std::cerr << Usage; + return 2; + } + try { + const auto threads = ParseOracleCount(argv[2], "threads"); + const auto outDwords = ParseOracleCount(argv[5], "out_dwords"); + if (outDwords != 16) throw std::runtime_error("out_dwords must be 16, the template stores 16 dwords per lane"); + const auto kernel = ParseOracleCodeObject(ReadFile(argv[1])); + const auto input = ReadFile(argv[3]); + AgcDriver::VulkanDevice device; + const auto output = RunOracleKernel(device, kernel, threads, input); + std::ofstream file(argv[4], std::ios::binary | std::ios::trunc); + file.write(reinterpret_cast(output.data()), static_cast(output.size())); + file.close(); + if (!file) throw std::runtime_error(std::string("cannot write ") + argv[4]); + return 0; + } catch (const std::exception& error) { + std::cerr << "oracle runner: " << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAjmi/Export.cpp b/core/libs/prx/libSceAjmi/Export.cpp index 925569074a..6c99d7f4c1 100644 --- a/core/libs/prx/libSceAjmi/Export.cpp +++ b/core/libs/prx/libSceAjmi/Export.cpp @@ -1,3 +1,5 @@ +#include "prx/libc/include/general/ExportMacros.hpp" +#include "prx/libSceAjmi/include/InventoryExports.hpp" #include "prx/libSceAjmi/include/Api.hpp" #include #include @@ -52,3 +54,115 @@ void* APS5_VABI sceAjmBatchJobRunBufferRa_nid_disambig1(void* position, uint32_t } } + +extern "C" { + +int APS5_VABI sceAjmiBatchCancel() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceAjmiBatchWait() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI libSceAjmiUnknown_4MsUK9kAnKg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("4MsUK9kAnKg", libSceAjmiUnknown_4MsUK9kAnKg_nid_no_patch); + +int APS5_VABI libSceAjmiUnknown_6fY_minus_ci3au2U_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("6fY-ci3au2U", libSceAjmiUnknown_6fY_minus_ci3au2U_nid_no_patch); + +int APS5_VABI libSceAjmiUnknown_9zA4YMlkR9M_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("9zA4YMlkR9M", libSceAjmiUnknown_9zA4YMlkR9M_nid_no_patch); + +int APS5_VABI libSceAjmiUnknown_BD002VBfhuE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("BD002VBfhuE", libSceAjmiUnknown_BD002VBfhuE_nid_no_patch); + +int APS5_VABI libSceAjmiUnknown_EYIibEpcHuw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("EYIibEpcHuw", libSceAjmiUnknown_EYIibEpcHuw_nid_no_patch); + +int APS5_VABI libSceAjmiUnknown_J3UwQ5e6sNU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("J3UwQ5e6sNU", libSceAjmiUnknown_J3UwQ5e6sNU_nid_no_patch); + +int APS5_VABI libSceAjmiUnknown_LESEGgm133w_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("LESEGgm133w", libSceAjmiUnknown_LESEGgm133w_nid_no_patch); + +int APS5_VABI libSceAjmiUnknown_MTdd8ApwO50_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("MTdd8ApwO50", libSceAjmiUnknown_MTdd8ApwO50_nid_no_patch); + +int APS5_VABI libSceAjmiUnknown_QhA7rTjJbro_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("QhA7rTjJbro", libSceAjmiUnknown_QhA7rTjJbro_nid_no_patch); + +int APS5_VABI libSceAjmiUnknown_RbLGSlJB6dc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("RbLGSlJB6dc", libSceAjmiUnknown_RbLGSlJB6dc_nid_no_patch); + +int APS5_VABI libSceAjmiUnknown_Uqtj4RSP_minus_JQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("Uqtj4RSP-JQ", libSceAjmiUnknown_Uqtj4RSP_minus_JQ_nid_no_patch); + +int APS5_VABI libSceAjmiUnknown_X8sT_minus_xi_minus_dVE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("X8sT-xi-dVE", libSceAjmiUnknown_X8sT_minus_xi_minus_dVE_nid_no_patch); + +int APS5_VABI libSceAjmiUnknown_YtubBgSeNH0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("YtubBgSeNH0", libSceAjmiUnknown_YtubBgSeNH0_nid_no_patch); + +int APS5_VABI libSceAjmiUnknown_cWZnCK7na5Y_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("cWZnCK7na5Y", libSceAjmiUnknown_cWZnCK7na5Y_nid_no_patch); + +int APS5_VABI libSceAjmiUnknown_dTsNpEt1AqQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("dTsNpEt1AqQ", libSceAjmiUnknown_dTsNpEt1AqQ_nid_no_patch); + +int APS5_VABI libSceAjmiUnknown_dmwcwCeItmE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("dmwcwCeItmE", libSceAjmiUnknown_dmwcwCeItmE_nid_no_patch); + +int APS5_VABI libSceAjmiUnknown_m0xisYC6U2w_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("m0xisYC6U2w", libSceAjmiUnknown_m0xisYC6U2w_nid_no_patch); + +int APS5_VABI libSceAjmiUnknown_ofRX8Vq0ss0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("ofRX8Vq0ss0", libSceAjmiUnknown_ofRX8Vq0ss0_nid_no_patch); + +int APS5_VABI libSceAjmiUnknown_pfPwiigOE3A_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("pfPwiigOE3A", libSceAjmiUnknown_pfPwiigOE3A_nid_no_patch); + +int APS5_VABI libSceAjmiUnknown_r7NMFYcJi08_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("r7NMFYcJi08", libSceAjmiUnknown_r7NMFYcJi08_nid_no_patch); + +} diff --git a/core/libs/prx/libSceAjmi/include/InventoryExports.hpp b/core/libs/prx/libSceAjmi/include/InventoryExports.hpp new file mode 100644 index 0000000000..67f6606388 --- /dev/null +++ b/core/libs/prx/libSceAjmi/include/InventoryExports.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include "SceTypes.hpp" + +extern "C" { + +int APS5_VABI sceAjmiBatchCancel(); +int APS5_VABI sceAjmiBatchWait(); +int APS5_VABI libSceAjmiUnknown_4MsUK9kAnKg_nid_no_patch(); +int APS5_VABI libSceAjmiUnknown_6fY_minus_ci3au2U_nid_no_patch(); +int APS5_VABI libSceAjmiUnknown_9zA4YMlkR9M_nid_no_patch(); +int APS5_VABI libSceAjmiUnknown_BD002VBfhuE_nid_no_patch(); +int APS5_VABI libSceAjmiUnknown_EYIibEpcHuw_nid_no_patch(); +int APS5_VABI libSceAjmiUnknown_J3UwQ5e6sNU_nid_no_patch(); +int APS5_VABI libSceAjmiUnknown_LESEGgm133w_nid_no_patch(); +int APS5_VABI libSceAjmiUnknown_MTdd8ApwO50_nid_no_patch(); +int APS5_VABI libSceAjmiUnknown_QhA7rTjJbro_nid_no_patch(); +int APS5_VABI libSceAjmiUnknown_RbLGSlJB6dc_nid_no_patch(); +int APS5_VABI libSceAjmiUnknown_Uqtj4RSP_minus_JQ_nid_no_patch(); +int APS5_VABI libSceAjmiUnknown_X8sT_minus_xi_minus_dVE_nid_no_patch(); +int APS5_VABI libSceAjmiUnknown_YtubBgSeNH0_nid_no_patch(); +int APS5_VABI libSceAjmiUnknown_cWZnCK7na5Y_nid_no_patch(); +int APS5_VABI libSceAjmiUnknown_dTsNpEt1AqQ_nid_no_patch(); +int APS5_VABI libSceAjmiUnknown_dmwcwCeItmE_nid_no_patch(); +int APS5_VABI libSceAjmiUnknown_m0xisYC6U2w_nid_no_patch(); +int APS5_VABI libSceAjmiUnknown_ofRX8Vq0ss0_nid_no_patch(); +int APS5_VABI libSceAjmiUnknown_pfPwiigOE3A_nid_no_patch(); +int APS5_VABI libSceAjmiUnknown_r7NMFYcJi08_nid_no_patch(); + +} diff --git a/core/libs/prx/libSceAjmi/libSceAjmi.hpp b/core/libs/prx/libSceAjmi/libSceAjmi.hpp index 739e63957f..1b39c77efc 100644 --- a/core/libs/prx/libSceAjmi/libSceAjmi.hpp +++ b/core/libs/prx/libSceAjmi/libSceAjmi.hpp @@ -1,3 +1,4 @@ #pragma once #include "prx/libSceAjmi/include/Api.hpp" +#include "prx/libSceAjmi/include/InventoryExports.hpp" diff --git a/core/libs/prx/libSceAppContent/Export.cpp b/core/libs/prx/libSceAppContent/Export.cpp index 9d53053c3b..cec9ad2536 100644 --- a/core/libs/prx/libSceAppContent/Export.cpp +++ b/core/libs/prx/libSceAppContent/Export.cpp @@ -312,6 +312,22 @@ int APS5_VABI sceAppContentDownload1Shrink(void) { return 0; } +int APS5_VABI sceAppContentDownload2Expand(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppContentDownload2Shrink(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +APS5_EXPORT("SWVxsi-ZBlw", sceAppContentUnknown_SWVxsi_ZBlw); +int APS5_VABI sceAppContentUnknown_SWVxsi_ZBlw(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI sceAppContentGetAddcontInfoByEntitlementId(const char* entitlementId, AppContentAddcontInfo* info) { if (!initialized.load()) return SCE_APP_CONTENT_ERROR_NOT_INITIALIZED; if (!entitlementId || !info) return SCE_APP_CONTENT_ERROR_PARAMETER; diff --git a/core/libs/prx/libSceAppContent/include/Api.hpp b/core/libs/prx/libSceAppContent/include/Api.hpp index ae24effec5..896f9793d5 100644 --- a/core/libs/prx/libSceAppContent/include/Api.hpp +++ b/core/libs/prx/libSceAppContent/include/Api.hpp @@ -38,6 +38,9 @@ int APS5_VABI sceAppContentDownload0Expand(void); int APS5_VABI sceAppContentDownload0Shrink(void); int APS5_VABI sceAppContentDownload1Expand(void); int APS5_VABI sceAppContentDownload1Shrink(void); +int APS5_VABI sceAppContentDownload2Expand(void); +int APS5_VABI sceAppContentDownload2Shrink(void); +int APS5_VABI sceAppContentUnknown_SWVxsi_ZBlw(void); int APS5_VABI sceAppContentGetAddcontInfoByEntitlementId(const char* entitlementId, AppContentAddcontInfo* info); int APS5_VABI sceAppContentGetAddcontInfoListByIroTag(uint32_t iroTag, AppContentAddcontInfo* list, uint32_t listNum, uint32_t* hitNum); int APS5_VABI sceAppContentGetDownloadedStoreCountry(void); diff --git a/core/libs/prx/libSceAppInstUtil/CMakeLists.txt b/core/libs/prx/libSceAppInstUtil/CMakeLists.txt new file mode 100644 index 0000000000..6d82b292a5 --- /dev/null +++ b/core/libs/prx/libSceAppInstUtil/CMakeLists.txt @@ -0,0 +1,19 @@ +add_library(libSceAppInstUtil SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceAppInstUtil PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceAppInstUtil) + +if(BUILD_TESTING) + add_test_executable(guest_app_inst_util_tests ${LIBS_INCLUDE_DIR}/tests/GuestAppInstUtil.cpp) + target_include_directories(guest_app_inst_util_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_app_inst_util_tests PRIVATE libSceAppInstUtil libc) + configure_windows_unwind(guest_app_inst_util_tests) + set(appInstUtilRun ${CMAKE_CURRENT_BINARY_DIR}/guest_app_inst_util) + file(WRITE ${appInstUtilRun}/app0/sce_sys/param.json + "{\"titleId\":\"PPSA00000\",\"localizedParameters\":{\"en-US\":{\"titleName\":\"Example\"}}," + "\"userDefinedParam1\":0,\"userDefinedParam2\":0,\"userDefinedParam3\":0,\"userDefinedParam4\":0}") + add_test(NAME guest_app_inst_util COMMAND guest_app_inst_util_tests WORKING_DIRECTORY ${appInstUtilRun}) + if(WIN32) + set_tests_properties(guest_app_inst_util PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceAppInstUtil/Export.cpp b/core/libs/prx/libSceAppInstUtil/Export.cpp index e2f0a0c8b6..5a6d52b414 100644 --- a/core/libs/prx/libSceAppInstUtil/Export.cpp +++ b/core/libs/prx/libSceAppInstUtil/Export.cpp @@ -3,17 +3,33 @@ #include "prx/libSceAppInstUtil/include/Api.hpp" #include #include +#include +#include +#include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libkernel/AppMetadata/include/AppMetadata.hpp" + +static std::mutex g_serviceMutex; +static bool g_serviceStarted = false; + +static void RequireStarted(const char* function) { + if (!g_serviceStarted) throw std::runtime_error(std::string(function) + ": called before sceAppInstUtilInitialize"); +} extern "C" { int APS5_VABI sceAppInstUtilInitialize(void) { - NotImplemented_nid_no_patch(__func__); + std::lock_guard lock(g_serviceMutex); + if (g_serviceStarted) throw std::runtime_error("sceAppInstUtilInitialize: the service is already initialized"); + g_serviceStarted = true; return 0; } int APS5_VABI sceAppInstUtilTerminate(void) { - NotImplemented_nid_no_patch(__func__); + std::lock_guard lock(g_serviceMutex); + RequireStarted(__func__); + g_serviceStarted = false; return 0; } int APS5_VABI sceAppInstUtilAppInstallAll(void* reserved) { @@ -33,7 +49,10 @@ int APS5_VABI sceAppInstUtilAppIsInUpdating(const char* titleId, bool* updating) return 0; } int APS5_VABI sceAppInstUtilAppExists(const char* titleId, int32_t* exists) { - NotImplemented_nid_no_patch(__func__); + std::lock_guard lock(g_serviceMutex); + RequireStarted(__func__); + if (titleId == nullptr || exists == nullptr) APS5_INVALID_ARG_EX; + *exists = std::strcmp(titleId, GetAppTitleId_nid_postfix().value) == 0 ? 1 : 0; return 0; } int APS5_VABI sceAppInstUtilAppGetSize(const char* titleId, std::uint64_t* size) { diff --git a/core/libs/prx/libSceAudioOut/CMakeLists.txt b/core/libs/prx/libSceAudioOut/CMakeLists.txt index 1d9722e3a1..48000b0f1d 100644 --- a/core/libs/prx/libSceAudioOut/CMakeLists.txt +++ b/core/libs/prx/libSceAudioOut/CMakeLists.txt @@ -246,6 +246,16 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_test_executable(audio_out2_speaker_array_tests tests/AudioOut2SpeakerArray.cpp) + target_include_directories(audio_out2_speaker_array_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(audio_out2_speaker_array_tests PRIVATE libSceAudioOut libc) + configure_windows_unwind(audio_out2_speaker_array_tests) + add_test(NAME audio_out2_speaker_array COMMAND audio_out2_speaker_array_tests) + if(WIN32) + set_tests_properties(audio_out2_speaker_array PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(audio_out_info_tests tests/AudioOutInfo.cpp) target_include_directories(audio_out_info_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(audio_out_info_tests PRIVATE libSceAudioOut libc) @@ -317,6 +327,16 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_test_executable(guest_audio_out_tests ${LIBS_INCLUDE_DIR}/tests/GuestAudioOut.cpp) + target_include_directories(guest_audio_out_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_audio_out_tests PRIVATE libSceAudioOut libc) + configure_windows_unwind(guest_audio_out_tests) + add_test(NAME guest_audio_out COMMAND guest_audio_out_tests) + if(WIN32) + set_tests_properties(guest_audio_out PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(audio_out2_context_handles_tests tests/AudioOut2ContextHandles.cpp) target_include_directories(audio_out2_context_handles_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(audio_out2_context_handles_tests PRIVATE libSceAudioOut libc) diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOut2SpeakerArray.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOut2SpeakerArray.cpp new file mode 100644 index 0000000000..d172e7709c --- /dev/null +++ b/core/libs/prx/libSceAudioOut/tests/AudioOut2SpeakerArray.cpp @@ -0,0 +1,81 @@ +#include "prx/libSceAudioOut/include/AudioOut2.hpp" +#include "SceTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include +#include +#include +#include +#include + +static void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "%s\n", message); + std::abort(); +} + +template +static bool RejectsArgument(TFunction&& function) { + try { + function(); + } catch (const std::invalid_argument&) { + return true; + } + return false; +} + +struct VbapParams { + const AudioOut2Position* positions; + std::uint32_t numSpeakers; + std::uint8_t is3d; + void* memory; + std::size_t memorySize; + std::uint32_t reserved; +}; + +static bool Near(float value, float expected) { + return std::fabs(value - expected) < 1e-5f; +} + +int main() { + const AudioOut2Position square[4] = {{1, 0, 0}, {0, 1, 0}, {-1, 0, 0}, {0, -1, 0}}; + alignas(16) unsigned char memory[512] = {}; + VbapParams params{square, 4, 0, memory, sizeof(memory), 0}; + AudioOut2SpeakerArrayHandle array = nullptr; + Require(RejectsArgument([&] { sceAudioOut2SpeakerArrayCreate(nullptr, ¶ms, nullptr); }), "a null handle output was accepted"); + Require(RejectsArgument([&] { sceAudioOut2SpeakerArrayCreate(&array, nullptr, nullptr); }), "null parameters were accepted"); + VbapParams none = params; + none.numSpeakers = 0; + Require(RejectsArgument([&] { sceAudioOut2SpeakerArrayCreate(&array, &none, nullptr); }), "an array without speakers was accepted"); + VbapParams tooMany = params; + tooMany.numSpeakers = 33; + Require(RejectsArgument([&] { sceAudioOut2SpeakerArrayCreate(&array, &tooMany, nullptr); }), "33 speakers were accepted"); + VbapParams small = params; + small.memorySize = 16; + Require(RejectsArgument([&] { sceAudioOut2SpeakerArrayCreate(&array, &small, nullptr); }), "memory too small for the array was accepted"); + VbapParams misaligned = params; + misaligned.memory = memory + 1; + Require(RejectsArgument([&] { sceAudioOut2SpeakerArrayCreate(&array, &misaligned, nullptr); }), "misaligned memory was accepted"); + Require(array == nullptr, "a rejected creation wrote the handle"); + Require(sceAudioOut2SpeakerArrayCreate(&array, ¶ms, nullptr) == 0 && array == memory, "the array was not placed in the given memory"); + + constexpr std::uint32_t ambisonics = 0x40; + float coefficients[4] = {}; + Require(sceAudioOut2GetSpeakerArrayAmbisonicsCoefficients(array, ambisonics | 0, coefficients, 4) == 0, "W channel coefficients failed"); + for (const float coefficient : coefficients) Require(Near(coefficient, 0.25f), "the W channel is not split evenly over four speakers"); + Require(sceAudioOut2GetSpeakerArrayAmbisonicsCoefficients(array, ambisonics | 1, coefficients, 4) == 0, "Y channel coefficients failed"); + Require(Near(coefficients[0], 0.0f) && Near(coefficients[1], 0.25f) && Near(coefficients[2], 0.0f) && Near(coefficients[3], -0.25f), "the Y channel does not follow the sine of each azimuth"); + Require(sceAudioOut2GetSpeakerArrayAmbisonicsCoefficients(array, ambisonics | 3, coefficients, 4) == 0, "X channel coefficients failed"); + Require(Near(coefficients[0], 0.25f) && Near(coefficients[1], 0.0f) && Near(coefficients[2], -0.25f) && Near(coefficients[3], 0.0f), "the X channel does not follow the cosine of each azimuth"); + Require(sceAudioOut2GetSpeakerArrayAmbisonicsCoefficients(array, ambisonics | 4, coefficients, 4) == 0, "second-order coefficients failed"); + for (const float coefficient : coefficients) Require(coefficient == 0.0f, "four speakers decoded a second-order channel"); + Require(RejectsArgument([&] { sceAudioOut2GetSpeakerArrayAmbisonicsCoefficients(array, 0, coefficients, 4); }), "a channel without the ambisonics flag was accepted"); + Require(RejectsArgument([&] { sceAudioOut2GetSpeakerArrayAmbisonicsCoefficients(array, ambisonics | 36, coefficients, 4); }), "a sixth-order channel was accepted"); + Require(RejectsArgument([&] { sceAudioOut2GetSpeakerArrayAmbisonicsCoefficients(array, ambisonics, coefficients, 3); }), "a coefficient count other than the speaker count was accepted"); + Require(RejectsArgument([&] { sceAudioOut2GetSpeakerArrayAmbisonicsCoefficients(array, ambisonics, nullptr, 4); }), "null coefficients were accepted"); + alignas(16) unsigned char notArray[512] = {}; + Require(RejectsArgument([&] { sceAudioOut2GetSpeakerArrayAmbisonicsCoefficients(notArray, ambisonics, coefficients, 4); }), "memory that holds no array was accepted"); + std::puts("AudioOut2 speaker array tests passed"); + return 0; +} diff --git a/core/libs/prx/libSceAudioSystem/Export.cpp b/core/libs/prx/libSceAudioSystem/Export.cpp index 09a91b16b5..d85e4945ba 100644 --- a/core/libs/prx/libSceAudioSystem/Export.cpp +++ b/core/libs/prx/libSceAudioSystem/Export.cpp @@ -1,3 +1,5 @@ +#include "prx/libc/include/general/ExportMacros.hpp" +#include "prx/libSceAudioSystem/include/InventoryExports.hpp" #include "prx/libSceAudioSystem/include/Api.hpp" #include #include @@ -41,3 +43,155 @@ int APS5_VABI sceAudioSystemTerminate() { } } + +extern "C" { + +int APS5_VABI sceAudioOutServerBusGetIoParam() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceAudioOutServerGetOption() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI libSceAudioSystemUnknown__minus_e5aJEqL34Y_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-e5aJEqL34Y", libSceAudioSystemUnknown__minus_e5aJEqL34Y_nid_no_patch); + +int APS5_VABI libSceAudioSystemUnknown_17nOsxccH5o_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("17nOsxccH5o", libSceAudioSystemUnknown_17nOsxccH5o_nid_no_patch); + +int APS5_VABI libSceAudioSystemUnknown_2bKJUeywpeY_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("2bKJUeywpeY", libSceAudioSystemUnknown_2bKJUeywpeY_nid_no_patch); + +int APS5_VABI libSceAudioSystemUnknown_3HOelmA2vcw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("3HOelmA2vcw", libSceAudioSystemUnknown_3HOelmA2vcw_nid_no_patch); + +int APS5_VABI libSceAudioSystemUnknown_5SM0uQwcgTQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("5SM0uQwcgTQ", libSceAudioSystemUnknown_5SM0uQwcgTQ_nid_no_patch); + +int APS5_VABI libSceAudioSystemUnknown_5oOYYvwIb0k_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("5oOYYvwIb0k", libSceAudioSystemUnknown_5oOYYvwIb0k_nid_no_patch); + +int APS5_VABI libSceAudioSystemUnknown_6PGbkl6OnvE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("6PGbkl6OnvE", libSceAudioSystemUnknown_6PGbkl6OnvE_nid_no_patch); + +int APS5_VABI libSceAudioSystemUnknown_9QaKJdd7jKk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("9QaKJdd7jKk", libSceAudioSystemUnknown_9QaKJdd7jKk_nid_no_patch); + +int APS5_VABI libSceAudioSystemUnknown_B_minus_JyTVr90kc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("B-JyTVr90kc", libSceAudioSystemUnknown_B_minus_JyTVr90kc_nid_no_patch); + +int APS5_VABI libSceAudioSystemUnknown_Fd9u7_minus_OKW_plus_Q_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("Fd9u7-OKW+Q", libSceAudioSystemUnknown_Fd9u7_minus_OKW_plus_Q_nid_no_patch); + +int APS5_VABI libSceAudioSystemUnknown_FkFcs7NTFDo_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("FkFcs7NTFDo", libSceAudioSystemUnknown_FkFcs7NTFDo_nid_no_patch); + +int APS5_VABI libSceAudioSystemUnknown_GUtNdBeMrcM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("GUtNdBeMrcM", libSceAudioSystemUnknown_GUtNdBeMrcM_nid_no_patch); + +int APS5_VABI libSceAudioSystemUnknown_HxLyu3KUv5U_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("HxLyu3KUv5U", libSceAudioSystemUnknown_HxLyu3KUv5U_nid_no_patch); + +int APS5_VABI libSceAudioSystemUnknown_IiO_plus_DdVz_minus_4E_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("IiO+DdVz-4E", libSceAudioSystemUnknown_IiO_plus_DdVz_minus_4E_nid_no_patch); + +int APS5_VABI libSceAudioSystemUnknown_J2Fx9JLKcqU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("J2Fx9JLKcqU", libSceAudioSystemUnknown_J2Fx9JLKcqU_nid_no_patch); + +int APS5_VABI libSceAudioSystemUnknown_Met2nIMg_plus_Ng_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("Met2nIMg+Ng", libSceAudioSystemUnknown_Met2nIMg_plus_Ng_nid_no_patch); + +int APS5_VABI libSceAudioSystemUnknown_WRDd2feDYzg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("WRDd2feDYzg", libSceAudioSystemUnknown_WRDd2feDYzg_nid_no_patch); + +int APS5_VABI libSceAudioSystemUnknown_c4m3OSO0lj8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("c4m3OSO0lj8", libSceAudioSystemUnknown_c4m3OSO0lj8_nid_no_patch); + +int APS5_VABI libSceAudioSystemUnknown_ds20PmJfSSk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("ds20PmJfSSk", libSceAudioSystemUnknown_ds20PmJfSSk_nid_no_patch); + +int APS5_VABI libSceAudioSystemUnknown_hjlhfF8Voz0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("hjlhfF8Voz0", libSceAudioSystemUnknown_hjlhfF8Voz0_nid_no_patch); + +int APS5_VABI libSceAudioSystemUnknown_jLjHQbmK9_plus_A_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("jLjHQbmK9+A", libSceAudioSystemUnknown_jLjHQbmK9_plus_A_nid_no_patch); + +int APS5_VABI libSceAudioSystemUnknown_jZPcy9MG4w0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("jZPcy9MG4w0", libSceAudioSystemUnknown_jZPcy9MG4w0_nid_no_patch); + +int APS5_VABI libSceAudioSystemUnknown_lSatNsWSmps_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("lSatNsWSmps", libSceAudioSystemUnknown_lSatNsWSmps_nid_no_patch); + +int APS5_VABI libSceAudioSystemUnknown_ltR0PMUM270_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("ltR0PMUM270", libSceAudioSystemUnknown_ltR0PMUM270_nid_no_patch); + +int APS5_VABI libSceAudioSystemUnknown_mS9gpe47Eio_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("mS9gpe47Eio", libSceAudioSystemUnknown_mS9gpe47Eio_nid_no_patch); + +int APS5_VABI libSceAudioSystemUnknown_tHodLaUv04w_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("tHodLaUv04w", libSceAudioSystemUnknown_tHodLaUv04w_nid_no_patch); + +int APS5_VABI libSceAudioSystemUnknown_uoPBJTVWhrg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("uoPBJTVWhrg", libSceAudioSystemUnknown_uoPBJTVWhrg_nid_no_patch); + +int APS5_VABI libSceAudioSystemUnknown_xG7GUeOKtBM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("xG7GUeOKtBM", libSceAudioSystemUnknown_xG7GUeOKtBM_nid_no_patch); + +} diff --git a/core/libs/prx/libSceAudioSystem/include/InventoryExports.hpp b/core/libs/prx/libSceAudioSystem/include/InventoryExports.hpp new file mode 100644 index 0000000000..a803a8dc45 --- /dev/null +++ b/core/libs/prx/libSceAudioSystem/include/InventoryExports.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include "SceTypes.hpp" + +extern "C" { + +int APS5_VABI sceAudioOutServerBusGetIoParam(); +int APS5_VABI sceAudioOutServerGetOption(); +int APS5_VABI libSceAudioSystemUnknown__minus_e5aJEqL34Y_nid_no_patch(); +int APS5_VABI libSceAudioSystemUnknown_17nOsxccH5o_nid_no_patch(); +int APS5_VABI libSceAudioSystemUnknown_2bKJUeywpeY_nid_no_patch(); +int APS5_VABI libSceAudioSystemUnknown_3HOelmA2vcw_nid_no_patch(); +int APS5_VABI libSceAudioSystemUnknown_5SM0uQwcgTQ_nid_no_patch(); +int APS5_VABI libSceAudioSystemUnknown_5oOYYvwIb0k_nid_no_patch(); +int APS5_VABI libSceAudioSystemUnknown_6PGbkl6OnvE_nid_no_patch(); +int APS5_VABI libSceAudioSystemUnknown_9QaKJdd7jKk_nid_no_patch(); +int APS5_VABI libSceAudioSystemUnknown_B_minus_JyTVr90kc_nid_no_patch(); +int APS5_VABI libSceAudioSystemUnknown_Fd9u7_minus_OKW_plus_Q_nid_no_patch(); +int APS5_VABI libSceAudioSystemUnknown_FkFcs7NTFDo_nid_no_patch(); +int APS5_VABI libSceAudioSystemUnknown_GUtNdBeMrcM_nid_no_patch(); +int APS5_VABI libSceAudioSystemUnknown_HxLyu3KUv5U_nid_no_patch(); +int APS5_VABI libSceAudioSystemUnknown_IiO_plus_DdVz_minus_4E_nid_no_patch(); +int APS5_VABI libSceAudioSystemUnknown_J2Fx9JLKcqU_nid_no_patch(); +int APS5_VABI libSceAudioSystemUnknown_Met2nIMg_plus_Ng_nid_no_patch(); +int APS5_VABI libSceAudioSystemUnknown_WRDd2feDYzg_nid_no_patch(); +int APS5_VABI libSceAudioSystemUnknown_c4m3OSO0lj8_nid_no_patch(); +int APS5_VABI libSceAudioSystemUnknown_ds20PmJfSSk_nid_no_patch(); +int APS5_VABI libSceAudioSystemUnknown_hjlhfF8Voz0_nid_no_patch(); +int APS5_VABI libSceAudioSystemUnknown_jLjHQbmK9_plus_A_nid_no_patch(); +int APS5_VABI libSceAudioSystemUnknown_jZPcy9MG4w0_nid_no_patch(); +int APS5_VABI libSceAudioSystemUnknown_lSatNsWSmps_nid_no_patch(); +int APS5_VABI libSceAudioSystemUnknown_ltR0PMUM270_nid_no_patch(); +int APS5_VABI libSceAudioSystemUnknown_mS9gpe47Eio_nid_no_patch(); +int APS5_VABI libSceAudioSystemUnknown_tHodLaUv04w_nid_no_patch(); +int APS5_VABI libSceAudioSystemUnknown_uoPBJTVWhrg_nid_no_patch(); +int APS5_VABI libSceAudioSystemUnknown_xG7GUeOKtBM_nid_no_patch(); + +} diff --git a/core/libs/prx/libSceAudioSystem/libSceAudioSystem.hpp b/core/libs/prx/libSceAudioSystem/libSceAudioSystem.hpp index 092b35b31a..1e0bfce65d 100644 --- a/core/libs/prx/libSceAudioSystem/libSceAudioSystem.hpp +++ b/core/libs/prx/libSceAudioSystem/libSceAudioSystem.hpp @@ -1,3 +1,4 @@ #pragma once #include "prx/libSceAudioSystem/include/Api.hpp" +#include "prx/libSceAudioSystem/include/InventoryExports.hpp" diff --git a/core/libs/prx/libSceAudiodec.native/CMakeLists.txt b/core/libs/prx/libSceAudiodec.native/CMakeLists.txt index 90195a7712..4c6cd37ac1 100644 --- a/core/libs/prx/libSceAudiodec.native/CMakeLists.txt +++ b/core/libs/prx/libSceAudiodec.native/CMakeLists.txt @@ -13,3 +13,15 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endif() + +if(BUILD_TESTING) + add_test_executable(guest_audiodec_native_tests ${LIBS_INCLUDE_DIR}/tests/GuestAudiodecNative.cpp) + target_include_directories(guest_audiodec_native_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_audiodec_native_tests PRIVATE libSceAudiodec.native libc) + configure_windows_unwind(guest_audiodec_native_tests) + add_test(NAME guest_audiodec_native COMMAND guest_audiodec_native_tests $) + if(WIN32) + set_tests_properties(guest_audiodec_native PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceAudiodec/src/Codecs.cpp b/core/libs/prx/libSceAudiodec/src/Codecs.cpp index cb91a87bdf..0f96e7b599 100644 --- a/core/libs/prx/libSceAudiodec/src/Codecs.cpp +++ b/core/libs/prx/libSceAudiodec/src/Codecs.cpp @@ -1,4 +1,5 @@ #include "prx/libSceAudiodec/src/Codecs.hpp" +#include "prx/libc/include/General.hpp" #include "libatrac9.h" extern "C" { @@ -8,6 +9,7 @@ extern "C" { #include #include +#include #include #include #include @@ -47,10 +49,13 @@ public: ~FfmpegDecoder() override { avcodec_free_context(&context); } - void Reset() override { avcodec_flush_buffers(context); } + void Reset() override { + if (context) avcodec_flush_buffers(context); + } protected: DecodeResult decodePacket(const std::uint8_t* data, std::size_t size, std::uint8_t* pcm, std::size_t pcmSize) { + if (!context) return {DecodeStatus::InvalidData}; const std::unique_ptr packet(av_packet_alloc()); const std::unique_ptr frame(av_frame_alloc()); if (!packet || !frame) throw std::bad_alloc(); @@ -85,10 +90,16 @@ protected: return result; } - void reopen(std::vector replacement) { + bool tryReopen(std::vector replacement) { avcodec_free_context(&context); extradata = std::move(replacement); - open(); + try { + open(); + return true; + } catch (const std::runtime_error& error) { + APS5_LOG_ERR("%s", error.what()); + return false; + } } private: @@ -102,7 +113,18 @@ private: std::memcpy(context->extradata, extradata.data(), extradata.size()); context->extradata_size = static_cast(extradata.size()); } - if (avcodec_open2(context, codec, nullptr) < 0) throw std::runtime_error("libSceAudiodec: cannot open an FFmpeg decoder"); + if (const int status = avcodec_open2(context, codec, nullptr); status < 0) { + avcodec_free_context(&context); + char reason[AV_ERROR_MAX_STRING_SIZE]{}; + av_strerror(status, reason, sizeof(reason)); + std::string config; + for (const auto byte : extradata) { + char hex[4]; + std::snprintf(hex, sizeof(hex), "%02x", byte); + config += hex; + } + throw std::runtime_error("libSceAudiodec: cannot open the FFmpeg " + std::string(avcodec_get_name(id)) + " decoder" + (config.empty() ? "" : " (config " + config + ")") + ": " + reason); + } } std::int32_t wordSize; @@ -186,14 +208,14 @@ public: if (layout.channelConfiguration == 0) { if (!layouts.empty()) { layouts.clear(); - reopen({}); + if (!tryReopen({})) return {DecodeStatus::InvalidData}; } return decodePacket(data, frameBytes, pcm, pcmSize); } if (layouts.empty() || layouts.front() != layout) { layouts = {layout}; sbr = highEfficiency ? Sbr::Implicit : Sbr::Absent; - reopen(audioSpecificConfig(layout, sbr, 0)); + if (!tryReopen(audioSpecificConfig(layout, sbr, 0))) return {DecodeStatus::InvalidData}; } DecodeResult result = decodeAccessUnit(data + headerBytes, frameBytes - headerBytes, pcm, pcmSize); result.consumed = frameBytes; @@ -203,18 +225,18 @@ public: private: DecodeResult decodeAccessUnit(const std::uint8_t* data, std::size_t size, std::uint8_t* pcm, std::size_t pcmSize) { for (std::size_t index = 0; index < layouts.size(); ++index) { - if (index != 0) reopen(audioSpecificConfig(layouts[index], sbr, 0)); + if (index != 0 && !tryReopen(audioSpecificConfig(layouts[index], sbr, 0))) continue; DecodeResult result = decodePacket(data, size, pcm, pcmSize); if (result.status == DecodeStatus::InvalidData) continue; layouts = {layouts[index]}; if (sbr == Sbr::Implicit && layouts.front().channelConfiguration == 1 && result.channels > 1) { sbr = Sbr::WithoutParametricStereo; - reopen(audioSpecificConfig(layouts.front(), sbr, result.sampleRate)); + if (!tryReopen(audioSpecificConfig(layouts.front(), sbr, result.sampleRate))) return {DecodeStatus::InvalidData}; result = decodePacket(data, size, pcm, pcmSize); } return result; } - if (layouts.size() > 1) reopen(audioSpecificConfig(layouts.front(), sbr, 0)); + if (layouts.size() > 1) tryReopen(audioSpecificConfig(layouts.front(), sbr, 0)); return {DecodeStatus::InvalidData}; } diff --git a/core/libs/prx/libSceAvPlayer.native/CMakeLists.txt b/core/libs/prx/libSceAvPlayer.native/CMakeLists.txt index b9a16defb7..76e32be96c 100644 --- a/core/libs/prx/libSceAvPlayer.native/CMakeLists.txt +++ b/core/libs/prx/libSceAvPlayer.native/CMakeLists.txt @@ -2,3 +2,14 @@ include(${CMAKE_CURRENT_SOURCE_DIR}/../libSceAvPlayer/AvPlayerLibrary.cmake) add_sce_avplayer_library(libSceAvPlayer.native) target_sources(libSceAvPlayer.native PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp) set_source_files_properties(${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp PROPERTIES COMPILE_OPTIONS -fvisibility=default) +if(BUILD_TESTING) + add_test_executable(guest_avplayer_native_tests ${LIBS_INCLUDE_DIR}/tests/GuestAvPlayerNative.cpp) + target_include_directories(guest_avplayer_native_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_avplayer_native_tests PRIVATE libSceAvPlayer.native libc) + configure_windows_unwind(guest_avplayer_native_tests) + add_test(NAME guest_avplayer_native COMMAND guest_avplayer_native_tests $) + if(WIN32) + set_tests_properties(guest_avplayer_native PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceAvSetting/CMakeLists.txt b/core/libs/prx/libSceAvSetting/CMakeLists.txt new file mode 100644 index 0000000000..02e20fee76 --- /dev/null +++ b/core/libs/prx/libSceAvSetting/CMakeLists.txt @@ -0,0 +1,15 @@ +add_library(libSceAvSetting SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceAvSetting PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceAvSetting) + +if(BUILD_TESTING) + add_test_executable(guest_avsetting_predicates_tests ${LIBS_INCLUDE_DIR}/tests/GuestAvSettingPredicates.cpp) + target_include_directories(guest_avsetting_predicates_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_avsetting_predicates_tests PRIVATE libSceAvSetting libc) + configure_windows_unwind(guest_avsetting_predicates_tests) + add_test(NAME guest_avsetting_predicates COMMAND guest_avsetting_predicates_tests) + if(WIN32) + set_tests_properties(guest_avsetting_predicates PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceAvSetting/Export.cpp b/core/libs/prx/libSceAvSetting/Export.cpp index de1d4295f5..7c8e48f6de 100644 --- a/core/libs/prx/libSceAvSetting/Export.cpp +++ b/core/libs/prx/libSceAvSetting/Export.cpp @@ -1,11 +1,28 @@ #include "prx/libc/include/general/ExportMacros.hpp" #include "prx/libSceAvSetting/include/InventoryExports.hpp" #include "prx/libSceAvSetting/include/Api.hpp" +#include #include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +namespace { + +template +bool ReadAvSettingField(const void* object, std::size_t offset, TValue& value) noexcept { + const auto address = reinterpret_cast(object); + if (address == 0 || offset > std::numeric_limits::max() - address) return false; + try { + return GuestCopyIn_nid_no_patch(&value, reinterpret_cast(address + offset), sizeof(value)); + } catch (...) { + return false; + } +} + +} + extern "C" { int APS5_VABI sceAvControlGetPuTypeByMonitorInfo(void) { @@ -217,14 +234,20 @@ int APS5_VABI sceAvControlIsAudioOutModeSupportedByMonitorInfo() { return 0; } -int APS5_VABI sceAvControlIsColorimetryHdr() { - NotImplemented_nid_no_patch(__func__); - return 0; +bool APS5_VABI sceAvControlIsColorimetryHdr(std::uint32_t colorimetry) { + constexpr std::array isHdr{1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1}; + return colorimetry >= 9 && colorimetry <= 20 && isHdr[colorimetry - 9] != 0; } -int APS5_VABI sceAvControlIsDeepColorSupportedByMonitorInfo() { - NotImplemented_nid_no_patch(__func__); - return 0; +bool APS5_VABI sceAvControlIsDeepColorSupportedByMonitorInfo(const void* monitorInfo) { + std::uint8_t deepColorFlags = 0; + if (!ReadAvSettingField(monitorInfo, 0x74, deepColorFlags)) return false; + if ((deepColorFlags & 0x0a) != 0) return true; + std::uint8_t deepColorFlags2 = 0; + if (!ReadAvSettingField(monitorInfo, 0x75, deepColorFlags2)) return false; + if ((deepColorFlags2 & 0x0a) != 0) return true; + std::uint8_t hdrFlags = 0; + return ReadAvSettingField(monitorInfo, 0x352, hdrFlags) && (hdrFlags & 0x03) != 0; } int APS5_VABI sceAvControlIsDeviceConnected() { @@ -232,19 +255,22 @@ int APS5_VABI sceAvControlIsDeviceConnected() { return 0; } -int APS5_VABI sceAvControlIsHdrSupportedByMonitorInfo() { - NotImplemented_nid_no_patch(__func__); - return 0; +bool APS5_VABI sceAvControlIsHdrSupportedByMonitorInfo(const void* monitorInfo) { + std::uint8_t capabilityFlags = 0; + if (!ReadAvSettingField(monitorInfo, 0x358, capabilityFlags) || (capabilityFlags & 0x04) == 0) + return false; + std::uint8_t hdrFlags = 0; + return ReadAvSettingField(monitorInfo, 0x78, hdrFlags) && (hdrFlags & 0x30) != 0; } -int APS5_VABI sceAvControlIsModeGen1Vr() { - NotImplemented_nid_no_patch(__func__); - return 0; +bool APS5_VABI sceAvControlIsModeGen1Vr(const void* videoOutMode) { + std::uint32_t modeFlags = 0; + return ReadAvSettingField(videoOutMode, 0x30, modeFlags) && (modeFlags & 0x02) != 0; } -int APS5_VABI sceAvControlIsModeSystemDefault() { - NotImplemented_nid_no_patch(__func__); - return 0; +bool APS5_VABI sceAvControlIsModeSystemDefault(const void* videoOutMode) { + std::uint32_t modeFlags = 0; + return ReadAvSettingField(videoOutMode, 0x30, modeFlags) && (modeFlags & 0x01) == 0; } int APS5_VABI sceAvControlIsSuspendedProcessOutputModeAvailable() { diff --git a/core/libs/prx/libSceAvSetting/include/Api.hpp b/core/libs/prx/libSceAvSetting/include/Api.hpp index ec0d032743..8c830608ed 100644 --- a/core/libs/prx/libSceAvSetting/include/Api.hpp +++ b/core/libs/prx/libSceAvSetting/include/Api.hpp @@ -46,12 +46,12 @@ int APS5_VABI sceAvControlGetPuStatus(void); int APS5_VABI sceAvControlGetPuType(void); int APS5_VABI sceAvControlInit(void); int APS5_VABI sceAvControlIsAudioOutModeSupportedByMonitorInfo(void); -int APS5_VABI sceAvControlIsColorimetryHdr(void); -int APS5_VABI sceAvControlIsDeepColorSupportedByMonitorInfo(void); +bool APS5_VABI sceAvControlIsColorimetryHdr(std::uint32_t colorimetry); +bool APS5_VABI sceAvControlIsDeepColorSupportedByMonitorInfo(const void* monitorInfo); int APS5_VABI sceAvControlIsDeviceConnected(void); -int APS5_VABI sceAvControlIsHdrSupportedByMonitorInfo(void); -int APS5_VABI sceAvControlIsModeGen1Vr(void); -int APS5_VABI sceAvControlIsModeSystemDefault(void); +bool APS5_VABI sceAvControlIsHdrSupportedByMonitorInfo(const void* monitorInfo); +bool APS5_VABI sceAvControlIsModeGen1Vr(const void* videoOutMode); +bool APS5_VABI sceAvControlIsModeSystemDefault(const void* videoOutMode); int APS5_VABI sceAvControlIsSuspendedProcessOutputModeAvailable(void); int APS5_VABI sceAvControlIsVideoOutModeSupportedByMonitorInfo(void); int APS5_VABI sceAvControlSet2dVrMode(void); diff --git a/core/libs/prx/libSceAvcap2/Export.cpp b/core/libs/prx/libSceAvcap2/Export.cpp index 00f142044e..5e58d0c704 100644 --- a/core/libs/prx/libSceAvcap2/Export.cpp +++ b/core/libs/prx/libSceAvcap2/Export.cpp @@ -1,3 +1,5 @@ +#include "prx/libc/include/general/ExportMacros.hpp" +#include "prx/libSceAvcap2/include/InventoryExports.hpp" #include "prx/libSceAvcap2/include/Api.hpp" #include #include @@ -182,3 +184,67 @@ int APS5_VABI sceAvcap2UnlockInputDataBuffer() { } } + +extern "C" { + +int APS5_VABI libSceAvcap2Unknown__plus_OspdnZ_minus_8mc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+OspdnZ-8mc", libSceAvcap2Unknown__plus_OspdnZ_minus_8mc_nid_no_patch); + +int APS5_VABI libSceAvcap2Unknown_1Xp85wUaVTE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("1Xp85wUaVTE", libSceAvcap2Unknown_1Xp85wUaVTE_nid_no_patch); + +int APS5_VABI libSceAvcap2Unknown_32gujAUoIMc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("32gujAUoIMc", libSceAvcap2Unknown_32gujAUoIMc_nid_no_patch); + +int APS5_VABI libSceAvcap2Unknown_4hkuDvkGu7U_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("4hkuDvkGu7U", libSceAvcap2Unknown_4hkuDvkGu7U_nid_no_patch); + +int APS5_VABI libSceAvcap2Unknown_7KBaem5GyBQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("7KBaem5GyBQ", libSceAvcap2Unknown_7KBaem5GyBQ_nid_no_patch); + +int APS5_VABI libSceAvcap2Unknown_88YEF_plus_XHfRY_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("88YEF+XHfRY", libSceAvcap2Unknown_88YEF_plus_XHfRY_nid_no_patch); + +int APS5_VABI libSceAvcap2Unknown_HAeczEM_minus_m7c_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("HAeczEM-m7c", libSceAvcap2Unknown_HAeczEM_minus_m7c_nid_no_patch); + +int APS5_VABI libSceAvcap2Unknown_HrGLkn9o4MY_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("HrGLkn9o4MY", libSceAvcap2Unknown_HrGLkn9o4MY_nid_no_patch); + +int APS5_VABI libSceAvcap2Unknown_Tzk7_minus_W5mp0A_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("Tzk7-W5mp0A", libSceAvcap2Unknown_Tzk7_minus_W5mp0A_nid_no_patch); + +int APS5_VABI libSceAvcap2Unknown_VNRuuZQx7gE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("VNRuuZQx7gE", libSceAvcap2Unknown_VNRuuZQx7gE_nid_no_patch); + +int APS5_VABI libSceAvcap2Unknown_iy6ZcsdeoMk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("iy6ZcsdeoMk", libSceAvcap2Unknown_iy6ZcsdeoMk_nid_no_patch); + +int APS5_VABI libSceAvcap2Unknown_k7gz608olKM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("k7gz608olKM", libSceAvcap2Unknown_k7gz608olKM_nid_no_patch); + +} diff --git a/core/libs/prx/libSceAvcap2/include/InventoryExports.hpp b/core/libs/prx/libSceAvcap2/include/InventoryExports.hpp new file mode 100644 index 0000000000..8398a94f6e --- /dev/null +++ b/core/libs/prx/libSceAvcap2/include/InventoryExports.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include "SceTypes.hpp" + +extern "C" { + +int APS5_VABI libSceAvcap2Unknown__plus_OspdnZ_minus_8mc_nid_no_patch(); +int APS5_VABI libSceAvcap2Unknown_1Xp85wUaVTE_nid_no_patch(); +int APS5_VABI libSceAvcap2Unknown_32gujAUoIMc_nid_no_patch(); +int APS5_VABI libSceAvcap2Unknown_4hkuDvkGu7U_nid_no_patch(); +int APS5_VABI libSceAvcap2Unknown_7KBaem5GyBQ_nid_no_patch(); +int APS5_VABI libSceAvcap2Unknown_88YEF_plus_XHfRY_nid_no_patch(); +int APS5_VABI libSceAvcap2Unknown_HAeczEM_minus_m7c_nid_no_patch(); +int APS5_VABI libSceAvcap2Unknown_HrGLkn9o4MY_nid_no_patch(); +int APS5_VABI libSceAvcap2Unknown_Tzk7_minus_W5mp0A_nid_no_patch(); +int APS5_VABI libSceAvcap2Unknown_VNRuuZQx7gE_nid_no_patch(); +int APS5_VABI libSceAvcap2Unknown_iy6ZcsdeoMk_nid_no_patch(); +int APS5_VABI libSceAvcap2Unknown_k7gz608olKM_nid_no_patch(); + +} diff --git a/core/libs/prx/libSceAvcap2/libSceAvcap2.hpp b/core/libs/prx/libSceAvcap2/libSceAvcap2.hpp index 7ca2e6f1de..9696a5e65b 100644 --- a/core/libs/prx/libSceAvcap2/libSceAvcap2.hpp +++ b/core/libs/prx/libSceAvcap2/libSceAvcap2.hpp @@ -1,3 +1,4 @@ #pragma once #include "prx/libSceAvcap2/include/Api.hpp" +#include "prx/libSceAvcap2/include/InventoryExports.hpp" diff --git a/core/libs/prx/libSceCdlgPlayerReview/Export.cpp b/core/libs/prx/libSceCdlgPlayerReview/Export.cpp index 75e23b282e..501c3be382 100644 --- a/core/libs/prx/libSceCdlgPlayerReview/Export.cpp +++ b/core/libs/prx/libSceCdlgPlayerReview/Export.cpp @@ -2,6 +2,7 @@ #include "prx/libSceUserService/UserService.hpp" #include "SceTypes.hpp" #include +#include #include #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libSceCommonDialog/CommonDialog.hpp" @@ -72,7 +73,11 @@ int APS5_VABI scePlayerReviewDialogGetResult(CommonDialogResultPrefix* result) { const int status = g_status.load(); if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; if (status != COMMON_DIALOG_STATUS_FINISHED) return COMMON_DIALOG_ERROR_NOT_FINISHED; - result->result = COMMON_DIALOG_RESULT_USER_CANCELED; + const std::int32_t cancelResult = COMMON_DIALOG_RESULT_USER_CANCELED; + const int savedErrno = errno; + const bool copied = GuestCopyOut_nid_no_patch(&result->result, &cancelResult, sizeof(cancelResult)); + errno = savedErrno; + if (!copied) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT; return COMMON_DIALOG_RESULT_USER_CANCELED; } diff --git a/core/libs/prx/libSceCloudMessaging/Export.cpp b/core/libs/prx/libSceCloudMessaging/Export.cpp new file mode 100644 index 0000000000..f6e741d37f --- /dev/null +++ b/core/libs/prx/libSceCloudMessaging/Export.cpp @@ -0,0 +1,30 @@ +#include "prx/libSceCloudMessaging/include/Api.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI _ZN17SceCloudMessaging19UnregisterMessagingEPKNS_21MessagingRegisterInfoE() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZN17SceCloudMessaging10InitializeEi() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZN17SceCloudMessaging13CreateMessageEPKNS_7MessageEPj() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZN17SceCloudMessaging16SendMessageAsyncEjPKNS_17MessageResultInfoE() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZN17SceCloudMessaging9TerminateEv() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZN17SceCloudMessaging17RegisterMessagingEPKNS_21MessagingRegisterInfoE() { + NotImplemented_nid_no_patch(__func__); +} + +} diff --git a/core/libs/prx/libSceCloudMessaging/include/Api.hpp b/core/libs/prx/libSceCloudMessaging/include/Api.hpp new file mode 100644 index 0000000000..52d1b89f52 --- /dev/null +++ b/core/libs/prx/libSceCloudMessaging/include/Api.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include "SceTypes.hpp" + +extern "C" { + +int APS5_VABI _ZN17SceCloudMessaging19UnregisterMessagingEPKNS_21MessagingRegisterInfoE(void); +int APS5_VABI _ZN17SceCloudMessaging10InitializeEi(void); +int APS5_VABI _ZN17SceCloudMessaging13CreateMessageEPKNS_7MessageEPj(void); +int APS5_VABI _ZN17SceCloudMessaging16SendMessageAsyncEjPKNS_17MessageResultInfoE(void); +int APS5_VABI _ZN17SceCloudMessaging9TerminateEv(void); +int APS5_VABI _ZN17SceCloudMessaging17RegisterMessagingEPKNS_21MessagingRegisterInfoE(void); + +} diff --git a/core/libs/prx/libSceCloudMessaging/libSceCloudMessaging.hpp b/core/libs/prx/libSceCloudMessaging/libSceCloudMessaging.hpp new file mode 100644 index 0000000000..3e78770cef --- /dev/null +++ b/core/libs/prx/libSceCloudMessaging/libSceCloudMessaging.hpp @@ -0,0 +1,3 @@ +#pragma once + +#include "prx/libSceCloudMessaging/include/Api.hpp" diff --git a/core/libs/prx/libSceComposite/Export.cpp b/core/libs/prx/libSceComposite/Export.cpp index 92183376c0..6c5e4210a5 100644 --- a/core/libs/prx/libSceComposite/Export.cpp +++ b/core/libs/prx/libSceComposite/Export.cpp @@ -1,3 +1,5 @@ +#include "prx/libc/include/general/ExportMacros.hpp" +#include "prx/libSceComposite/include/InventoryExports.hpp" #include "prx/libSceComposite/include/Api.hpp" #include #include @@ -419,8 +421,8 @@ int APS5_VABI sceCompositorGetVirtualCanvasHandle() { NotImplemented_nid_no_patch(__func__); } -int APS5_VABI sceCompositorIsVirtualCanvasHandle() { - NotImplemented_nid_no_patch(__func__); +bool APS5_VABI sceCompositorIsVirtualCanvasHandle(std::uint32_t handle) { + return (handle & 0xff00) == 0xff00; } int APS5_VABI sceCompositorMemoryPoolCommit() { @@ -484,3 +486,151 @@ int APS5_VABI sceCompositorWaitEndOfRenderingIfGpuWorking() { } } + +extern "C" { + +int APS5_VABI sceCompositorSecondaryMemoryRelease() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI libSceCompositeUnknown__plus_A5zYfomrKo_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+A5zYfomrKo", libSceCompositeUnknown__plus_A5zYfomrKo_nid_no_patch); + +int APS5_VABI libSceCompositeUnknown__plus_DMKcAbpcrQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+DMKcAbpcrQ", libSceCompositeUnknown__plus_DMKcAbpcrQ_nid_no_patch); + +int APS5_VABI libSceCompositeUnknown_0hVj7ycuNjY_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0hVj7ycuNjY", libSceCompositeUnknown_0hVj7ycuNjY_nid_no_patch); + +int APS5_VABI libSceCompositeUnknown_3znAWbdvzNo_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("3znAWbdvzNo", libSceCompositeUnknown_3znAWbdvzNo_nid_no_patch); + +int APS5_VABI libSceCompositeUnknown_4iL4YvcEKdU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("4iL4YvcEKdU", libSceCompositeUnknown_4iL4YvcEKdU_nid_no_patch); + +int APS5_VABI libSceCompositeUnknown_5vAisOgay3M_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("5vAisOgay3M", libSceCompositeUnknown_5vAisOgay3M_nid_no_patch); + +int APS5_VABI libSceCompositeUnknown_8l316W539Zw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("8l316W539Zw", libSceCompositeUnknown_8l316W539Zw_nid_no_patch); + +int APS5_VABI libSceCompositeUnknown_FDHuP_plus_z8KnM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("FDHuP+z8KnM", libSceCompositeUnknown_FDHuP_plus_z8KnM_nid_no_patch); + +int APS5_VABI libSceCompositeUnknown_HXxq9giIX_minus_I_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("HXxq9giIX-I", libSceCompositeUnknown_HXxq9giIX_minus_I_nid_no_patch); + +int APS5_VABI libSceCompositeUnknown_KpuS3CIcckw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("KpuS3CIcckw", libSceCompositeUnknown_KpuS3CIcckw_nid_no_patch); + +int APS5_VABI libSceCompositeUnknown_NMGq0MJ2iGo_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("NMGq0MJ2iGo", libSceCompositeUnknown_NMGq0MJ2iGo_nid_no_patch); + +int APS5_VABI libSceCompositeUnknown_RPdjyM2WKcw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("RPdjyM2WKcw", libSceCompositeUnknown_RPdjyM2WKcw_nid_no_patch); + +int APS5_VABI libSceCompositeUnknown_SpfMcnnpfDA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("SpfMcnnpfDA", libSceCompositeUnknown_SpfMcnnpfDA_nid_no_patch); + +int APS5_VABI libSceCompositeUnknown_UEW01n5qkdA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("UEW01n5qkdA", libSceCompositeUnknown_UEW01n5qkdA_nid_no_patch); + +int APS5_VABI libSceCompositeUnknown_XZzyQX4T5ts_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("XZzyQX4T5ts", libSceCompositeUnknown_XZzyQX4T5ts_nid_no_patch); + +int APS5_VABI libSceCompositeUnknown_Yck3L7ExyVc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("Yck3L7ExyVc", libSceCompositeUnknown_Yck3L7ExyVc_nid_no_patch); + +int APS5_VABI libSceCompositeUnknown_Z25ikyBUYy0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("Z25ikyBUYy0", libSceCompositeUnknown_Z25ikyBUYy0_nid_no_patch); + +int APS5_VABI libSceCompositeUnknown_aczOzib8mzI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("aczOzib8mzI", libSceCompositeUnknown_aczOzib8mzI_nid_no_patch); + +int APS5_VABI libSceCompositeUnknown_asXqPHp3O5g_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("asXqPHp3O5g", libSceCompositeUnknown_asXqPHp3O5g_nid_no_patch); + +int APS5_VABI libSceCompositeUnknown_bwXAoMVv94M_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("bwXAoMVv94M", libSceCompositeUnknown_bwXAoMVv94M_nid_no_patch); + +int APS5_VABI libSceCompositeUnknown_dTPMTtYqPX4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("dTPMTtYqPX4", libSceCompositeUnknown_dTPMTtYqPX4_nid_no_patch); + +int APS5_VABI libSceCompositeUnknown_gLAJaS1VbsA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("gLAJaS1VbsA", libSceCompositeUnknown_gLAJaS1VbsA_nid_no_patch); + +int APS5_VABI libSceCompositeUnknown_inplO6BVf5k_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("inplO6BVf5k", libSceCompositeUnknown_inplO6BVf5k_nid_no_patch); + +int APS5_VABI libSceCompositeUnknown_j2CEWhcK0gc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("j2CEWhcK0gc", libSceCompositeUnknown_j2CEWhcK0gc_nid_no_patch); + +int APS5_VABI libSceCompositeUnknown_lvaCIMhYNh0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("lvaCIMhYNh0", libSceCompositeUnknown_lvaCIMhYNh0_nid_no_patch); + +int APS5_VABI libSceCompositeUnknown_mzPjqJ_plus_unSw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("mzPjqJ+unSw", libSceCompositeUnknown_mzPjqJ_plus_unSw_nid_no_patch); + +int APS5_VABI libSceCompositeUnknown_va_plus_6TiqnE1Y_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("va+6TiqnE1Y", libSceCompositeUnknown_va_plus_6TiqnE1Y_nid_no_patch); + +int APS5_VABI libSceCompositeUnknown_yZcGEBwnDAM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("yZcGEBwnDAM", libSceCompositeUnknown_yZcGEBwnDAM_nid_no_patch); + +} diff --git a/core/libs/prx/libSceComposite/include/Api.hpp b/core/libs/prx/libSceComposite/include/Api.hpp index 8a37a31779..d6cd40e745 100644 --- a/core/libs/prx/libSceComposite/include/Api.hpp +++ b/core/libs/prx/libSceComposite/include/Api.hpp @@ -90,7 +90,7 @@ int APS5_VABI sceCompositorGetSecondaryMemoryAddress(); int APS5_VABI sceCompositorGetSecondaryMemorySize(); int APS5_VABI sceCompositorGetTFRing(); int APS5_VABI sceCompositorGetVirtualCanvasHandle(); -int APS5_VABI sceCompositorIsVirtualCanvasHandle(); +bool APS5_VABI sceCompositorIsVirtualCanvasHandle(std::uint32_t handle); int APS5_VABI sceCompositorMemoryPoolCommit(); int APS5_VABI sceCompositorMemoryPoolCreate(); int APS5_VABI sceCompositorPermanentMemoryCreate(); diff --git a/core/libs/prx/libSceComposite/include/InventoryExports.hpp b/core/libs/prx/libSceComposite/include/InventoryExports.hpp new file mode 100644 index 0000000000..2a654f169a --- /dev/null +++ b/core/libs/prx/libSceComposite/include/InventoryExports.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include "SceTypes.hpp" + +extern "C" { + +int APS5_VABI sceCompositorSecondaryMemoryRelease(); +int APS5_VABI libSceCompositeUnknown__plus_A5zYfomrKo_nid_no_patch(); +int APS5_VABI libSceCompositeUnknown__plus_DMKcAbpcrQ_nid_no_patch(); +int APS5_VABI libSceCompositeUnknown_0hVj7ycuNjY_nid_no_patch(); +int APS5_VABI libSceCompositeUnknown_3znAWbdvzNo_nid_no_patch(); +int APS5_VABI libSceCompositeUnknown_4iL4YvcEKdU_nid_no_patch(); +int APS5_VABI libSceCompositeUnknown_5vAisOgay3M_nid_no_patch(); +int APS5_VABI libSceCompositeUnknown_8l316W539Zw_nid_no_patch(); +int APS5_VABI libSceCompositeUnknown_FDHuP_plus_z8KnM_nid_no_patch(); +int APS5_VABI libSceCompositeUnknown_HXxq9giIX_minus_I_nid_no_patch(); +int APS5_VABI libSceCompositeUnknown_KpuS3CIcckw_nid_no_patch(); +int APS5_VABI libSceCompositeUnknown_NMGq0MJ2iGo_nid_no_patch(); +int APS5_VABI libSceCompositeUnknown_RPdjyM2WKcw_nid_no_patch(); +int APS5_VABI libSceCompositeUnknown_SpfMcnnpfDA_nid_no_patch(); +int APS5_VABI libSceCompositeUnknown_UEW01n5qkdA_nid_no_patch(); +int APS5_VABI libSceCompositeUnknown_XZzyQX4T5ts_nid_no_patch(); +int APS5_VABI libSceCompositeUnknown_Yck3L7ExyVc_nid_no_patch(); +int APS5_VABI libSceCompositeUnknown_Z25ikyBUYy0_nid_no_patch(); +int APS5_VABI libSceCompositeUnknown_aczOzib8mzI_nid_no_patch(); +int APS5_VABI libSceCompositeUnknown_asXqPHp3O5g_nid_no_patch(); +int APS5_VABI libSceCompositeUnknown_bwXAoMVv94M_nid_no_patch(); +int APS5_VABI libSceCompositeUnknown_dTPMTtYqPX4_nid_no_patch(); +int APS5_VABI libSceCompositeUnknown_gLAJaS1VbsA_nid_no_patch(); +int APS5_VABI libSceCompositeUnknown_inplO6BVf5k_nid_no_patch(); +int APS5_VABI libSceCompositeUnknown_j2CEWhcK0gc_nid_no_patch(); +int APS5_VABI libSceCompositeUnknown_lvaCIMhYNh0_nid_no_patch(); +int APS5_VABI libSceCompositeUnknown_mzPjqJ_plus_unSw_nid_no_patch(); +int APS5_VABI libSceCompositeUnknown_va_plus_6TiqnE1Y_nid_no_patch(); +int APS5_VABI libSceCompositeUnknown_yZcGEBwnDAM_nid_no_patch(); + +} diff --git a/core/libs/prx/libSceComposite/libSceComposite.hpp b/core/libs/prx/libSceComposite/libSceComposite.hpp index 2829781c84..afedaca96d 100644 --- a/core/libs/prx/libSceComposite/libSceComposite.hpp +++ b/core/libs/prx/libSceComposite/libSceComposite.hpp @@ -1,3 +1,4 @@ #pragma once #include "prx/libSceComposite/include/Api.hpp" +#include "prx/libSceComposite/include/InventoryExports.hpp" diff --git a/core/libs/prx/libSceContentExport/Export.cpp b/core/libs/prx/libSceContentExport/Export.cpp index 5ec29d9900..f57e93465d 100644 --- a/core/libs/prx/libSceContentExport/Export.cpp +++ b/core/libs/prx/libSceContentExport/Export.cpp @@ -44,18 +44,24 @@ int APS5_VABI sceContentExportFinish(std::int32_t expId) { } int APS5_VABI sceContentExportFromData(std::int32_t expId, const ContentExportParam* param, std::size_t contentLength, ContentExportDataCallback callback, void* userData, char* path, std::size_t pathBufferLength) { - NotImplemented_nid_no_patch(__func__); - return 0; + std::lock_guard lock(g_sessionMutex); + RequireSession(__func__, expId); + NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI sceContentExportFromFile(std::int32_t expId, const ContentExportParam* param, const char* sourcePath, char* path, std::size_t pathBufferLength) { - NotImplemented_nid_no_patch(__func__); - return 0; + std::lock_guard lock(g_sessionMutex); + RequireSession(__func__, expId); + NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI sceContentExportFromFileWithThumbnail(std::int32_t expId, const ContentExportParam* param, const char* sourcePath, const char* thumbnailPath, char* path, std::size_t pathBufferLength) { - NotImplemented_nid_no_patch(__func__); - return 0; + std::lock_guard lock(g_sessionMutex); + RequireSession(__func__, expId); + NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI sceContentExportAddToFavorites(std::int64_t contentId) { diff --git a/core/libs/prx/libSceConvertKeycode/Export.cpp b/core/libs/prx/libSceConvertKeycode/Export.cpp index 4464089494..585890186a 100644 --- a/core/libs/prx/libSceConvertKeycode/Export.cpp +++ b/core/libs/prx/libSceConvertKeycode/Export.cpp @@ -226,6 +226,7 @@ constexpr auto VirtualKeys = [] { }(); constexpr int kErrInvalidKeyboardType = static_cast(0x80BC001Du); +constexpr int kErrInvalidParameter = static_cast(0x80BC0030u); constexpr auto CharacterTables = [] { std::array, 111> tables{}; tables[1][4] = 327777; @@ -2453,14 +2454,10 @@ int APS5_VABI sceConvertKeycodeGetVirtualKeycode(std::uint16_t keycode, std::uin int APS5_VABI sceConvertKeycodeGetCharacterFromKeyboardData(const KeyboardData* keyData, std::int32_t index, std::uint32_t state, std::uint32_t keyboardType, char16_t* character) { if (keyData == nullptr || character == nullptr) return kErrInvalidAddress; - if (keyboardType == 0 || keyboardType > 39) return kErrInvalidKeyboardType; - if (!KeyboardTypeAvailable(keyboardType, CompiledSdkVersion(), Ps4ClientOnce())) return kErrInvalidKeyboardType; - if (index < 0 || index >= keyData->length || index >= static_cast(KEYBOARD_MAX_KEYCODES)) { - throw std::runtime_error("sceConvertKeycodeGetCharacterFromKeyboardData: result for an index outside the key data is unknown"); - } - const std::uint16_t keycode = keyData->key_code[index]; - *character = ConvertCharacter(keycode, state, keyboardType); - return 0; + const auto length = static_cast(keyData->length); + if (length > KEYBOARD_MAX_KEYCODES || keyData->led >= 8 || keyData->modifier_key >= 0x100 || (state & 0xE3FFFF7Fu) != 0) return kErrInvalidParameter; + if (index < 0 || static_cast(index) >= length) return kErrInvalidParameter; + return sceConvertKeycodeGetCharacter(keyData->key_code[index], state | keyData->modifier_key << 8 | keyData->led << 16, keyboardType, character); } int APS5_VABI sceConvertKeycodeGetCharacter(std::uint16_t keycode, std::uint32_t state, std::uint32_t type, char16_t* character) { diff --git a/core/libs/prx/libSceCustomMusicSysCallWrapper/Export.cpp b/core/libs/prx/libSceCustomMusicSysCallWrapper/Export.cpp index e64d1e4444..30750a5fc3 100644 --- a/core/libs/prx/libSceCustomMusicSysCallWrapper/Export.cpp +++ b/core/libs/prx/libSceCustomMusicSysCallWrapper/Export.cpp @@ -1,3 +1,5 @@ +#include "prx/libc/include/general/ExportMacros.hpp" +#include "prx/libSceCustomMusicSysCallWrapper/include/InventoryExports.hpp" #include "prx/libSceCustomMusicSysCallWrapper/include/Api.hpp" #include #include @@ -6,9 +8,490 @@ extern "C" { +int APS5_VABI sceCustomMusicNetInetNtop() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceCustomMusicNetShutdown() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceCustomMusicNetSocketAbort() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceCustomMusicPthreadAttrGet() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceCustomMusicPthreadSelf() { + NotImplemented_nid_no_patch(__func__); +} + int APS5_VABI sceCustomMusicSelect() { NotImplemented_nid_no_patch(__func__); return 0; } } + +extern "C" { + +int APS5_VABI sceCustomMusicAudiodecClearContext() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceCustomMusicAudiodecCpuDecode() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceCustomMusicAudiodecCreateDecoder() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceCustomMusicAudiodecDecode() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceCustomMusicAudiodecDeleteDecoder() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceCustomMusicFileClose() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceCustomMusicFileLseek() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceCustomMusicFileOpenRead() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceCustomMusicFileRead() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceCustomMusicKernelGettimeofday() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceCustomMusicKernelNanosleep() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceCustomMusicNetAccept() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceCustomMusicNetBind() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceCustomMusicNetConnect() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceCustomMusicNetCtlInit() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceCustomMusicNetCtlTerm() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceCustomMusicNetEpollAbort() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceCustomMusicNetEpollControl() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceCustomMusicNetEpollCreate() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceCustomMusicNetEpollDestroy() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceCustomMusicNetEpollWait() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceCustomMusicNetErrnoLoc() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceCustomMusicNetInetPton() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceCustomMusicNetListen() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceCustomMusicNetPoolCreate() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceCustomMusicNetPoolDestroy() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceCustomMusicNetRecv() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceCustomMusicNetResolverAbort() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceCustomMusicNetResolverCreate() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceCustomMusicNetResolverDestroy() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceCustomMusicNetSend() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceCustomMusicNetSetsockopt() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceCustomMusicNetSocketClose() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceCustomMusicNetSocket() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceCustomMusicPthreadAttrDestroy() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceCustomMusicPthreadAttrInit() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceCustomMusicPthreadCondDestroy() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceCustomMusicPthreadCondInit() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceCustomMusicPthreadCondWait() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceCustomMusicPthreadCreate() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceCustomMusicPthreadDetach() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceCustomMusicPthreadExit() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceCustomMusicPthreadJoin() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceCustomMusicPthreadMutexDestroy() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceCustomMusicPthreadMutexInit() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceCustomMusicPthreadMutexLock() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceCustomMusicPthreadSetprio() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceCustomMusicPthreadYield() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown__plus_LdekyVYugM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+LdekyVYugM", libSceCustomMusicSysCallWrapperUnknown__plus_LdekyVYugM_nid_no_patch); + +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown__minus_f8PcMdF8L0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-f8PcMdF8L0", libSceCustomMusicSysCallWrapperUnknown__minus_f8PcMdF8L0_nid_no_patch); + +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_0WBrHbI33Ec_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0WBrHbI33Ec", libSceCustomMusicSysCallWrapperUnknown_0WBrHbI33Ec_nid_no_patch); + +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_0d4Aw8u3xzE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0d4Aw8u3xzE", libSceCustomMusicSysCallWrapperUnknown_0d4Aw8u3xzE_nid_no_patch); + +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_1auNQHxva3I_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("1auNQHxva3I", libSceCustomMusicSysCallWrapperUnknown_1auNQHxva3I_nid_no_patch); + +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_24ZHuc5b0RA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("24ZHuc5b0RA", libSceCustomMusicSysCallWrapperUnknown_24ZHuc5b0RA_nid_no_patch); + +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_2yE7PehS6iI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("2yE7PehS6iI", libSceCustomMusicSysCallWrapperUnknown_2yE7PehS6iI_nid_no_patch); + +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_3DyCYcJudI4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("3DyCYcJudI4", libSceCustomMusicSysCallWrapperUnknown_3DyCYcJudI4_nid_no_patch); + +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_5I2IqwB0Hes_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("5I2IqwB0Hes", libSceCustomMusicSysCallWrapperUnknown_5I2IqwB0Hes_nid_no_patch); + +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_5p7ZpbyUKuo_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("5p7ZpbyUKuo", libSceCustomMusicSysCallWrapperUnknown_5p7ZpbyUKuo_nid_no_patch); + +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_6PNhaGAw6Ho_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("6PNhaGAw6Ho", libSceCustomMusicSysCallWrapperUnknown_6PNhaGAw6Ho_nid_no_patch); + +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_70NNIr_minus_nfFo_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("70NNIr-nfFo", libSceCustomMusicSysCallWrapperUnknown_70NNIr_minus_nfFo_nid_no_patch); + +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_7uOVo0lL97g_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("7uOVo0lL97g", libSceCustomMusicSysCallWrapperUnknown_7uOVo0lL97g_nid_no_patch); + +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_8t8UK2XkJ1Y_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("8t8UK2XkJ1Y", libSceCustomMusicSysCallWrapperUnknown_8t8UK2XkJ1Y_nid_no_patch); + +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_CQv51NIK3i4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("CQv51NIK3i4", libSceCustomMusicSysCallWrapperUnknown_CQv51NIK3i4_nid_no_patch); + +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_DABJz_plus_GrAWg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("DABJz+GrAWg", libSceCustomMusicSysCallWrapperUnknown_DABJz_plus_GrAWg_nid_no_patch); + +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_EaC0JWOBvy4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("EaC0JWOBvy4", libSceCustomMusicSysCallWrapperUnknown_EaC0JWOBvy4_nid_no_patch); + +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_HNx_minus_bHhat18_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("HNx-bHhat18", libSceCustomMusicSysCallWrapperUnknown_HNx_minus_bHhat18_nid_no_patch); + +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_J4_plus_14VzlTHM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("J4+14VzlTHM", libSceCustomMusicSysCallWrapperUnknown_J4_plus_14VzlTHM_nid_no_patch); + +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_JSZXadyt_minus_nk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("JSZXadyt-nk", libSceCustomMusicSysCallWrapperUnknown_JSZXadyt_minus_nk_nid_no_patch); + +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_Kxq7i4lQjGg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("Kxq7i4lQjGg", libSceCustomMusicSysCallWrapperUnknown_Kxq7i4lQjGg_nid_no_patch); + +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_L0MmfM0N_minus_y8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("L0MmfM0N-y8", libSceCustomMusicSysCallWrapperUnknown_L0MmfM0N_minus_y8_nid_no_patch); + +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_Mk6mbbOvaBM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("Mk6mbbOvaBM", libSceCustomMusicSysCallWrapperUnknown_Mk6mbbOvaBM_nid_no_patch); + +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_NbAJfB19zUk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("NbAJfB19zUk", libSceCustomMusicSysCallWrapperUnknown_NbAJfB19zUk_nid_no_patch); + +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_QEVVJH35Fis_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("QEVVJH35Fis", libSceCustomMusicSysCallWrapperUnknown_QEVVJH35Fis_nid_no_patch); + +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_ROnMWfJ56t8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("ROnMWfJ56t8", libSceCustomMusicSysCallWrapperUnknown_ROnMWfJ56t8_nid_no_patch); + +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_SneVIJUdrLw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("SneVIJUdrLw", libSceCustomMusicSysCallWrapperUnknown_SneVIJUdrLw_nid_no_patch); + +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_Tr_minus_kghWgcoI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("Tr-kghWgcoI", libSceCustomMusicSysCallWrapperUnknown_Tr_minus_kghWgcoI_nid_no_patch); + +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_VDAd516qud8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("VDAd516qud8", libSceCustomMusicSysCallWrapperUnknown_VDAd516qud8_nid_no_patch); + +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_VTS_plus_zlRk_minus_wo_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("VTS+zlRk-wo", libSceCustomMusicSysCallWrapperUnknown_VTS_plus_zlRk_minus_wo_nid_no_patch); + +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_Xkm5tRxA0UQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("Xkm5tRxA0UQ", libSceCustomMusicSysCallWrapperUnknown_Xkm5tRxA0UQ_nid_no_patch); + +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_Y3l3L6A4OYE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("Y3l3L6A4OYE", libSceCustomMusicSysCallWrapperUnknown_Y3l3L6A4OYE_nid_no_patch); + +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_ZiO9aSV1ZKc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("ZiO9aSV1ZKc", libSceCustomMusicSysCallWrapperUnknown_ZiO9aSV1ZKc_nid_no_patch); + +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_bzddXxOnr30_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("bzddXxOnr30", libSceCustomMusicSysCallWrapperUnknown_bzddXxOnr30_nid_no_patch); + +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_eTHTXNt0B2k_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("eTHTXNt0B2k", libSceCustomMusicSysCallWrapperUnknown_eTHTXNt0B2k_nid_no_patch); + +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_fsQhNY_plus_RJf8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("fsQhNY+RJf8", libSceCustomMusicSysCallWrapperUnknown_fsQhNY_plus_RJf8_nid_no_patch); + +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_gxew9cxe_plus_xk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("gxew9cxe+xk", libSceCustomMusicSysCallWrapperUnknown_gxew9cxe_plus_xk_nid_no_patch); + +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_hd0MG4ZvfRA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("hd0MG4ZvfRA", libSceCustomMusicSysCallWrapperUnknown_hd0MG4ZvfRA_nid_no_patch); + +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_jETJjJLQtas_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("jETJjJLQtas", libSceCustomMusicSysCallWrapperUnknown_jETJjJLQtas_nid_no_patch); + +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_jVD2jFJAQ28_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("jVD2jFJAQ28", libSceCustomMusicSysCallWrapperUnknown_jVD2jFJAQ28_nid_no_patch); + +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_kso0cI0kq1U_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("kso0cI0kq1U", libSceCustomMusicSysCallWrapperUnknown_kso0cI0kq1U_nid_no_patch); + +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_lgRL6Oblmyg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("lgRL6Oblmyg", libSceCustomMusicSysCallWrapperUnknown_lgRL6Oblmyg_nid_no_patch); + +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_mgSEL99WSwA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("mgSEL99WSwA", libSceCustomMusicSysCallWrapperUnknown_mgSEL99WSwA_nid_no_patch); + +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_n0aVoomYuOM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("n0aVoomYuOM", libSceCustomMusicSysCallWrapperUnknown_n0aVoomYuOM_nid_no_patch); + +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_n4jZ2HjzXUo_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("n4jZ2HjzXUo", libSceCustomMusicSysCallWrapperUnknown_n4jZ2HjzXUo_nid_no_patch); + +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_oaaW7CuQLV0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("oaaW7CuQLV0", libSceCustomMusicSysCallWrapperUnknown_oaaW7CuQLV0_nid_no_patch); + +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_qqNVevi1LlY_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("qqNVevi1LlY", libSceCustomMusicSysCallWrapperUnknown_qqNVevi1LlY_nid_no_patch); + +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_sWVPeXELNss_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("sWVPeXELNss", libSceCustomMusicSysCallWrapperUnknown_sWVPeXELNss_nid_no_patch); + +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_sak_plus_IpFmBkk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("sak+IpFmBkk", libSceCustomMusicSysCallWrapperUnknown_sak_plus_IpFmBkk_nid_no_patch); + +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_y2Fu_plus_vFwEXo_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("y2Fu+vFwEXo", libSceCustomMusicSysCallWrapperUnknown_y2Fu_plus_vFwEXo_nid_no_patch); + +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_yM_minus_etuyf_minus_2M_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("yM-etuyf-2M", libSceCustomMusicSysCallWrapperUnknown_yM_minus_etuyf_minus_2M_nid_no_patch); + +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_zRDEHOFDvzQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("zRDEHOFDvzQ", libSceCustomMusicSysCallWrapperUnknown_zRDEHOFDvzQ_nid_no_patch); + +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_zUgaEYLIysQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("zUgaEYLIysQ", libSceCustomMusicSysCallWrapperUnknown_zUgaEYLIysQ_nid_no_patch); + +} diff --git a/core/libs/prx/libSceCustomMusicSysCallWrapper/include/Api.hpp b/core/libs/prx/libSceCustomMusicSysCallWrapper/include/Api.hpp index 63ad57d532..c6ace1453b 100644 --- a/core/libs/prx/libSceCustomMusicSysCallWrapper/include/Api.hpp +++ b/core/libs/prx/libSceCustomMusicSysCallWrapper/include/Api.hpp @@ -4,6 +4,11 @@ extern "C" { +int APS5_VABI sceCustomMusicNetInetNtop(); +int APS5_VABI sceCustomMusicNetShutdown(); +int APS5_VABI sceCustomMusicNetSocketAbort(); +int APS5_VABI sceCustomMusicPthreadAttrGet(); +int APS5_VABI sceCustomMusicPthreadSelf(); int APS5_VABI sceCustomMusicSelect(); } diff --git a/core/libs/prx/libSceCustomMusicSysCallWrapper/include/InventoryExports.hpp b/core/libs/prx/libSceCustomMusicSysCallWrapper/include/InventoryExports.hpp new file mode 100644 index 0000000000..65b7c2db68 --- /dev/null +++ b/core/libs/prx/libSceCustomMusicSysCallWrapper/include/InventoryExports.hpp @@ -0,0 +1,109 @@ +#pragma once + +#include "SceTypes.hpp" + +extern "C" { + +int APS5_VABI sceCustomMusicAudiodecClearContext(); +int APS5_VABI sceCustomMusicAudiodecCpuDecode(); +int APS5_VABI sceCustomMusicAudiodecCreateDecoder(); +int APS5_VABI sceCustomMusicAudiodecDecode(); +int APS5_VABI sceCustomMusicAudiodecDeleteDecoder(); +int APS5_VABI sceCustomMusicFileClose(); +int APS5_VABI sceCustomMusicFileLseek(); +int APS5_VABI sceCustomMusicFileOpenRead(); +int APS5_VABI sceCustomMusicFileRead(); +int APS5_VABI sceCustomMusicKernelGettimeofday(); +int APS5_VABI sceCustomMusicKernelNanosleep(); +int APS5_VABI sceCustomMusicNetAccept(); +int APS5_VABI sceCustomMusicNetBind(); +int APS5_VABI sceCustomMusicNetConnect(); +int APS5_VABI sceCustomMusicNetCtlInit(); +int APS5_VABI sceCustomMusicNetCtlTerm(); +int APS5_VABI sceCustomMusicNetEpollAbort(); +int APS5_VABI sceCustomMusicNetEpollControl(); +int APS5_VABI sceCustomMusicNetEpollCreate(); +int APS5_VABI sceCustomMusicNetEpollDestroy(); +int APS5_VABI sceCustomMusicNetEpollWait(); +int APS5_VABI sceCustomMusicNetErrnoLoc(); +int APS5_VABI sceCustomMusicNetInetPton(); +int APS5_VABI sceCustomMusicNetListen(); +int APS5_VABI sceCustomMusicNetPoolCreate(); +int APS5_VABI sceCustomMusicNetPoolDestroy(); +int APS5_VABI sceCustomMusicNetRecv(); +int APS5_VABI sceCustomMusicNetResolverAbort(); +int APS5_VABI sceCustomMusicNetResolverCreate(); +int APS5_VABI sceCustomMusicNetResolverDestroy(); +int APS5_VABI sceCustomMusicNetSend(); +int APS5_VABI sceCustomMusicNetSetsockopt(); +int APS5_VABI sceCustomMusicNetSocketClose(); +int APS5_VABI sceCustomMusicNetSocket(); +int APS5_VABI sceCustomMusicPthreadAttrDestroy(); +int APS5_VABI sceCustomMusicPthreadAttrInit(); +int APS5_VABI sceCustomMusicPthreadCondDestroy(); +int APS5_VABI sceCustomMusicPthreadCondInit(); +int APS5_VABI sceCustomMusicPthreadCondWait(); +int APS5_VABI sceCustomMusicPthreadCreate(); +int APS5_VABI sceCustomMusicPthreadDetach(); +int APS5_VABI sceCustomMusicPthreadExit(); +int APS5_VABI sceCustomMusicPthreadJoin(); +int APS5_VABI sceCustomMusicPthreadMutexDestroy(); +int APS5_VABI sceCustomMusicPthreadMutexInit(); +int APS5_VABI sceCustomMusicPthreadMutexLock(); +int APS5_VABI sceCustomMusicPthreadSetprio(); +int APS5_VABI sceCustomMusicPthreadYield(); +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown__plus_LdekyVYugM_nid_no_patch(); +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown__minus_f8PcMdF8L0_nid_no_patch(); +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_0WBrHbI33Ec_nid_no_patch(); +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_0d4Aw8u3xzE_nid_no_patch(); +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_1auNQHxva3I_nid_no_patch(); +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_24ZHuc5b0RA_nid_no_patch(); +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_2yE7PehS6iI_nid_no_patch(); +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_3DyCYcJudI4_nid_no_patch(); +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_5I2IqwB0Hes_nid_no_patch(); +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_5p7ZpbyUKuo_nid_no_patch(); +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_6PNhaGAw6Ho_nid_no_patch(); +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_70NNIr_minus_nfFo_nid_no_patch(); +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_7uOVo0lL97g_nid_no_patch(); +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_8t8UK2XkJ1Y_nid_no_patch(); +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_CQv51NIK3i4_nid_no_patch(); +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_DABJz_plus_GrAWg_nid_no_patch(); +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_EaC0JWOBvy4_nid_no_patch(); +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_HNx_minus_bHhat18_nid_no_patch(); +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_J4_plus_14VzlTHM_nid_no_patch(); +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_JSZXadyt_minus_nk_nid_no_patch(); +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_Kxq7i4lQjGg_nid_no_patch(); +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_L0MmfM0N_minus_y8_nid_no_patch(); +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_Mk6mbbOvaBM_nid_no_patch(); +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_NbAJfB19zUk_nid_no_patch(); +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_QEVVJH35Fis_nid_no_patch(); +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_ROnMWfJ56t8_nid_no_patch(); +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_SneVIJUdrLw_nid_no_patch(); +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_Tr_minus_kghWgcoI_nid_no_patch(); +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_VDAd516qud8_nid_no_patch(); +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_VTS_plus_zlRk_minus_wo_nid_no_patch(); +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_Xkm5tRxA0UQ_nid_no_patch(); +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_Y3l3L6A4OYE_nid_no_patch(); +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_ZiO9aSV1ZKc_nid_no_patch(); +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_bzddXxOnr30_nid_no_patch(); +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_eTHTXNt0B2k_nid_no_patch(); +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_fsQhNY_plus_RJf8_nid_no_patch(); +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_gxew9cxe_plus_xk_nid_no_patch(); +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_hd0MG4ZvfRA_nid_no_patch(); +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_jETJjJLQtas_nid_no_patch(); +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_jVD2jFJAQ28_nid_no_patch(); +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_kso0cI0kq1U_nid_no_patch(); +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_lgRL6Oblmyg_nid_no_patch(); +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_mgSEL99WSwA_nid_no_patch(); +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_n0aVoomYuOM_nid_no_patch(); +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_n4jZ2HjzXUo_nid_no_patch(); +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_oaaW7CuQLV0_nid_no_patch(); +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_qqNVevi1LlY_nid_no_patch(); +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_sWVPeXELNss_nid_no_patch(); +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_sak_plus_IpFmBkk_nid_no_patch(); +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_y2Fu_plus_vFwEXo_nid_no_patch(); +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_yM_minus_etuyf_minus_2M_nid_no_patch(); +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_zRDEHOFDvzQ_nid_no_patch(); +int APS5_VABI libSceCustomMusicSysCallWrapperUnknown_zUgaEYLIysQ_nid_no_patch(); + +} diff --git a/core/libs/prx/libSceCustomMusicSysCallWrapper/libSceCustomMusicSysCallWrapper.hpp b/core/libs/prx/libSceCustomMusicSysCallWrapper/libSceCustomMusicSysCallWrapper.hpp index 22ad944a67..0365dcbb17 100644 --- a/core/libs/prx/libSceCustomMusicSysCallWrapper/libSceCustomMusicSysCallWrapper.hpp +++ b/core/libs/prx/libSceCustomMusicSysCallWrapper/libSceCustomMusicSysCallWrapper.hpp @@ -1,3 +1,4 @@ #pragma once #include "prx/libSceCustomMusicSysCallWrapper/include/Api.hpp" +#include "prx/libSceCustomMusicSysCallWrapper/include/InventoryExports.hpp" diff --git a/core/libs/prx/libSceEsvm/Export.cpp b/core/libs/prx/libSceEsvm/Export.cpp index 3935b8b5ad..6ae877dcb1 100644 --- a/core/libs/prx/libSceEsvm/Export.cpp +++ b/core/libs/prx/libSceEsvm/Export.cpp @@ -1,3 +1,4 @@ +#include "prx/libSceEsvm/include/InventoryExports.hpp" #include "prx/libSceEsvm/include/Api.hpp" #include #include @@ -207,4 +208,24 @@ int APS5_VABI sceEsvmVideoOutSetContentType() { } +} + +extern "C" { + +int APS5_VABI sceEsvmInitializePlayReady() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceEsvmInitializeWidevine() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceEsvmRequestMediaKeySystemAccessPlayReady() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceEsvmRequestMediaKeySystemAccessWidevine() { + NotImplemented_nid_no_patch(__func__); +} + } diff --git a/core/libs/prx/libSceEsvm/include/InventoryExports.hpp b/core/libs/prx/libSceEsvm/include/InventoryExports.hpp new file mode 100644 index 0000000000..a56ada51c1 --- /dev/null +++ b/core/libs/prx/libSceEsvm/include/InventoryExports.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include "SceTypes.hpp" + +extern "C" { + +int APS5_VABI sceEsvmInitializePlayReady(); +int APS5_VABI sceEsvmInitializeWidevine(); +int APS5_VABI sceEsvmRequestMediaKeySystemAccessPlayReady(); +int APS5_VABI sceEsvmRequestMediaKeySystemAccessWidevine(); + +} diff --git a/core/libs/prx/libSceEsvm/libSceEsvm.hpp b/core/libs/prx/libSceEsvm/libSceEsvm.hpp index 015c133208..fda00d64ee 100644 --- a/core/libs/prx/libSceEsvm/libSceEsvm.hpp +++ b/core/libs/prx/libSceEsvm/libSceEsvm.hpp @@ -1,3 +1,4 @@ #pragma once #include "prx/libSceEsvm/include/Api.hpp" +#include "prx/libSceEsvm/include/InventoryExports.hpp" diff --git a/core/libs/prx/libSceFios2/CMakeLists.txt b/core/libs/prx/libSceFios2/CMakeLists.txt new file mode 100644 index 0000000000..f242ac0634 --- /dev/null +++ b/core/libs/prx/libSceFios2/CMakeLists.txt @@ -0,0 +1,16 @@ +add_library(libSceFios2 SHARED EXCLUDE_FROM_ALL Export.cpp src/Runtime.cpp src/Control.cpp src/PathQueries.cpp src/PathOperations.cpp src/FileHandles.cpp src/DirectoryHandles.cpp src/Time.cpp) +target_include_directories(libSceFios2 PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceFios2) + +if(BUILD_TESTING) + add_test_executable(guest_fios2_tests ${LIBS_INCLUDE_DIR}/tests/GuestFios2.cpp) + target_include_directories(guest_fios2_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_fios2_tests PRIVATE libSceFios2 libkernel libc) + configure_windows_unwind(guest_fios2_tests) + add_test(NAME guest_fios2 COMMAND guest_fios2_tests) + set_tests_properties(guest_fios2 PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_fios2 PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceFios2/Export.cpp b/core/libs/prx/libSceFios2/Export.cpp index b97176c204..d20a3d8090 100644 --- a/core/libs/prx/libSceFios2/Export.cpp +++ b/core/libs/prx/libSceFios2/Export.cpp @@ -121,56 +121,11 @@ int APS5_VABI sceFiosCachePrefetchFileSync() { return 0; } -int APS5_VABI sceFiosCancelAllOps() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceFiosClearTimeStamps() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFiosCloseAllFiles() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosDHClose() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosDHCloseSync() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosDHGetPath() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosDHOpen() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosDHOpenSync() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosDHRead() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosDHReadSync() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceFiosDLLInitialize() { NotImplemented_nid_no_patch(__func__); return 0; @@ -181,21 +136,6 @@ int APS5_VABI sceFiosDLLTerminate() { return 0; } -int APS5_VABI sceFiosDateFromComponents() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosDateGetCurrent() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosDateToComponents() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceFiosDeallocatePassthruFH() { NotImplemented_nid_no_patch(__func__); return 0; @@ -226,26 +166,6 @@ int APS5_VABI sceFiosDebugDumpOp() { return 0; } -int APS5_VABI sceFiosDelete() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosDeleteSync() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosDirectoryCreate() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosDirectoryCreateSync() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceFiosDirectoryCreateWithMode() { NotImplemented_nid_no_patch(__func__); return 0; @@ -256,306 +176,16 @@ int APS5_VABI sceFiosDirectoryCreateWithModeSync() { return 0; } -int APS5_VABI sceFiosDirectoryDelete() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosDirectoryDeleteSync() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosDirectoryExists() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosDirectoryExistsSync() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosExists() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosExistsSync() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosFHClose() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosFHCloseSync() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosFHGetOpenParams() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosFHGetPath() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosFHGetSize() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosFHOpen() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosFHOpenSync() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosFHOpenWithMode() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosFHOpenWithModeSync() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosFHPread() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosFHPreadSync() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosFHPreadv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosFHPreadvSync() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosFHPwrite() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosFHPwriteSync() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosFHPwritev() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosFHPwritevSync() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosFHRead() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosFHReadSync() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosFHReadv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosFHReadvSync() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosFHSeek() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosFHStat() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosFHStatSync() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosFHSync() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosFHSyncSync() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosFHTell() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceFiosFHToFileno() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFiosFHTruncate() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosFHTruncateSync() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosFHWrite() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosFHWriteSync() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosFHWritev() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosFHWritevSync() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosFileDelete() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosFileDeleteSync() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosFileExists() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosFileExistsSync() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosFileGetSize() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosFileGetSizeSync() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosFileRead() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosFileReadSync() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosFileTruncate() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosFileTruncateSync() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosFileWrite() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosFileWriteSync() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceFiosFilenoToFH() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFiosGetAllDHs() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosGetAllFHs() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosGetAllOps() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosGetDefaultOpAttr() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosGetGlobalDefaultOpAttr() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosGetSuspendCount() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosGetThreadDefaultOpAttr() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceFiosIOFilterAdd() { NotImplemented_nid_no_patch(__func__); return 0; @@ -581,86 +211,6 @@ int APS5_VABI sceFiosIOFilterRemove() { return 0; } -int APS5_VABI sceFiosInitialize() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosIsIdle() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosIsInitialized() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosIsSuspended() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosIsValidHandle() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosOpCancel() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosOpDelete() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosOpGetActualCount() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosOpGetAttr() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosOpGetBuffer() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosOpGetError() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosOpGetOffset() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosOpGetPath() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosOpGetRequestCount() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosOpIsCancelled() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosOpIsDone() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceFiosOpReschedule() { NotImplemented_nid_no_patch(__func__); return 0; @@ -676,21 +226,6 @@ int APS5_VABI sceFiosOpSetBuffer() { return 0; } -int APS5_VABI sceFiosOpSyncWait() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosOpSyncWaitForIO() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosOpWait() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceFiosOpWaitUntil() { NotImplemented_nid_no_patch(__func__); return 0; @@ -751,16 +286,6 @@ int APS5_VABI sceFiosPrintf() { return 0; } -int APS5_VABI sceFiosRename() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosRenameSync() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceFiosResolve() { NotImplemented_nid_no_patch(__func__); return 0; @@ -771,76 +296,21 @@ int APS5_VABI sceFiosResolveSync() { return 0; } -int APS5_VABI sceFiosResume() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceFiosSaveTimeStamp() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFiosSetGlobalDefaultOpAttr() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosSetThreadDefaultOpAttr() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosShutdownAndCancelOps() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosStat() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosStatSync() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosSuspend() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosTerminate() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceFiosTimeGetCurrent() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFiosTimeIntervalFromNanoseconds() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFiosTimeIntervalToNanoseconds() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceFiosTraceTimestamp() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFiosUpdateParameters() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceFiosVprintf() { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceFios2/include/Api.hpp b/core/libs/prx/libSceFios2/include/Api.hpp index 354858a01d..8e9c93d9dd 100644 --- a/core/libs/prx/libSceFios2/include/Api.hpp +++ b/core/libs/prx/libSceFios2/include/Api.hpp @@ -1,9 +1,117 @@ #pragma once #include "SceTypes.hpp" +#include "prx/libSceFios2/include/Types.hpp" extern "C" { +int APS5_VABI sceFiosInitialize(const SceFiosParams* params); +void APS5_VABI sceFiosTerminate(); +bool APS5_VABI sceFiosIsInitialized(SceFiosParams* outParams); +void APS5_VABI sceFiosUpdateParameters(const SceFiosParams* params); +void APS5_VABI sceFiosSetGlobalDefaultOpAttr(const SceFiosOpAttr* attr); +bool APS5_VABI sceFiosGetGlobalDefaultOpAttr(SceFiosOpAttr* outAttr); +void APS5_VABI sceFiosSetThreadDefaultOpAttr(const SceFiosOpAttr* attr); +bool APS5_VABI sceFiosGetThreadDefaultOpAttr(SceFiosOpAttr* outAttr); +void APS5_VABI sceFiosGetDefaultOpAttr(SceFiosOpAttr* outAttr); +void APS5_VABI sceFiosSuspend(); +void APS5_VABI sceFiosResume(); +std::uint32_t APS5_VABI sceFiosGetSuspendCount(); +bool APS5_VABI sceFiosIsSuspended(); +void APS5_VABI sceFiosCancelAllOps(); +void APS5_VABI sceFiosShutdownAndCancelOps(); +void APS5_VABI sceFiosCloseAllFiles(); +bool APS5_VABI sceFiosIsIdle(); +std::uint32_t APS5_VABI sceFiosGetAllFHs(SceFiosFH* outArray, std::uint32_t arraySize); +std::uint32_t APS5_VABI sceFiosGetAllDHs(SceFiosDH* outArray, std::uint32_t arraySize); +std::uint32_t APS5_VABI sceFiosGetAllOps(SceFiosOp* outArray, std::uint32_t arraySize); +bool APS5_VABI sceFiosIsValidHandle(SceFiosHandle handle); +bool APS5_VABI sceFiosOpIsDone(SceFiosOp op); +int APS5_VABI sceFiosOpWait(SceFiosOp op); +void APS5_VABI sceFiosOpDelete(SceFiosOp op); +int APS5_VABI sceFiosOpSyncWait(SceFiosOp op); +std::int64_t APS5_VABI sceFiosOpSyncWaitForIO(SceFiosOp op); +int APS5_VABI sceFiosOpGetError(SceFiosOp op); +void APS5_VABI sceFiosOpCancel(SceFiosOp op); +bool APS5_VABI sceFiosOpIsCancelled(SceFiosOp op); +const SceFiosOpAttr* APS5_VABI sceFiosOpGetAttr(SceFiosOp op); +const char* APS5_VABI sceFiosOpGetPath(SceFiosOp op); +void* APS5_VABI sceFiosOpGetBuffer(SceFiosOp op); +std::int64_t APS5_VABI sceFiosOpGetOffset(SceFiosOp op); +std::int64_t APS5_VABI sceFiosOpGetRequestCount(SceFiosOp op); +std::int64_t APS5_VABI sceFiosOpGetActualCount(SceFiosOp op); +SceFiosOp APS5_VABI sceFiosExists(const SceFiosOpAttr* attr, const char* path, bool* outExists); +bool APS5_VABI sceFiosExistsSync(const SceFiosOpAttr* attr, const char* path); +SceFiosOp APS5_VABI sceFiosFileExists(const SceFiosOpAttr* attr, const char* path, bool* outExists); +bool APS5_VABI sceFiosFileExistsSync(const SceFiosOpAttr* attr, const char* path); +SceFiosOp APS5_VABI sceFiosDirectoryExists(const SceFiosOpAttr* attr, const char* path, bool* outExists); +bool APS5_VABI sceFiosDirectoryExistsSync(const SceFiosOpAttr* attr, const char* path); +SceFiosOp APS5_VABI sceFiosFileGetSize(const SceFiosOpAttr* attr, const char* path); +SceFiosSize APS5_VABI sceFiosFileGetSizeSync(const SceFiosOpAttr* attr, const char* path); +SceFiosOp APS5_VABI sceFiosStat(const SceFiosOpAttr* attr, const char* path, SceFiosStat* outStatus); +int APS5_VABI sceFiosStatSync(const SceFiosOpAttr* attr, const char* path, SceFiosStat* outStatus); +SceFiosOp APS5_VABI sceFiosDelete(const SceFiosOpAttr* attr, const char* path); +int APS5_VABI sceFiosDeleteSync(const SceFiosOpAttr* attr, const char* path); +SceFiosOp APS5_VABI sceFiosFileDelete(const SceFiosOpAttr* attr, const char* path); +int APS5_VABI sceFiosFileDeleteSync(const SceFiosOpAttr* attr, const char* path); +SceFiosOp APS5_VABI sceFiosDirectoryCreate(const SceFiosOpAttr* attr, const char* path); +int APS5_VABI sceFiosDirectoryCreateSync(const SceFiosOpAttr* attr, const char* path); +SceFiosOp APS5_VABI sceFiosDirectoryDelete(const SceFiosOpAttr* attr, const char* path); +int APS5_VABI sceFiosDirectoryDeleteSync(const SceFiosOpAttr* attr, const char* path); +SceFiosOp APS5_VABI sceFiosRename(const SceFiosOpAttr* attr, const char* oldPath, const char* newPath); +int APS5_VABI sceFiosRenameSync(const SceFiosOpAttr* attr, const char* oldPath, const char* newPath); +SceFiosOp APS5_VABI sceFiosFileRead(const SceFiosOpAttr* attr, const char* path, void* buffer, SceFiosSize length, SceFiosOffset offset); +SceFiosSize APS5_VABI sceFiosFileReadSync(const SceFiosOpAttr* attr, const char* path, void* buffer, SceFiosSize length, SceFiosOffset offset); +SceFiosOp APS5_VABI sceFiosFileWrite(const SceFiosOpAttr* attr, const char* path, const void* buffer, SceFiosSize length, SceFiosOffset offset); +SceFiosSize APS5_VABI sceFiosFileWriteSync(const SceFiosOpAttr* attr, const char* path, const void* buffer, SceFiosSize length, SceFiosOffset offset); +SceFiosOp APS5_VABI sceFiosFileTruncate(const SceFiosOpAttr* attr, const char* path, SceFiosSize length); +int APS5_VABI sceFiosFileTruncateSync(const SceFiosOpAttr* attr, const char* path, SceFiosSize length); +SceFiosOp APS5_VABI sceFiosFHOpen(const SceFiosOpAttr* attr, SceFiosFH* outFH, const char* path, const SceFiosOpenParams* openParams); +int APS5_VABI sceFiosFHOpenSync(const SceFiosOpAttr* attr, SceFiosFH* outFH, const char* path, const SceFiosOpenParams* openParams); +SceFiosOp APS5_VABI sceFiosFHOpenWithMode(const SceFiosOpAttr* attr, SceFiosFH* outFH, const char* path, const SceFiosOpenParams* openParams, std::int32_t nativeMode); +int APS5_VABI sceFiosFHOpenWithModeSync(const SceFiosOpAttr* attr, SceFiosFH* outFH, const char* path, const SceFiosOpenParams* openParams, std::int32_t nativeMode); +SceFiosOp APS5_VABI sceFiosFHClose(const SceFiosOpAttr* attr, SceFiosFH fh); +int APS5_VABI sceFiosFHCloseSync(const SceFiosOpAttr* attr, SceFiosFH fh); +SceFiosOp APS5_VABI sceFiosFHStat(const SceFiosOpAttr* attr, SceFiosFH fh, SceFiosStat* outStatus); +int APS5_VABI sceFiosFHStatSync(const SceFiosOpAttr* attr, SceFiosFH fh, SceFiosStat* outStatus); +SceFiosOp APS5_VABI sceFiosFHTruncate(const SceFiosOpAttr* attr, SceFiosFH fh, SceFiosSize length); +int APS5_VABI sceFiosFHTruncateSync(const SceFiosOpAttr* attr, SceFiosFH fh, SceFiosSize length); +SceFiosOp APS5_VABI sceFiosFHSync(const SceFiosOpAttr* attr, SceFiosFH fh); +int APS5_VABI sceFiosFHSyncSync(const SceFiosOpAttr* attr, SceFiosFH fh); +SceFiosOp APS5_VABI sceFiosFHRead(const SceFiosOpAttr* attr, SceFiosFH fh, void* buffer, SceFiosSize length); +SceFiosSize APS5_VABI sceFiosFHReadSync(const SceFiosOpAttr* attr, SceFiosFH fh, void* buffer, SceFiosSize length); +SceFiosOp APS5_VABI sceFiosFHWrite(const SceFiosOpAttr* attr, SceFiosFH fh, const void* buffer, SceFiosSize length); +SceFiosSize APS5_VABI sceFiosFHWriteSync(const SceFiosOpAttr* attr, SceFiosFH fh, const void* buffer, SceFiosSize length); +SceFiosOp APS5_VABI sceFiosFHPread(const SceFiosOpAttr* attr, SceFiosFH fh, void* buffer, SceFiosSize length, SceFiosOffset offset); +SceFiosSize APS5_VABI sceFiosFHPreadSync(const SceFiosOpAttr* attr, SceFiosFH fh, void* buffer, SceFiosSize length, SceFiosOffset offset); +SceFiosOp APS5_VABI sceFiosFHPwrite(const SceFiosOpAttr* attr, SceFiosFH fh, const void* buffer, SceFiosSize length, SceFiosOffset offset); +SceFiosSize APS5_VABI sceFiosFHPwriteSync(const SceFiosOpAttr* attr, SceFiosFH fh, const void* buffer, SceFiosSize length, SceFiosOffset offset); +SceFiosOp APS5_VABI sceFiosFHReadv(const SceFiosOpAttr* attr, SceFiosFH fh, const SceFiosBuffer* iov, int iovcnt); +SceFiosSize APS5_VABI sceFiosFHReadvSync(const SceFiosOpAttr* attr, SceFiosFH fh, const SceFiosBuffer* iov, int iovcnt); +SceFiosOp APS5_VABI sceFiosFHWritev(const SceFiosOpAttr* attr, SceFiosFH fh, const SceFiosBuffer* iov, int iovcnt); +SceFiosSize APS5_VABI sceFiosFHWritevSync(const SceFiosOpAttr* attr, SceFiosFH fh, const SceFiosBuffer* iov, int iovcnt); +SceFiosOp APS5_VABI sceFiosFHPreadv(const SceFiosOpAttr* attr, SceFiosFH fh, const SceFiosBuffer* iov, int iovcnt, SceFiosOffset offset); +SceFiosSize APS5_VABI sceFiosFHPreadvSync(const SceFiosOpAttr* attr, SceFiosFH fh, const SceFiosBuffer* iov, int iovcnt, SceFiosOffset offset); +SceFiosOp APS5_VABI sceFiosFHPwritev(const SceFiosOpAttr* attr, SceFiosFH fh, const SceFiosBuffer* iov, int iovcnt, SceFiosOffset offset); +SceFiosSize APS5_VABI sceFiosFHPwritevSync(const SceFiosOpAttr* attr, SceFiosFH fh, const SceFiosBuffer* iov, int iovcnt, SceFiosOffset offset); +SceFiosOffset APS5_VABI sceFiosFHSeek(SceFiosFH fh, SceFiosOffset offset, std::int32_t whence); +SceFiosOffset APS5_VABI sceFiosFHTell(SceFiosFH fh); +SceFiosSize APS5_VABI sceFiosFHGetSize(SceFiosFH fh); +const char* APS5_VABI sceFiosFHGetPath(SceFiosFH fh); +const SceFiosOpenParams* APS5_VABI sceFiosFHGetOpenParams(SceFiosFH fh); +SceFiosOp APS5_VABI sceFiosDHOpen(const SceFiosOpAttr* attr, SceFiosDH* outDH, const char* path, SceFiosBuffer buffer); +int APS5_VABI sceFiosDHOpenSync(const SceFiosOpAttr* attr, SceFiosDH* outDH, const char* path, SceFiosBuffer buffer); +SceFiosOp APS5_VABI sceFiosDHRead(const SceFiosOpAttr* attr, SceFiosDH dh, SceFiosDirEntry* outEntry); +int APS5_VABI sceFiosDHReadSync(const SceFiosOpAttr* attr, SceFiosDH dh, SceFiosDirEntry* outEntry); +SceFiosOp APS5_VABI sceFiosDHClose(const SceFiosOpAttr* attr, SceFiosDH dh); +int APS5_VABI sceFiosDHCloseSync(const SceFiosOpAttr* attr, SceFiosDH dh); +const char* APS5_VABI sceFiosDHGetPath(SceFiosDH dh); +SceFiosDate APS5_VABI sceFiosDateGetCurrent(); +SceFiosDate APS5_VABI sceFiosDateFromComponents(const SceFiosTm* components); +SceFiosTm* APS5_VABI sceFiosDateToComponents(SceFiosDate date, SceFiosTm* outComponents); +SceFiosTimeInterval APS5_VABI sceFiosTimeIntervalFromNanoseconds(std::int64_t nanoseconds); +std::int64_t APS5_VABI sceFiosTimeIntervalToNanoseconds(SceFiosTimeInterval interval); + int APS5_VABI sceFiosArchiveGetDecompressorThreadCount(); int APS5_VABI sceFiosArchiveGetMountBufferSize(); int APS5_VABI sceFiosArchiveGetMountBufferSizeSync(); @@ -27,120 +135,27 @@ int APS5_VABI sceFiosCachePrefetchFile(); int APS5_VABI sceFiosCachePrefetchFileRange(); int APS5_VABI sceFiosCachePrefetchFileRangeSync(); int APS5_VABI sceFiosCachePrefetchFileSync(); -int APS5_VABI sceFiosCancelAllOps(); int APS5_VABI sceFiosClearTimeStamps(); -int APS5_VABI sceFiosCloseAllFiles(); -int APS5_VABI sceFiosDHClose(); -int APS5_VABI sceFiosDHCloseSync(); -int APS5_VABI sceFiosDHGetPath(); -int APS5_VABI sceFiosDHOpen(); -int APS5_VABI sceFiosDHOpenSync(); -int APS5_VABI sceFiosDHRead(); -int APS5_VABI sceFiosDHReadSync(); int APS5_VABI sceFiosDLLInitialize(); int APS5_VABI sceFiosDLLTerminate(); -int APS5_VABI sceFiosDateFromComponents(); -int APS5_VABI sceFiosDateGetCurrent(); -int APS5_VABI sceFiosDateToComponents(); int APS5_VABI sceFiosDeallocatePassthruFH(); int APS5_VABI sceFiosDebugDumpDH(); int APS5_VABI sceFiosDebugDumpDate(); int APS5_VABI sceFiosDebugDumpError(); int APS5_VABI sceFiosDebugDumpFH(); int APS5_VABI sceFiosDebugDumpOp(); -int APS5_VABI sceFiosDelete(); -int APS5_VABI sceFiosDeleteSync(); -int APS5_VABI sceFiosDirectoryCreate(); -int APS5_VABI sceFiosDirectoryCreateSync(); int APS5_VABI sceFiosDirectoryCreateWithMode(); int APS5_VABI sceFiosDirectoryCreateWithModeSync(); -int APS5_VABI sceFiosDirectoryDelete(); -int APS5_VABI sceFiosDirectoryDeleteSync(); -int APS5_VABI sceFiosDirectoryExists(); -int APS5_VABI sceFiosDirectoryExistsSync(); -int APS5_VABI sceFiosExists(); -int APS5_VABI sceFiosExistsSync(); -int APS5_VABI sceFiosFHClose(); -int APS5_VABI sceFiosFHCloseSync(); -int APS5_VABI sceFiosFHGetOpenParams(); -int APS5_VABI sceFiosFHGetPath(); -int APS5_VABI sceFiosFHGetSize(); -int APS5_VABI sceFiosFHOpen(); -int APS5_VABI sceFiosFHOpenSync(); -int APS5_VABI sceFiosFHOpenWithMode(); -int APS5_VABI sceFiosFHOpenWithModeSync(); -int APS5_VABI sceFiosFHPread(); -int APS5_VABI sceFiosFHPreadSync(); -int APS5_VABI sceFiosFHPreadv(); -int APS5_VABI sceFiosFHPreadvSync(); -int APS5_VABI sceFiosFHPwrite(); -int APS5_VABI sceFiosFHPwriteSync(); -int APS5_VABI sceFiosFHPwritev(); -int APS5_VABI sceFiosFHPwritevSync(); -int APS5_VABI sceFiosFHRead(); -int APS5_VABI sceFiosFHReadSync(); -int APS5_VABI sceFiosFHReadv(); -int APS5_VABI sceFiosFHReadvSync(); -int APS5_VABI sceFiosFHSeek(); -int APS5_VABI sceFiosFHStat(); -int APS5_VABI sceFiosFHStatSync(); -int APS5_VABI sceFiosFHSync(); -int APS5_VABI sceFiosFHSyncSync(); -int APS5_VABI sceFiosFHTell(); int APS5_VABI sceFiosFHToFileno(); -int APS5_VABI sceFiosFHTruncate(); -int APS5_VABI sceFiosFHTruncateSync(); -int APS5_VABI sceFiosFHWrite(); -int APS5_VABI sceFiosFHWriteSync(); -int APS5_VABI sceFiosFHWritev(); -int APS5_VABI sceFiosFHWritevSync(); -int APS5_VABI sceFiosFileDelete(); -int APS5_VABI sceFiosFileDeleteSync(); -int APS5_VABI sceFiosFileExists(); -int APS5_VABI sceFiosFileExistsSync(); -int APS5_VABI sceFiosFileGetSize(); -int APS5_VABI sceFiosFileGetSizeSync(); -int APS5_VABI sceFiosFileRead(); -int APS5_VABI sceFiosFileReadSync(); -int APS5_VABI sceFiosFileTruncate(); -int APS5_VABI sceFiosFileTruncateSync(); -int APS5_VABI sceFiosFileWrite(); -int APS5_VABI sceFiosFileWriteSync(); int APS5_VABI sceFiosFilenoToFH(); -int APS5_VABI sceFiosGetAllDHs(); -int APS5_VABI sceFiosGetAllFHs(); -int APS5_VABI sceFiosGetAllOps(); -int APS5_VABI sceFiosGetDefaultOpAttr(); -int APS5_VABI sceFiosGetGlobalDefaultOpAttr(); -int APS5_VABI sceFiosGetSuspendCount(); -int APS5_VABI sceFiosGetThreadDefaultOpAttr(); int APS5_VABI sceFiosIOFilterAdd(); int APS5_VABI sceFiosIOFilterCache(); int APS5_VABI sceFiosIOFilterGetInfo(); int APS5_VABI sceFiosIOFilterPsarcDearchiver(); int APS5_VABI sceFiosIOFilterRemove(); -int APS5_VABI sceFiosInitialize(); -int APS5_VABI sceFiosIsIdle(); -int APS5_VABI sceFiosIsInitialized(); -int APS5_VABI sceFiosIsSuspended(); -int APS5_VABI sceFiosIsValidHandle(); -int APS5_VABI sceFiosOpCancel(); -int APS5_VABI sceFiosOpDelete(); -int APS5_VABI sceFiosOpGetActualCount(); -int APS5_VABI sceFiosOpGetAttr(); -int APS5_VABI sceFiosOpGetBuffer(); -int APS5_VABI sceFiosOpGetError(); -int APS5_VABI sceFiosOpGetOffset(); -int APS5_VABI sceFiosOpGetPath(); -int APS5_VABI sceFiosOpGetRequestCount(); -int APS5_VABI sceFiosOpIsCancelled(); -int APS5_VABI sceFiosOpIsDone(); int APS5_VABI sceFiosOpReschedule(); int APS5_VABI sceFiosOpRescheduleWithPriority(); int APS5_VABI sceFiosOpSetBuffer(); -int APS5_VABI sceFiosOpSyncWait(); -int APS5_VABI sceFiosOpSyncWaitForIO(); -int APS5_VABI sceFiosOpWait(); int APS5_VABI sceFiosOpWaitUntil(); int APS5_VABI sceFiosOverlayAdd(); int APS5_VABI sceFiosOverlayGetInfo(); @@ -153,24 +168,11 @@ int APS5_VABI sceFiosPathcmp(); int APS5_VABI sceFiosPathncmp(); int APS5_VABI sceFiosPrintTimeStamps(); int APS5_VABI sceFiosPrintf(); -int APS5_VABI sceFiosRename(); -int APS5_VABI sceFiosRenameSync(); int APS5_VABI sceFiosResolve(); int APS5_VABI sceFiosResolveSync(); -int APS5_VABI sceFiosResume(); int APS5_VABI sceFiosSaveTimeStamp(); -int APS5_VABI sceFiosSetGlobalDefaultOpAttr(); -int APS5_VABI sceFiosSetThreadDefaultOpAttr(); -int APS5_VABI sceFiosShutdownAndCancelOps(); -int APS5_VABI sceFiosStat(); -int APS5_VABI sceFiosStatSync(); -int APS5_VABI sceFiosSuspend(); -int APS5_VABI sceFiosTerminate(); int APS5_VABI sceFiosTimeGetCurrent(); -int APS5_VABI sceFiosTimeIntervalFromNanoseconds(); -int APS5_VABI sceFiosTimeIntervalToNanoseconds(); int APS5_VABI sceFiosTraceTimestamp(); -int APS5_VABI sceFiosUpdateParameters(); int APS5_VABI sceFiosVprintf(); } diff --git a/core/libs/prx/libSceFios2/include/Types.hpp b/core/libs/prx/libSceFios2/include/Types.hpp new file mode 100644 index 0000000000..f112aace65 --- /dev/null +++ b/core/libs/prx/libSceFios2/include/Types.hpp @@ -0,0 +1,128 @@ +#pragma once + +#include +#include +#include "prx/libc/include/general/VabiMacros.hpp" + +using SceFiosHandle = std::int32_t; +using SceFiosOp = SceFiosHandle; +using SceFiosFH = SceFiosHandle; +using SceFiosDH = SceFiosHandle; +using SceFiosTime = std::int64_t; +using SceFiosTimeInterval = std::int64_t; +using SceFiosDate = std::uint64_t; +using SceFiosSize = std::int64_t; +using SceFiosOffset = std::int64_t; +using SceFiosOpEvent = std::uint8_t; + +using SceFiosOpCallback = int (APS5_VABI*)(void* context, SceFiosOp op, SceFiosOpEvent event, int error); + +constexpr std::int32_t SCE_FIOS_OK = 0; +constexpr std::int32_t SCE_FIOS_ERROR_UNIMPLEMENTED = static_cast(0x80820000u); +constexpr std::int32_t SCE_FIOS_ERROR_CANT_ALLOCATE_OP = static_cast(0x80820001u); +constexpr std::int32_t SCE_FIOS_ERROR_CANT_ALLOCATE_FH = static_cast(0x80820002u); +constexpr std::int32_t SCE_FIOS_ERROR_CANT_ALLOCATE_DH = static_cast(0x80820003u); +constexpr std::int32_t SCE_FIOS_ERROR_BAD_PATH = static_cast(0x80820005u); +constexpr std::int32_t SCE_FIOS_ERROR_BAD_PTR = static_cast(0x80820006u); +constexpr std::int32_t SCE_FIOS_ERROR_BAD_OFFSET = static_cast(0x80820007u); +constexpr std::int32_t SCE_FIOS_ERROR_BAD_SIZE = static_cast(0x80820008u); +constexpr std::int32_t SCE_FIOS_ERROR_BAD_IOVCNT = static_cast(0x80820009u); +constexpr std::int32_t SCE_FIOS_ERROR_BAD_OP = static_cast(0x8082000Au); +constexpr std::int32_t SCE_FIOS_ERROR_BAD_FH = static_cast(0x8082000Bu); +constexpr std::int32_t SCE_FIOS_ERROR_BAD_DH = static_cast(0x8082000Cu); +constexpr std::int32_t SCE_FIOS_ERROR_NOT_A_FILE = static_cast(0x8082000Eu); +constexpr std::int32_t SCE_FIOS_ERROR_NOT_A_DIRECTORY = static_cast(0x8082000Fu); +constexpr std::int32_t SCE_FIOS_ERROR_EOF = static_cast(0x80820010u); +constexpr std::int32_t SCE_FIOS_ERROR_CANCELLED = static_cast(0x80820012u); +constexpr std::int32_t SCE_FIOS_ERROR_ACCESS = static_cast(0x80820013u); +constexpr std::int32_t SCE_FIOS_ERROR_READ_ONLY = static_cast(0x80820015u); +constexpr std::int32_t SCE_FIOS_ERROR_WRITE_ONLY = static_cast(0x80820016u); +constexpr std::int32_t SCE_FIOS_ERROR_PATH_TOO_LONG = static_cast(0x80820018u); +constexpr std::int32_t SCE_FIOS_ERROR_BAD_FLAGS = static_cast(0x80820021u); +constexpr std::int32_t SCE_FIOS_ERROR_UNKNOWN = static_cast(0x80820022u); +constexpr std::int32_t SCE_FIOS_ERROR_ALREADY_EXISTS = static_cast(0x80820023u); + +constexpr SceFiosOpEvent SCE_FIOS_OPEVENT_COMPLETE = 1; +constexpr SceFiosOpEvent SCE_FIOS_OPEVENT_DELETE = 2; + +constexpr std::int32_t SCE_FIOS_SEEK_SET = 0; +constexpr std::int32_t SCE_FIOS_SEEK_CUR = 1; +constexpr std::int32_t SCE_FIOS_SEEK_END = 2; + +constexpr std::uint32_t SCE_FIOS_O_READ = 1u << 0; +constexpr std::uint32_t SCE_FIOS_O_WRITE = 1u << 1; +constexpr std::uint32_t SCE_FIOS_O_APPEND = 1u << 2; +constexpr std::uint32_t SCE_FIOS_O_CREAT = 1u << 3; +constexpr std::uint32_t SCE_FIOS_O_TRUNC = 1u << 4; +constexpr std::uint32_t SCE_FIOS_O_DIRECT = 1u << 12; + +constexpr std::uint32_t SCE_FIOS_STAT_DIRECTORY = 1u << 0; + +constexpr std::size_t SCE_FIOS_PATH_MAX = 1024; + +struct SceFiosBuffer { + void* pPtr; + std::uint64_t length; +}; + +struct SceFiosOpAttr { + SceFiosTime deadline; + SceFiosOpCallback pCallback; + void* pCallbackContext; + std::int32_t priority : 8; + std::uint32_t opflags : 24; + std::uint32_t userTag; + void* userPtr; + void* pReserved; +}; + +struct SceFiosOpenParams { + std::uint32_t openFlags : 16; + std::uint32_t opFlags : 16; + std::uint32_t reserved; + SceFiosBuffer buffer; +}; + +struct SceFiosStat { + SceFiosOffset fileSize; + SceFiosDate accessDate; + SceFiosDate modificationDate; + SceFiosDate creationDate; + std::uint32_t statFlags; + std::uint32_t reserved; + std::int64_t uid; + std::int64_t gid; + std::int64_t dev; + std::int64_t ino; + std::int64_t mode; +}; + +struct SceFiosDirEntry { + SceFiosOffset fileSize; + std::uint32_t statFlags; + std::uint16_t nameLength; + std::uint16_t fullPathLength; + std::uint16_t offsetToName; + std::uint16_t reserved[3]; + char fullPath[SCE_FIOS_PATH_MAX]; +}; + +struct SceFiosParams; + +static_assert(sizeof(SceFiosBuffer) == 16); +static_assert(sizeof(SceFiosOpAttr) == 48); +static_assert(sizeof(SceFiosOpenParams) == 24); +static_assert(sizeof(SceFiosStat) == 80); +static_assert(sizeof(SceFiosDirEntry) == 24 + SCE_FIOS_PATH_MAX); + +struct SceFiosTm { + std::int32_t tm_sec; + std::int32_t tm_min; + std::int32_t tm_hour; + std::int32_t tm_mday; + std::int32_t tm_mon; + std::int32_t tm_year; + std::int32_t tm_wday; + std::int32_t tm_yday; + std::int32_t tm_isdst; +}; diff --git a/core/libs/prx/libSceFios2/src/Control.cpp b/core/libs/prx/libSceFios2/src/Control.cpp new file mode 100644 index 0000000000..5a46b93b69 --- /dev/null +++ b/core/libs/prx/libSceFios2/src/Control.cpp @@ -0,0 +1,175 @@ +#include "prx/libSceFios2/include/Api.hpp" +#include "Runtime.hpp" +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" +#include +#include + +extern "C" { + +int APS5_VABI sceFiosInitialize(const SceFiosParams* params) { + return Fios::Initialize(params) ? SCE_FIOS_OK : SCE_FIOS_ERROR_BAD_PTR; +} + +void APS5_VABI sceFiosTerminate() { + Fios::Terminate(); +} + +bool APS5_VABI sceFiosIsInitialized(SceFiosParams* outParams) { + return Fios::IsInitialized(outParams); +} + +void APS5_VABI sceFiosUpdateParameters(const SceFiosParams* params) { + Fios::UpdateParameters(params); +} + +void APS5_VABI sceFiosSetGlobalDefaultOpAttr(const SceFiosOpAttr* attr) { + Fios::SetGlobalAttr(attr); +} + +bool APS5_VABI sceFiosGetGlobalDefaultOpAttr(SceFiosOpAttr* outAttr) { + SceFiosOpAttr attr{}; + const bool present = Fios::GlobalAttr(attr); + if (outAttr != nullptr && present) Fios::CopyOut(outAttr, &attr, sizeof(attr)); + return present; +} + +void APS5_VABI sceFiosSetThreadDefaultOpAttr(const SceFiosOpAttr* attr) { + Fios::SetThreadAttr(attr); +} + +bool APS5_VABI sceFiosGetThreadDefaultOpAttr(SceFiosOpAttr* outAttr) { + SceFiosOpAttr attr{}; + const bool present = Fios::ThreadAttr(attr); + if (outAttr != nullptr && present) Fios::CopyOut(outAttr, &attr, sizeof(attr)); + return present; +} + +void APS5_VABI sceFiosGetDefaultOpAttr(SceFiosOpAttr* outAttr) { + SceFiosOpAttr attr{}; + if (!Fios::ThreadAttr(attr)) Fios::GlobalAttr(attr); + if (outAttr != nullptr) Fios::CopyOut(outAttr, &attr, sizeof(attr)); +} + +void APS5_VABI sceFiosSuspend() { + Fios::Suspend(); +} + +void APS5_VABI sceFiosResume() { + Fios::Resume(); +} + +std::uint32_t APS5_VABI sceFiosGetSuspendCount() { + return Fios::SuspendCount(); +} + +bool APS5_VABI sceFiosIsSuspended() { + return Fios::SuspendCount() != 0; +} + +void APS5_VABI sceFiosCancelAllOps() { + Fios::CancelAll(); +} + +void APS5_VABI sceFiosShutdownAndCancelOps() { + Fios::Terminate(); +} + +void APS5_VABI sceFiosCloseAllFiles() { + Fios::CloseAllFiles(); +} + +bool APS5_VABI sceFiosIsIdle() { + return Fios::IsIdle(); +} + +static std::uint32_t CopyHandles(std::uint32_t (*list)(SceFiosHandle*, std::uint32_t), SceFiosHandle* out, std::uint32_t capacity) { + std::vector handles(capacity); + const std::uint32_t total = list(handles.data(), capacity); + const std::uint32_t count = std::min(total, capacity); + if (out != nullptr && count != 0) Fios::CopyOut(out, handles.data(), count * sizeof(SceFiosHandle)); + return total; +} + +std::uint32_t APS5_VABI sceFiosGetAllFHs(SceFiosFH* outArray, std::uint32_t arraySize) { + return CopyHandles(Fios::ListFiles, outArray, arraySize); +} + +std::uint32_t APS5_VABI sceFiosGetAllDHs(SceFiosDH* outArray, std::uint32_t arraySize) { + return CopyHandles(Fios::ListDirectories, outArray, arraySize); +} + +std::uint32_t APS5_VABI sceFiosGetAllOps(SceFiosOp* outArray, std::uint32_t arraySize) { + return CopyHandles(Fios::ListOps, outArray, arraySize); +} + +bool APS5_VABI sceFiosIsValidHandle(SceFiosHandle handle) { + return Fios::IsValidHandle(handle); +} + +bool APS5_VABI sceFiosOpIsDone(SceFiosOp op) { + return Fios::OpDone(op); +} + +int APS5_VABI sceFiosOpWait(SceFiosOp op) { + return Fios::Wait(op); +} + +void APS5_VABI sceFiosOpDelete(SceFiosOp op) { + Fios::DeleteOp(op); +} + +int APS5_VABI sceFiosOpSyncWait(SceFiosOp op) { + Fios::Completion completion; + if (!Fios::SyncWait(op, completion)) return SCE_FIOS_ERROR_BAD_OP; + return completion.error != SCE_FIOS_OK ? completion.error : static_cast(completion.result); +} + +std::int64_t APS5_VABI sceFiosOpSyncWaitForIO(SceFiosOp op) { + Fios::Completion completion; + if (!Fios::SyncWait(op, completion)) return SCE_FIOS_ERROR_BAD_OP; + return completion.error != SCE_FIOS_OK ? completion.error : completion.result; +} + +int APS5_VABI sceFiosOpGetError(SceFiosOp op) { + std::int32_t error = SCE_FIOS_ERROR_BAD_OP; + Fios::OpError(op, error); + return error; +} + +void APS5_VABI sceFiosOpCancel(SceFiosOp op) { + Fios::CancelOp(op); +} + +bool APS5_VABI sceFiosOpIsCancelled(SceFiosOp op) { + return Fios::OpIsCancelled(op); +} + +const SceFiosOpAttr* APS5_VABI sceFiosOpGetAttr(SceFiosOp op) { + return Fios::OpAttr(op); +} + +const char* APS5_VABI sceFiosOpGetPath(SceFiosOp op) { + return Fios::OpPath(op); +} + +void* APS5_VABI sceFiosOpGetBuffer(SceFiosOp op) { + return Fios::OpBuffer(op); +} + +std::int64_t APS5_VABI sceFiosOpGetOffset(SceFiosOp op) { + std::int64_t request = 0, actual = 0, offset = 0; + return Fios::QueryOp(op, request, actual, offset) ? offset : SCE_FIOS_ERROR_BAD_OP; +} + +std::int64_t APS5_VABI sceFiosOpGetRequestCount(SceFiosOp op) { + std::int64_t request = 0, actual = 0, offset = 0; + return Fios::QueryOp(op, request, actual, offset) ? request : SCE_FIOS_ERROR_BAD_OP; +} + +std::int64_t APS5_VABI sceFiosOpGetActualCount(SceFiosOp op) { + std::int64_t request = 0, actual = 0, offset = 0; + return Fios::QueryOp(op, request, actual, offset) ? actual : SCE_FIOS_ERROR_BAD_OP; +} + +} diff --git a/core/libs/prx/libSceFios2/src/DirectoryHandles.cpp b/core/libs/prx/libSceFios2/src/DirectoryHandles.cpp new file mode 100644 index 0000000000..f35909ad40 --- /dev/null +++ b/core/libs/prx/libSceFios2/src/DirectoryHandles.cpp @@ -0,0 +1,183 @@ +#include "prx/libSceFios2/include/Api.hpp" +#include "Runtime.hpp" +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" +#include "prx/libkernel/include/Api.hpp" +#include "prx/libkernel/File/include/File.hpp" +#include "prx/libkernel/File/include/FileFlags.hpp" +#include +#include + +namespace { + +constexpr std::size_t DirentHeaderBytes = 8; +constexpr int DirentBufferBytes = 8192; +constexpr std::uint64_t ListingOverheadBytes = 16; + +std::int32_t ListNames(const std::string& path, std::vector& names) { + const int descriptor = sceKernelOpen(path.c_str(), SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); + if (descriptor < 0) return Fios::FromKernelError(descriptor); + std::vector buffer(DirentBufferBytes); + std::int32_t error = SCE_FIOS_OK; + for (;;) { + const int count = sceKernelGetdirentries(descriptor, buffer.data(), static_cast(buffer.size()), nullptr); + if (count < 0) { + error = Fios::FromKernelError(count); + break; + } + if (count == 0) break; + for (int offset = 0; offset + static_cast(DirentHeaderBytes) <= count;) { + std::uint16_t length = 0; + std::memcpy(&length, buffer.data() + offset + 4, sizeof(length)); + const auto nameLength = static_cast(buffer[static_cast(offset) + 7]); + if (length < DirentHeaderBytes) break; + std::string name(buffer.data() + offset + DirentHeaderBytes, nameLength); + if (name != "." && name != "..") names.push_back(std::move(name)); + offset += length; + } + } + sceKernelClose(descriptor); + return error; +} + +std::uint64_t RequiredBytes(const std::vector& names) { + std::uint64_t bytes = ListingOverheadBytes; + for (const std::string& name : names) bytes += name.size() + 1; + return bytes; +} + +SceFiosOp StartOpen(const SceFiosOpAttr* attr, SceFiosDH* outDH, const char* path, SceFiosBuffer buffer) { + Fios::OpInfo info; + info.buffer = buffer.pPtr; + info.request = static_cast(buffer.length); + const std::int32_t invalid = outDH == nullptr ? SCE_FIOS_ERROR_BAD_PTR : Fios::CopyPath(path, info.path); + const std::string target = info.path; + return Fios::Submit(attr, std::move(info), [=](Fios::Completion& completion) { + if (invalid != SCE_FIOS_OK) { + completion.error = invalid; + return; + } + SceFiosStat stat{}; + if (const std::int32_t error = Fios::StatPath(target, stat)) { + completion.error = error; + return; + } + if ((stat.statFlags & SCE_FIOS_STAT_DIRECTORY) == 0) { + completion.error = SCE_FIOS_ERROR_NOT_A_DIRECTORY; + return; + } + auto directory = std::make_shared(); + directory->path = target; + if (const std::int32_t error = ListNames(target, directory->names)) { + completion.error = error; + return; + } + const std::uint64_t required = RequiredBytes(directory->names); + completion.result = static_cast(required); + if (buffer.pPtr == nullptr || buffer.length < required) { + completion.error = SCE_FIOS_ERROR_BAD_SIZE; + return; + } + const SceFiosDH handle = Fios::AddDirectory(directory); + if (handle < 0) { + completion.error = handle; + return; + } + if (!Fios::CopyOut(outDH, &handle, sizeof(handle))) { + Fios::RemoveDirectory(handle); + completion.error = SCE_FIOS_ERROR_BAD_PTR; + } + }); +} + +SceFiosOp StartRead(const SceFiosOpAttr* attr, SceFiosDH handle, SceFiosDirEntry* outEntry) { + Fios::OpInfo info; + info.buffer = outEntry; + if (const auto directory = Fios::FindDirectory(handle)) info.path = directory->path; + return Fios::Submit(attr, std::move(info), [handle, outEntry](Fios::Completion& completion) { + const auto directory = Fios::FindDirectory(handle); + if (!directory) { + completion.error = SCE_FIOS_ERROR_BAD_DH; + return; + } + if (outEntry == nullptr) { + completion.error = SCE_FIOS_ERROR_BAD_PTR; + return; + } + const std::lock_guard lock(directory->mutex); + if (directory->next >= directory->names.size()) { + completion.error = SCE_FIOS_ERROR_EOF; + return; + } + std::string prefix = directory->path; + while (prefix.size() > 1 && prefix.back() == '/') prefix.pop_back(); + if (prefix.back() != '/') prefix += '/'; + const std::string& name = directory->names[directory->next]; + const std::string full = prefix + name; + if (full.size() >= SCE_FIOS_PATH_MAX) { + completion.error = SCE_FIOS_ERROR_PATH_TOO_LONG; + return; + } + SceFiosStat stat{}; + Fios::StatPath(full, stat); + SceFiosDirEntry entry{}; + entry.fileSize = stat.fileSize; + entry.statFlags = stat.statFlags; + entry.nameLength = static_cast(name.size()); + entry.fullPathLength = static_cast(full.size()); + entry.offsetToName = static_cast(prefix.size()); + std::memcpy(entry.fullPath, full.c_str(), full.size()); + if (!Fios::CopyOut(outEntry, &entry, sizeof(entry))) { + completion.error = SCE_FIOS_ERROR_BAD_PTR; + return; + } + ++directory->next; + }); +} + +SceFiosOp StartClose(const SceFiosOpAttr* attr, SceFiosDH handle) { + Fios::OpInfo info; + if (const auto directory = Fios::FindDirectory(handle)) info.path = directory->path; + return Fios::Submit(attr, std::move(info), [handle](Fios::Completion& completion) { + if (!Fios::RemoveDirectory(handle)) completion.error = SCE_FIOS_ERROR_BAD_DH; + }); +} + +int StatusSync(SceFiosOp op) { + return Fios::SyncStatus(op); +} + +} + +extern "C" { + +SceFiosOp APS5_VABI sceFiosDHOpen(const SceFiosOpAttr* attr, SceFiosDH* outDH, const char* path, SceFiosBuffer buffer) { + return StartOpen(attr, outDH, path, buffer); +} + +int APS5_VABI sceFiosDHOpenSync(const SceFiosOpAttr* attr, SceFiosDH* outDH, const char* path, SceFiosBuffer buffer) { + return StatusSync(StartOpen(attr, outDH, path, buffer)); +} + +SceFiosOp APS5_VABI sceFiosDHRead(const SceFiosOpAttr* attr, SceFiosDH dh, SceFiosDirEntry* outEntry) { + return StartRead(attr, dh, outEntry); +} + +int APS5_VABI sceFiosDHReadSync(const SceFiosOpAttr* attr, SceFiosDH dh, SceFiosDirEntry* outEntry) { + return StatusSync(StartRead(attr, dh, outEntry)); +} + +SceFiosOp APS5_VABI sceFiosDHClose(const SceFiosOpAttr* attr, SceFiosDH dh) { + return StartClose(attr, dh); +} + +int APS5_VABI sceFiosDHCloseSync(const SceFiosOpAttr* attr, SceFiosDH dh) { + return StatusSync(StartClose(attr, dh)); +} + +const char* APS5_VABI sceFiosDHGetPath(SceFiosDH dh) { + const auto directory = Fios::FindDirectory(dh); + return directory ? directory->path.c_str() : nullptr; +} + +} diff --git a/core/libs/prx/libSceFios2/src/FileHandles.cpp b/core/libs/prx/libSceFios2/src/FileHandles.cpp new file mode 100644 index 0000000000..2af25b2e28 --- /dev/null +++ b/core/libs/prx/libSceFios2/src/FileHandles.cpp @@ -0,0 +1,356 @@ +#include "prx/libSceFios2/include/Api.hpp" +#include "Runtime.hpp" +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" +#include "prx/libkernel/include/Api.hpp" +#include "prx/libkernel/File/include/File.hpp" +#include "prx/libkernel/File/include/FileFlags.hpp" +#include +#include + +namespace { + +constexpr std::int32_t DefaultNativeMode = -1; +constexpr std::uint16_t CreateMode = 0x1ff; +constexpr std::int64_t MaxVectors = 1024; + +using HandleWork = std::function; + +bool CanRead(const Fios::FileHandle& file) { + return (file.params.openFlags & SCE_FIOS_O_READ) != 0; +} + +bool CanWrite(const Fios::FileHandle& file) { + return (file.params.openFlags & SCE_FIOS_O_WRITE) != 0; +} + +SceFiosOp SubmitHandle(const SceFiosOpAttr* attr, SceFiosFH handle, std::int64_t request, std::int64_t offset, void* buffer, std::int32_t invalid, HandleWork work) { + Fios::OpInfo info; + info.request = request; + info.offset = offset; + info.buffer = buffer; + if (const auto file = Fios::FindFile(handle)) info.path = file->path; + return Fios::Submit(attr, std::move(info), [handle, invalid, work = std::move(work)](Fios::Completion& completion) { + if (invalid != SCE_FIOS_OK) { + completion.error = invalid; + return; + } + const auto file = Fios::FindFile(handle); + if (!file) { + completion.error = SCE_FIOS_ERROR_BAD_FH; + return; + } + const std::lock_guard lock(file->mutex); + if (file->descriptor < 0) { + completion.error = SCE_FIOS_ERROR_BAD_FH; + return; + } + work(*file, completion); + }); +} + +std::int32_t CheckRange(const void* buffer, SceFiosSize length, SceFiosOffset offset, bool positioned) { + if (length < 0) return SCE_FIOS_ERROR_BAD_SIZE; + if (positioned && offset < 0) return SCE_FIOS_ERROR_BAD_OFFSET; + if (buffer == nullptr && length != 0) return SCE_FIOS_ERROR_BAD_PTR; + return SCE_FIOS_OK; +} + +void Transfer(Fios::FileHandle& file, Fios::Completion& completion, void* buffer, SceFiosSize length, SceFiosOffset offset, bool positioned, bool write) { + if (write ? !CanWrite(file) : !CanRead(file)) { + completion.error = write ? SCE_FIOS_ERROR_READ_ONLY : SCE_FIOS_ERROR_WRITE_ONLY; + return; + } + const std::int64_t count = write ? Fios::WriteFully(file.descriptor, buffer, static_cast(length), offset, positioned) + : Fios::ReadFully(file.descriptor, buffer, static_cast(length), offset, positioned); + if (count < 0) completion.error = static_cast(count); + else completion.result = count; +} + +SceFiosOp StartTransfer(const SceFiosOpAttr* attr, SceFiosFH handle, void* buffer, SceFiosSize length, SceFiosOffset offset, bool positioned, bool write) { + const std::int32_t invalid = CheckRange(buffer, length, offset, positioned); + return SubmitHandle(attr, handle, length, positioned ? offset : 0, buffer, invalid, [=](Fios::FileHandle& file, Fios::Completion& completion) { + Transfer(file, completion, buffer, length, offset, positioned, write); + }); +} + +SceFiosOp StartVectors(const SceFiosOpAttr* attr, SceFiosFH handle, const SceFiosBuffer* iov, int iovcnt, SceFiosOffset offset, bool positioned, bool write) { + std::int32_t invalid = SCE_FIOS_OK; + std::vector vectors; + if (iovcnt <= 0 || iovcnt > MaxVectors) invalid = SCE_FIOS_ERROR_BAD_IOVCNT; + else if (iov == nullptr) invalid = SCE_FIOS_ERROR_BAD_PTR; + else if (positioned && offset < 0) invalid = SCE_FIOS_ERROR_BAD_OFFSET; + else { + vectors.resize(static_cast(iovcnt)); + if (!Fios::CopyIn(vectors.data(), iov, vectors.size() * sizeof(SceFiosBuffer))) invalid = SCE_FIOS_ERROR_BAD_PTR; + } + std::int64_t request = 0; + for (const SceFiosBuffer& vector : vectors) { + if (vector.pPtr == nullptr && vector.length != 0) invalid = SCE_FIOS_ERROR_BAD_PTR; + if (vector.length > static_cast(INT64_MAX) || request > INT64_MAX - static_cast(vector.length)) invalid = SCE_FIOS_ERROR_BAD_SIZE; + else request += static_cast(vector.length); + } + return SubmitHandle(attr, handle, request, positioned ? offset : 0, nullptr, invalid, [=](Fios::FileHandle& file, Fios::Completion& completion) { + if (write ? !CanWrite(file) : !CanRead(file)) { + completion.error = write ? SCE_FIOS_ERROR_READ_ONLY : SCE_FIOS_ERROR_WRITE_ONLY; + return; + } + std::int64_t position = offset; + for (const SceFiosBuffer& vector : vectors) { + const std::int64_t count = write ? Fios::WriteFully(file.descriptor, vector.pPtr, vector.length, position, positioned) + : Fios::ReadFully(file.descriptor, vector.pPtr, vector.length, position, positioned); + if (count < 0) { + if (completion.result == 0) completion.error = static_cast(count); + return; + } + completion.result += count; + position += count; + if (static_cast(count) < vector.length) return; + } + }); +} + +SceFiosOp StartOpen(const SceFiosOpAttr* attr, SceFiosFH* outFH, const char* path, const SceFiosOpenParams* openParams, std::int32_t nativeMode) { + Fios::OpInfo info; + std::int32_t invalid = outFH == nullptr ? SCE_FIOS_ERROR_BAD_PTR : Fios::CopyPath(path, info.path); + SceFiosOpenParams params{}; + if (openParams == nullptr) params.openFlags = SCE_FIOS_O_READ; + else if (!Fios::CopyIn(¶ms, openParams, sizeof(params)) && invalid == SCE_FIOS_OK) invalid = SCE_FIOS_ERROR_BAD_PTR; + const std::string target = info.path; + return Fios::Submit(attr, std::move(info), [=](Fios::Completion& completion) { + if (invalid != SCE_FIOS_OK) { + completion.error = invalid; + return; + } + const std::uint32_t flags = params.openFlags; + std::int32_t native = (flags & (SCE_FIOS_O_READ | SCE_FIOS_O_WRITE)) == (SCE_FIOS_O_READ | SCE_FIOS_O_WRITE) ? SCE_KERNEL_O_RDWR + : (flags & SCE_FIOS_O_WRITE) != 0 ? SCE_KERNEL_O_WRONLY : SCE_KERNEL_O_RDONLY; + if ((flags & SCE_FIOS_O_APPEND) != 0) native |= SCE_KERNEL_O_APPEND; + if ((flags & SCE_FIOS_O_CREAT) != 0) native |= SCE_KERNEL_O_CREAT; + if ((flags & SCE_FIOS_O_TRUNC) != 0) native |= SCE_KERNEL_O_TRUNC; + const std::uint16_t mode = nativeMode == DefaultNativeMode ? CreateMode : static_cast(nativeMode); + const int descriptor = sceKernelOpen(target.c_str(), native, (flags & SCE_FIOS_O_CREAT) != 0 ? mode : 0); + if (descriptor < 0) { + completion.error = Fios::FromKernelError(descriptor); + return; + } + SceFiosStat stat{}; + if (const std::int32_t error = Fios::StatDescriptor(descriptor, stat); error != SCE_FIOS_OK || (stat.statFlags & SCE_FIOS_STAT_DIRECTORY) != 0) { + sceKernelClose(descriptor); + completion.error = error != SCE_FIOS_OK ? error : SCE_FIOS_ERROR_NOT_A_FILE; + return; + } + auto file = std::make_shared(); + file->descriptor = descriptor; + file->path = target; + file->params = params; + if ((file->params.openFlags & (SCE_FIOS_O_READ | SCE_FIOS_O_WRITE)) == 0) file->params.openFlags |= SCE_FIOS_O_READ; + const SceFiosFH handle = Fios::AddFile(file); + if (handle < 0) { + sceKernelClose(descriptor); + completion.error = handle; + return; + } + if (!Fios::CopyOut(outFH, &handle, sizeof(handle))) { + Fios::RemoveFile(handle); + sceKernelClose(descriptor); + completion.error = SCE_FIOS_ERROR_BAD_PTR; + } + }); +} + +SceFiosOp StartClose(const SceFiosOpAttr* attr, SceFiosFH handle) { + Fios::OpInfo info; + if (const auto file = Fios::FindFile(handle)) info.path = file->path; + return Fios::Submit(attr, std::move(info), [handle](Fios::Completion& completion) { + const auto file = Fios::RemoveFile(handle); + if (!file) { + completion.error = SCE_FIOS_ERROR_BAD_FH; + return; + } + const std::lock_guard lock(file->mutex); + completion.error = Fios::FromKernelError(sceKernelClose(file->descriptor)); + file->descriptor = -1; + }); +} + +SceFiosOp StartStat(const SceFiosOpAttr* attr, SceFiosFH handle, SceFiosStat* outStatus) { + return SubmitHandle(attr, handle, 0, 0, nullptr, SCE_FIOS_OK, [outStatus](Fios::FileHandle& file, Fios::Completion& completion) { + SceFiosStat stat{}; + if (const std::int32_t error = Fios::StatDescriptor(file.descriptor, stat)) completion.error = error; + else if (outStatus == nullptr || !Fios::CopyOut(outStatus, &stat, sizeof(stat))) completion.error = SCE_FIOS_ERROR_BAD_PTR; + }); +} + +SceFiosOp StartTruncate(const SceFiosOpAttr* attr, SceFiosFH handle, SceFiosSize length) { + return SubmitHandle(attr, handle, length, 0, nullptr, length < 0 ? SCE_FIOS_ERROR_BAD_SIZE : SCE_FIOS_OK, [length](Fios::FileHandle& file, Fios::Completion& completion) { + if (!CanWrite(file)) completion.error = SCE_FIOS_ERROR_READ_ONLY; + else completion.error = Fios::FromKernelError(sceKernelFtruncate(file.descriptor, length)); + }); +} + +SceFiosOp StartSync(const SceFiosOpAttr* attr, SceFiosFH handle) { + return SubmitHandle(attr, handle, 0, 0, nullptr, SCE_FIOS_OK, [](Fios::FileHandle& file, Fios::Completion& completion) { + completion.error = Fios::FromKernelError(sceKernelFsync(file.descriptor)); + }); +} + +int StatusSync(SceFiosOp op) { + return Fios::SyncStatus(op); +} + +std::int64_t CountSync(SceFiosOp op) { + return Fios::SyncCount(op); +} + +} + +extern "C" { + +SceFiosOp APS5_VABI sceFiosFHOpen(const SceFiosOpAttr* attr, SceFiosFH* outFH, const char* path, const SceFiosOpenParams* openParams) { + return StartOpen(attr, outFH, path, openParams, DefaultNativeMode); +} + +int APS5_VABI sceFiosFHOpenSync(const SceFiosOpAttr* attr, SceFiosFH* outFH, const char* path, const SceFiosOpenParams* openParams) { + return StatusSync(StartOpen(attr, outFH, path, openParams, DefaultNativeMode)); +} + +SceFiosOp APS5_VABI sceFiosFHOpenWithMode(const SceFiosOpAttr* attr, SceFiosFH* outFH, const char* path, const SceFiosOpenParams* openParams, std::int32_t nativeMode) { + return StartOpen(attr, outFH, path, openParams, nativeMode); +} + +int APS5_VABI sceFiosFHOpenWithModeSync(const SceFiosOpAttr* attr, SceFiosFH* outFH, const char* path, const SceFiosOpenParams* openParams, std::int32_t nativeMode) { + return StatusSync(StartOpen(attr, outFH, path, openParams, nativeMode)); +} + +SceFiosOp APS5_VABI sceFiosFHClose(const SceFiosOpAttr* attr, SceFiosFH fh) { + return StartClose(attr, fh); +} + +int APS5_VABI sceFiosFHCloseSync(const SceFiosOpAttr* attr, SceFiosFH fh) { + return StatusSync(StartClose(attr, fh)); +} + +SceFiosOp APS5_VABI sceFiosFHStat(const SceFiosOpAttr* attr, SceFiosFH fh, SceFiosStat* outStatus) { + return StartStat(attr, fh, outStatus); +} + +int APS5_VABI sceFiosFHStatSync(const SceFiosOpAttr* attr, SceFiosFH fh, SceFiosStat* outStatus) { + return StatusSync(StartStat(attr, fh, outStatus)); +} + +SceFiosOp APS5_VABI sceFiosFHTruncate(const SceFiosOpAttr* attr, SceFiosFH fh, SceFiosSize length) { + return StartTruncate(attr, fh, length); +} + +int APS5_VABI sceFiosFHTruncateSync(const SceFiosOpAttr* attr, SceFiosFH fh, SceFiosSize length) { + return StatusSync(StartTruncate(attr, fh, length)); +} + +SceFiosOp APS5_VABI sceFiosFHSync(const SceFiosOpAttr* attr, SceFiosFH fh) { + return StartSync(attr, fh); +} + +int APS5_VABI sceFiosFHSyncSync(const SceFiosOpAttr* attr, SceFiosFH fh) { + return StatusSync(StartSync(attr, fh)); +} + +SceFiosOp APS5_VABI sceFiosFHRead(const SceFiosOpAttr* attr, SceFiosFH fh, void* buffer, SceFiosSize length) { + return StartTransfer(attr, fh, buffer, length, 0, false, false); +} + +SceFiosSize APS5_VABI sceFiosFHReadSync(const SceFiosOpAttr* attr, SceFiosFH fh, void* buffer, SceFiosSize length) { + return CountSync(StartTransfer(attr, fh, buffer, length, 0, false, false)); +} + +SceFiosOp APS5_VABI sceFiosFHWrite(const SceFiosOpAttr* attr, SceFiosFH fh, const void* buffer, SceFiosSize length) { + return StartTransfer(attr, fh, const_cast(buffer), length, 0, false, true); +} + +SceFiosSize APS5_VABI sceFiosFHWriteSync(const SceFiosOpAttr* attr, SceFiosFH fh, const void* buffer, SceFiosSize length) { + return CountSync(StartTransfer(attr, fh, const_cast(buffer), length, 0, false, true)); +} + +SceFiosOp APS5_VABI sceFiosFHPread(const SceFiosOpAttr* attr, SceFiosFH fh, void* buffer, SceFiosSize length, SceFiosOffset offset) { + return StartTransfer(attr, fh, buffer, length, offset, true, false); +} + +SceFiosSize APS5_VABI sceFiosFHPreadSync(const SceFiosOpAttr* attr, SceFiosFH fh, void* buffer, SceFiosSize length, SceFiosOffset offset) { + return CountSync(StartTransfer(attr, fh, buffer, length, offset, true, false)); +} + +SceFiosOp APS5_VABI sceFiosFHPwrite(const SceFiosOpAttr* attr, SceFiosFH fh, const void* buffer, SceFiosSize length, SceFiosOffset offset) { + return StartTransfer(attr, fh, const_cast(buffer), length, offset, true, true); +} + +SceFiosSize APS5_VABI sceFiosFHPwriteSync(const SceFiosOpAttr* attr, SceFiosFH fh, const void* buffer, SceFiosSize length, SceFiosOffset offset) { + return CountSync(StartTransfer(attr, fh, const_cast(buffer), length, offset, true, true)); +} + +SceFiosOp APS5_VABI sceFiosFHReadv(const SceFiosOpAttr* attr, SceFiosFH fh, const SceFiosBuffer* iov, int iovcnt) { + return StartVectors(attr, fh, iov, iovcnt, 0, false, false); +} + +SceFiosSize APS5_VABI sceFiosFHReadvSync(const SceFiosOpAttr* attr, SceFiosFH fh, const SceFiosBuffer* iov, int iovcnt) { + return CountSync(StartVectors(attr, fh, iov, iovcnt, 0, false, false)); +} + +SceFiosOp APS5_VABI sceFiosFHWritev(const SceFiosOpAttr* attr, SceFiosFH fh, const SceFiosBuffer* iov, int iovcnt) { + return StartVectors(attr, fh, iov, iovcnt, 0, false, true); +} + +SceFiosSize APS5_VABI sceFiosFHWritevSync(const SceFiosOpAttr* attr, SceFiosFH fh, const SceFiosBuffer* iov, int iovcnt) { + return CountSync(StartVectors(attr, fh, iov, iovcnt, 0, false, true)); +} + +SceFiosOp APS5_VABI sceFiosFHPreadv(const SceFiosOpAttr* attr, SceFiosFH fh, const SceFiosBuffer* iov, int iovcnt, SceFiosOffset offset) { + return StartVectors(attr, fh, iov, iovcnt, offset, true, false); +} + +SceFiosSize APS5_VABI sceFiosFHPreadvSync(const SceFiosOpAttr* attr, SceFiosFH fh, const SceFiosBuffer* iov, int iovcnt, SceFiosOffset offset) { + return CountSync(StartVectors(attr, fh, iov, iovcnt, offset, true, false)); +} + +SceFiosOp APS5_VABI sceFiosFHPwritev(const SceFiosOpAttr* attr, SceFiosFH fh, const SceFiosBuffer* iov, int iovcnt, SceFiosOffset offset) { + return StartVectors(attr, fh, iov, iovcnt, offset, true, true); +} + +SceFiosSize APS5_VABI sceFiosFHPwritevSync(const SceFiosOpAttr* attr, SceFiosFH fh, const SceFiosBuffer* iov, int iovcnt, SceFiosOffset offset) { + return CountSync(StartVectors(attr, fh, iov, iovcnt, offset, true, true)); +} + +SceFiosOffset APS5_VABI sceFiosFHSeek(SceFiosFH fh, SceFiosOffset offset, std::int32_t whence) { + if (whence < SCE_FIOS_SEEK_SET || whence > SCE_FIOS_SEEK_END) return SCE_FIOS_ERROR_BAD_OFFSET; + const auto file = Fios::FindFile(fh); + if (!file) return SCE_FIOS_ERROR_BAD_FH; + const std::lock_guard lock(file->mutex); + if (file->descriptor < 0) return SCE_FIOS_ERROR_BAD_FH; + const std::int64_t position = sceKernelLseek(file->descriptor, offset, whence); + return position < 0 ? Fios::FromKernelError(position) : position; +} + +SceFiosOffset APS5_VABI sceFiosFHTell(SceFiosFH fh) { + return sceFiosFHSeek(fh, 0, SCE_FIOS_SEEK_CUR); +} + +SceFiosSize APS5_VABI sceFiosFHGetSize(SceFiosFH fh) { + const auto file = Fios::FindFile(fh); + if (!file) return SCE_FIOS_ERROR_BAD_FH; + const std::lock_guard lock(file->mutex); + SceFiosStat stat{}; + if (file->descriptor < 0) return SCE_FIOS_ERROR_BAD_FH; + if (const std::int32_t error = Fios::StatDescriptor(file->descriptor, stat)) return error; + return stat.fileSize; +} + +const char* APS5_VABI sceFiosFHGetPath(SceFiosFH fh) { + const auto file = Fios::FindFile(fh); + return file ? file->path.c_str() : nullptr; +} + +const SceFiosOpenParams* APS5_VABI sceFiosFHGetOpenParams(SceFiosFH fh) { + const auto file = Fios::FindFile(fh); + return file ? &file->params : nullptr; +} + +} diff --git a/core/libs/prx/libSceFios2/src/PathOperations.cpp b/core/libs/prx/libSceFios2/src/PathOperations.cpp new file mode 100644 index 0000000000..2f00137178 --- /dev/null +++ b/core/libs/prx/libSceFios2/src/PathOperations.cpp @@ -0,0 +1,214 @@ +#include "prx/libSceFios2/include/Api.hpp" +#include "Runtime.hpp" +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" +#include "prx/libkernel/include/Api.hpp" +#include "prx/libkernel/File/include/File.hpp" +#include "prx/libkernel/File/include/FileFlags.hpp" + +namespace { + +constexpr std::uint16_t DefaultFileMode = 0666; +constexpr std::uint16_t DefaultDirectoryMode = 0777; + +using PathWork = std::function; + +SceFiosOp SubmitPath(const SceFiosOpAttr* attr, const char* path, std::int64_t request, std::int64_t offset, void* buffer, PathWork work) { + Fios::OpInfo info; + info.request = request; + info.offset = offset; + info.buffer = buffer; + if (const std::int32_t error = Fios::CopyPath(path, info.path)) return Fios::SubmitFailed(attr, std::move(info), error); + const std::string target = info.path; + return Fios::Submit(attr, std::move(info), [target, work = std::move(work)](Fios::Completion& completion) { work(target, completion); }); +} + +int StatusSync(SceFiosOp op) { + return Fios::SyncStatus(op); +} + +std::int64_t CountSync(SceFiosOp op) { + return Fios::SyncCount(op); +} + +bool IsDirectory(const std::string& path, std::int32_t& error) { + SceFiosStat stat{}; + error = Fios::StatPath(path, stat); + return error == SCE_FIOS_OK && (stat.statFlags & SCE_FIOS_STAT_DIRECTORY) != 0; +} + +void RemoveFile(const std::string& path, Fios::Completion& completion) { + std::int32_t error = SCE_FIOS_OK; + if (IsDirectory(path, error)) completion.error = SCE_FIOS_ERROR_NOT_A_FILE; + else if (error != SCE_FIOS_OK) completion.error = error; + else completion.error = Fios::FromKernelError(sceKernelUnlink(path.c_str())); +} + +void RemoveDirectoryAt(const std::string& path, Fios::Completion& completion) { + std::int32_t error = SCE_FIOS_OK; + if (!IsDirectory(path, error)) completion.error = error != SCE_FIOS_OK ? error : SCE_FIOS_ERROR_NOT_A_DIRECTORY; + else completion.error = Fios::FromKernelError(sceKernelRmdir(path.c_str())); +} + +SceFiosOp StartDelete(const SceFiosOpAttr* attr, const char* path) { + return SubmitPath(attr, path, 0, 0, nullptr, [](const std::string& target, Fios::Completion& completion) { + std::int32_t error = SCE_FIOS_OK; + if (IsDirectory(target, error)) RemoveDirectoryAt(target, completion); + else if (error != SCE_FIOS_OK) completion.error = error; + else RemoveFile(target, completion); + }); +} + +SceFiosOp StartFileDelete(const SceFiosOpAttr* attr, const char* path) { + return SubmitPath(attr, path, 0, 0, nullptr, RemoveFile); +} + +SceFiosOp StartDirectoryCreate(const SceFiosOpAttr* attr, const char* path) { + return SubmitPath(attr, path, 0, 0, nullptr, [](const std::string& target, Fios::Completion& completion) { + completion.error = Fios::FromKernelError(sceKernelMkdir(target.c_str(), DefaultDirectoryMode)); + }); +} + +SceFiosOp StartDirectoryDelete(const SceFiosOpAttr* attr, const char* path) { + return SubmitPath(attr, path, 0, 0, nullptr, RemoveDirectoryAt); +} + +SceFiosOp StartRename(const SceFiosOpAttr* attr, const char* oldPath, const char* newPath) { + std::string destination; + if (const std::int32_t error = Fios::CopyPath(newPath, destination)) { + return SubmitPath(attr, oldPath, 0, 0, nullptr, [error](const std::string&, Fios::Completion& completion) { completion.error = error; }); + } + return SubmitPath(attr, oldPath, 0, 0, nullptr, [destination](const std::string& source, Fios::Completion& completion) { + completion.error = Fios::FromKernelError(sceKernelRename(source.c_str(), destination.c_str())); + }); +} + +SceFiosOp StartFileRead(const SceFiosOpAttr* attr, const char* path, void* buffer, SceFiosSize length, SceFiosOffset offset) { + std::int32_t invalid = SCE_FIOS_OK; + if (length < 0) invalid = SCE_FIOS_ERROR_BAD_SIZE; + else if (offset < 0) invalid = SCE_FIOS_ERROR_BAD_OFFSET; + else if (buffer == nullptr && length != 0) invalid = SCE_FIOS_ERROR_BAD_PTR; + return SubmitPath(attr, path, length, offset, buffer, [=](const std::string& target, Fios::Completion& completion) { + if (invalid != SCE_FIOS_OK) { + completion.error = invalid; + return; + } + const int descriptor = sceKernelOpen(target.c_str(), SCE_KERNEL_O_RDONLY, 0); + if (descriptor < 0) { + completion.error = Fios::FromKernelError(descriptor); + return; + } + const std::int64_t count = Fios::ReadFully(descriptor, buffer, static_cast(length), offset, true); + sceKernelClose(descriptor); + if (count < 0) completion.error = static_cast(count); + else completion.result = count; + }); +} + +SceFiosOp StartFileWrite(const SceFiosOpAttr* attr, const char* path, const void* buffer, SceFiosSize length, SceFiosOffset offset) { + std::int32_t invalid = SCE_FIOS_OK; + if (length < 0) invalid = SCE_FIOS_ERROR_BAD_SIZE; + else if (offset < 0) invalid = SCE_FIOS_ERROR_BAD_OFFSET; + else if (buffer == nullptr && length != 0) invalid = SCE_FIOS_ERROR_BAD_PTR; + return SubmitPath(attr, path, length, offset, const_cast(buffer), [=](const std::string& target, Fios::Completion& completion) { + if (invalid != SCE_FIOS_OK) { + completion.error = invalid; + return; + } + const int descriptor = sceKernelOpen(target.c_str(), SCE_KERNEL_O_WRONLY | SCE_KERNEL_O_CREAT, DefaultFileMode); + if (descriptor < 0) { + completion.error = Fios::FromKernelError(descriptor); + return; + } + const std::int64_t count = Fios::WriteFully(descriptor, buffer, static_cast(length), offset, true); + sceKernelClose(descriptor); + if (count < 0) completion.error = static_cast(count); + else completion.result = count; + }); +} + +SceFiosOp StartFileTruncate(const SceFiosOpAttr* attr, const char* path, SceFiosSize length) { + return SubmitPath(attr, path, length, 0, nullptr, [length](const std::string& target, Fios::Completion& completion) { + if (length < 0) { + completion.error = SCE_FIOS_ERROR_BAD_SIZE; + return; + } + const int descriptor = sceKernelOpen(target.c_str(), SCE_KERNEL_O_WRONLY, 0); + if (descriptor < 0) { + completion.error = Fios::FromKernelError(descriptor); + return; + } + completion.error = Fios::FromKernelError(sceKernelFtruncate(descriptor, length)); + sceKernelClose(descriptor); + }); +} + +} + +extern "C" { + +SceFiosOp APS5_VABI sceFiosDelete(const SceFiosOpAttr* attr, const char* path) { + return StartDelete(attr, path); +} + +int APS5_VABI sceFiosDeleteSync(const SceFiosOpAttr* attr, const char* path) { + return StatusSync(StartDelete(attr, path)); +} + +SceFiosOp APS5_VABI sceFiosFileDelete(const SceFiosOpAttr* attr, const char* path) { + return StartFileDelete(attr, path); +} + +int APS5_VABI sceFiosFileDeleteSync(const SceFiosOpAttr* attr, const char* path) { + return StatusSync(StartFileDelete(attr, path)); +} + +SceFiosOp APS5_VABI sceFiosDirectoryCreate(const SceFiosOpAttr* attr, const char* path) { + return StartDirectoryCreate(attr, path); +} + +int APS5_VABI sceFiosDirectoryCreateSync(const SceFiosOpAttr* attr, const char* path) { + return StatusSync(StartDirectoryCreate(attr, path)); +} + +SceFiosOp APS5_VABI sceFiosDirectoryDelete(const SceFiosOpAttr* attr, const char* path) { + return StartDirectoryDelete(attr, path); +} + +int APS5_VABI sceFiosDirectoryDeleteSync(const SceFiosOpAttr* attr, const char* path) { + return StatusSync(StartDirectoryDelete(attr, path)); +} + +SceFiosOp APS5_VABI sceFiosRename(const SceFiosOpAttr* attr, const char* oldPath, const char* newPath) { + return StartRename(attr, oldPath, newPath); +} + +int APS5_VABI sceFiosRenameSync(const SceFiosOpAttr* attr, const char* oldPath, const char* newPath) { + return StatusSync(StartRename(attr, oldPath, newPath)); +} + +SceFiosOp APS5_VABI sceFiosFileRead(const SceFiosOpAttr* attr, const char* path, void* buffer, SceFiosSize length, SceFiosOffset offset) { + return StartFileRead(attr, path, buffer, length, offset); +} + +SceFiosSize APS5_VABI sceFiosFileReadSync(const SceFiosOpAttr* attr, const char* path, void* buffer, SceFiosSize length, SceFiosOffset offset) { + return CountSync(StartFileRead(attr, path, buffer, length, offset)); +} + +SceFiosOp APS5_VABI sceFiosFileWrite(const SceFiosOpAttr* attr, const char* path, const void* buffer, SceFiosSize length, SceFiosOffset offset) { + return StartFileWrite(attr, path, buffer, length, offset); +} + +SceFiosSize APS5_VABI sceFiosFileWriteSync(const SceFiosOpAttr* attr, const char* path, const void* buffer, SceFiosSize length, SceFiosOffset offset) { + return CountSync(StartFileWrite(attr, path, buffer, length, offset)); +} + +SceFiosOp APS5_VABI sceFiosFileTruncate(const SceFiosOpAttr* attr, const char* path, SceFiosSize length) { + return StartFileTruncate(attr, path, length); +} + +int APS5_VABI sceFiosFileTruncateSync(const SceFiosOpAttr* attr, const char* path, SceFiosSize length) { + return StatusSync(StartFileTruncate(attr, path, length)); +} + +} diff --git a/core/libs/prx/libSceFios2/src/PathQueries.cpp b/core/libs/prx/libSceFios2/src/PathQueries.cpp new file mode 100644 index 0000000000..a5a1ff59e2 --- /dev/null +++ b/core/libs/prx/libSceFios2/src/PathQueries.cpp @@ -0,0 +1,108 @@ +#include "prx/libSceFios2/include/Api.hpp" +#include "Runtime.hpp" +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +namespace { + +enum class Kind { Any, File, Directory }; + +SceFiosOp QueryExists(const SceFiosOpAttr* attr, const char* path, bool* outExists, Kind kind) { + Fios::OpInfo info; + if (const std::int32_t error = Fios::CopyPath(path, info.path)) return Fios::SubmitFailed(attr, std::move(info), error); + const std::string target = info.path; + return Fios::Submit(attr, std::move(info), [target, outExists, kind](Fios::Completion& completion) { + SceFiosStat stat{}; + const std::int32_t error = Fios::StatPath(target, stat); + bool exists = false; + if (error == SCE_FIOS_OK) { + const bool directory = (stat.statFlags & SCE_FIOS_STAT_DIRECTORY) != 0; + exists = kind == Kind::Any || (kind == Kind::Directory) == directory; + } else if (error != SCE_FIOS_ERROR_BAD_PATH) { + completion.error = error; + return; + } + completion.result = exists ? 1 : 0; + if (outExists != nullptr && !Fios::CopyOut(outExists, &exists, sizeof(exists))) completion.error = SCE_FIOS_ERROR_BAD_PTR; + }); +} + +bool ExistsSync(SceFiosOp op) { + return op >= 0 && Fios::SyncCount(op) == 1; +} + +} + +extern "C" { + +SceFiosOp APS5_VABI sceFiosExists(const SceFiosOpAttr* attr, const char* path, bool* outExists) { + return QueryExists(attr, path, outExists, Kind::Any); +} + +bool APS5_VABI sceFiosExistsSync(const SceFiosOpAttr* attr, const char* path) { + return ExistsSync(QueryExists(attr, path, nullptr, Kind::Any)); +} + +SceFiosOp APS5_VABI sceFiosFileExists(const SceFiosOpAttr* attr, const char* path, bool* outExists) { + return QueryExists(attr, path, outExists, Kind::File); +} + +bool APS5_VABI sceFiosFileExistsSync(const SceFiosOpAttr* attr, const char* path) { + return ExistsSync(QueryExists(attr, path, nullptr, Kind::File)); +} + +SceFiosOp APS5_VABI sceFiosDirectoryExists(const SceFiosOpAttr* attr, const char* path, bool* outExists) { + return QueryExists(attr, path, outExists, Kind::Directory); +} + +bool APS5_VABI sceFiosDirectoryExistsSync(const SceFiosOpAttr* attr, const char* path) { + return ExistsSync(QueryExists(attr, path, nullptr, Kind::Directory)); +} + +static SceFiosOp QuerySize(const SceFiosOpAttr* attr, const char* path) { + Fios::OpInfo info; + if (const std::int32_t error = Fios::CopyPath(path, info.path)) return Fios::SubmitFailed(attr, std::move(info), error); + const std::string target = info.path; + return Fios::Submit(attr, std::move(info), [target](Fios::Completion& completion) { + SceFiosStat stat{}; + if (const std::int32_t error = Fios::StatPath(target, stat)) { + completion.error = error; + } else if ((stat.statFlags & SCE_FIOS_STAT_DIRECTORY) != 0) { + completion.error = SCE_FIOS_ERROR_NOT_A_FILE; + } else { + completion.result = stat.fileSize; + } + }); +} + +SceFiosOp APS5_VABI sceFiosFileGetSize(const SceFiosOpAttr* attr, const char* path) { + return QuerySize(attr, path); +} + +SceFiosSize APS5_VABI sceFiosFileGetSizeSync(const SceFiosOpAttr* attr, const char* path) { + return Fios::SyncCount(QuerySize(attr, path)); +} + +static SceFiosOp QueryStat(const SceFiosOpAttr* attr, const char* path, SceFiosStat* outStatus) { + Fios::OpInfo info; + if (const std::int32_t error = Fios::CopyPath(path, info.path)) return Fios::SubmitFailed(attr, std::move(info), error); + const std::string target = info.path; + return Fios::Submit(attr, std::move(info), [target, outStatus](Fios::Completion& completion) { + SceFiosStat stat{}; + if (const std::int32_t error = Fios::StatPath(target, stat)) { + completion.error = error; + } else if (outStatus == nullptr || !Fios::CopyOut(outStatus, &stat, sizeof(stat))) { + completion.error = SCE_FIOS_ERROR_BAD_PTR; + } + }); +} + +SceFiosOp APS5_VABI sceFiosStat(const SceFiosOpAttr* attr, const char* path, SceFiosStat* outStatus) { + return QueryStat(attr, path, outStatus); +} + +int APS5_VABI sceFiosStatSync(const SceFiosOpAttr* attr, const char* path, SceFiosStat* outStatus) { + return Fios::SyncStatus(QueryStat(attr, path, outStatus)); +} + +} diff --git a/core/libs/prx/libSceFios2/src/Runtime.cpp b/core/libs/prx/libSceFios2/src/Runtime.cpp new file mode 100644 index 0000000000..70c2ff6c66 --- /dev/null +++ b/core/libs/prx/libSceFios2/src/Runtime.cpp @@ -0,0 +1,597 @@ +#include "Runtime.hpp" +#include "prx/libc/include/General.hpp" +#include "prx/libkernel/include/Api.hpp" +#include "prx/libkernel/File/include/File.hpp" +#include +#include +#include +#include +#include +#include + +namespace Fios { +namespace { + +constexpr std::size_t MaxStoredParameters = 512; +constexpr std::uint64_t IoChunk = 1ull << 30; + +struct Op { + SceFiosOp id = 0; + SceFiosOpAttr attr{}; + std::string path; + std::int64_t offset = 0; + std::int64_t request = 0; + void* buffer = nullptr; + Work work; + bool done = false; + bool queued = false; + bool cancelled = false; + std::int32_t error = SCE_FIOS_OK; + std::int64_t result = 0; +}; + +struct State { + std::mutex mutex; + std::condition_variable finished; + std::map> ops; + std::map> files; + std::map> directories; + std::deque pending; + SceFiosHandle lastHandle = 0; + bool initialized = false; + std::vector parameters; + std::uint32_t suspendCount = 0; + std::optional globalAttr; +}; + +State& Global() { + static State state; + return state; +} + +thread_local std::optional g_threadAttr; + +bool HandleInUse(const State& state, SceFiosHandle handle) { + return state.ops.count(handle) != 0 || state.files.count(handle) != 0 || state.directories.count(handle) != 0; +} + +SceFiosHandle NextHandle(State& state) { + for (int attempts = 0; attempts < 0x7fffffff; ++attempts) { + state.lastHandle = state.lastHandle >= 0x7ffffffe ? 1 : state.lastHandle + 1; + if (!HandleInUse(state, state.lastHandle)) return state.lastHandle; + } + return 0; +} + +void Finish(const std::shared_ptr& op, const Completion& completion) { + SceFiosOpCallback callback = nullptr; + void* context = nullptr; + { + const std::lock_guard lock(Global().mutex); + op->error = completion.error; + op->result = completion.result; + op->done = true; + op->queued = false; + op->work = nullptr; + callback = op->attr.pCallback; + context = op->attr.pCallbackContext; + Global().finished.notify_all(); + } + if (callback != nullptr) callback(context, op->id, SCE_FIOS_OPEVENT_COMPLETE, completion.error); +} + +void Run(const std::shared_ptr& op) { + Work work; + bool cancelled = false; + { + const std::lock_guard lock(Global().mutex); + work = std::move(op->work); + cancelled = op->cancelled; + } + Completion completion; + if (cancelled || !work) { + completion.error = SCE_FIOS_ERROR_CANCELLED; + Finish(op, completion); + return; + } + try { + work(completion); + } catch (...) { + completion.error = SCE_FIOS_ERROR_UNKNOWN; + completion.result = 0; + Finish(op, completion); + throw; + } + Finish(op, completion); +} + +std::shared_ptr FindOp(State& state, SceFiosOp id) { + const auto found = state.ops.find(id); + return found == state.ops.end() ? nullptr : found->second; +} + +void FireDelete(const std::shared_ptr& op) { + if (op->attr.pCallback != nullptr) op->attr.pCallback(op->attr.pCallbackContext, op->id, SCE_FIOS_OPEVENT_DELETE, SCE_FIOS_OK); +} + +bool CancelQueued(const std::shared_ptr& op) { + { + const std::lock_guard lock(Global().mutex); + if (op->done || !op->queued) return false; + auto& pending = Global().pending; + pending.erase(std::remove(pending.begin(), pending.end(), op->id), pending.end()); + op->cancelled = true; + } + Completion completion; + completion.error = SCE_FIOS_ERROR_CANCELLED; + Finish(op, completion); + return true; +} + +} + +std::int32_t FromKernelError(std::int64_t kernelResult) { + if (kernelResult >= 0) return SCE_FIOS_OK; + const auto code = static_cast(kernelResult); + if ((code & 0xffff0000u) != 0x80020000u) return SCE_FIOS_ERROR_UNKNOWN; + switch (code & 0xffffu) { + case 1: case 13: case 66: return SCE_FIOS_ERROR_ACCESS; + case 2: case 20: return SCE_FIOS_ERROR_BAD_PATH; + case 9: return SCE_FIOS_ERROR_BAD_FH; + case 14: return SCE_FIOS_ERROR_BAD_PTR; + case 17: return SCE_FIOS_ERROR_ALREADY_EXISTS; + case 21: return SCE_FIOS_ERROR_NOT_A_FILE; + case 22: return SCE_FIOS_ERROR_BAD_OFFSET; + case 23: case 24: return SCE_FIOS_ERROR_CANT_ALLOCATE_FH; + case 30: return SCE_FIOS_ERROR_READ_ONLY; + case 63: return SCE_FIOS_ERROR_PATH_TOO_LONG; + default: return SCE_FIOS_ERROR_UNKNOWN; + } +} + +std::int32_t CopyPath(const char* source, std::string& destination) { + if (source == nullptr) return SCE_FIOS_ERROR_BAD_PTR; + switch (CopyGuestString_nid_no_patch(source, SCE_FIOS_PATH_MAX, destination)) { + case 0: break; + case 14: return SCE_FIOS_ERROR_BAD_PTR; + case 63: return SCE_FIOS_ERROR_PATH_TOO_LONG; + default: return SCE_FIOS_ERROR_UNKNOWN; + } + return destination.empty() ? SCE_FIOS_ERROR_BAD_PATH : SCE_FIOS_OK; +} + +bool CopyOut(void* destination, const void* source, std::size_t bytes) { + return GuestCopyOut_nid_no_patch(destination, source, bytes); +} + +bool CopyIn(void* destination, const void* source, std::size_t bytes) { + return GuestCopyIn_nid_no_patch(destination, source, bytes); +} + +std::uint64_t TimespecToDate(std::int64_t seconds, std::int64_t nanoseconds) { + if (seconds < 0) return 0; + return static_cast(seconds) * 1000000000ull + static_cast(nanoseconds); +} + +SceFiosOp Submit(const SceFiosOpAttr* attr, OpInfo info, Work work) { + auto op = std::make_shared(); + bool runNow = false; + SceFiosOpAttr given{}; + if (attr != nullptr && !CopyIn(&given, attr, sizeof(given))) return SCE_FIOS_ERROR_BAD_PTR; + { + const std::lock_guard lock(Global().mutex); + State& state = Global(); + const SceFiosHandle id = NextHandle(state); + if (id == 0) return SCE_FIOS_ERROR_CANT_ALLOCATE_OP; + if (attr != nullptr) op->attr = given; + else if (g_threadAttr) op->attr = *g_threadAttr; + else if (state.globalAttr) op->attr = *state.globalAttr; + op->id = id; + op->path = std::move(info.path); + op->offset = info.offset; + op->request = info.request; + op->buffer = info.buffer; + op->work = std::move(work); + state.ops[id] = op; + if (state.suspendCount > 0) { + op->queued = true; + state.pending.push_back(id); + } else { + runNow = true; + } + } + if (runNow) Run(op); + return op->id; +} + +SceFiosOp SubmitFailed(const SceFiosOpAttr* attr, OpInfo info, std::int32_t error) { + return Submit(attr, std::move(info), [error](Completion& completion) { completion.error = error; }); +} + +std::int32_t Wait(SceFiosOp op) { + std::unique_lock lock(Global().mutex); + const auto found = FindOp(Global(), op); + if (!found) return SCE_FIOS_ERROR_BAD_OP; + Global().finished.wait(lock, [&] { return found->done; }); + return found->error; +} + +std::int32_t SyncStatus(SceFiosOp op) { + if (op < 0) return op; + Completion completion; + return SyncWait(op, completion) ? completion.error : SCE_FIOS_ERROR_BAD_OP; +} + +std::int64_t SyncCount(SceFiosOp op) { + if (op < 0) return op; + Completion completion; + if (!SyncWait(op, completion)) return SCE_FIOS_ERROR_BAD_OP; + return completion.error != SCE_FIOS_OK ? completion.error : completion.result; +} + +bool SyncWait(SceFiosOp op, Completion& completion) { + std::shared_ptr found; + { + std::unique_lock lock(Global().mutex); + found = FindOp(Global(), op); + if (!found) return false; + Global().finished.wait(lock, [&] { return found->done; }); + completion.error = found->error; + completion.result = found->result; + } + DeleteOp(op); + return true; +} + +bool OpDone(SceFiosOp op) { + const std::lock_guard lock(Global().mutex); + const auto found = FindOp(Global(), op); + return found && found->done; +} + +bool DeleteOp(SceFiosOp op) { + std::shared_ptr found; + { + const std::lock_guard lock(Global().mutex); + found = FindOp(Global(), op); + } + if (!found) return false; + CancelQueued(found); + { + std::unique_lock lock(Global().mutex); + Global().finished.wait(lock, [&] { return found->done; }); + if (Global().ops.erase(op) == 0) return false; + } + FireDelete(found); + return true; +} + +bool CancelOp(SceFiosOp op) { + std::shared_ptr found; + { + const std::lock_guard lock(Global().mutex); + found = FindOp(Global(), op); + } + return found && CancelQueued(found); +} + +bool OpIsCancelled(SceFiosOp op) { + const std::lock_guard lock(Global().mutex); + const auto found = FindOp(Global(), op); + return found && found->cancelled; +} + +bool QueryOp(SceFiosOp op, std::int64_t& request, std::int64_t& actual, std::int64_t& offset) { + const std::lock_guard lock(Global().mutex); + const auto found = FindOp(Global(), op); + if (!found) return false; + request = found->request; + actual = found->done ? found->result : 0; + offset = found->offset; + return true; +} + +bool OpError(SceFiosOp op, std::int32_t& error) { + const std::lock_guard lock(Global().mutex); + const auto found = FindOp(Global(), op); + if (!found) return false; + error = found->done ? found->error : 1; + return true; +} + +const SceFiosOpAttr* OpAttr(SceFiosOp op) { + const std::lock_guard lock(Global().mutex); + const auto found = FindOp(Global(), op); + return found ? &found->attr : nullptr; +} + +const char* OpPath(SceFiosOp op) { + const std::lock_guard lock(Global().mutex); + const auto found = FindOp(Global(), op); + return found ? found->path.c_str() : nullptr; +} + +void* OpBuffer(SceFiosOp op) { + const std::lock_guard lock(Global().mutex); + const auto found = FindOp(Global(), op); + return found ? found->buffer : nullptr; +} + +template +std::uint32_t ListKeys(const TMap& map, SceFiosHandle* out, std::uint32_t capacity) { + std::uint32_t index = 0; + for (const auto& entry : map) { + if (out != nullptr && index < capacity) out[index] = entry.first; + ++index; + } + return index; +} + +std::uint32_t ListOps(SceFiosOp* out, std::uint32_t capacity) { + const std::lock_guard lock(Global().mutex); + return ListKeys(Global().ops, out, capacity); +} + +void CancelAll() { + std::vector> queued; + { + const std::lock_guard lock(Global().mutex); + for (const SceFiosOp id : Global().pending) { + if (const auto found = FindOp(Global(), id)) queued.push_back(found); + } + } + for (const auto& op : queued) CancelQueued(op); +} + +SceFiosFH AddFile(std::shared_ptr file) { + const std::lock_guard lock(Global().mutex); + const SceFiosHandle id = NextHandle(Global()); + if (id == 0) return SCE_FIOS_ERROR_CANT_ALLOCATE_FH; + Global().files[id] = std::move(file); + return id; +} + +std::shared_ptr FindFile(SceFiosFH handle) { + const std::lock_guard lock(Global().mutex); + const auto found = Global().files.find(handle); + return found == Global().files.end() ? nullptr : found->second; +} + +std::shared_ptr RemoveFile(SceFiosFH handle) { + const std::lock_guard lock(Global().mutex); + const auto found = Global().files.find(handle); + if (found == Global().files.end()) return nullptr; + auto file = std::move(found->second); + Global().files.erase(found); + return file; +} + +std::uint32_t ListFiles(SceFiosFH* out, std::uint32_t capacity) { + const std::lock_guard lock(Global().mutex); + return ListKeys(Global().files, out, capacity); +} + +void CloseAllFiles() { + std::map> files; + { + const std::lock_guard lock(Global().mutex); + files.swap(Global().files); + } + for (auto& entry : files) { + const std::lock_guard lock(entry.second->mutex); + if (entry.second->descriptor >= 0) sceKernelClose(entry.second->descriptor); + entry.second->descriptor = -1; + } +} + +SceFiosDH AddDirectory(std::shared_ptr directory) { + const std::lock_guard lock(Global().mutex); + const SceFiosHandle id = NextHandle(Global()); + if (id == 0) return SCE_FIOS_ERROR_CANT_ALLOCATE_DH; + Global().directories[id] = std::move(directory); + return id; +} + +std::shared_ptr FindDirectory(SceFiosDH handle) { + const std::lock_guard lock(Global().mutex); + const auto found = Global().directories.find(handle); + return found == Global().directories.end() ? nullptr : found->second; +} + +std::shared_ptr RemoveDirectory(SceFiosDH handle) { + const std::lock_guard lock(Global().mutex); + const auto found = Global().directories.find(handle); + if (found == Global().directories.end()) return nullptr; + auto directory = std::move(found->second); + Global().directories.erase(found); + return directory; +} + +std::uint32_t ListDirectories(SceFiosDH* out, std::uint32_t capacity) { + const std::lock_guard lock(Global().mutex); + return ListKeys(Global().directories, out, capacity); +} + +bool IsValidHandle(SceFiosHandle handle) { + const std::lock_guard lock(Global().mutex); + return HandleInUse(Global(), handle); +} + +bool StoreParameters(State& state, const void* params) { + if (params == nullptr) { + state.parameters.clear(); + return true; + } + std::uint32_t first = 0; + if (!CopyIn(&first, params, sizeof(first))) return false; + const std::size_t size = std::min((first >> 1) & 0x7fff, MaxStoredParameters); + std::vector copy(size); + if (size != 0 && !CopyIn(copy.data(), params, size)) return false; + state.parameters = std::move(copy); + return true; +} + +bool Initialize(const void* params) { + const std::lock_guard lock(Global().mutex); + if (!StoreParameters(Global(), params)) return false; + Global().initialized = true; + return true; +} + +bool UpdateParameters(const void* params) { + const std::lock_guard lock(Global().mutex); + return Global().initialized && StoreParameters(Global(), params); +} + +void Terminate() { + CancelAll(); + CloseAllFiles(); + const std::lock_guard lock(Global().mutex); + State& state = Global(); + state.directories.clear(); + for (auto found = state.ops.begin(); found != state.ops.end();) { + found = found->second->done ? state.ops.erase(found) : std::next(found); + } + state.parameters.clear(); + state.suspendCount = 0; + state.initialized = false; +} + +bool IsInitialized(void* out) { + std::vector copy; + { + const std::lock_guard lock(Global().mutex); + if (!Global().initialized) return false; + copy = Global().parameters; + } + if (out != nullptr && !copy.empty()) CopyOut(out, copy.data(), copy.size()); + return true; +} + +void Suspend() { + const std::lock_guard lock(Global().mutex); + ++Global().suspendCount; +} + +void Resume() { + std::vector> ready; + { + const std::lock_guard lock(Global().mutex); + State& state = Global(); + if (state.suspendCount == 0) return; + if (--state.suspendCount != 0) return; + for (const SceFiosOp id : state.pending) { + if (const auto found = FindOp(state, id)) { + found->queued = false; + ready.push_back(found); + } + } + state.pending.clear(); + } + for (const auto& op : ready) Run(op); +} + +std::uint32_t SuspendCount() { + const std::lock_guard lock(Global().mutex); + return Global().suspendCount; +} + +bool IsIdle() { + const std::lock_guard lock(Global().mutex); + return Global().pending.empty(); +} + +bool SetGlobalAttr(const SceFiosOpAttr* attr) { + SceFiosOpAttr copy{}; + if (attr != nullptr && !CopyIn(©, attr, sizeof(copy))) return false; + const std::lock_guard lock(Global().mutex); + if (attr == nullptr) Global().globalAttr.reset(); + else Global().globalAttr = copy; + return true; +} + +bool GlobalAttr(SceFiosOpAttr& attr) { + const std::lock_guard lock(Global().mutex); + if (!Global().globalAttr) return false; + attr = *Global().globalAttr; + return true; +} + +bool SetThreadAttr(const SceFiosOpAttr* attr) { + SceFiosOpAttr copy{}; + if (attr != nullptr && !CopyIn(©, attr, sizeof(copy))) return false; + if (attr == nullptr) g_threadAttr.reset(); + else g_threadAttr = copy; + return true; +} + +bool ThreadAttr(SceFiosOpAttr& attr) { + if (!g_threadAttr) return false; + attr = *g_threadAttr; + return true; +} + +std::int64_t ReadFully(int descriptor, void* buffer, std::uint64_t length, std::int64_t offset, bool positioned) { + std::uint64_t total = 0; + while (total < length) { + const std::uint64_t chunk = std::min(length - total, IoChunk); + auto* destination = static_cast(buffer) + total; + const std::int64_t count = positioned ? sceKernelPread(descriptor, destination, chunk, offset + static_cast(total)) + : sceKernelRead(descriptor, destination, chunk); + if (count < 0) return total == 0 ? FromKernelError(count) : static_cast(total); + if (count == 0) break; + total += static_cast(count); + } + return static_cast(total); +} + +std::int64_t WriteFully(int descriptor, const void* buffer, std::uint64_t length, std::int64_t offset, bool positioned) { + std::uint64_t total = 0; + while (total < length) { + const std::uint64_t chunk = std::min(length - total, IoChunk); + const auto* source = static_cast(buffer) + total; + const std::int64_t count = positioned ? sceKernelPwrite(descriptor, source, chunk, offset + static_cast(total)) + : sceKernelWrite(descriptor, source, chunk); + if (count < 0) return total == 0 ? FromKernelError(count) : static_cast(total); + if (count == 0) break; + total += static_cast(count); + } + return static_cast(total); +} + +namespace { + +constexpr std::uint16_t FileTypeMask = 0170000; +constexpr std::uint16_t DirectoryType = 0040000; + +void FillStat(const FileStat& native, SceFiosStat& stat) { + stat = SceFiosStat{}; + stat.fileSize = native.st_size; + stat.accessDate = TimespecToDate(native.st_atim.tv_sec, native.st_atim.tv_nsec); + stat.modificationDate = TimespecToDate(native.st_mtim.tv_sec, native.st_mtim.tv_nsec); + stat.creationDate = TimespecToDate(native.st_birthtim.tv_sec, native.st_birthtim.tv_nsec); + stat.statFlags = (native.st_mode & FileTypeMask) == DirectoryType ? SCE_FIOS_STAT_DIRECTORY : 0; + stat.uid = native.st_uid; + stat.gid = native.st_gid; + stat.dev = native.st_dev; + stat.ino = native.st_ino; + stat.mode = native.st_mode; +} + +} + +std::int32_t StatPath(const std::string& path, SceFiosStat& stat) { + FileStat native{}; + if (const int result = sceKernelStat(path.c_str(), &native); result < 0) return FromKernelError(result); + FillStat(native, stat); + return SCE_FIOS_OK; +} + +std::int32_t StatDescriptor(int descriptor, SceFiosStat& stat) { + FileStat native{}; + if (const int result = sceKernelFstat(descriptor, &native); result < 0) return FromKernelError(result); + FillStat(native, stat); + return SCE_FIOS_OK; +} + +} diff --git a/core/libs/prx/libSceFios2/src/Runtime.hpp b/core/libs/prx/libSceFios2/src/Runtime.hpp new file mode 100644 index 0000000000..81d924bf3b --- /dev/null +++ b/core/libs/prx/libSceFios2/src/Runtime.hpp @@ -0,0 +1,97 @@ +#pragma once + +#include "prx/libSceFios2/include/Types.hpp" +#include +#include +#include +#include +#include +#include + +namespace Fios { + +struct Completion { + std::int32_t error = SCE_FIOS_OK; + std::int64_t result = 0; +}; + +using Work = std::function; + +struct OpInfo { + std::string path; + std::int64_t offset = 0; + std::int64_t request = 0; + void* buffer = nullptr; +}; + +struct FileHandle { + std::mutex mutex; + int descriptor = -1; + std::string path; + SceFiosOpenParams params{}; +}; + +struct DirectoryHandle { + std::mutex mutex; + std::string path; + std::vector names; + std::size_t next = 0; +}; + +std::int32_t FromKernelError(std::int64_t kernelResult); +std::int32_t CopyPath(const char* source, std::string& destination); +bool CopyOut(void* destination, const void* source, std::size_t bytes); +bool CopyIn(void* destination, const void* source, std::size_t bytes); +std::int32_t SyncStatus(SceFiosOp op); +std::int64_t SyncCount(SceFiosOp op); +std::uint64_t TimespecToDate(std::int64_t seconds, std::int64_t nanoseconds); + +SceFiosOp Submit(const SceFiosOpAttr* attr, OpInfo info, Work work); +SceFiosOp SubmitFailed(const SceFiosOpAttr* attr, OpInfo info, std::int32_t error); +std::int32_t Wait(SceFiosOp op); +bool SyncWait(SceFiosOp op, Completion& completion); +bool OpDone(SceFiosOp op); +bool DeleteOp(SceFiosOp op); +bool CancelOp(SceFiosOp op); +bool OpIsCancelled(SceFiosOp op); +bool QueryOp(SceFiosOp op, std::int64_t& request, std::int64_t& actual, std::int64_t& offset); +bool OpError(SceFiosOp op, std::int32_t& error); +const SceFiosOpAttr* OpAttr(SceFiosOp op); +const char* OpPath(SceFiosOp op); +void* OpBuffer(SceFiosOp op); +std::uint32_t ListOps(SceFiosOp* out, std::uint32_t capacity); +void CancelAll(); + +SceFiosFH AddFile(std::shared_ptr file); +std::shared_ptr FindFile(SceFiosFH handle); +std::shared_ptr RemoveFile(SceFiosFH handle); +std::uint32_t ListFiles(SceFiosFH* out, std::uint32_t capacity); +void CloseAllFiles(); + +SceFiosDH AddDirectory(std::shared_ptr directory); +std::shared_ptr FindDirectory(SceFiosDH handle); +std::shared_ptr RemoveDirectory(SceFiosDH handle); +std::uint32_t ListDirectories(SceFiosDH* out, std::uint32_t capacity); + +bool IsValidHandle(SceFiosHandle handle); + +bool Initialize(const void* params); +bool UpdateParameters(const void* params); +void Terminate(); +bool IsInitialized(void* out); +void Suspend(); +void Resume(); +std::uint32_t SuspendCount(); +bool IsIdle(); + +bool SetGlobalAttr(const SceFiosOpAttr* attr); +bool GlobalAttr(SceFiosOpAttr& attr); +bool SetThreadAttr(const SceFiosOpAttr* attr); +bool ThreadAttr(SceFiosOpAttr& attr); + +std::int64_t ReadFully(int descriptor, void* buffer, std::uint64_t length, std::int64_t offset, bool positioned); +std::int64_t WriteFully(int descriptor, const void* buffer, std::uint64_t length, std::int64_t offset, bool positioned); +std::int32_t StatPath(const std::string& path, SceFiosStat& stat); +std::int32_t StatDescriptor(int descriptor, SceFiosStat& stat); + +} diff --git a/core/libs/prx/libSceFios2/src/Time.cpp b/core/libs/prx/libSceFios2/src/Time.cpp new file mode 100644 index 0000000000..f77013d804 --- /dev/null +++ b/core/libs/prx/libSceFios2/src/Time.cpp @@ -0,0 +1,87 @@ +#include "prx/libSceFios2/include/Api.hpp" +#include "Runtime.hpp" +#include "prx/libc/include/General.hpp" +#include +#include + +namespace { + +constexpr std::int64_t NanosecondsPerSecond = 1000000000; +constexpr std::int64_t SecondsPerDay = 86400; + +std::int64_t DaysFromCivil(std::int64_t year, std::int64_t month, std::int64_t day) { + year -= month <= 2 ? 1 : 0; + const std::int64_t era = (year >= 0 ? year : year - 399) / 400; + const std::int64_t yearOfEra = year - era * 400; + const std::int64_t dayOfYear = (153 * (month + (month > 2 ? -3 : 9)) + 2) / 5 + day - 1; + const std::int64_t dayOfEra = yearOfEra * 365 + yearOfEra / 4 - yearOfEra / 100 + dayOfYear; + return era * 146097 + dayOfEra - 719468; +} + +void CivilFromDays(std::int64_t days, std::int64_t& year, std::int64_t& month, std::int64_t& day) { + days += 719468; + const std::int64_t era = (days >= 0 ? days : days - 146096) / 146097; + const std::int64_t dayOfEra = days - era * 146097; + const std::int64_t yearOfEra = (dayOfEra - dayOfEra / 1460 + dayOfEra / 36524 - dayOfEra / 146096) / 365; + const std::int64_t dayOfYear = dayOfEra - (365 * yearOfEra + yearOfEra / 4 - yearOfEra / 100); + const std::int64_t shifted = (5 * dayOfYear + 2) / 153; + day = dayOfYear - (153 * shifted + 2) / 5 + 1; + month = shifted < 10 ? shifted + 3 : shifted - 9; + year = yearOfEra + era * 400 + (month <= 2 ? 1 : 0); +} + +} + +extern "C" { + +SceFiosDate APS5_VABI sceFiosDateGetCurrent() { + const auto since = std::chrono::system_clock::now().time_since_epoch(); + return static_cast(std::chrono::duration_cast(since).count()); +} + +SceFiosDate APS5_VABI sceFiosDateFromComponents(const SceFiosTm* components) { + if (components == nullptr) return 0; + SceFiosTm tm{}; + if (!Fios::CopyIn(&tm, components, sizeof(tm))) return 0; + std::int64_t year = static_cast(tm.tm_year) + 1900; + std::int64_t month = tm.tm_mon; + year += month / 12; + month %= 12; + if (month < 0) { + month += 12; + --year; + } + const std::int64_t days = DaysFromCivil(year, month + 1, 1) + tm.tm_mday - 1; + const std::int64_t seconds = days * SecondsPerDay + static_cast(tm.tm_hour) * 3600 + static_cast(tm.tm_min) * 60 + tm.tm_sec; + return seconds < 0 ? 0 : static_cast(seconds) * NanosecondsPerSecond; +} + +SceFiosTm* APS5_VABI sceFiosDateToComponents(SceFiosDate date, SceFiosTm* outComponents) { + if (outComponents == nullptr) return nullptr; + const auto seconds = static_cast(date / NanosecondsPerSecond); + const std::int64_t days = seconds / SecondsPerDay; + const std::int64_t rest = seconds % SecondsPerDay; + std::int64_t year = 0, month = 0, day = 0; + CivilFromDays(days, year, month, day); + SceFiosTm tm{}; + tm.tm_sec = static_cast(rest % 60); + tm.tm_min = static_cast(rest / 60 % 60); + tm.tm_hour = static_cast(rest / 3600); + tm.tm_mday = static_cast(day); + tm.tm_mon = static_cast(month - 1); + tm.tm_year = static_cast(year - 1900); + tm.tm_wday = static_cast((days + 4) % 7); + tm.tm_yday = static_cast(days - DaysFromCivil(year, 1, 1)); + tm.tm_isdst = 0; + return Fios::CopyOut(outComponents, &tm, sizeof(tm)) ? outComponents : nullptr; +} + +SceFiosTimeInterval APS5_VABI sceFiosTimeIntervalFromNanoseconds(std::int64_t nanoseconds) { + return nanoseconds; +} + +std::int64_t APS5_VABI sceFiosTimeIntervalToNanoseconds(SceFiosTimeInterval interval) { + return interval; +} + +} diff --git a/core/libs/prx/libSceFont/CMakeLists.txt b/core/libs/prx/libSceFont/CMakeLists.txt index e9516dff55..41e715f805 100644 --- a/core/libs/prx/libSceFont/CMakeLists.txt +++ b/core/libs/prx/libSceFont/CMakeLists.txt @@ -37,6 +37,16 @@ if(BUILD_TESTING) set_tests_properties(guest_font_renderer_lifecycle PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_font_vertical_layout_tests ${LIBS_INCLUDE_DIR}/tests/GuestFontVerticalLayout.cpp) + target_include_directories(guest_font_vertical_layout_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_font_vertical_layout_tests PRIVATE libSceFont libSceFontFt libc) + configure_windows_unwind(guest_font_vertical_layout_tests) + add_test(NAME guest_font_vertical_layout COMMAND guest_font_vertical_layout_tests) + set_tests_properties(guest_font_vertical_layout PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_font_vertical_layout PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_system_font_tests ${LIBS_INCLUDE_DIR}/tests/GuestSystemFont.cpp) target_include_directories(guest_system_font_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_system_font_tests PRIVATE libSceFont libSceFontFt libc) diff --git a/core/libs/prx/libSceFont/src/Library.cpp b/core/libs/prx/libSceFont/src/Library.cpp index 54d3f48d0c..1df3df0008 100644 --- a/core/libs/prx/libSceFont/src/Library.cpp +++ b/core/libs/prx/libSceFont/src/Library.cpp @@ -413,6 +413,7 @@ int APS5_VABI sceFontDettachDeviceCacheBuffer(FontLibrary library, void** buffer } int APS5_VABI sceFontSupportExternalFonts(FontLibrary library, std::uint32_t fontMax, std::uint32_t formats) { + if (fontMax > (UINT32_MAX >> 6u)) return SCE_FONT_ERROR_INVALID_PARAMETER; return SupportFonts(library, &FontLibNative::external_fonts_ctx, fontMax, (fontMax << 6) | 0x20u, formats); } diff --git a/core/libs/prx/libSceFontGs.native/CMakeLists.txt b/core/libs/prx/libSceFontGs.native/CMakeLists.txt new file mode 100644 index 0000000000..5469275d06 --- /dev/null +++ b/core/libs/prx/libSceFontGs.native/CMakeLists.txt @@ -0,0 +1,18 @@ +add_library(libSceFontGs.native SHARED EXCLUDE_FROM_ALL + ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp +) +target_include_directories(libSceFontGs.native PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(libSceFontGs.native PRIVATE libSceFont) +configure_windows_unwind(libSceFontGs.native) + +if(BUILD_TESTING) + add_test_executable(guest_font_gs_agc_tests ${LIBS_INCLUDE_DIR}/tests/GuestFontGsAgc.cpp) + target_include_directories(guest_font_gs_agc_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_font_gs_agc_tests PRIVATE libSceFontGs.native libSceFont libkernel libc) + configure_windows_unwind(guest_font_gs_agc_tests) + add_test(NAME guest_font_gs_agc COMMAND guest_font_gs_agc_tests) + if(WIN32) + set_tests_properties(guest_font_gs_agc PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$;PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceFontGs.native/Export.cpp b/core/libs/prx/libSceFontGs.native/Export.cpp index 258ca4598f..d1fd85ff53 100644 --- a/core/libs/prx/libSceFontGs.native/Export.cpp +++ b/core/libs/prx/libSceFontGs.native/Export.cpp @@ -1,38 +1,203 @@ #include "prx/libSceFontGs.native/include/Api.hpp" +#include "prx/libSceFont/include/Api.hpp" #include #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +namespace { + +constexpr int FontGsInvalidParameter = static_cast(0x80460002); +constexpr int FontGsNoCommands = static_cast(0x80460001); +constexpr int FontGsInvalidSequence = static_cast(0x8046009a); +constexpr int FontGsSequenceLocked = static_cast(0x804600b8); +constexpr std::uint8_t FontAgcSurfaceTag = 0x58; +constexpr std::uint16_t FontAgcSequenceTag = 0xf80; +constexpr float FontAgcDrawupLimit = 16834.0f; +constexpr std::size_t FontAgcRecordStateOffset = 0x10; + +struct FontAgcSurface { + void* head; + std::uint8_t reserved8[5]; + std::uint8_t tag; + std::uint8_t reserved0e; + std::uint8_t mode; + std::uint8_t reserved10[0xc]; + std::uint32_t busy; + std::uint8_t reserved20[8]; + const void* fillObject; + std::uint8_t reserved30[8]; + std::uint32_t initialized; +}; + +static_assert(offsetof(FontAgcSurface, tag) == 0x0d); +static_assert(offsetof(FontAgcSurface, mode) == 0x0f); +static_assert(offsetof(FontAgcSurface, busy) == 0x1c); +static_assert(offsetof(FontAgcSurface, fillObject) == 0x28); +static_assert(offsetof(FontAgcSurface, initialized) == 0x38); + +struct FontAgcCommands { + std::uint8_t reserved[0x28]; + const void* first; + std::uint32_t firstSize; + std::uint32_t secondCount; + const void* second; +}; + +static_assert(offsetof(FontAgcCommands, first) == 0x28); +static_assert(offsetof(FontAgcCommands, firstSize) == 0x30); +static_assert(offsetof(FontAgcCommands, secondCount) == 0x34); +static_assert(offsetof(FontAgcCommands, second) == 0x38); + +struct FontAgcSequence { + std::uint16_t tag; + std::uint8_t reserved2[0x19]; + std::uint8_t lock; + std::uint8_t reserved1c[0xc]; + FontAgcCommands* commands; +}; + +static_assert(offsetof(FontAgcSequence, lock) == 0x1b); +static_assert(offsetof(FontAgcSequence, commands) == 0x28); + +const FontAgcSurface* ReadAgcSurface(const void* surface) { + const auto* const agc = static_cast(surface); + if (agc->tag != FontAgcSurfaceTag || agc->head == nullptr || agc->initialized != 1 || agc->busy != 0) { + return nullptr; + } + return agc; +} + +void MarkAgcRecord(void* output) { + *reinterpret_cast(static_cast(output) + FontAgcRecordStateOffset) = 1; +} + +} + extern "C" { -int APS5_VABI sceFontSelectGraphicsAgc(void) { +void* APS5_VABI sceFontSelectGraphicsAgc(std::uint32_t index) { + if (index == 0) { + NotImplemented_nid_no_patch(__func__); + } + return nullptr; +} + +int APS5_VABI sceFontGraphicsAgcSurfaceInit(void* surface, const void* config) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsAgcSurfaceInit(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceFontGraphicsAgcDrawupFillTextureImage(FontGraphicsFrame* graphicsObject, const void* surface, void** output, + float x, float y, float width, float height) { + if (output == nullptr) { + return FontGsInvalidParameter; + } + *output = nullptr; + if (graphicsObject == nullptr || surface == nullptr) { + return FontGsInvalidParameter; + } + const auto* const agc = static_cast(surface); + if (agc->tag != FontAgcSurfaceTag || agc->head == nullptr || agc->initialized != 1 || !(x >= 0.0f) || agc->busy != 0 || + !(y >= 0.0f)) { + return FontGsInvalidParameter; + } + const float right = x + width; + const float bottom = y + height; + if (!(right >= x) || !(right <= FontAgcDrawupLimit) || !(bottom >= y) || !(bottom <= FontAgcDrawupLimit)) { + return FontGsInvalidParameter; + } + const int error = sceFontGraphicsDrawupFillTextureImageObject(graphicsObject, agc->fillObject, agc->mode, output, x, y, + width, height); + if (error != 0) { + return error; + } + MarkAgcRecord(*output); return 0; } -int APS5_VABI sceFontGraphicsAgcDrawupFillTextureImage(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceFontGraphicsAgcDrawupFillTexturePattern(FontGraphicsFrame* graphicsObject, const void* surface, std::uint32_t format, + std::uint32_t select, std::uint32_t dimension1, std::uint32_t option1, + std::uint32_t option2, std::uint32_t dimension2, void** output) { + if (output == nullptr) { + return FontGsInvalidParameter; + } + *output = nullptr; + if (graphicsObject == nullptr || surface == nullptr) { + return FontGsInvalidParameter; + } + const auto* const agc = ReadAgcSurface(surface); + if (agc == nullptr) { + return FontGsInvalidParameter; + } + const int error = sceFontGraphicsDrawupFillTexturePatternObject(graphicsObject, agc->fillObject, agc->mode, format, + select, dimension1, option1, option2, dimension2, output); + if (error != 0) { + return error; + } + MarkAgcRecord(*output); return 0; } -int APS5_VABI sceFontGraphicsAgcDrawupFillTexturePattern(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceFontGraphicsSequenceGetAgcCommandsForJump(FontGraphicsCanvasSequence* sequence, void** commands, + std::uint32_t* numCommands, void** commands2, std::uint32_t* numCommands2) { + if (commands != nullptr) { + *commands = nullptr; + } + if (numCommands != nullptr) { + *numCommands = 0; + } + if (commands2 != nullptr) { + *commands2 = nullptr; + } + if (numCommands2 != nullptr) { + *numCommands2 = 0; + } + const auto* const seq = reinterpret_cast(sequence); + if (seq == nullptr || seq->tag != FontAgcSequenceTag) { + return FontGsInvalidSequence; + } + if (commands == nullptr || numCommands == nullptr || commands2 == nullptr || numCommands2 == nullptr) { + return FontGsInvalidParameter; + } + if ((seq->lock & 1) != 0) { + return FontGsSequenceLocked; + } + const auto* const agc = seq->commands; + if (agc == nullptr) { + return FontGsNoCommands; + } + *commands = const_cast(agc->first); + *numCommands = agc->firstSize >> 2; + *commands2 = const_cast(agc->second); + *numCommands2 = agc->secondCount; return 0; } -int APS5_VABI sceFontGraphicsSequenceGetAgcCommandsForJump(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontGraphicsSequenceGetAgcCommandsForSubmit(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceFontGraphicsSequenceGetAgcCommandsForSubmit(FontGraphicsCanvasSequence* sequence, void** commands, + std::uint32_t* numCommands) { + if (commands != nullptr) { + *commands = nullptr; + } + if (numCommands != nullptr) { + *numCommands = 0; + } + const auto* const seq = reinterpret_cast(sequence); + if (seq == nullptr || seq->tag != FontAgcSequenceTag) { + return FontGsInvalidSequence; + } + if (commands == nullptr || numCommands == nullptr) { + return FontGsInvalidParameter; + } + if ((seq->lock & 1) != 0) { + return FontGsSequenceLocked; + } + const auto* const agc = seq->commands; + if (agc == nullptr) { + return FontGsNoCommands; + } + *commands = const_cast(agc->first); + *numCommands = agc->firstSize; return 0; } diff --git a/core/libs/prx/libSceFontGs.native/include/Api.hpp b/core/libs/prx/libSceFontGs.native/include/Api.hpp index d2de1415bc..94c4d61421 100644 --- a/core/libs/prx/libSceFontGs.native/include/Api.hpp +++ b/core/libs/prx/libSceFontGs.native/include/Api.hpp @@ -1,14 +1,28 @@ #pragma once #include "SceTypes.hpp" +#include + +struct FontGraphicsFrame; +struct FontGraphicsCanvasSequence; extern "C" { -int APS5_VABI sceFontSelectGraphicsAgc(void); -int APS5_VABI sceFontGraphicsAgcSurfaceInit(void); -int APS5_VABI sceFontGraphicsAgcDrawupFillTextureImage(void); -int APS5_VABI sceFontGraphicsAgcDrawupFillTexturePattern(void); -int APS5_VABI sceFontGraphicsSequenceGetAgcCommandsForJump(void); -int APS5_VABI sceFontGraphicsSequenceGetAgcCommandsForSubmit(void); +void* APS5_VABI sceFontSelectGraphicsAgc(std::uint32_t index); + +int APS5_VABI sceFontGraphicsAgcSurfaceInit(void* surface, const void* config); + +int APS5_VABI sceFontGraphicsAgcDrawupFillTextureImage(FontGraphicsFrame* graphicsObject, const void* surface, void** output, + float x, float y, float width, float height); + +int APS5_VABI sceFontGraphicsAgcDrawupFillTexturePattern(FontGraphicsFrame* graphicsObject, const void* surface, std::uint32_t format, + std::uint32_t select, std::uint32_t dimension1, std::uint32_t option1, + std::uint32_t option2, std::uint32_t dimension2, void** output); + +int APS5_VABI sceFontGraphicsSequenceGetAgcCommandsForJump(FontGraphicsCanvasSequence* sequence, void** commands, + std::uint32_t* numCommands, void** commands2, std::uint32_t* numCommands2); + +int APS5_VABI sceFontGraphicsSequenceGetAgcCommandsForSubmit(FontGraphicsCanvasSequence* sequence, void** commands, + std::uint32_t* numCommands); } diff --git a/core/libs/prx/libSceFreeType/CMakeLists.txt b/core/libs/prx/libSceFreeType/CMakeLists.txt index e9548707d7..99c2a3fcc8 100644 --- a/core/libs/prx/libSceFreeType/CMakeLists.txt +++ b/core/libs/prx/libSceFreeType/CMakeLists.txt @@ -4,7 +4,7 @@ add_library(libSceFreeType SHARED EXCLUDE_FROM_ALL src/Outline.cpp src/Charmap.cpp src/FaceInfo.cpp - src/Stroker.cpp src/OutlineStorage.cpp + src/Stroker.cpp src/OutlineStorage.cpp src/FaceCompat.cpp src/GuestObjects.cpp src/Names.cpp src/Trigonometry.cpp @@ -133,6 +133,15 @@ if(BUILD_TESTING) set_tests_properties(guest_freetype_stroker PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_freetype_face_compat_tests ${LIBS_INCLUDE_DIR}/tests/GuestFreeTypeFaceCompat.cpp) + target_include_directories(guest_freetype_face_compat_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_freetype_face_compat_tests PRIVATE libSceFreeType freetype libc) + configure_windows_unwind(guest_freetype_face_compat_tests) + add_test(NAME guest_freetype_face_compat COMMAND guest_freetype_face_compat_tests) + if(WIN32) + set_tests_properties(guest_freetype_face_compat PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_freetype_outline_storage_tests ${LIBS_INCLUDE_DIR}/tests/GuestFreeTypeOutlineStorage.cpp) target_include_directories(guest_freetype_outline_storage_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_freetype_outline_storage_tests PRIVATE libSceFreeType freetype libc) @@ -190,4 +199,6 @@ if(BUILD_TESTING) endif() endif() +include("${CMAKE_CURRENT_LIST_DIR}/MemoryRecord.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/ModuleProperties.cmake") diff --git a/core/libs/prx/libSceFreeType/MemoryRecord.cmake b/core/libs/prx/libSceFreeType/MemoryRecord.cmake new file mode 100644 index 0000000000..ae81636b8e --- /dev/null +++ b/core/libs/prx/libSceFreeType/MemoryRecord.cmake @@ -0,0 +1,21 @@ +target_sources(libSceFreeType PRIVATE ${LIBS_INCLUDE_DIR}/prx/libSceFreeType/src/MemoryRecord.cpp) + +if(BUILD_TESTING) + add_test_executable(guest_freetype_memory_record_tests ${LIBS_INCLUDE_DIR}/tests/GuestFreeTypeMemoryRecord.cpp) + target_include_directories(guest_freetype_memory_record_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_freetype_memory_record_tests PRIVATE ${CMAKE_DL_LIBS}) + if(WIN32) + target_link_libraries(guest_freetype_memory_record_tests PRIVATE libc) + endif() + configure_windows_unwind(guest_freetype_memory_record_tests) + add_dependencies(guest_freetype_memory_record_tests libs) + add_test(NAME guest_freetype_memory_record COMMAND guest_freetype_memory_record_tests $ unpatched) + add_test(NAME guest_freetype_memory_record_nid COMMAND guest_freetype_memory_record_tests ${PATCHED_DIR}/libSceFreeType.prx patched) + set_tests_properties(guest_freetype_memory_record guest_freetype_memory_record_nid PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_freetype_memory_record PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + set_tests_properties(guest_freetype_memory_record_nid PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:${PATCHED_DIR}") + endif() +endif() diff --git a/core/libs/prx/libSceFreeType/include/Api.hpp b/core/libs/prx/libSceFreeType/include/Api.hpp index 280bc0d158..9a1e2bfa64 100644 --- a/core/libs/prx/libSceFreeType/include/Api.hpp +++ b/core/libs/prx/libSceFreeType/include/Api.hpp @@ -4,6 +4,7 @@ #include "prx/libSceFreeType/include/GuestFreeType.hpp" #include "prx/libSceFreeType/include/PostScriptInfo.hpp" #include "SceTypes.hpp" +#include "prx/libSceFreeType/include/MemoryRecord.hpp" extern "C" { @@ -109,6 +110,9 @@ int APS5_VABI FT_Stroker_BeginSubPath_nid_postfix(void* stroker, const FreeType: int APS5_VABI FT_Stroker_ConicTo_nid_postfix(void* stroker, const FreeType::GuestVector* control, const FreeType::GuestVector* to); int APS5_VABI ft_corner_is_flat_nid_postfix(std::int64_t in_x, std::int64_t in_y, std::int64_t out_x, std::int64_t out_y); int APS5_VABI ft_corner_orientation_nid_postfix(std::int64_t in_x, std::int64_t in_y, std::int64_t out_x, std::int64_t out_y); +const char* APS5_VABI FT_Get_Font_Format_nid_postfix(FreeType::GuestFaceRec* face); +std::uint8_t APS5_VABI FT_Face_CheckTrueTypePatents_nid_postfix(FreeType::GuestFaceRec* face); +std::uint8_t APS5_VABI FT_Face_SetUnpatentedHinting_nid_postfix(FreeType::GuestFaceRec* face, std::uint8_t value); int APS5_VABI FT_Outline_New_nid_postfix(FreeType::GuestLibrary* library, std::uint32_t numPoints, std::int32_t numContours, FreeType::GuestOutline* outline); int APS5_VABI FT_Outline_Done_nid_postfix(FreeType::GuestLibrary* library, FreeType::GuestOutline* outline); diff --git a/core/libs/prx/libSceFreeType/include/MemoryRecord.hpp b/core/libs/prx/libSceFreeType/include/MemoryRecord.hpp new file mode 100644 index 0000000000..a84868e4ca --- /dev/null +++ b/core/libs/prx/libSceFreeType/include/MemoryRecord.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include "SceTypes.hpp" +#include "prx/libSceFreeType/include/GuestList.hpp" +#include + +extern "C" { + +FreeType::GuestMemoryRec* APS5_VABI FT_New_Memory_nid_postfix(); +void APS5_VABI FT_Done_Memory_nid_postfix(FreeType::GuestMemoryRec* memory); +int APS5_VABI sceFreeTypeOtNewMemory_nid_no_patch(); + +} diff --git a/core/libs/prx/libSceFreeType/src/FaceCompat.cpp b/core/libs/prx/libSceFreeType/src/FaceCompat.cpp new file mode 100644 index 0000000000..3d9f60c004 --- /dev/null +++ b/core/libs/prx/libSceFreeType/src/FaceCompat.cpp @@ -0,0 +1,28 @@ +#include "prx/libSceFreeType/include/Api.hpp" +#include "prx/libSceFreeType/include/GuestObjects.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" + +#include FT_FONT_FORMATS_H + +#include + +using namespace FreeType; + +extern "C" { + +const char* APS5_VABI FT_Get_Font_Format_nid_postfix(GuestFaceRec* face) { + const GuestFace* owner = FaceOf(face); + return FT_Get_Font_Format(owner ? owner->host : nullptr); +} + +std::uint8_t APS5_VABI FT_Face_CheckTrueTypePatents_nid_postfix(GuestFaceRec* face) { + const GuestFace* owner = FaceOf(face); + return FT_Face_CheckTrueTypePatents(owner ? owner->host : nullptr); +} + +std::uint8_t APS5_VABI FT_Face_SetUnpatentedHinting_nid_postfix(GuestFaceRec* face, std::uint8_t value) { + const GuestFace* owner = FaceOf(face); + return FT_Face_SetUnpatentedHinting(owner ? owner->host : nullptr, value); +} + +} diff --git a/core/libs/prx/libSceFreeType/src/MemoryRecord.cpp b/core/libs/prx/libSceFreeType/src/MemoryRecord.cpp new file mode 100644 index 0000000000..f19c7dd010 --- /dev/null +++ b/core/libs/prx/libSceFreeType/src/MemoryRecord.cpp @@ -0,0 +1,29 @@ +#include "prx/libSceFreeType/include/MemoryRecord.hpp" + +#include + +namespace { + +void* APS5_VABI HostAlloc(FreeType::GuestMemoryRec* memory, std::int64_t size) { + return std::malloc(static_cast(size)); +} + +void APS5_VABI HostFree(FreeType::GuestMemoryRec* memory, void* block) { + std::free(block); +} + +void* APS5_VABI HostRealloc(FreeType::GuestMemoryRec* memory, std::int64_t current, std::int64_t size, void* block) { + return std::realloc(block, static_cast(size)); +} + +} + +extern "C" FreeType::GuestMemoryRec* APS5_VABI FT_New_Memory_nid_postfix() { + const auto memory = static_cast(std::malloc(sizeof(FreeType::GuestMemoryRec))); + if (memory) *memory = {nullptr, HostAlloc, HostFree, HostRealloc}; + return memory; +} + +extern "C" void APS5_VABI FT_Done_Memory_nid_postfix(FreeType::GuestMemoryRec* memory) { + std::free(memory); +} diff --git a/core/libs/prx/libSceFreeTypeOl/CMakeLists.txt b/core/libs/prx/libSceFreeTypeOl/CMakeLists.txt index 6d5e65cab1..d206765a57 100644 --- a/core/libs/prx/libSceFreeTypeOl/CMakeLists.txt +++ b/core/libs/prx/libSceFreeTypeOl/CMakeLists.txt @@ -11,6 +11,8 @@ if(ANYPS5_USE_SYSTEM_DEPS) endif() if(MINGW OR CMAKE_SYSTEM_NAME STREQUAL "Linux") target_link_options(libSceFreeTypeOl PRIVATE LINKER:--exclude-libs,ALL) +elseif(APPLE) + target_link_options(libSceFreeTypeOl PRIVATE "LINKER:-exported_symbol,*_nid_*") endif() configure_windows_unwind(libSceFreeTypeOl) diff --git a/core/libs/prx/libSceFreeTypeOt/Export.cpp b/core/libs/prx/libSceFreeTypeOt/Export.cpp index fe11b00793..8a34f36711 100644 --- a/core/libs/prx/libSceFreeTypeOt/Export.cpp +++ b/core/libs/prx/libSceFreeTypeOt/Export.cpp @@ -194,10 +194,10 @@ int APS5_VABI sceFreeTypeOtUnknown_yrtsLmTfa48_nid_no_patch() { } APS5_EXPORT("yrtsLmTfa48", sceFreeTypeOtUnknown_yrtsLmTfa48_nid_no_patch); -int APS5_VABI sceFreeTypeOtUnknown_tCMhTh7h8D4_nid_no_patch() { +int APS5_VABI sceFreeTypeOtNewMemory_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("tCMhTh7h8D4", sceFreeTypeOtUnknown_tCMhTh7h8D4_nid_no_patch); +APS5_EXPORT("tCMhTh7h8D4", sceFreeTypeOtNewMemory_nid_no_patch); int APS5_VABI sceFreeTypeOtNewSize_nid_no_patch() { NotImplemented_nid_no_patch(__func__); diff --git a/core/libs/prx/libSceFsInternalForVsh/Export.cpp b/core/libs/prx/libSceFsInternalForVsh/Export.cpp index e98aa4a828..6806f0c3c7 100644 --- a/core/libs/prx/libSceFsInternalForVsh/Export.cpp +++ b/core/libs/prx/libSceFsInternalForVsh/Export.cpp @@ -1,3 +1,5 @@ +#include "prx/libc/include/general/ExportMacros.hpp" +#include "prx/libSceFsInternalForVsh/include/InventoryExports.hpp" #include "prx/libSceFsInternalForVsh/include/Api.hpp" #include #include @@ -885,3 +887,734 @@ int APS5_VABI sceFsWsUmount() { } } + +extern "C" { + +int APS5_VABI sceFsBfsAllocateAdditionalContents() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsBfsAllocateGameImage() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsBfsAllocatePatchImage() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsBfsAllocateSaveData() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsBfsAllocateTrophyData() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsBfsApplyPatch() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsBfsCancelPatch() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsBfsCommitPatch() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsBfsContainerBind() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsBfsContainerCreate() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsBfsContainerQuery() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsBfsContainerUnbind() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsBfsContainerUpdate() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsBfsExtendFile() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsBfsFormat() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsBfsFsck() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsBfsGcGetStatus() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsBfsGcMove() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsBfsGetAvailableSize() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsBfsGetBindStatus() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsBfsGetContainers() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsBfsGetFileSize() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsBfsGetMapping() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsBfsGetSCFStatus() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsBfsGetStatus() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsBfsM2Format() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsBfsMount() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsBfsTrim() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsBfsUnprotect() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsCreatePprPfsSaveDataImage() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsCreateWorkspace() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsDeleteWorkspace() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsDevpfsAllocateFile() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsDevpfsAllocateSparseFile() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsDevpfsMountForVSH() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsDevpfsSystemPowerEvent() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsDevpfsTermFetchMetaEntryCtx() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsDevpfsUmountForFSMP() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsDevpfsUmountForVSH() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsDevpfsWriteChunk() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsDrfsDestroyContext() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsDrfsDisconnect() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsDrfsInitContext() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsDrfsMount() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsDrfsSuspendSocket() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsDrfsUmount() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsDrfsUnregisterSocket() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsDrfsWaitEvent() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsFetchWorkspaceFileMetadata() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsInitCreatePprPfsSaveDataOpt() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsInitCreateWorkspaceOpt() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsInitDeleteWorkspaceOpt() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsInitMountWorkspaceOpt() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsInitUmountWorkspaceOpt() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsIsActiveBankSystemEx() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsIsActiveBankSystem() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsM2StorageCheckCleanFlag() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsM2StorageDeconfigure() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsM2StorageFsckWithMM() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsM2StorageGetM2UniqueId() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsM2StorageInitialize() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsM2StorageMount() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsM2StorageUnmount() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsM2StorageVerifyMetadata() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsMountPprSaveData() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsMountWorkspace() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsNewfsDefault() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsNsfsMount() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsPartitionSwitchBank() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsPathMappingResolve() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsPlaygoDisable() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsPlaygoEnable() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsPlaygoInitializeParams() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsPlaygoSetParams() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsTransactionCommit() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsTransactionPprNoCowSaveDataMount() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsTransactionPprSaveDataMount() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsTransactionSaveDataReadParam() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsTransactionSaveDataUmount() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsTransactionSaveDataWriteParam() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsUfsCoerceCryptMode() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsUfsMkfsProsperoApplicationTemporaryData() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsUfsMkfsProsperoDownloadData() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsUmountPprPkg() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsUpdatePackageMetadataApp0() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsWorkspaceCoreDump() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsWsWriteFile() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceFsWsWriteFileExtdata() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI libSceFsInternalForVshUnknown__plus_gVdjPejLwc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+gVdjPejLwc", libSceFsInternalForVshUnknown__plus_gVdjPejLwc_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown__minus_HW6neCER_plus_A_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-HW6neCER+A", libSceFsInternalForVshUnknown__minus_HW6neCER_plus_A_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_0JhonchgIa8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0JhonchgIa8", libSceFsInternalForVshUnknown_0JhonchgIa8_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_0XlyVxRNeIw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0XlyVxRNeIw", libSceFsInternalForVshUnknown_0XlyVxRNeIw_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_1lLh9mukbz0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("1lLh9mukbz0", libSceFsInternalForVshUnknown_1lLh9mukbz0_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_2UgTmJt2dsc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("2UgTmJt2dsc", libSceFsInternalForVshUnknown_2UgTmJt2dsc_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_3TFUof4RLWM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("3TFUof4RLWM", libSceFsInternalForVshUnknown_3TFUof4RLWM_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_41PqBeun5cU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("41PqBeun5cU", libSceFsInternalForVshUnknown_41PqBeun5cU_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_4uqQOvVpISA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("4uqQOvVpISA", libSceFsInternalForVshUnknown_4uqQOvVpISA_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_68F1C7m2pO4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("68F1C7m2pO4", libSceFsInternalForVshUnknown_68F1C7m2pO4_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_6Bku_plus_Im1Pew_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("6Bku+Im1Pew", libSceFsInternalForVshUnknown_6Bku_plus_Im1Pew_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_89zs8_plus_EWQYQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("89zs8+EWQYQ", libSceFsInternalForVshUnknown_89zs8_plus_EWQYQ_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_9X3ydcDhbhw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("9X3ydcDhbhw", libSceFsInternalForVshUnknown_9X3ydcDhbhw_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_AnMWzsLhsYk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("AnMWzsLhsYk", libSceFsInternalForVshUnknown_AnMWzsLhsYk_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_CFlYeKz_plus_HYs_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("CFlYeKz+HYs", libSceFsInternalForVshUnknown_CFlYeKz_plus_HYs_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_DEHwNbsX2BA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("DEHwNbsX2BA", libSceFsInternalForVshUnknown_DEHwNbsX2BA_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_DNjr3qYr_minus_lY_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("DNjr3qYr-lY", libSceFsInternalForVshUnknown_DNjr3qYr_minus_lY_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_DSmBrz8enws_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("DSmBrz8enws", libSceFsInternalForVshUnknown_DSmBrz8enws_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_Elqj7jkJd5s_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("Elqj7jkJd5s", libSceFsInternalForVshUnknown_Elqj7jkJd5s_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_GNeCtmqY_minus_58_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("GNeCtmqY-58", libSceFsInternalForVshUnknown_GNeCtmqY_minus_58_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_GsHxp1IXkfA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("GsHxp1IXkfA", libSceFsInternalForVshUnknown_GsHxp1IXkfA_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_KesAmkrIb9Y_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("KesAmkrIb9Y", libSceFsInternalForVshUnknown_KesAmkrIb9Y_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_KrZEB_plus_TsZ5k_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("KrZEB+TsZ5k", libSceFsInternalForVshUnknown_KrZEB_plus_TsZ5k_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_LGPQXeAAFdc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("LGPQXeAAFdc", libSceFsInternalForVshUnknown_LGPQXeAAFdc_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_LrpsS5hGlyY_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("LrpsS5hGlyY", libSceFsInternalForVshUnknown_LrpsS5hGlyY_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_M9342PuS_plus_y0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("M9342PuS+y0", libSceFsInternalForVshUnknown_M9342PuS_plus_y0_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_MQs7RB_minus_KfYM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("MQs7RB-KfYM", libSceFsInternalForVshUnknown_MQs7RB_minus_KfYM_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_N7SnQKgSGA4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("N7SnQKgSGA4", libSceFsInternalForVshUnknown_N7SnQKgSGA4_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_NHH7572Mh30_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("NHH7572Mh30", libSceFsInternalForVshUnknown_NHH7572Mh30_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_NUfgeY29b9E_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("NUfgeY29b9E", libSceFsInternalForVshUnknown_NUfgeY29b9E_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_NdL2fJF8e78_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("NdL2fJF8e78", libSceFsInternalForVshUnknown_NdL2fJF8e78_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_NrSqjlYMetU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("NrSqjlYMetU", libSceFsInternalForVshUnknown_NrSqjlYMetU_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_O6akvoAE6k0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("O6akvoAE6k0", libSceFsInternalForVshUnknown_O6akvoAE6k0_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_P8F9U9uTPqA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("P8F9U9uTPqA", libSceFsInternalForVshUnknown_P8F9U9uTPqA_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_Rwk7z377rco_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("Rwk7z377rco", libSceFsInternalForVshUnknown_Rwk7z377rco_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_SyNglOzKCV8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("SyNglOzKCV8", libSceFsInternalForVshUnknown_SyNglOzKCV8_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_T3LiDkpiS7s_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("T3LiDkpiS7s", libSceFsInternalForVshUnknown_T3LiDkpiS7s_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_UD35isX9168_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("UD35isX9168", libSceFsInternalForVshUnknown_UD35isX9168_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_W8CmA_plus_wZ9_plus_w_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("W8CmA+wZ9+w", libSceFsInternalForVshUnknown_W8CmA_plus_wZ9_plus_w_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_WwEWgYYuAmo_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("WwEWgYYuAmo", libSceFsInternalForVshUnknown_WwEWgYYuAmo_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_XwPXO_plus_lxKy0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("XwPXO+lxKy0", libSceFsInternalForVshUnknown_XwPXO_plus_lxKy0_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_Y_minus_AXyBKGn8I_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("Y-AXyBKGn8I", libSceFsInternalForVshUnknown_Y_minus_AXyBKGn8I_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_Z9o0nKENoCQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("Z9o0nKENoCQ", libSceFsInternalForVshUnknown_Z9o0nKENoCQ_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_ZcrLUvsmmrU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("ZcrLUvsmmrU", libSceFsInternalForVshUnknown_ZcrLUvsmmrU_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_a5ex0HGaufY_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("a5ex0HGaufY", libSceFsInternalForVshUnknown_a5ex0HGaufY_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_b5kZJiwZPT8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("b5kZJiwZPT8", libSceFsInternalForVshUnknown_b5kZJiwZPT8_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_bGBEeJ2B6XE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("bGBEeJ2B6XE", libSceFsInternalForVshUnknown_bGBEeJ2B6XE_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_cDZDkhZeK4o_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("cDZDkhZeK4o", libSceFsInternalForVshUnknown_cDZDkhZeK4o_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_cUyUz68FKBc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("cUyUz68FKBc", libSceFsInternalForVshUnknown_cUyUz68FKBc_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_czWRa7s1ogE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("czWRa7s1ogE", libSceFsInternalForVshUnknown_czWRa7s1ogE_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_dKQBYtuMOWA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("dKQBYtuMOWA", libSceFsInternalForVshUnknown_dKQBYtuMOWA_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_eV9ctY_plus_OiOo_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("eV9ctY+OiOo", libSceFsInternalForVshUnknown_eV9ctY_plus_OiOo_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_g3Lf8TiVEBM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("g3Lf8TiVEBM", libSceFsInternalForVshUnknown_g3Lf8TiVEBM_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_gcOITai35ls_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("gcOITai35ls", libSceFsInternalForVshUnknown_gcOITai35ls_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_gerE9e47NoU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("gerE9e47NoU", libSceFsInternalForVshUnknown_gerE9e47NoU_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_gtzIEP_plus_e2jo_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("gtzIEP+e2jo", libSceFsInternalForVshUnknown_gtzIEP_plus_e2jo_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_hfb5Y906dYA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("hfb5Y906dYA", libSceFsInternalForVshUnknown_hfb5Y906dYA_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_hhPrQak7oFg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("hhPrQak7oFg", libSceFsInternalForVshUnknown_hhPrQak7oFg_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_iJQ7aolqS1A_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("iJQ7aolqS1A", libSceFsInternalForVshUnknown_iJQ7aolqS1A_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_juY2ugdopus_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("juY2ugdopus", libSceFsInternalForVshUnknown_juY2ugdopus_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_k_minus_WaWvM74jI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("k-WaWvM74jI", libSceFsInternalForVshUnknown_k_minus_WaWvM74jI_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_lZ_plus_oSeAyI8Q_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("lZ+oSeAyI8Q", libSceFsInternalForVshUnknown_lZ_plus_oSeAyI8Q_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_lxyJjALNmp4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("lxyJjALNmp4", libSceFsInternalForVshUnknown_lxyJjALNmp4_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_mvNIgoNVeWA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("mvNIgoNVeWA", libSceFsInternalForVshUnknown_mvNIgoNVeWA_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_nIMoeyl3SRY_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("nIMoeyl3SRY", libSceFsInternalForVshUnknown_nIMoeyl3SRY_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_noVGZAYgjI8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("noVGZAYgjI8", libSceFsInternalForVshUnknown_noVGZAYgjI8_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_oP09BdVEs7o_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("oP09BdVEs7o", libSceFsInternalForVshUnknown_oP09BdVEs7o_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_pt_plus_umHHWgvM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("pt+umHHWgvM", libSceFsInternalForVshUnknown_pt_plus_umHHWgvM_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_rOoqs69ugy8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("rOoqs69ugy8", libSceFsInternalForVshUnknown_rOoqs69ugy8_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_rakMRjDRGS4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("rakMRjDRGS4", libSceFsInternalForVshUnknown_rakMRjDRGS4_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_s7X_minus_JxrF650_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("s7X-JxrF650", libSceFsInternalForVshUnknown_s7X_minus_JxrF650_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_v3jcGxw1mR8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("v3jcGxw1mR8", libSceFsInternalForVshUnknown_v3jcGxw1mR8_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_v9uqOhwlhjs_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("v9uqOhwlhjs", libSceFsInternalForVshUnknown_v9uqOhwlhjs_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_yadgQseaDKA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("yadgQseaDKA", libSceFsInternalForVshUnknown_yadgQseaDKA_nid_no_patch); + +int APS5_VABI libSceFsInternalForVshUnknown_ydRWehw26Kk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("ydRWehw26Kk", libSceFsInternalForVshUnknown_ydRWehw26Kk_nid_no_patch); + +} diff --git a/core/libs/prx/libSceFsInternalForVsh/include/InventoryExports.hpp b/core/libs/prx/libSceFsInternalForVsh/include/InventoryExports.hpp new file mode 100644 index 0000000000..4209df27dd --- /dev/null +++ b/core/libs/prx/libSceFsInternalForVsh/include/InventoryExports.hpp @@ -0,0 +1,171 @@ +#pragma once + +#include "SceTypes.hpp" + +extern "C" { + +int APS5_VABI sceFsBfsAllocateAdditionalContents(); +int APS5_VABI sceFsBfsAllocateGameImage(); +int APS5_VABI sceFsBfsAllocatePatchImage(); +int APS5_VABI sceFsBfsAllocateSaveData(); +int APS5_VABI sceFsBfsAllocateTrophyData(); +int APS5_VABI sceFsBfsApplyPatch(); +int APS5_VABI sceFsBfsCancelPatch(); +int APS5_VABI sceFsBfsCommitPatch(); +int APS5_VABI sceFsBfsContainerBind(); +int APS5_VABI sceFsBfsContainerCreate(); +int APS5_VABI sceFsBfsContainerQuery(); +int APS5_VABI sceFsBfsContainerUnbind(); +int APS5_VABI sceFsBfsContainerUpdate(); +int APS5_VABI sceFsBfsExtendFile(); +int APS5_VABI sceFsBfsFormat(); +int APS5_VABI sceFsBfsFsck(); +int APS5_VABI sceFsBfsGcGetStatus(); +int APS5_VABI sceFsBfsGcMove(); +int APS5_VABI sceFsBfsGetAvailableSize(); +int APS5_VABI sceFsBfsGetBindStatus(); +int APS5_VABI sceFsBfsGetContainers(); +int APS5_VABI sceFsBfsGetFileSize(); +int APS5_VABI sceFsBfsGetMapping(); +int APS5_VABI sceFsBfsGetSCFStatus(); +int APS5_VABI sceFsBfsGetStatus(); +int APS5_VABI sceFsBfsM2Format(); +int APS5_VABI sceFsBfsMount(); +int APS5_VABI sceFsBfsTrim(); +int APS5_VABI sceFsBfsUnprotect(); +int APS5_VABI sceFsCreatePprPfsSaveDataImage(); +int APS5_VABI sceFsCreateWorkspace(); +int APS5_VABI sceFsDeleteWorkspace(); +int APS5_VABI sceFsDevpfsAllocateFile(); +int APS5_VABI sceFsDevpfsAllocateSparseFile(); +int APS5_VABI sceFsDevpfsMountForVSH(); +int APS5_VABI sceFsDevpfsSystemPowerEvent(); +int APS5_VABI sceFsDevpfsTermFetchMetaEntryCtx(); +int APS5_VABI sceFsDevpfsUmountForFSMP(); +int APS5_VABI sceFsDevpfsUmountForVSH(); +int APS5_VABI sceFsDevpfsWriteChunk(); +int APS5_VABI sceFsDrfsDestroyContext(); +int APS5_VABI sceFsDrfsDisconnect(); +int APS5_VABI sceFsDrfsInitContext(); +int APS5_VABI sceFsDrfsMount(); +int APS5_VABI sceFsDrfsSuspendSocket(); +int APS5_VABI sceFsDrfsUmount(); +int APS5_VABI sceFsDrfsUnregisterSocket(); +int APS5_VABI sceFsDrfsWaitEvent(); +int APS5_VABI sceFsFetchWorkspaceFileMetadata(); +int APS5_VABI sceFsInitCreatePprPfsSaveDataOpt(); +int APS5_VABI sceFsInitCreateWorkspaceOpt(); +int APS5_VABI sceFsInitDeleteWorkspaceOpt(); +int APS5_VABI sceFsInitMountWorkspaceOpt(); +int APS5_VABI sceFsInitUmountWorkspaceOpt(); +int APS5_VABI sceFsIsActiveBankSystemEx(); +int APS5_VABI sceFsIsActiveBankSystem(); +int APS5_VABI sceFsM2StorageCheckCleanFlag(); +int APS5_VABI sceFsM2StorageDeconfigure(); +int APS5_VABI sceFsM2StorageFsckWithMM(); +int APS5_VABI sceFsM2StorageGetM2UniqueId(); +int APS5_VABI sceFsM2StorageInitialize(); +int APS5_VABI sceFsM2StorageMount(); +int APS5_VABI sceFsM2StorageUnmount(); +int APS5_VABI sceFsM2StorageVerifyMetadata(); +int APS5_VABI sceFsMountPprSaveData(); +int APS5_VABI sceFsMountWorkspace(); +int APS5_VABI sceFsNewfsDefault(); +int APS5_VABI sceFsNsfsMount(); +int APS5_VABI sceFsPartitionSwitchBank(); +int APS5_VABI sceFsPathMappingResolve(); +int APS5_VABI sceFsPlaygoDisable(); +int APS5_VABI sceFsPlaygoEnable(); +int APS5_VABI sceFsPlaygoInitializeParams(); +int APS5_VABI sceFsPlaygoSetParams(); +int APS5_VABI sceFsTransactionCommit(); +int APS5_VABI sceFsTransactionPprNoCowSaveDataMount(); +int APS5_VABI sceFsTransactionPprSaveDataMount(); +int APS5_VABI sceFsTransactionSaveDataReadParam(); +int APS5_VABI sceFsTransactionSaveDataUmount(); +int APS5_VABI sceFsTransactionSaveDataWriteParam(); +int APS5_VABI sceFsUfsCoerceCryptMode(); +int APS5_VABI sceFsUfsMkfsProsperoApplicationTemporaryData(); +int APS5_VABI sceFsUfsMkfsProsperoDownloadData(); +int APS5_VABI sceFsUmountPprPkg(); +int APS5_VABI sceFsUpdatePackageMetadataApp0(); +int APS5_VABI sceFsWorkspaceCoreDump(); +int APS5_VABI sceFsWsWriteFile(); +int APS5_VABI sceFsWsWriteFileExtdata(); +int APS5_VABI libSceFsInternalForVshUnknown__plus_gVdjPejLwc_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown__minus_HW6neCER_plus_A_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_0JhonchgIa8_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_0XlyVxRNeIw_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_1lLh9mukbz0_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_2UgTmJt2dsc_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_3TFUof4RLWM_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_41PqBeun5cU_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_4uqQOvVpISA_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_68F1C7m2pO4_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_6Bku_plus_Im1Pew_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_89zs8_plus_EWQYQ_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_9X3ydcDhbhw_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_AnMWzsLhsYk_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_CFlYeKz_plus_HYs_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_DEHwNbsX2BA_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_DNjr3qYr_minus_lY_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_DSmBrz8enws_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_Elqj7jkJd5s_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_GNeCtmqY_minus_58_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_GsHxp1IXkfA_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_KesAmkrIb9Y_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_KrZEB_plus_TsZ5k_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_LGPQXeAAFdc_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_LrpsS5hGlyY_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_M9342PuS_plus_y0_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_MQs7RB_minus_KfYM_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_N7SnQKgSGA4_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_NHH7572Mh30_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_NUfgeY29b9E_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_NdL2fJF8e78_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_NrSqjlYMetU_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_O6akvoAE6k0_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_P8F9U9uTPqA_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_Rwk7z377rco_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_SyNglOzKCV8_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_T3LiDkpiS7s_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_UD35isX9168_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_W8CmA_plus_wZ9_plus_w_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_WwEWgYYuAmo_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_XwPXO_plus_lxKy0_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_Y_minus_AXyBKGn8I_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_Z9o0nKENoCQ_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_ZcrLUvsmmrU_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_a5ex0HGaufY_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_b5kZJiwZPT8_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_bGBEeJ2B6XE_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_cDZDkhZeK4o_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_cUyUz68FKBc_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_czWRa7s1ogE_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_dKQBYtuMOWA_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_eV9ctY_plus_OiOo_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_g3Lf8TiVEBM_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_gcOITai35ls_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_gerE9e47NoU_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_gtzIEP_plus_e2jo_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_hfb5Y906dYA_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_hhPrQak7oFg_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_iJQ7aolqS1A_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_juY2ugdopus_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_k_minus_WaWvM74jI_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_lZ_plus_oSeAyI8Q_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_lxyJjALNmp4_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_mvNIgoNVeWA_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_nIMoeyl3SRY_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_noVGZAYgjI8_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_oP09BdVEs7o_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_pt_plus_umHHWgvM_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_rOoqs69ugy8_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_rakMRjDRGS4_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_s7X_minus_JxrF650_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_v3jcGxw1mR8_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_v9uqOhwlhjs_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_yadgQseaDKA_nid_no_patch(); +int APS5_VABI libSceFsInternalForVshUnknown_ydRWehw26Kk_nid_no_patch(); + +} diff --git a/core/libs/prx/libSceFsInternalForVsh/libSceFsInternalForVsh.hpp b/core/libs/prx/libSceFsInternalForVsh/libSceFsInternalForVsh.hpp index bba8925b6e..65d7230487 100644 --- a/core/libs/prx/libSceFsInternalForVsh/libSceFsInternalForVsh.hpp +++ b/core/libs/prx/libSceFsInternalForVsh/libSceFsInternalForVsh.hpp @@ -1,3 +1,4 @@ #pragma once #include "prx/libSceFsInternalForVsh/include/Api.hpp" +#include "prx/libSceFsInternalForVsh/include/InventoryExports.hpp" diff --git a/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp b/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp index d620fa5a3f..11c37236a8 100644 --- a/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp +++ b/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp @@ -352,8 +352,8 @@ int APS5_VABI sceGnmDrawInitToDefaultContextStateInternalCommand(std::uint32_t* } int APS5_VABI sceGnmDrawInitToDefaultContextStateInternalSize(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + if (CompiledSdkVersion() < ContextState400SdkVersion) return 0x20; + return 0x100; } int APS5_VABI sceGnmDrawOpaqueAuto(void) { diff --git a/core/libs/prx/libSceHmd2/CMakeLists.txt b/core/libs/prx/libSceHmd2/CMakeLists.txt index ee0e0ffd8c..8c6c0c183d 100644 --- a/core/libs/prx/libSceHmd2/CMakeLists.txt +++ b/core/libs/prx/libSceHmd2/CMakeLists.txt @@ -26,3 +26,16 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endif() + +if(BUILD_TESTING) + add_test_executable(guest_hmd2_gaze_args_tests tests/GazeArgumentChecks.cpp) + target_include_directories(guest_hmd2_gaze_args_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_hmd2_gaze_args_tests PRIVATE libSceHmd2 libc) + configure_windows_unwind(guest_hmd2_gaze_args_tests) + add_test(NAME guest_hmd2_gaze_args COMMAND guest_hmd2_gaze_args_tests) + set_tests_properties(guest_hmd2_gaze_args PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_hmd2_gaze_args 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 b8a719f448..78fe4c0b2c 100644 --- a/core/libs/prx/libSceHmd2/Export.cpp +++ b/core/libs/prx/libSceHmd2/Export.cpp @@ -237,7 +237,12 @@ int APS5_VABI sceHmd2ReprojectionDisableVrMode() { } int APS5_VABI sceHmd2ReprojectionEnableMirroring(const Hmd2::MirroringParameter* parameter) { - return Hmd2::ReprojectionNotInitialized; + { + std::lock_guard lock(reprojectionMutex); + if (reprojectionState == nullptr) return Hmd2::ReprojectionNotInitialized; + } + NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI sceHmd2ReprojectionEnableVrMode(std::uint64_t mode) { @@ -258,7 +263,13 @@ Hmd2::SizeAlign APS5_VABI sceHmd2ReprojectionGetMirroringWorkMemorySizeAlign() { } int APS5_VABI sceHmd2ReprojectionGetPredictedDisplayTime(std::uint64_t frameId, std::uint64_t* time) { - return Hmd2::ReprojectionNotInitialized; + { + std::lock_guard lock(reprojectionMutex); + if (reprojectionState == nullptr) return Hmd2::ReprojectionNotInitialized; + } + if (time == nullptr) return Hmd2::ParameterNull; + NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI sceHmd2ReprojectionInitialize(const Hmd2::ReprojectionInitializeParameter* parameter, const void* reserved) { @@ -318,7 +329,13 @@ int APS5_VABI sceHmd2ReprojectionSetParamWithBuffer(std::uint32_t bufferSetId, s } int APS5_VABI sceHmd2ReprojectionSetRenderConfig(std::uint64_t flags, std::uint64_t reserved, std::uint64_t reserved2) { - return Hmd2::ReprojectionNotInitialized; + { + std::lock_guard lock(reprojectionMutex); + if (reprojectionState == nullptr) return Hmd2::ReprojectionNotInitialized; + } + if ((flags & ~(1ull | (1ull << 32))) != 0 || reserved != 0 || reserved2 != 0) return Hmd2::ParameterInvalid; + NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI sceHmd2ReprojectionSetTiming(std::int32_t timing) { diff --git a/core/libs/prx/libSceHmd2/tests/GazeArgumentChecks.cpp b/core/libs/prx/libSceHmd2/tests/GazeArgumentChecks.cpp new file mode 100644 index 0000000000..9a06d93385 --- /dev/null +++ b/core/libs/prx/libSceHmd2/tests/GazeArgumentChecks.cpp @@ -0,0 +1,74 @@ +#include "prx/libSceHmd2/include/Api.hpp" +#include "prx/libSceHmd2/Hmd2.hpp" + +#include +#include +#include + +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); + +constexpr std::uint8_t Sentinel = 0x5a; +constexpr std::size_t ProbeSize = 16 * 1024; + +void Require(bool value, const char* what) { + if (value) return; + std::fprintf(stderr, "FAIL: %s\n", what); + std::abort(); +} + +struct Probe { + unsigned char* bytes; + Probe() : bytes(new unsigned char[ProbeSize]) { + for (std::size_t index = 0; index < ProbeSize; ++index) bytes[index] = Sentinel; + } + ~Probe() { delete[] bytes; } + bool Untouched() const { + for (std::size_t index = 0; index < ProbeSize; ++index) { + if (bytes[index] != Sentinel) return false; + } + return true; + } +}; + +} + +int main() { + Probe first; + Probe second; + Require(sceHmd2GazeObjectSelectionQueryMemory(nullptr, nullptr) == ParameterNull, "query memory with null arguments"); + Require(sceHmd2GazeObjectSelectionQueryMemory(nullptr, reinterpret_cast(second.bytes)) == ParameterNull, + "query memory with a null parameter"); + Require(second.Untouched(), "query memory left the output buffer untouched with a null parameter"); + Require(sceHmd2GazeObjectSelectionQueryMemory(reinterpret_cast(first.bytes), nullptr) == ParameterNull, + "query memory with a null info"); + Require(first.Untouched(), "query memory left the parameter buffer untouched with a null info"); + Require(sceHmd2GazeObjectSelectionQueryMemory(reinterpret_cast(first.bytes), + reinterpret_cast(second.bytes)) == ParameterInvalid, + "query memory before initialization with present arguments"); + Require(first.Untouched() && second.Untouched(), "query memory wrote nothing before initialization"); + + Require(sceHmd2GazeObjectSelectionGetSelectedObject(nullptr, nullptr) == ParameterNull, "get selected object with null arguments"); + Require(sceHmd2GazeObjectSelectionGetSelectedObject(nullptr, reinterpret_cast(second.bytes)) == ParameterNull, + "get selected object with a null parameter"); + Require(sceHmd2GazeObjectSelectionGetSelectedObject(reinterpret_cast(first.bytes), nullptr) == ParameterNull, + "get selected object with a null result"); + Require(sceHmd2GazeObjectSelectionGetSelectedObject(reinterpret_cast(first.bytes), + reinterpret_cast(second.bytes)) == ParameterInvalid, + "get selected object before initialization with present arguments"); + Require(first.Untouched() && second.Untouched(), "get selected object wrote nothing before initialization"); + + Require(sceHmd2GazeGetCalibrationDataMaxSize(0, nullptr) == ParameterNull, "calibration data max size with a null output"); + Require(sceHmd2GazeGetCalibrationDataMaxSize(0, reinterpret_cast(first.bytes)) == ParameterInvalid, + "calibration data max size before initialization with a present output"); + Require(first.Untouched(), "calibration data max size wrote nothing before initialization"); + + Require(sceHmd2GazeInsertRawDataWithHandle(0, nullptr) == ParameterNull, "insert raw data with a null buffer"); + Require(sceHmd2GazeInsertRawDataWithHandle(0, reinterpret_cast(first.bytes)) == InvalidHandle, + "insert raw data before initialization with a present buffer"); + Require(first.Untouched(), "insert raw data wrote nothing before initialization"); + return 0; +} diff --git a/core/libs/prx/libSceHmdSetupDialog/Export.cpp b/core/libs/prx/libSceHmdSetupDialog/Export.cpp index 156643bf24..a5985d5a03 100644 --- a/core/libs/prx/libSceHmdSetupDialog/Export.cpp +++ b/core/libs/prx/libSceHmdSetupDialog/Export.cpp @@ -32,11 +32,12 @@ int APS5_VABI sceHmdSetupDialogInitialize(void) { } int APS5_VABI sceHmdSetupDialogOpen(const void* param) { - 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_BUSY; - if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; - g_status = COMMON_DIALOG_STATUS_FINISHED; + int status = g_status.load(); + do { + if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (status == COMMON_DIALOG_STATUS_RUNNING) return COMMON_DIALOG_ERROR_BUSY; + if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + } while (!g_status.compare_exchange_weak(status, COMMON_DIALOG_STATUS_FINISHED)); return 0; } @@ -59,8 +60,10 @@ int APS5_VABI sceHmdSetupDialogGetResult(void* result) { } int APS5_VABI sceHmdSetupDialogClose(void) { - if (g_status.load() == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; - g_status = COMMON_DIALOG_STATUS_FINISHED; + int status = g_status.load(); + do { + if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + } while (!g_status.compare_exchange_weak(status, COMMON_DIALOG_STATUS_FINISHED)); return 0; } diff --git a/core/libs/prx/libSceHttp2/Transport.cpp b/core/libs/prx/libSceHttp2/Transport.cpp index 0ce4beb61a..ef4f845543 100644 --- a/core/libs/prx/libSceHttp2/Transport.cpp +++ b/core/libs/prx/libSceHttp2/Transport.cpp @@ -280,8 +280,10 @@ int AddHeader(Settings& settings, const char* name, const char* value, std::uint "sceHttp2AddRequestHeader: invalid header name"); Require(nghttp2_check_header_value_rfc9113(reinterpret_cast(value), std::strlen(value)), "sceHttp2AddRequestHeader: invalid header value"); + if (key == "host") Require(value[0] != '\0' && std::string_view(value).find_first_of(" \t") == std::string_view::npos, + "sceHttp2AddRequestHeader: invalid host header value"); Require(key != "connection" && key != "proxy-connection" && key != "keep-alive" && key != "upgrade" && - key != "transfer-encoding" && key != "host" && key != "content-length" && + key != "transfer-encoding" && key != "content-length" && (key != "te" || std::string_view(value) == "trailers"), "sceHttp2AddRequestHeader: unsupported HTTP/2 control header"); auto updated = settings.headers; if (mode == 0) std::erase_if(updated, [&](const auto& header) { return header.first == key; }); @@ -603,11 +605,13 @@ struct Request::State { CheckNg(nghttp2_session_client_new2(&session, raw, this, optionsRaw)); const nghttp2_settings_entry settingsEntry{NGHTTP2_SETTINGS_ENABLE_PUSH, 0}; CheckNg(nghttp2_submit_settings(session, NGHTTP2_FLAG_NONE, &settingsEntry, 1)); + std::string authority = url.authority; + for (const auto& [name, value] : settings.headers) if (name == "host") authority = value; std::vector> fields{ - {":method", method}, {":scheme", "https"}, {":authority", url.authority}, {":path", url.path}}; + {":method", method}, {":scheme", "https"}, {":authority", authority}, {":path", url.path}}; if (!settings.userAgent.empty() && std::none_of(settings.headers.begin(), settings.headers.end(), [](const auto& header) { return header.first == "user-agent"; })) fields.emplace_back("user-agent", settings.userAgent); - fields.insert(fields.end(), settings.headers.begin(), settings.headers.end()); + for (const auto& header : settings.headers) if (header.first != "host") fields.push_back(header); if (!settings.noContentLength) fields.emplace_back("content-length", std::to_string(settings.contentLength)); std::vector headers; for (auto& [name, value] : fields) headers.push_back({reinterpret_cast(name.data()), diff --git a/core/libs/prx/libSceIcu/CMakeLists.txt b/core/libs/prx/libSceIcu/CMakeLists.txt index 3b1804f26d..b67f4053a4 100644 --- a/core/libs/prx/libSceIcu/CMakeLists.txt +++ b/core/libs/prx/libSceIcu/CMakeLists.txt @@ -1,5 +1,6 @@ -add_library(libSceIcu SHARED EXCLUDE_FROM_ALL Export.cpp Invariant.cpp Version.cpp Unescape.cpp Tokenize.cpp) +add_library(libSceIcu SHARED EXCLUDE_FROM_ALL ConstrainedFieldPosition.cpp ErrorName.cpp Export.cpp Invariant.cpp Numeric.cpp Tokenize.cpp Unescape.cpp Version.cpp) target_include_directories(libSceIcu PRIVATE ${LIBS_INCLUDE_DIR}) +target_sources(libSceIcu PRIVATE RecoveredInventory.cpp) configure_windows_unwind(libSceIcu) target_sources(libSceIcu PRIVATE ConverterCallbacks.cpp) target_sources(libSceIcu PRIVATE LegacyExports.cpp) @@ -55,8 +56,26 @@ if(BUILD_TESTING) configure_windows_unwind(guest_icu_version_tests) add_test(NAME guest_icu_version COMMAND guest_icu_version_tests) set_tests_properties(guest_icu_version PROPERTIES TIMEOUT 10) + add_test_executable(guest_icu_numeric_tests ${LIBS_INCLUDE_DIR}/tests/GuestIcuNumeric.cpp) + target_include_directories(guest_icu_numeric_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_icu_numeric_tests PRIVATE libSceIcu libc) + configure_windows_unwind(guest_icu_numeric_tests) + add_test(NAME guest_icu_numeric COMMAND guest_icu_numeric_tests) + set_tests_properties(guest_icu_numeric PROPERTIES TIMEOUT 10) + add_test_executable(guest_icu_error_name_tests ${LIBS_INCLUDE_DIR}/tests/GuestIcuErrorName.cpp) + target_include_directories(guest_icu_error_name_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_icu_error_name_tests PRIVATE libSceIcu libc) + configure_windows_unwind(guest_icu_error_name_tests) + add_test(NAME guest_icu_error_name COMMAND guest_icu_error_name_tests) + set_tests_properties(guest_icu_error_name PROPERTIES TIMEOUT 10) + add_test_executable(guest_icu_constrained_field_position_tests ${LIBS_INCLUDE_DIR}/tests/GuestIcuConstrainedFieldPosition.cpp) + target_include_directories(guest_icu_constrained_field_position_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_icu_constrained_field_position_tests PRIVATE libSceIcu libc) + configure_windows_unwind(guest_icu_constrained_field_position_tests) + add_test(NAME guest_icu_constrained_field_position COMMAND guest_icu_constrained_field_position_tests) + set_tests_properties(guest_icu_constrained_field_position PROPERTIES TIMEOUT 10) if(WIN32) - set_tests_properties(guest_icu_version PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_icu_version guest_icu_numeric guest_icu_error_name guest_icu_constrained_field_position PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/prx/libSceIcu/ConstrainedFieldPosition.cpp b/core/libs/prx/libSceIcu/ConstrainedFieldPosition.cpp new file mode 100644 index 0000000000..e1ece68921 --- /dev/null +++ b/core/libs/prx/libSceIcu/ConstrainedFieldPosition.cpp @@ -0,0 +1,105 @@ +#include "prx/libSceIcu/libSceIcu.hpp" +#include "prx/libc/include/General.hpp" +#include +#include +#include + +namespace { + +constexpr std::int32_t Magic = 0x55434600; +constexpr std::int32_t IllegalArgumentError = 1; +constexpr std::int32_t InvalidFormatError = 3; +constexpr std::int32_t MemoryAllocationError = 7; +constexpr std::int32_t CategoryUndefined = 0; + +enum class Constraint : std::int8_t { + None, + Category, + Field +}; + +struct Position { + std::int32_t magic = Magic; + std::int32_t field = 0; + std::int32_t start = 0; + std::int32_t limit = 0; + std::int32_t category = CategoryUndefined; + Constraint constraint = Constraint::None; +}; + +Position* Validate(const UConstrainedFieldPosition* handle, std::int32_t& error) { + if (error > 0) return nullptr; + if (!handle) { + error = IllegalArgumentError; + return nullptr; + } + std::int32_t magic = 0; + std::memcpy(&magic, handle, sizeof(magic)); + if (magic != Magic) { + error = InvalidFormatError; + return nullptr; + } + return reinterpret_cast(const_cast(handle)); +} + +Position* Validate(const UConstrainedFieldPosition* handle, std::int32_t* error) { + if (!error) APS5_INVALID_ARG_EX; + return Validate(handle, *error); +} + +} + +extern "C" { + +UConstrainedFieldPosition* APS5_VABI ucfpos_open_nid_postfix(std::int32_t* error) { + auto* position = new (std::nothrow) Position(); + if (!position) { + if (!error) APS5_INVALID_ARG_EX; + *error = MemoryAllocationError; + return nullptr; + } + return reinterpret_cast(position); +} + +void APS5_VABI ucfpos_close_nid_postfix(UConstrainedFieldPosition* handle) { + std::int32_t error = 0; + auto* position = Validate(handle, error); + if (!position) return; + position->magic = 0; + delete position; +} + +void APS5_VABI ucfpos_constrainCategory_nid_postfix(UConstrainedFieldPosition* handle, std::int32_t category, std::int32_t* error) { + auto* position = Validate(handle, error); + if (!position) return; + position->constraint = Constraint::Category; + position->category = category; +} + +void APS5_VABI ucfpos_constrainField_nid_postfix(UConstrainedFieldPosition* handle, std::int32_t category, std::int32_t field, std::int32_t* error) { + auto* position = Validate(handle, error); + if (!position) return; + position->constraint = Constraint::Field; + position->category = category; + position->field = field; +} + +std::int32_t APS5_VABI ucfpos_getCategory_nid_postfix(const UConstrainedFieldPosition* handle, std::int32_t* error) { + const auto* position = Validate(handle, error); + return position ? position->category : CategoryUndefined; +} + +std::int32_t APS5_VABI ucfpos_getField_nid_postfix(const UConstrainedFieldPosition* handle, std::int32_t* error) { + const auto* position = Validate(handle, error); + return position ? position->field : 0; +} + +void APS5_VABI ucfpos_getIndexes_nid_postfix(const UConstrainedFieldPosition* handle, std::int32_t* start, std::int32_t* limit, std::int32_t* error) { + const auto* position = Validate(handle, error); + if (!position) return; + if (!start || !limit) APS5_INVALID_ARG_EX; + *start = position->start; + *limit = position->limit; +} + +} diff --git a/core/libs/prx/libSceIcu/ConstrainedFieldPosition.hpp b/core/libs/prx/libSceIcu/ConstrainedFieldPosition.hpp new file mode 100644 index 0000000000..15778f9894 --- /dev/null +++ b/core/libs/prx/libSceIcu/ConstrainedFieldPosition.hpp @@ -0,0 +1,15 @@ +#pragma once +#include "prx/libc/include/general/VabiMacros.hpp" +#include + +struct UConstrainedFieldPosition; + +extern "C" { +UConstrainedFieldPosition* APS5_VABI ucfpos_open_nid_postfix(std::int32_t*); +void APS5_VABI ucfpos_close_nid_postfix(UConstrainedFieldPosition*); +void APS5_VABI ucfpos_constrainCategory_nid_postfix(UConstrainedFieldPosition*, std::int32_t, std::int32_t*); +void APS5_VABI ucfpos_constrainField_nid_postfix(UConstrainedFieldPosition*, std::int32_t, std::int32_t, std::int32_t*); +std::int32_t APS5_VABI ucfpos_getCategory_nid_postfix(const UConstrainedFieldPosition*, std::int32_t*); +std::int32_t APS5_VABI ucfpos_getField_nid_postfix(const UConstrainedFieldPosition*, std::int32_t*); +void APS5_VABI ucfpos_getIndexes_nid_postfix(const UConstrainedFieldPosition*, std::int32_t*, std::int32_t*, std::int32_t*); +} diff --git a/core/libs/prx/libSceIcu/ErrorName.cpp b/core/libs/prx/libSceIcu/ErrorName.cpp new file mode 100644 index 0000000000..065074ec38 --- /dev/null +++ b/core/libs/prx/libSceIcu/ErrorName.cpp @@ -0,0 +1,217 @@ +#include "prx/libSceIcu/libSceIcu.hpp" +#include +#include +#include +#include + +namespace { + +constexpr std::array WarningNames{ + "U_USING_FALLBACK_WARNING", + "U_USING_DEFAULT_WARNING", + "U_SAFECLONE_ALLOCATED_WARNING", + "U_STATE_OLD_WARNING", + "U_STRING_NOT_TERMINATED_WARNING", + "U_SORT_KEY_TOO_SHORT_WARNING", + "U_AMBIGUOUS_ALIAS_WARNING", + "U_DIFFERENT_UCA_VERSION", + "U_PLUGIN_CHANGED_LEVEL_WARNING" +}; + +constexpr std::array StandardNames{ + "U_ZERO_ERROR", + "U_ILLEGAL_ARGUMENT_ERROR", + "U_MISSING_RESOURCE_ERROR", + "U_INVALID_FORMAT_ERROR", + "U_FILE_ACCESS_ERROR", + "U_INTERNAL_PROGRAM_ERROR", + "U_MESSAGE_PARSE_ERROR", + "U_MEMORY_ALLOCATION_ERROR", + "U_INDEX_OUTOFBOUNDS_ERROR", + "U_PARSE_ERROR", + "U_INVALID_CHAR_FOUND", + "U_TRUNCATED_CHAR_FOUND", + "U_ILLEGAL_CHAR_FOUND", + "U_INVALID_TABLE_FORMAT", + "U_INVALID_TABLE_FILE", + "U_BUFFER_OVERFLOW_ERROR", + "U_UNSUPPORTED_ERROR", + "U_RESOURCE_TYPE_MISMATCH", + "U_ILLEGAL_ESCAPE_SEQUENCE", + "U_UNSUPPORTED_ESCAPE_SEQUENCE", + "U_NO_SPACE_AVAILABLE", + "U_CE_NOT_FOUND_ERROR", + "U_PRIMARY_TOO_LONG_ERROR", + "U_STATE_TOO_OLD_ERROR", + "U_TOO_MANY_ALIASES_ERROR", + "U_ENUM_OUT_OF_SYNC_ERROR", + "U_INVARIANT_CONVERSION_ERROR", + "U_INVALID_STATE_ERROR", + "U_COLLATOR_VERSION_MISMATCH", + "U_USELESS_COLLATOR_ERROR", + "U_NO_WRITE_PERMISSION", + "U_INPUT_TOO_LONG_ERROR" +}; + +constexpr std::array TransliteratorNames{ + "U_BAD_VARIABLE_DEFINITION", + "U_MALFORMED_RULE", + "U_MALFORMED_SET", + "U_MALFORMED_SYMBOL_REFERENCE", + "U_MALFORMED_UNICODE_ESCAPE", + "U_MALFORMED_VARIABLE_DEFINITION", + "U_MALFORMED_VARIABLE_REFERENCE", + "U_MISMATCHED_SEGMENT_DELIMITERS", + "U_MISPLACED_ANCHOR_START", + "U_MISPLACED_CURSOR_OFFSET", + "U_MISPLACED_QUANTIFIER", + "U_MISSING_OPERATOR", + "U_MISSING_SEGMENT_CLOSE", + "U_MULTIPLE_ANTE_CONTEXTS", + "U_MULTIPLE_CURSORS", + "U_MULTIPLE_POST_CONTEXTS", + "U_TRAILING_BACKSLASH", + "U_UNDEFINED_SEGMENT_REFERENCE", + "U_UNDEFINED_VARIABLE", + "U_UNQUOTED_SPECIAL", + "U_UNTERMINATED_QUOTE", + "U_RULE_MASK_ERROR", + "U_MISPLACED_COMPOUND_FILTER", + "U_MULTIPLE_COMPOUND_FILTERS", + "U_INVALID_RBT_SYNTAX", + "U_INVALID_PROPERTY_PATTERN", + "U_MALFORMED_PRAGMA", + "U_UNCLOSED_SEGMENT", + "U_ILLEGAL_CHAR_IN_SEGMENT", + "U_VARIABLE_RANGE_EXHAUSTED", + "U_VARIABLE_RANGE_OVERLAP", + "U_ILLEGAL_CHARACTER", + "U_INTERNAL_TRANSLITERATOR_ERROR", + "U_INVALID_ID", + "U_INVALID_FUNCTION" +}; + +constexpr std::array FormatNames{ + "U_UNEXPECTED_TOKEN", + "U_MULTIPLE_DECIMAL_SEPARATORS", + "U_MULTIPLE_EXPONENTIAL_SYMBOLS", + "U_MALFORMED_EXPONENTIAL_PATTERN", + "U_MULTIPLE_PERCENT_SYMBOLS", + "U_MULTIPLE_PERMILL_SYMBOLS", + "U_MULTIPLE_PAD_SPECIFIERS", + "U_PATTERN_SYNTAX_ERROR", + "U_ILLEGAL_PAD_POSITION", + "U_UNMATCHED_BRACES", + "U_UNSUPPORTED_PROPERTY", + "U_UNSUPPORTED_ATTRIBUTE", + "U_ARGUMENT_TYPE_MISMATCH", + "U_DUPLICATE_KEYWORD", + "U_UNDEFINED_KEYWORD", + "U_DEFAULT_KEYWORD_MISSING", + "U_DECIMAL_NUMBER_SYNTAX_ERROR", + "U_FORMAT_INEXACT_ERROR", + "U_NUMBER_ARG_OUTOFBOUNDS_ERROR", + "U_NUMBER_SKELETON_SYNTAX_ERROR", + "U_MF_UNRESOLVED_VARIABLE_ERROR", + "U_MF_SYNTAX_ERROR", + "U_MF_UNKNOWN_FUNCTION_ERROR", + "U_MF_VARIANT_KEY_MISMATCH_ERROR", + "U_MF_FORMATTING_ERROR", + "U_MF_NONEXHAUSTIVE_PATTERN_ERROR", + "U_MF_DUPLICATE_OPTION_NAME_ERROR", + "U_MF_SELECTOR_ERROR", + "U_MF_MISSING_SELECTOR_ANNOTATION_ERROR", + "U_MF_DUPLICATE_DECLARATION_ERROR", + "U_MF_OPERAND_MISMATCH_ERROR", + "U_MF_DUPLICATE_VARIANT_ERROR", + "U_MF_BAD_OPTION" +}; + +constexpr std::array BreakIteratorNames{ + "U_BRK_INTERNAL_ERROR", + "U_BRK_HEX_DIGITS_EXPECTED", + "U_BRK_SEMICOLON_EXPECTED", + "U_BRK_RULE_SYNTAX", + "U_BRK_UNCLOSED_SET", + "U_BRK_ASSIGN_ERROR", + "U_BRK_VARIABLE_REDFINITION", + "U_BRK_MISMATCHED_PAREN", + "U_BRK_NEW_LINE_IN_QUOTED_STRING", + "U_BRK_UNDEFINED_VARIABLE", + "U_BRK_INIT_ERROR", + "U_BRK_RULE_EMPTY_SET", + "U_BRK_UNRECOGNIZED_OPTION", + "U_BRK_MALFORMED_RULE_TAG" +}; + +constexpr std::array RegexNames{ + "U_REGEX_INTERNAL_ERROR", + "U_REGEX_RULE_SYNTAX", + "U_REGEX_INVALID_STATE", + "U_REGEX_BAD_ESCAPE_SEQUENCE", + "U_REGEX_PROPERTY_SYNTAX", + "U_REGEX_UNIMPLEMENTED", + "U_REGEX_MISMATCHED_PAREN", + "U_REGEX_NUMBER_TOO_BIG", + "U_REGEX_BAD_INTERVAL", + "U_REGEX_MAX_LT_MIN", + "U_REGEX_INVALID_BACK_REF", + "U_REGEX_INVALID_FLAG", + "U_REGEX_LOOK_BEHIND_LIMIT", + "U_REGEX_SET_CONTAINS_STRING", + "U_REGEX_OCTAL_TOO_BIG", + "U_REGEX_MISSING_CLOSE_BRACKET", + "U_REGEX_INVALID_RANGE", + "U_REGEX_STACK_OVERFLOW", + "U_REGEX_TIME_OUT", + "U_REGEX_STOPPED_BY_CALLER", + "U_REGEX_PATTERN_TOO_BIG", + "U_REGEX_INVALID_CAPTURE_GROUP_NAME" +}; + +constexpr std::array IdnaNames{ + "U_STRINGPREP_PROHIBITED_ERROR", + "U_STRINGPREP_UNASSIGNED_ERROR", + "U_STRINGPREP_CHECK_BIDI_ERROR", + "U_IDNA_STD3_ASCII_RULES_ERROR", + "U_IDNA_ACE_PREFIX_ERROR", + "U_IDNA_VERIFICATION_ERROR", + "U_IDNA_LABEL_TOO_LONG_ERROR", + "U_IDNA_ZERO_LENGTH_LABEL_ERROR", + "U_IDNA_DOMAIN_NAME_TOO_LONG_ERROR" +}; + +constexpr std::array PluginNames{ + "U_PLUGIN_TOO_HIGH", + "U_PLUGIN_DIDNT_SET_LEVEL" +}; + +struct ErrorRange { + std::int64_t start; + std::span names; +}; + +constexpr std::array ErrorRanges{{ + {-128, WarningNames}, + {0, StandardNames}, + {0x10000, TransliteratorNames}, + {0x10100, FormatNames}, + {0x10200, BreakIteratorNames}, + {0x10300, RegexNames}, + {0x10400, IdnaNames}, + {0x10500, PluginNames} +}}; + +} + +extern "C" { + +const char* APS5_VABI u_errorName_nid_postfix(std::int32_t code) { + for (const auto& range : ErrorRanges) { + const std::int64_t index = code - range.start; + if (index >= 0 && index < static_cast(range.names.size())) return range.names[static_cast(index)]; + } + return "[BOGUS UErrorCode]"; +} + +} diff --git a/core/libs/prx/libSceIcu/ErrorName.hpp b/core/libs/prx/libSceIcu/ErrorName.hpp new file mode 100644 index 0000000000..0fb50447aa --- /dev/null +++ b/core/libs/prx/libSceIcu/ErrorName.hpp @@ -0,0 +1,7 @@ +#pragma once +#include "prx/libc/include/general/VabiMacros.hpp" +#include + +extern "C" { +const char* APS5_VABI u_errorName_nid_postfix(std::int32_t); +} diff --git a/core/libs/prx/libSceIcu/Numeric.cpp b/core/libs/prx/libSceIcu/Numeric.cpp new file mode 100644 index 0000000000..ce6e610c5e --- /dev/null +++ b/core/libs/prx/libSceIcu/Numeric.cpp @@ -0,0 +1,137 @@ +#include "prx/libSceIcu/libSceIcu.hpp" +#include "prx/libc/include/Api.hpp" +#include "prx/libc/include/General.hpp" +#include +#include +#include +#include + +namespace { + +constexpr std::int64_t InfinityBits = 0x7ff0000000000000; +constexpr std::int64_t NaNBits = 0x7ff8000000000000; + +std::int64_t Magnitude(double value) { + return std::bit_cast(value) & std::numeric_limits::max(); +} + +bool SignBit(double value) { + return std::bit_cast(value) < 0; +} + +} + +extern "C" { + +std::int8_t APS5_VABI uprv_isNaN_nid_postfix(double number) { + return Magnitude(number) > InfinityBits; +} + +std::int8_t APS5_VABI uprv_isInfinite_nid_postfix(double number) { + return Magnitude(number) == InfinityBits; +} + +std::int8_t APS5_VABI uprv_isPositiveInfinity_nid_postfix(double number) { + return number > 0 && uprv_isInfinite_nid_postfix(number); +} + +std::int8_t APS5_VABI uprv_isNegativeInfinity_nid_postfix(double number) { + return number < 0 && uprv_isInfinite_nid_postfix(number); +} + +double APS5_VABI uprv_getNaN_nid_postfix() { + return std::bit_cast(NaNBits); +} + +double APS5_VABI uprv_getInfinity_nid_postfix() { + return std::bit_cast(InfinityBits); +} + +double APS5_VABI uprv_floor_nid_postfix(double x) { + return floor_nid_postfix(x); +} + +double APS5_VABI uprv_ceil_nid_postfix(double x) { + return ceil_nid_postfix(x); +} + +double APS5_VABI uprv_round_nid_postfix(double x) { + return uprv_floor_nid_postfix(x + 0.5); +} + +double APS5_VABI uprv_fabs_nid_postfix(double x) { + return fabs_nid_postfix(x); +} + +double APS5_VABI uprv_modf_nid_postfix(double x, double* y) { + if (!y) APS5_INVALID_ARG_EX; + return modf_nid_postfix(x, y); +} + +double APS5_VABI uprv_fmod_nid_postfix(double x, double y) { + return fmod_nid_postfix(x, y); +} + +double APS5_VABI uprv_pow_nid_postfix(double x, double y) { + return pow_nid_postfix(x, y); +} + +double APS5_VABI uprv_pow10_nid_postfix(std::int32_t x) { + return pow_nid_postfix(10.0, static_cast(x)); +} + +double APS5_VABI uprv_fmax_nid_postfix(double x, double y) { + if (uprv_isNaN_nid_postfix(x) || uprv_isNaN_nid_postfix(y)) return uprv_getNaN_nid_postfix(); + if (x == 0.0 && y == 0.0 && SignBit(x)) return y; + return x > y ? x : y; +} + +double APS5_VABI uprv_fmin_nid_postfix(double x, double y) { + if (uprv_isNaN_nid_postfix(x) || uprv_isNaN_nid_postfix(y)) return uprv_getNaN_nid_postfix(); + if (x == 0.0 && y == 0.0 && SignBit(y)) return y; + return x > y ? y : x; +} + +std::int8_t APS5_VABI uprv_add32_overflow_nid_postfix(std::int32_t a, std::int32_t b, std::int32_t* res) { + if (!res) APS5_INVALID_ARG_EX; + const std::int64_t result = static_cast(a) + b; + *res = static_cast(result); + return result != *res; +} + +std::int8_t APS5_VABI uprv_mul32_overflow_nid_postfix(std::int32_t a, std::int32_t b, std::int32_t* res) { + if (!res) APS5_INVALID_ARG_EX; + const std::int64_t result = static_cast(a) * b; + *res = static_cast(result); + return result != *res; +} + +double APS5_VABI uprv_trunc_nid_postfix(double d) { + if (uprv_isNaN_nid_postfix(d)) return uprv_getNaN_nid_postfix(); + if (uprv_isInfinite_nid_postfix(d)) return uprv_getInfinity_nid_postfix(); + return SignBit(d) ? ceil_nid_postfix(d) : floor_nid_postfix(d); +} + +double APS5_VABI uprv_maxMantissa_nid_postfix() { + return pow_nid_postfix(2.0, DBL_MANT_DIG + 1.0) - 1.0; +} + +double APS5_VABI uprv_log_nid_postfix(double d) { + return log_nid_postfix(d); +} + +void* APS5_VABI uprv_maximumPtr_nid_postfix(void* base) { + const auto address = reinterpret_cast(base); + const std::uintptr_t limit = address + 0x7fffffffu; + return reinterpret_cast(limit > address ? limit : std::numeric_limits::max()); +} + +std::int32_t APS5_VABI uprv_max_nid_postfix(std::int32_t x, std::int32_t y) { + return x > y ? x : y; +} + +std::int32_t APS5_VABI uprv_min_nid_postfix(std::int32_t x, std::int32_t y) { + return x > y ? y : x; +} + +} diff --git a/core/libs/prx/libSceIcu/Numeric.hpp b/core/libs/prx/libSceIcu/Numeric.hpp new file mode 100644 index 0000000000..71bd499e5d --- /dev/null +++ b/core/libs/prx/libSceIcu/Numeric.hpp @@ -0,0 +1,30 @@ +#pragma once +#include "prx/libc/include/general/VabiMacros.hpp" +#include + +extern "C" { +std::int8_t APS5_VABI uprv_isNaN_nid_postfix(double); +std::int8_t APS5_VABI uprv_isInfinite_nid_postfix(double); +std::int8_t APS5_VABI uprv_isPositiveInfinity_nid_postfix(double); +std::int8_t APS5_VABI uprv_isNegativeInfinity_nid_postfix(double); +double APS5_VABI uprv_getNaN_nid_postfix(); +double APS5_VABI uprv_getInfinity_nid_postfix(); +double APS5_VABI uprv_floor_nid_postfix(double); +double APS5_VABI uprv_ceil_nid_postfix(double); +double APS5_VABI uprv_round_nid_postfix(double); +double APS5_VABI uprv_fabs_nid_postfix(double); +double APS5_VABI uprv_modf_nid_postfix(double, double*); +double APS5_VABI uprv_fmod_nid_postfix(double, double); +double APS5_VABI uprv_pow_nid_postfix(double, double); +double APS5_VABI uprv_pow10_nid_postfix(std::int32_t); +double APS5_VABI uprv_fmax_nid_postfix(double, double); +double APS5_VABI uprv_fmin_nid_postfix(double, double); +std::int8_t APS5_VABI uprv_add32_overflow_nid_postfix(std::int32_t, std::int32_t, std::int32_t*); +std::int8_t APS5_VABI uprv_mul32_overflow_nid_postfix(std::int32_t, std::int32_t, std::int32_t*); +double APS5_VABI uprv_trunc_nid_postfix(double); +double APS5_VABI uprv_maxMantissa_nid_postfix(); +double APS5_VABI uprv_log_nid_postfix(double); +void* APS5_VABI uprv_maximumPtr_nid_postfix(void*); +std::int32_t APS5_VABI uprv_max_nid_postfix(std::int32_t, std::int32_t); +std::int32_t APS5_VABI uprv_min_nid_postfix(std::int32_t, std::int32_t); +} diff --git a/core/libs/prx/libSceIcu/RecoveredInventory.cpp b/core/libs/prx/libSceIcu/RecoveredInventory.cpp new file mode 100644 index 0000000000..3d5a7b8836 --- /dev/null +++ b/core/libs/prx/libSceIcu/RecoveredInventory.cpp @@ -0,0 +1,542 @@ +#include "prx/libSceIcu/libSceIcu.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI u_strToTitle_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI ubidi_close_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI ubidi_countRuns_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI ubidi_getBaseDirection_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI ubidi_getLogicalRun_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI ubidi_getParaLevel_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI ubidi_getVisualRun_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI ubidi_open_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI ubidi_reorderVisual_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI ubidi_setClassCallback_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI ubidi_setPara_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI ucal_clone_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI ucal_close_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI ucal_get_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI ucal_getAttribute_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI ucal_getCanonicalTimeZoneID_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI ucal_getDayOfWeekType_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI ucal_getDefaultTimeZone_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI ucal_getKeywordValuesForLocale_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI ucal_getTimeZoneDisplayName_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI ucal_open_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI ucal_openTimeZoneIDEnumeration_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI ucal_openTimeZones_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI ucal_setGregorianChange_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI ucal_setMillis_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI ucol_close_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI ucol_countAvailable_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI ucol_getAttribute_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI ucol_getAvailable_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI ucol_getContractionsAndExpansions_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI ucol_getKeywordValues_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI ucol_getKeywordValuesForLocale_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI ucol_getReorderCodes_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI ucol_getRules_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI ucol_getStrength_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI ucol_getTailoredSet_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI ucol_open_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI ucol_setAttribute_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI ucol_setStrength_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI ucol_strcoll_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI ucol_strcollIter_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI ucol_strcollUTF8_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI ucsdet_close_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI ucsdet_detectAll_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI ucsdet_enableInputFilter_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI ucsdet_getConfidence_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI ucsdet_getName_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI ucsdet_open_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI ucsdet_setText_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI udat_close_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI udat_countSymbols_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI udat_format_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI udat_formatForFields_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI udat_getCalendar_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI udat_getSymbols_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI udat_open_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI udat_toPattern_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI udatpg_close_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI udatpg_getBestPattern_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI udatpg_getBestPatternWithOptions_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI udatpg_getFieldDisplayName_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI udatpg_getSkeleton_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI udatpg_open_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI udtitvfmt_close_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI udtitvfmt_closeResult_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI udtitvfmt_formatCalendarToResult_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI udtitvfmt_formatToResult_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI udtitvfmt_open_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI udtitvfmt_openResult_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI udtitvfmt_resultAsValue_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI ufieldpositer_close_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI ufieldpositer_next_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI ufieldpositer_open_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI ufmtval_getString_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI ufmtval_nextPosition_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI uidna_close_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI uidna_nameToASCII_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI uidna_nameToUnicode_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI uidna_openUTS46_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI uldn_close_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI uldn_keyValueDisplayName_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI uldn_localeDisplayName_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI uldn_openForContext_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI uldn_regionDisplayName_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI uldn_scriptDisplayName_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI ulistfmt_close_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI ulistfmt_closeResult_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI ulistfmt_format_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI ulistfmt_formatStringsToResult_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI ulistfmt_openForType_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI ulistfmt_openResult_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI ulistfmt_resultAsValue_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI unum_clone_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI unum_close_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI unum_formatDecimal_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI unum_formatDouble_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI unum_formatDoubleForFields_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI unum_getSymbol_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI unum_getTextAttribute_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI unum_open_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI unum_setAttribute_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI unum_setTextAttribute_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI unumf_close_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI unumf_closeResult_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI unumf_formatDecimal_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI unumf_formatDouble_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI unumf_openForSkeletonAndLocale_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI unumf_openResult_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI unumf_resultGetAllFieldPositions_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI unumf_resultToString_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI unumsys_close_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI unumsys_getName_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI unumsys_isAlgorithmic_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI unumsys_open_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI unumsys_openAvailableNames_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI unumsys_openByName_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI uplrules_close_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI uplrules_getKeywords_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI uplrules_openForType_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI uplrules_selectWithFormat_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI ureldatefmt_close_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI ureldatefmt_format_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI ureldatefmt_formatNumeric_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI ureldatefmt_open_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI uscript_getCode_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI usearch_getCollator_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI usearch_getMatchedLength_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI usearch_next_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI usearch_open_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI usearch_reset_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI usearch_setAttribute_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI usearch_setOffset_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI usearch_setPattern_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI usearch_setText_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +} diff --git a/core/libs/prx/libSceIcu/RecoveredInventory.hpp b/core/libs/prx/libSceIcu/RecoveredInventory.hpp new file mode 100644 index 0000000000..466642db3b --- /dev/null +++ b/core/libs/prx/libSceIcu/RecoveredInventory.hpp @@ -0,0 +1,139 @@ +#pragma once +#include "prx/libc/include/general/VabiMacros.hpp" + +extern "C" { +int APS5_VABI u_strToTitle_nid_postfix(); +int APS5_VABI ubidi_close_nid_postfix(); +int APS5_VABI ubidi_countRuns_nid_postfix(); +int APS5_VABI ubidi_getBaseDirection_nid_postfix(); +int APS5_VABI ubidi_getLogicalRun_nid_postfix(); +int APS5_VABI ubidi_getParaLevel_nid_postfix(); +int APS5_VABI ubidi_getVisualRun_nid_postfix(); +int APS5_VABI ubidi_open_nid_postfix(); +int APS5_VABI ubidi_reorderVisual_nid_postfix(); +int APS5_VABI ubidi_setClassCallback_nid_postfix(); +int APS5_VABI ubidi_setPara_nid_postfix(); +int APS5_VABI ucal_clone_nid_postfix(); +int APS5_VABI ucal_close_nid_postfix(); +int APS5_VABI ucal_get_nid_postfix(); +int APS5_VABI ucal_getAttribute_nid_postfix(); +int APS5_VABI ucal_getCanonicalTimeZoneID_nid_postfix(); +int APS5_VABI ucal_getDayOfWeekType_nid_postfix(); +int APS5_VABI ucal_getDefaultTimeZone_nid_postfix(); +int APS5_VABI ucal_getKeywordValuesForLocale_nid_postfix(); +int APS5_VABI ucal_getTimeZoneDisplayName_nid_postfix(); +int APS5_VABI ucal_open_nid_postfix(); +int APS5_VABI ucal_openTimeZoneIDEnumeration_nid_postfix(); +int APS5_VABI ucal_openTimeZones_nid_postfix(); +int APS5_VABI ucal_setGregorianChange_nid_postfix(); +int APS5_VABI ucal_setMillis_nid_postfix(); +int APS5_VABI ucol_close_nid_postfix(); +int APS5_VABI ucol_countAvailable_nid_postfix(); +int APS5_VABI ucol_getAttribute_nid_postfix(); +int APS5_VABI ucol_getAvailable_nid_postfix(); +int APS5_VABI ucol_getContractionsAndExpansions_nid_postfix(); +int APS5_VABI ucol_getKeywordValues_nid_postfix(); +int APS5_VABI ucol_getKeywordValuesForLocale_nid_postfix(); +int APS5_VABI ucol_getReorderCodes_nid_postfix(); +int APS5_VABI ucol_getRules_nid_postfix(); +int APS5_VABI ucol_getStrength_nid_postfix(); +int APS5_VABI ucol_getTailoredSet_nid_postfix(); +int APS5_VABI ucol_open_nid_postfix(); +int APS5_VABI ucol_setAttribute_nid_postfix(); +int APS5_VABI ucol_setStrength_nid_postfix(); +int APS5_VABI ucol_strcoll_nid_postfix(); +int APS5_VABI ucol_strcollIter_nid_postfix(); +int APS5_VABI ucol_strcollUTF8_nid_postfix(); +int APS5_VABI ucsdet_close_nid_postfix(); +int APS5_VABI ucsdet_detectAll_nid_postfix(); +int APS5_VABI ucsdet_enableInputFilter_nid_postfix(); +int APS5_VABI ucsdet_getConfidence_nid_postfix(); +int APS5_VABI ucsdet_getName_nid_postfix(); +int APS5_VABI ucsdet_open_nid_postfix(); +int APS5_VABI ucsdet_setText_nid_postfix(); +int APS5_VABI udat_close_nid_postfix(); +int APS5_VABI udat_countSymbols_nid_postfix(); +int APS5_VABI udat_format_nid_postfix(); +int APS5_VABI udat_formatForFields_nid_postfix(); +int APS5_VABI udat_getCalendar_nid_postfix(); +int APS5_VABI udat_getSymbols_nid_postfix(); +int APS5_VABI udat_open_nid_postfix(); +int APS5_VABI udat_toPattern_nid_postfix(); +int APS5_VABI udatpg_close_nid_postfix(); +int APS5_VABI udatpg_getBestPattern_nid_postfix(); +int APS5_VABI udatpg_getBestPatternWithOptions_nid_postfix(); +int APS5_VABI udatpg_getFieldDisplayName_nid_postfix(); +int APS5_VABI udatpg_getSkeleton_nid_postfix(); +int APS5_VABI udatpg_open_nid_postfix(); +int APS5_VABI udtitvfmt_close_nid_postfix(); +int APS5_VABI udtitvfmt_closeResult_nid_postfix(); +int APS5_VABI udtitvfmt_formatCalendarToResult_nid_postfix(); +int APS5_VABI udtitvfmt_formatToResult_nid_postfix(); +int APS5_VABI udtitvfmt_open_nid_postfix(); +int APS5_VABI udtitvfmt_openResult_nid_postfix(); +int APS5_VABI udtitvfmt_resultAsValue_nid_postfix(); +int APS5_VABI ufieldpositer_close_nid_postfix(); +int APS5_VABI ufieldpositer_next_nid_postfix(); +int APS5_VABI ufieldpositer_open_nid_postfix(); +int APS5_VABI ufmtval_getString_nid_postfix(); +int APS5_VABI ufmtval_nextPosition_nid_postfix(); +int APS5_VABI uidna_close_nid_postfix(); +int APS5_VABI uidna_nameToASCII_nid_postfix(); +int APS5_VABI uidna_nameToUnicode_nid_postfix(); +int APS5_VABI uidna_openUTS46_nid_postfix(); +int APS5_VABI uldn_close_nid_postfix(); +int APS5_VABI uldn_keyValueDisplayName_nid_postfix(); +int APS5_VABI uldn_localeDisplayName_nid_postfix(); +int APS5_VABI uldn_openForContext_nid_postfix(); +int APS5_VABI uldn_regionDisplayName_nid_postfix(); +int APS5_VABI uldn_scriptDisplayName_nid_postfix(); +int APS5_VABI ulistfmt_close_nid_postfix(); +int APS5_VABI ulistfmt_closeResult_nid_postfix(); +int APS5_VABI ulistfmt_format_nid_postfix(); +int APS5_VABI ulistfmt_formatStringsToResult_nid_postfix(); +int APS5_VABI ulistfmt_openForType_nid_postfix(); +int APS5_VABI ulistfmt_openResult_nid_postfix(); +int APS5_VABI ulistfmt_resultAsValue_nid_postfix(); +int APS5_VABI unum_clone_nid_postfix(); +int APS5_VABI unum_close_nid_postfix(); +int APS5_VABI unum_formatDecimal_nid_postfix(); +int APS5_VABI unum_formatDouble_nid_postfix(); +int APS5_VABI unum_formatDoubleForFields_nid_postfix(); +int APS5_VABI unum_getSymbol_nid_postfix(); +int APS5_VABI unum_getTextAttribute_nid_postfix(); +int APS5_VABI unum_open_nid_postfix(); +int APS5_VABI unum_setAttribute_nid_postfix(); +int APS5_VABI unum_setTextAttribute_nid_postfix(); +int APS5_VABI unumf_close_nid_postfix(); +int APS5_VABI unumf_closeResult_nid_postfix(); +int APS5_VABI unumf_formatDecimal_nid_postfix(); +int APS5_VABI unumf_formatDouble_nid_postfix(); +int APS5_VABI unumf_openForSkeletonAndLocale_nid_postfix(); +int APS5_VABI unumf_openResult_nid_postfix(); +int APS5_VABI unumf_resultGetAllFieldPositions_nid_postfix(); +int APS5_VABI unumf_resultToString_nid_postfix(); +int APS5_VABI unumsys_close_nid_postfix(); +int APS5_VABI unumsys_getName_nid_postfix(); +int APS5_VABI unumsys_isAlgorithmic_nid_postfix(); +int APS5_VABI unumsys_open_nid_postfix(); +int APS5_VABI unumsys_openAvailableNames_nid_postfix(); +int APS5_VABI unumsys_openByName_nid_postfix(); +int APS5_VABI uplrules_close_nid_postfix(); +int APS5_VABI uplrules_getKeywords_nid_postfix(); +int APS5_VABI uplrules_openForType_nid_postfix(); +int APS5_VABI uplrules_selectWithFormat_nid_postfix(); +int APS5_VABI ureldatefmt_close_nid_postfix(); +int APS5_VABI ureldatefmt_format_nid_postfix(); +int APS5_VABI ureldatefmt_formatNumeric_nid_postfix(); +int APS5_VABI ureldatefmt_open_nid_postfix(); +int APS5_VABI uscript_getCode_nid_postfix(); +int APS5_VABI usearch_getCollator_nid_postfix(); +int APS5_VABI usearch_getMatchedLength_nid_postfix(); +int APS5_VABI usearch_next_nid_postfix(); +int APS5_VABI usearch_open_nid_postfix(); +int APS5_VABI usearch_reset_nid_postfix(); +int APS5_VABI usearch_setAttribute_nid_postfix(); +int APS5_VABI usearch_setOffset_nid_postfix(); +int APS5_VABI usearch_setPattern_nid_postfix(); +int APS5_VABI usearch_setText_nid_postfix(); +} diff --git a/core/libs/prx/libSceIcu/Version.cpp b/core/libs/prx/libSceIcu/Version.cpp index 3c55a14e17..c558a37716 100644 --- a/core/libs/prx/libSceIcu/Version.cpp +++ b/core/libs/prx/libSceIcu/Version.cpp @@ -8,6 +8,7 @@ constexpr std::uint16_t VersionLength = 4; constexpr std::int32_t VersionStringLength = 20; constexpr char VersionDelimiter = '.'; constexpr std::uint8_t UnicodeVersion[VersionLength] = {17, 0, 0, 0}; +constexpr const char* IcuVersion = "78.1"; char* WriteField(char* text, std::uint8_t field) { if (field >= 100) { @@ -72,4 +73,8 @@ void APS5_VABI u_getUnicodeVersion_nid_postfix(std::uint8_t* version) { for (std::uint16_t part = 0; part < VersionLength; ++part) version[part] = UnicodeVersion[part]; } +void APS5_VABI u_getVersion_nid_postfix(std::uint8_t* version) { + u_versionFromString_nid_postfix(version, IcuVersion); +} + } diff --git a/core/libs/prx/libSceIcu/Version.hpp b/core/libs/prx/libSceIcu/Version.hpp index 7bfee06d7b..63b5039624 100644 --- a/core/libs/prx/libSceIcu/Version.hpp +++ b/core/libs/prx/libSceIcu/Version.hpp @@ -7,4 +7,5 @@ void APS5_VABI u_versionFromString_nid_postfix(std::uint8_t*, const char*); void APS5_VABI u_versionFromUString_nid_postfix(std::uint8_t*, const char16_t*); void APS5_VABI u_versionToString_nid_postfix(const std::uint8_t*, char*); void APS5_VABI u_getUnicodeVersion_nid_postfix(std::uint8_t*); +void APS5_VABI u_getVersion_nid_postfix(std::uint8_t*); } diff --git a/core/libs/prx/libSceIcu/libSceIcu.hpp b/core/libs/prx/libSceIcu/libSceIcu.hpp index 785600df04..12d2bf54c3 100644 --- a/core/libs/prx/libSceIcu/libSceIcu.hpp +++ b/core/libs/prx/libSceIcu/libSceIcu.hpp @@ -1,10 +1,14 @@ #pragma once #include "prx/libSceIcu/include/LegacyExports.hpp" +#include "ConstrainedFieldPosition.hpp" +#include "ErrorName.hpp" #include "Invariant.hpp" #include "Iterator.hpp" #include "prx/libSceIcu/include/InventoryDispatch.hpp" #include "Utf8.hpp" +#include "Numeric.hpp" #include "Version.hpp" +#include "RecoveredInventory.hpp" #include "ConverterCallbacks.hpp" #include "CString.hpp" #include "Space.hpp" diff --git a/core/libs/prx/libSceIme/CMakeLists.txt b/core/libs/prx/libSceIme/CMakeLists.txt index e91845fe5a..2b0080363a 100644 --- a/core/libs/prx/libSceIme/CMakeLists.txt +++ b/core/libs/prx/libSceIme/CMakeLists.txt @@ -13,6 +13,16 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_test_executable(ime_host_text_tests tests/HostText.cpp) + target_include_directories(ime_host_text_tests PRIVATE ${LIBS_INCLUDE_DIR}) + configure_host_test_executable(ime_host_text_tests) + add_test(NAME ime_host_text COMMAND ime_host_text_tests) + + add_test_executable(ime_composition_tests tests/Composition.cpp) + target_include_directories(ime_composition_tests PRIVATE ${LIBS_INCLUDE_DIR}) + configure_host_test_executable(ime_composition_tests) + add_test(NAME ime_composition COMMAND ime_composition_tests) + add_test_executable(guest_ime_pad_route_tests ${LIBS_INCLUDE_DIR}/tests/GuestImePadRoute.cpp) target_include_directories(guest_ime_pad_route_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_ime_pad_route_tests PRIVATE libSceIme libc) @@ -68,6 +78,26 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_test_executable(guest_ime_composition_tests ${LIBS_INCLUDE_DIR}/tests/GuestImeComposition.cpp) + target_include_directories(guest_ime_composition_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_ime_composition_tests PRIVATE libSceIme libc) + configure_windows_unwind(guest_ime_composition_tests) + add_test(NAME guest_ime_composition COMMAND guest_ime_composition_tests) + if(WIN32) + set_tests_properties(guest_ime_composition PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + + add_test_executable(guest_ime_paste_tests ${LIBS_INCLUDE_DIR}/tests/GuestImePaste.cpp) + target_include_directories(guest_ime_paste_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_ime_paste_tests PRIVATE libSceIme libc) + configure_windows_unwind(guest_ime_paste_tests) + add_test(NAME guest_ime_paste COMMAND guest_ime_paste_tests) + if(WIN32) + set_tests_properties(guest_ime_paste PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_ime_open_tests ${LIBS_INCLUDE_DIR}/tests/GuestImeOpen.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../libSceVideoOut/src/ImeTextInput.cpp) target_include_directories(guest_ime_open_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_SDL2_INCLUDE_DIR}) @@ -78,4 +108,14 @@ if(BUILD_TESTING) set_tests_properties(guest_ime_open PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + + add_test_executable(guest_ime_for_test_function_tests ${LIBS_INCLUDE_DIR}/tests/GuestImeForTestFunction.cpp) + target_include_directories(guest_ime_for_test_function_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_ime_for_test_function_tests PRIVATE libSceIme libc) + configure_windows_unwind(guest_ime_for_test_function_tests) + add_test(NAME guest_ime_for_test_function COMMAND guest_ime_for_test_function_tests) + if(WIN32) + set_tests_properties(guest_ime_for_test_function PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() diff --git a/core/libs/prx/libSceIme/Export.cpp b/core/libs/prx/libSceIme/Export.cpp index 29b09103c1..b34475101f 100644 --- a/core/libs/prx/libSceIme/Export.cpp +++ b/core/libs/prx/libSceIme/Export.cpp @@ -15,7 +15,9 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libSceIme/include/ImeComposition.hpp" #include "prx/libSceIme/include/ImeFilterText.hpp" +#include "prx/libSceIme/include/ImeHostText.hpp" #include "prx/libSceIme/include/ImeInput.hpp" #include "prx/libSceIme/include/ImeKeyboardView.hpp" @@ -69,13 +71,19 @@ constexpr uint32_t EventPressEnter = 5; constexpr uint32_t CaretMoveLeft = 1; constexpr uint32_t CaretMoveRight = 2; +struct PendingInput { + ImeHostInput input; + std::u16string paste; +}; + struct Session { Param param{}; ImeExtKeyboardFilter extKeyboardFilter = nullptr; std::u16string text; std::uint32_t caret = 0; std::deque events; - std::deque hostInputs; + std::deque hostInputs; + ImeComposition composition; std::uint64_t id = 0; std::uint64_t revision = 0; }; @@ -89,6 +97,7 @@ std::set g_openKeyboards; std::mutex g_imeMutex; std::optional g_session; std::atomic g_textSink{nullptr}; +std::atomic g_preeditSink{nullptr}; std::atomic g_padSink{nullptr}; std::atomic g_viewProvider{nullptr}; std::uint64_t g_nextSessionId = 0; @@ -271,7 +280,15 @@ std::size_t UnitsAfter(const Session& session) { return caret + 1 < session.text.size() && IsHighSurrogate(session.text[caret]) && IsLowSurrogate(session.text[caret + 1]) ? 2 : 1; } -std::optional Apply(Session& session, const ImeHostInput& input) { +std::optional Apply(Session& session, const PendingInput& pending) { + const ImeHostInput& input = pending.input; + if (input.kind == ImeHostInputKind::Preedit) { + std::size_t size = 0; + while (size < sizeof(input.text) && input.text[size] != '\0') ++size; + session.composition.Set(ImeHostText::PreeditUnits(input.text, size), input.start, input.length); + return std::nullopt; + } + if (ImeRouteWhileComposing(session.composition, input.kind) == ImeCompositionRoute::Swallow) return std::nullopt; switch (input.kind) { case ImeHostInputKind::Text: { std::size_t size = 0; @@ -326,6 +343,10 @@ std::optional Apply(Session& session, const ImeHostInput& input) case ImeHostInputKind::Escape: QueueText(session, EventPressClose, session.caret, 0); return std::nullopt; + case ImeHostInputKind::Paste: + return Insert(session, pending.paste); + case ImeHostInputKind::Preedit: + return std::nullopt; } throw std::invalid_argument("IME host input: unknown input kind"); } @@ -369,10 +390,19 @@ void ImePublishInput_nid_postfix(const ImeHostInput& input) { { std::lock_guard lock(g_imeMutex); if (g_session) { - g_session->hostInputs.push_back(input); + g_session->hostInputs.push_back({input, {}}); return; } } + if (input.kind == ImeHostInputKind::Preedit) { + const ImeHostPreeditSink preeditSink = g_preeditSink.load(); + if (preeditSink == nullptr) return; + std::size_t size = 0; + while (size < sizeof(input.text) && input.text[size] != '\0') ++size; + const std::u16string units = ImeHostText::PreeditUnits(input.text, size); + preeditSink(units.data(), units.size(), input.start, input.length); + return; + } const ImeHostTextSink sink = g_textSink.load(); if (sink == nullptr) return; std::u16string units; @@ -384,6 +414,26 @@ void ImePublishInput_nid_postfix(const ImeHostInput& input) { sink(input.kind, units.data(), units.size()); } +void ImePublishPaste_nid_postfix(const char* text, std::size_t size) { + if (text == nullptr) return; + { + std::lock_guard lock(g_imeMutex); + if (g_session) { + std::u16string units = ImeHostText::ClipboardUnits(text, size, (g_session->param.option & OptionMultiline) != 0); + if (units.empty()) return; + ImeHostInput input; + input.kind = ImeHostInputKind::Paste; + g_session->hostInputs.push_back({input, std::move(units)}); + return; + } + } + const ImeHostTextSink sink = g_textSink.load(); + if (sink == nullptr) return; + const std::u16string units = ImeHostText::ClipboardUnits(text, size, false); + if (units.empty()) return; + sink(ImeHostInputKind::Paste, units.data(), units.size()); +} + bool ImeCapturesKeyboard_nid_postfix() { std::lock_guard lock(g_imeMutex); return g_session.has_value() || g_textSink.load() != nullptr; @@ -393,6 +443,10 @@ void ImeSetHostTextSink_nid_no_patch(ImeHostTextSink sink) { g_textSink = sink; } +void ImeSetHostPreeditSink_nid_no_patch(ImeHostPreeditSink sink) { + g_preeditSink = sink; +} + bool ImeCapturesPad_nid_postfix() { return g_padSink.load() != nullptr; } @@ -573,9 +627,9 @@ int APS5_VABI sceImeUpdate(EventHandler handler) { std::lock_guard lock(g_imeMutex); if (!g_session || g_session->param.handler != handler) return 0; if (!g_session->hostInputs.empty()) { - const ImeHostInput input = g_session->hostInputs.front(); + const PendingInput pending = std::move(g_session->hostInputs.front()); g_session->hostInputs.pop_front(); - if (const std::optional deferred = Apply(*g_session, input)) { + if (const std::optional deferred = Apply(*g_session, pending)) { runFilter = true; filter = g_session->param.filter; filterText = g_session->text; diff --git a/core/libs/prx/libSceIme/include/ImeComposition.hpp b/core/libs/prx/libSceIme/include/ImeComposition.hpp new file mode 100644 index 0000000000..6954aa918b --- /dev/null +++ b/core/libs/prx/libSceIme/include/ImeComposition.hpp @@ -0,0 +1,70 @@ +#ifndef CORE_LIBS_PRX_LIBSCEIME_INCLUDE_IMECOMPOSITION_HPP +#define CORE_LIBS_PRX_LIBSCEIME_INCLUDE_IMECOMPOSITION_HPP + +#include +#include +#include +#include + +#include "prx/libSceIme/include/ImeInput.hpp" + +struct ImeComposition { + std::u16string text; + std::size_t cursor = 0; + std::size_t selection = 0; + + bool Active() const { return !text.empty(); } + + void Clear() { + text.clear(); + cursor = 0; + selection = 0; + } + + void Set(std::u16string units, std::int32_t startCharacters, std::int32_t lengthCharacters) { + text = std::move(units); + const std::int64_t start = startCharacters > 0 ? startCharacters : 0; + const std::int64_t length = lengthCharacters > 0 ? lengthCharacters : 0; + const std::size_t begin = Offset(start); + cursor = begin; + selection = Offset(start + length) - begin; + } + +private: + std::size_t Offset(std::int64_t characters) const { + std::size_t unit = 0; + for (std::int64_t seen = 0; seen < characters && unit < text.size(); ++seen) { + const bool pair = unit + 1 < text.size() && text[unit] >= 0xd800 && text[unit] <= 0xdbff && text[unit + 1] >= 0xdc00 && text[unit + 1] <= 0xdfff; + unit += pair ? 2 : 1; + } + return unit; + } +}; + +enum class ImeCompositionRoute { Apply, Swallow }; + +inline ImeCompositionRoute ImeRouteWhileComposing(ImeComposition& composition, ImeHostInputKind kind) { + switch (kind) { + case ImeHostInputKind::Text: + composition.Clear(); + return ImeCompositionRoute::Apply; + case ImeHostInputKind::Enter: + case ImeHostInputKind::Escape: + if (!composition.Active()) return ImeCompositionRoute::Apply; + composition.Clear(); + return ImeCompositionRoute::Swallow; + case ImeHostInputKind::Preedit: + return ImeCompositionRoute::Swallow; + case ImeHostInputKind::Backspace: + case ImeHostInputKind::Delete: + case ImeHostInputKind::Left: + case ImeHostInputKind::Right: + case ImeHostInputKind::Home: + case ImeHostInputKind::End: + case ImeHostInputKind::Paste: + return composition.Active() ? ImeCompositionRoute::Swallow : ImeCompositionRoute::Apply; + } + return ImeCompositionRoute::Apply; +} + +#endif diff --git a/core/libs/prx/libSceIme/include/ImeHostText.hpp b/core/libs/prx/libSceIme/include/ImeHostText.hpp new file mode 100644 index 0000000000..f96d42bc2a --- /dev/null +++ b/core/libs/prx/libSceIme/include/ImeHostText.hpp @@ -0,0 +1,98 @@ +#ifndef CORE_LIBS_PRX_LIBSCEIME_INCLUDE_IMEHOSTTEXT_HPP +#define CORE_LIBS_PRX_LIBSCEIME_INCLUDE_IMEHOSTTEXT_HPP + +#include +#include +#include + +namespace ImeHostText { + +inline constexpr std::size_t MaxClipboardBytes = 65536; + +inline std::size_t BoundedUtf8Size(const char* text, std::size_t size) { + if (size <= MaxClipboardBytes) return size; + std::size_t end = MaxClipboardBytes; + while (end > 0 && (static_cast(text[end]) & 0xc0) == 0x80) --end; + return end; +} + +inline std::u32string DecodeUtf8Lenient(const char* text, std::size_t size, bool dropIncompleteTail) { + std::u32string points; + std::size_t i = 0; + while (i < size) { + const auto lead = static_cast(text[i]); + if (lead < 0x80) { + points.push_back(lead); + ++i; + continue; + } + std::size_t extra = 0; + char32_t point = 0; + char32_t minimum = 0; + if (lead >= 0xc2 && lead <= 0xdf) { extra = 1; point = lead & 0x1f; minimum = 0x80; } + else if ((lead & 0xf0) == 0xe0) { extra = 2; point = lead & 0x0f; minimum = 0x800; } + else if (lead >= 0xf0 && lead <= 0xf4) { extra = 3; point = lead & 0x07; minimum = 0x10000; } + else { + points.push_back(0xfffd); + ++i; + continue; + } + std::size_t present = 0; + while (present < extra && i + 1 + present < size && (static_cast(text[i + 1 + present]) & 0xc0) == 0x80) { + point = (point << 6) | (static_cast(text[i + 1 + present]) & 0x3f); + ++present; + } + if (present < extra) { + const bool endOfInput = i + 1 + present >= size; + if (!(endOfInput && dropIncompleteTail)) points.push_back(0xfffd); + i += 1 + present; + continue; + } + i += 1 + extra; + const bool surrogate = point >= 0xd800 && point <= 0xdfff; + points.push_back(point < minimum || point > 0x10ffff || surrogate ? char32_t{0xfffd} : point); + } + return points; +} + +inline void AppendUtf16(std::u16string& units, char32_t point) { + if (point >= 0x10000) { + point -= 0x10000; + units.push_back(static_cast(0xd800 + (point >> 10))); + units.push_back(static_cast(0xdc00 + (point & 0x3ff))); + } else { + units.push_back(static_cast(point)); + } +} + +inline bool IsControl(char32_t point) { + return point < 0x20 || (point >= 0x7f && point <= 0x9f); +} + +inline std::u16string PreeditUnits(const char* text, std::size_t size) { + std::u16string units; + for (const char32_t point : DecodeUtf8Lenient(text, size, true)) { + if (!IsControl(point)) AppendUtf16(units, point); + } + return units; +} + +inline std::u16string ClipboardUnits(const char* text, std::size_t size, bool multiline) { + std::u16string units; + const std::u32string points = DecodeUtf8Lenient(text, BoundedUtf8Size(text, size), false); + for (std::size_t i = 0; i < points.size(); ++i) { + const char32_t point = points[i]; + const bool lineBreak = point == U'\n' || point == U'\r' || point == 0x85 || point == 0x2028 || point == 0x2029; + if (lineBreak) { + if (point == U'\r' && i + 1 < points.size() && points[i + 1] == U'\n') ++i; + if (multiline) units.push_back(u'\n'); + continue; + } + if (!IsControl(point)) AppendUtf16(units, point); + } + return units; +} + +} + +#endif diff --git a/core/libs/prx/libSceIme/include/ImeInput.hpp b/core/libs/prx/libSceIme/include/ImeInput.hpp index c04b2dd9a3..90b7a0bcb5 100644 --- a/core/libs/prx/libSceIme/include/ImeInput.hpp +++ b/core/libs/prx/libSceIme/include/ImeInput.hpp @@ -14,20 +14,29 @@ enum class ImeHostInputKind : std::uint32_t { End, Enter, Escape, + Preedit, + Paste, }; struct ImeHostInput { ImeHostInputKind kind = ImeHostInputKind::Text; char text[32] = {}; + std::int32_t start = 0; + std::int32_t length = 0; }; extern "C" void ImePublishInput_nid_postfix(const ImeHostInput& input); +extern "C" void ImePublishPaste_nid_postfix(const char* utf8, std::size_t size); extern "C" bool ImeCapturesKeyboard_nid_postfix(); using ImeHostTextSink = void (*)(ImeHostInputKind kind, const char16_t* text, std::size_t length); extern "C" void ImeSetHostTextSink_nid_no_patch(ImeHostTextSink sink); +using ImeHostPreeditSink = void (*)(const char16_t* text, std::size_t size, std::int32_t start, std::int32_t selected); + +extern "C" void ImeSetHostPreeditSink_nid_no_patch(ImeHostPreeditSink sink); + struct ImeHostPad { std::uint32_t buttons = 0; std::uint8_t leftX = 128; diff --git a/core/libs/prx/libSceIme/include/ImeKeyboardView.hpp b/core/libs/prx/libSceIme/include/ImeKeyboardView.hpp index ef8d9dc57d..544bd24f20 100644 --- a/core/libs/prx/libSceIme/include/ImeKeyboardView.hpp +++ b/core/libs/prx/libSceIme/include/ImeKeyboardView.hpp @@ -22,6 +22,9 @@ struct ImeKeyCell { struct ImeKeyboardView { std::u16string text; std::size_t caret = 0; + std::u16string preedit; + std::size_t preeditCursor = 0; + std::size_t preeditSelection = 0; std::uint8_t rows = 0; std::uint8_t columns = 0; std::vector keys; diff --git a/core/libs/prx/libSceIme/tests/Composition.cpp b/core/libs/prx/libSceIme/tests/Composition.cpp new file mode 100644 index 0000000000..dc5544a2c8 --- /dev/null +++ b/core/libs/prx/libSceIme/tests/Composition.cpp @@ -0,0 +1,91 @@ +#include "prx/libSceIme/include/ImeComposition.hpp" + +#include +#include + +namespace { + +void check(bool condition, const char* reason) { + if (!condition) throw std::runtime_error(reason); +} + +ImeComposition composing(const std::u16string& text, std::int32_t start = 0, std::int32_t length = 0) { + ImeComposition composition; + composition.Set(text, start, length); + return composition; +} + +void sets_cursor_and_selection_in_characters() { + auto composition = composing(u"\u3053\u3093\u306B\u3061\u306F", 2, 2); + check(composition.Active() && composition.cursor == 2 && composition.selection == 2, "start and length count characters"); + composition = composing(u"a\U0001F600b", 2, 1); + check(composition.cursor == 3 && composition.selection == 1, "a surrogate pair is one character"); + composition = composing(u"ab", 9, 4); + check(composition.cursor == 2 && composition.selection == 0, "a start past the end is clamped"); + composition = composing(u"abc", 1, 99); + check(composition.cursor == 1 && composition.selection == 2, "a length past the end is clamped"); + composition = composing(u"abc", -3, -1); + check(composition.cursor == 0 && composition.selection == 0, "negative values are zero"); + composition = composing(u"abc", -1, 3); + check(composition.cursor == 0 && composition.selection == 3, "a negative start is zero and the length is kept"); + composition = composing(u"abc", 2147483647, 2147483647); + check(composition.cursor == 3 && composition.selection == 0, "huge values do not overflow"); +} + +void empty_text_is_not_active() { + auto composition = composing(u"", 3, 3); + check(!composition.Active() && composition.cursor == 0 && composition.selection == 0, "an empty composition is inactive"); + composition = composing(u"x"); + composition.Clear(); + check(!composition.Active() && composition.text.empty(), "Clear empties the composition"); +} + +void commit_clears_the_composition() { + auto composition = composing(u"ni"); + check(ImeRouteWhileComposing(composition, ImeHostInputKind::Text) == ImeCompositionRoute::Apply, "a commit is applied"); + check(!composition.Active(), "a commit ends the composition"); + check(ImeRouteWhileComposing(composition, ImeHostInputKind::Text) == ImeCompositionRoute::Apply, "text without a composition is applied"); +} + +void enter_and_escape_end_a_composition_first() { + for (const auto kind : {ImeHostInputKind::Enter, ImeHostInputKind::Escape}) { + auto composition = composing(u"ni"); + check(ImeRouteWhileComposing(composition, kind) == ImeCompositionRoute::Swallow, "the first press only discards the composition"); + check(!composition.Active(), "the composition is gone after that press"); + check(ImeRouteWhileComposing(composition, kind) == ImeCompositionRoute::Apply, "the next press reaches the dialog"); + } +} + +void edit_keys_belong_to_the_input_method_while_composing() { + for (const auto kind : {ImeHostInputKind::Backspace, ImeHostInputKind::Delete, ImeHostInputKind::Left, ImeHostInputKind::Right, ImeHostInputKind::Home, ImeHostInputKind::End, ImeHostInputKind::Paste}) { + auto composition = composing(u"ni"); + check(ImeRouteWhileComposing(composition, kind) == ImeCompositionRoute::Swallow, "an edit key is swallowed while composing"); + check(composition.Active(), "an edit key does not end the composition"); + composition.Clear(); + check(ImeRouteWhileComposing(composition, kind) == ImeCompositionRoute::Apply, "an edit key is applied when not composing"); + } +} + +void preedit_is_never_applied() { + ImeComposition composition; + check(ImeRouteWhileComposing(composition, ImeHostInputKind::Preedit) == ImeCompositionRoute::Swallow, "a preedit is swallowed"); + composition = composing(u"ni"); + check(ImeRouteWhileComposing(composition, ImeHostInputKind::Preedit) == ImeCompositionRoute::Swallow && composition.Active(), "a preedit leaves the state to the caller"); +} + +} + +int main() { + try { + sets_cursor_and_selection_in_characters(); + empty_text_is_not_active(); + commit_clears_the_composition(); + enter_and_escape_end_a_composition_first(); + edit_keys_belong_to_the_input_method_while_composing(); + preedit_is_never_applied(); + } catch (const std::exception& error) { + std::fprintf(stderr, "ime composition: %s\n", error.what()); + return 1; + } + return 0; +} diff --git a/core/libs/prx/libSceIme/tests/HostText.cpp b/core/libs/prx/libSceIme/tests/HostText.cpp new file mode 100644 index 0000000000..2fb4e5a2c5 --- /dev/null +++ b/core/libs/prx/libSceIme/tests/HostText.cpp @@ -0,0 +1,99 @@ +#include "prx/libSceIme/include/ImeHostText.hpp" + +#include +#include +#include + +namespace { + +using namespace ImeHostText; + +void check(bool condition, const char* reason) { + if (!condition) throw std::runtime_error(reason); +} + +std::u16string clip(const std::string& utf8, bool multiline = false) { + return ClipboardUnits(utf8.data(), utf8.size(), multiline); +} + +std::u16string preedit(const std::string& utf8) { + return PreeditUnits(utf8.data(), utf8.size()); +} + +void decodes_valid_utf8() { + const std::string text = "a\xc3\xa9\xe3\x81\x82\xf0\x9f\x98\x80"; + const auto points = DecodeUtf8Lenient(text.data(), text.size(), false); + check(points == std::u32string(U"a\u00E9\u3042\U0001F600"), "valid UTF-8 decodes to its code points"); + check(clip(text) == u"a\u00E9\u3042\U0001F600", "valid UTF-8 becomes UTF-16 with a surrogate pair"); + check(clip("\xf0\x9f\x98\x80")[0] == 0xd83d && clip("\xf0\x9f\x98\x80")[1] == 0xde00, "the pair is high then low"); +} + +void replaces_invalid_utf8() { + check(clip("a\xffz") == u"a\uFFFDz", "a byte that never starts a sequence is replaced"); + check(clip("\xc0\x80") == u"\uFFFD\uFFFD", "an overlong NUL is replaced and not decoded"); + check(clip("\xe0\x80\xaf") == u"\uFFFD", "an overlong three-byte slash is replaced"); + check(clip("\xf0\x80\x80\x80") == u"\uFFFD", "an overlong four-byte NUL is replaced"); + check(clip("\xed\xa0\x80") == u"\uFFFD", "an encoded surrogate is replaced"); + check(clip("\xf4\x90\x80\x80") == u"\uFFFD", "a code point past U+10FFFF is replaced"); + check(clip("\xe3\x81" "A") == u"\uFFFDA", "a sequence cut by an ASCII byte is replaced once"); + check(clip("a\xe3\x81") == u"a\uFFFD", "an incomplete tail is replaced in a clipboard"); + check(preedit("a\xe3\x81") == u"a", "an incomplete tail is dropped in a preedit cut at 32 bytes"); + check(preedit("\xe3\x81\x82\xe3\x81") == u"\u3042", "only the cut sequence of a preedit is dropped"); + check(preedit("\xe3\x81" "A") == u"\uFFFDA", "a sequence broken inside a preedit is still replaced"); +} + +void strips_control_characters() { + check(clip(std::string("a\0b", 3)) == u"ab", "NUL is stripped"); + check(clip("a\tb\x01" "c\x7f" "d") == u"abcd", "tab and other C0 controls and DEL are stripped"); + check(clip("a\xc2\x80" "b\xc2\x9f" "c") == u"abc", "C1 controls are stripped"); + check(clip("a\x1b[31mb") == u"a[31mb", "an escape byte is stripped and the rest stays text"); + check(preedit("a\x01" "b") == u"ab", "a preedit loses control characters too"); + check(clip("\xc2\xa0z") == u"\u00A0z", "a no-break space is not a control character"); +} + +void handles_line_breaks() { + check(clip("a\nb") == u"ab", "a single-line field drops LF"); + check(clip("a\r\nb") == u"ab", "a single-line field drops CRLF as one break"); + check(clip("a\rb") == u"ab", "a single-line field drops CR"); + check(clip("a\xe2\x80\xa8" "b\xe2\x80\xa9" "c") == u"abc", "a single-line field drops U+2028 and U+2029"); + check(clip("a\nb", true) == u"a\nb", "a multi-line field keeps LF"); + check(clip("a\r\nb", true) == u"a\nb", "CRLF is one LF"); + check(clip("a\rb", true) == u"a\nb", "CR is one LF"); + check(clip("a\n\nb", true) == u"a\n\nb", "two LFs stay two"); + check(clip("a\r\n\r\nb", true) == u"a\n\nb", "two CRLFs are two LFs"); + check(clip("a\xe2\x80\xa8" "b", true) == u"a\nb", "U+2028 is a line break in a multi-line field"); + check(clip("a\xc2\x85" "b", true) == u"a\nb", "NEL is a line break in a multi-line field"); +} + +void bounds_huge_clipboards() { + check(BoundedUtf8Size("abc", 3) == 3, "a small clipboard is not cut"); + std::string huge(MaxClipboardBytes * 4, 'x'); + check(BoundedUtf8Size(huge.data(), huge.size()) == MaxClipboardBytes, "an ASCII clipboard is cut at the bound"); + check(clip(huge).size() == MaxClipboardBytes, "the converted text is bounded"); + std::string wide; + while (wide.size() < MaxClipboardBytes + 16) wide += "\xe3\x81\x82"; + const std::size_t cut = BoundedUtf8Size(wide.data(), wide.size()); + check(cut <= MaxClipboardBytes && cut % 3 == 0, "the cut falls between code points"); + check(clip(wide).find(u'\uFFFD') == std::u16string::npos, "a bounded clipboard has no replacement character"); + std::string pairs; + while (pairs.size() < MaxClipboardBytes + 16) pairs += "\xf0\x9f\x98\x80"; + const std::u16string converted = clip(pairs); + check(converted.size() % 2 == 0 && converted.size() <= MaxClipboardBytes, "the cut never leaves half a pair"); + check(BoundedUtf8Size(pairs.data(), pairs.size()) % 4 == 0, "a four-byte cut stays on a boundary"); +} + +} + +int main() { + try { + decodes_valid_utf8(); + replaces_invalid_utf8(); + strips_control_characters(); + handles_line_breaks(); + bounds_huge_clipboards(); + } catch (const std::exception& error) { + std::fprintf(stderr, "ime host text: %s\n", error.what()); + return 1; + } + return 0; +} diff --git a/core/libs/prx/libSceImeDialog/CMakeLists.txt b/core/libs/prx/libSceImeDialog/CMakeLists.txt index e3b25d8fa2..4379838a9c 100644 --- a/core/libs/prx/libSceImeDialog/CMakeLists.txt +++ b/core/libs/prx/libSceImeDialog/CMakeLists.txt @@ -22,4 +22,35 @@ if(BUILD_TESTING) set_tests_properties(guest_ime_dialog PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + + add_test_executable(guest_ime_dialog_composition_tests ${LIBS_INCLUDE_DIR}/tests/GuestImeDialogComposition.cpp) + target_include_directories(guest_ime_dialog_composition_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_ime_dialog_composition_tests PRIVATE libSceImeDialog libSceIme libc) + configure_windows_unwind(guest_ime_dialog_composition_tests) + add_test(NAME guest_ime_dialog_composition COMMAND guest_ime_dialog_composition_tests) + if(WIN32) + set_tests_properties(guest_ime_dialog_composition PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + + add_test_executable(guest_ime_dialog_paste_tests ${LIBS_INCLUDE_DIR}/tests/GuestImeDialogPaste.cpp) + target_include_directories(guest_ime_dialog_paste_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_ime_dialog_paste_tests PRIVATE libSceImeDialog libSceIme libc) + configure_windows_unwind(guest_ime_dialog_paste_tests) + add_test(NAME guest_ime_dialog_paste COMMAND guest_ime_dialog_paste_tests) + if(WIN32) + set_tests_properties(guest_ime_dialog_paste PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + + add_test_executable(guest_ime_text_input_tests ${LIBS_INCLUDE_DIR}/tests/GuestImeTextInput.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../libSceVideoOut/src/ImeTextInput.cpp) + target_include_directories(guest_ime_text_input_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_SDL2_INCLUDE_DIR}) + target_link_libraries(guest_ime_text_input_tests PRIVATE libSceImeDialog libSceIme libc) + configure_windows_unwind(guest_ime_text_input_tests) + add_test(NAME guest_ime_text_input COMMAND guest_ime_text_input_tests) + if(WIN32) + set_tests_properties(guest_ime_text_input PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() diff --git a/core/libs/prx/libSceImeDialog/Export.cpp b/core/libs/prx/libSceImeDialog/Export.cpp index 60d826a5d9..0b01e3f984 100644 --- a/core/libs/prx/libSceImeDialog/Export.cpp +++ b/core/libs/prx/libSceImeDialog/Export.cpp @@ -8,6 +8,7 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libSceIme/include/ImeComposition.hpp" #include "prx/libSceIme/include/ImeInput.hpp" #include "prx/libSceIme/include/ImeKeyboardView.hpp" @@ -43,6 +44,7 @@ struct Dialog { std::u16string text; std::size_t caret = 0; ImeDialog::PadKeyboardState keyboard; + ImeComposition composition; }; std::mutex g_mutex; @@ -63,6 +65,7 @@ std::size_t UnitsAfter(const Dialog& dialog) { void ReleaseInput() { ImeSetHostTextSink_nid_no_patch(nullptr); + ImeSetHostPreeditSink_nid_no_patch(nullptr); ImeSetHostPadSink_nid_no_patch(nullptr); ImeSetKeyboardViewProvider_nid_no_patch(nullptr); } @@ -70,6 +73,7 @@ void ReleaseInput() { void Finish(std::uint32_t endStatus) { g_dialog.endStatus = endStatus; g_dialog.status = StatusFinished; + g_dialog.composition.Clear(); ReleaseInput(); } @@ -119,15 +123,27 @@ void Apply(Dialog& dialog, ImeHostInputKind kind, const char16_t* units, std::si case ImeHostInputKind::Escape: Finish(EndStatusUserCanceled); return; + case ImeHostInputKind::Paste: + Insert(dialog, units, length); + return; + case ImeHostInputKind::Preedit: + return; } } void Receive(ImeHostInputKind kind, const char16_t* units, std::size_t length) { std::lock_guard lock(g_mutex); if (g_dialog.status != StatusRunning) return; + if (ImeRouteWhileComposing(g_dialog.composition, kind) == ImeCompositionRoute::Swallow) return; Apply(g_dialog, kind, units, length); } +void ReceivePreedit(const char16_t* units, std::size_t length, std::int32_t start, std::int32_t selected) { + std::lock_guard lock(g_mutex); + if (g_dialog.status != StatusRunning) return; + g_dialog.composition.Set(std::u16string(units, length), start, selected); +} + void ReceivePad(const ImeHostPad& pad) { std::lock_guard lock(g_mutex); Dialog& dialog = g_dialog; @@ -183,6 +199,9 @@ bool ProvideView(ImeKeyboardView& view) { if (dialog.status != StatusRunning) return false; view.text = dialog.text; view.caret = dialog.caret; + view.preedit = dialog.composition.text; + view.preeditCursor = dialog.composition.cursor; + view.preeditSelection = dialog.composition.selection; view.rows = ImeDialog::PadKeyboardRows(dialog.keyboard); view.columns = ImeDialog::PadKeyboardColumns(); view.keys.clear(); @@ -238,10 +257,13 @@ int APS5_VABI sceImeDialogGetPanelSizeExtended(const Param* param, const Extende } int APS5_VABI sceImeDialogGetResult(Result* result) { - std::lock_guard lock(g_mutex); - if (result == nullptr || g_dialog.status != StatusFinished) return ErrorNotOpened; - *result = Result{}; - result->endstatus = g_dialog.endStatus; + Result res{}; + { + std::lock_guard lock(g_mutex); + if (result == nullptr || g_dialog.status != StatusFinished) return ErrorNotOpened; + res.endstatus = g_dialog.endStatus; + } + if (!GuestCopyOut_nid_no_patch(result, &res, sizeof(res))) return ErrorNotOpened; return 0; } @@ -266,6 +288,7 @@ int APS5_VABI sceImeDialogInit(const Param* param, const ExtendedParam* extended g_dialog.keyboard = ImeDialog::PadKeyboardOpen(KeyboardOptions(*param, extended)); g_dialog.status = StatusRunning; ImeSetHostTextSink_nid_no_patch(&Receive); + ImeSetHostPreeditSink_nid_no_patch(&ReceivePreedit); ImeSetHostPadSink_nid_no_patch(&ReceivePad); ImeSetKeyboardViewProvider_nid_no_patch(&ProvideView); return 0; diff --git a/core/libs/prx/libSceIpmi/Export.cpp b/core/libs/prx/libSceIpmi/Export.cpp index 6bd6f70161..7d74d75265 100644 --- a/core/libs/prx/libSceIpmi/Export.cpp +++ b/core/libs/prx/libSceIpmi/Export.cpp @@ -34,205 +34,165 @@ int APS5_VABI sceIpmiMgrStartDump(void) { return 0; } -int APS5_VABI sceIpmiUnknown_nJthG9040U8_nid_no_patch() { +int APS5_VABI _ZN4IPMI4impl10ClientImpl10disconnectEv() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("nJthG9040U8", sceIpmiUnknown_nJthG9040U8_nid_no_patch); -int APS5_VABI sceIpmiUnknown_64umtbwiizI_nid_no_patch() { +int APS5_VABI _ZN4IPMI4impl10ClientImpl10initializeEPKNS_6Client6ConfigEPv() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("64umtbwiizI", sceIpmiUnknown_64umtbwiizI_nid_no_patch); -int APS5_VABI sceIpmiUnknown_X8bM2L_G59E_nid_no_patch() { +int APS5_VABI _ZN4IPMI4impl10ClientImpl11getUserDataEv() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("X8bM2L-G59E", sceIpmiUnknown_X8bM2L_G59E_nid_no_patch); -int APS5_VABI sceIpmiUnknown_pLNZVjiFPKE_nid_no_patch() { +int APS5_VABI _ZN4IPMI4impl10ClientImpl12tryGetResultEjPiPvPmm() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("pLNZVjiFPKE", sceIpmiUnknown_pLNZVjiFPKE_nid_no_patch); -int APS5_VABI sceIpmiUnknown_exZt85cnx08_nid_no_patch() { +int APS5_VABI _ZN4IPMI4impl10ClientImpl12tryGetResultEjjPiPNS_10BufferInfoEj() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("exZt85cnx08", sceIpmiUnknown_exZt85cnx08_nid_no_patch); -int APS5_VABI sceIpmiUnknown_y29rp3N5qf4_nid_no_patch() { +int APS5_VABI _ZN4IPMI4impl10ClientImpl13pollEventFlagEjmjPm() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("y29rp3N5qf4", sceIpmiUnknown_y29rp3N5qf4_nid_no_patch); -int APS5_VABI sceIpmiUnknown_eVn6vS9O8EI_nid_no_patch() { +int APS5_VABI _ZN4IPMI4impl10ClientImpl13waitEventFlagEjmjPmPj() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("eVn6vS9O8EI", sceIpmiUnknown_eVn6vS9O8EI_nid_no_patch); -int APS5_VABI sceIpmiUnknown_lsbUGZCjQy8_nid_no_patch() { +int APS5_VABI _ZN4IPMI4impl10ClientImpl16invokeSyncMethodEjPKNS_8DataInfoEjPiPNS_10BufferInfoEj() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("lsbUGZCjQy8", sceIpmiUnknown_lsbUGZCjQy8_nid_no_patch); -int APS5_VABI sceIpmiUnknown_57XmD42P9gQ_nid_no_patch() { +int APS5_VABI _ZN4IPMI4impl10ClientImpl16invokeSyncMethodEjPKvmPiPvPmm() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("57XmD42P9gQ", sceIpmiUnknown_57XmD42P9gQ_nid_no_patch); -int APS5_VABI sceIpmiUnknown_FsiuQCKSE5Q_nid_no_patch() { +int APS5_VABI _ZN4IPMI4impl10ClientImpl17invokeAsyncMethodEjPKNS_8DataInfoEjPjPKNS_6Client12EventNotifeeE() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("FsiuQCKSE5Q", sceIpmiUnknown_FsiuQCKSE5Q_nid_no_patch); -int APS5_VABI sceIpmiUnknown_8kU11lW12ss_nid_no_patch() { +int APS5_VABI _ZN4IPMI4impl10ClientImpl17invokeAsyncMethodEjPKvmPjPKNS_6Client12EventNotifeeE() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("8kU11lW12ss", sceIpmiUnknown_8kU11lW12ss_nid_no_patch); -int APS5_VABI sceIpmiUnknown_cOli1uePSwY_nid_no_patch() { +int APS5_VABI _ZN4IPMI4impl10ClientImpl19terminateConnectionEv() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("cOli1uePSwY", sceIpmiUnknown_cOli1uePSwY_nid_no_patch); -int APS5_VABI sceIpmiUnknown_rU0UzNMZDIs_nid_no_patch() { +int APS5_VABI _ZN4IPMI4impl10ClientImpl6getMsgEjPvPmmPj() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("rU0UzNMZDIs", sceIpmiUnknown_rU0UzNMZDIs_nid_no_patch); -int APS5_VABI sceIpmiUnknown_vogma5QshU4_nid_no_patch() { +int APS5_VABI _ZN4IPMI4impl10ClientImpl7connectEPKvmPi() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("vogma5QshU4", sceIpmiUnknown_vogma5QshU4_nid_no_patch); -int APS5_VABI sceIpmiUnknown_R9CB6TmSpeU_nid_no_patch() { +int APS5_VABI _ZN4IPMI4impl10ClientImpl7destroyEv() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("R9CB6TmSpeU", sceIpmiUnknown_R9CB6TmSpeU_nid_no_patch); -int APS5_VABI sceIpmiUnknown_k9ixmB0yrP4_nid_no_patch() { +int APS5_VABI _ZN4IPMI4impl10ClientImpl9tryGetMsgEjPvPmm() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("k9ixmB0yrP4", sceIpmiUnknown_k9ixmB0yrP4_nid_no_patch); -int APS5_VABI sceIpmiUnknown_o8Es_aQYa2Q_nid_no_patch() { +int APS5_VABI _ZN4IPMI4impl10ClientImplC1Ev() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("o8Es+aQYa2Q", sceIpmiUnknown_o8Es_aQYa2Q_nid_no_patch); -int APS5_VABI sceIpmiUnknown_N0iXfw1xf1Q_nid_no_patch() { +int APS5_VABI _ZN4IPMI4impl10ClientImplC2Ev() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("N0iXfw1xf1Q", sceIpmiUnknown_N0iXfw1xf1Q_nid_no_patch); -int APS5_VABI sceIpmiUnknown_EsoUVUbGMdg_nid_no_patch() { +int APS5_VABI _ZN4IPMI4impl10ClientImplD0Ev() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("EsoUVUbGMdg", sceIpmiUnknown_EsoUVUbGMdg_nid_no_patch); -int APS5_VABI sceIpmiUnknown_H2HLRQerr_g_nid_no_patch() { +int APS5_VABI _ZN4IPMI4impl10ClientImplD1Ev() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("H2HLRQerr+g", sceIpmiUnknown_H2HLRQerr_g_nid_no_patch); -int APS5_VABI sceIpmiUnknown_lxtXq6qmrC8_nid_no_patch() { +int APS5_VABI _ZN4IPMI4impl10ClientImplD2Ev() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("lxtXq6qmrC8", sceIpmiUnknown_lxtXq6qmrC8_nid_no_patch); -int APS5_VABI sceIpmiUnknown_w9uM8jBkjhw_nid_no_patch() { +int APS5_VABI _ZN4IPMI4impl10ServerImpl11getUserDataEv() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("w9uM8jBkjhw", sceIpmiUnknown_w9uM8jBkjhw_nid_no_patch); -int APS5_VABI sceIpmiUnknown_GIniJssLcts_nid_no_patch() { +int APS5_VABI _ZN4IPMI4impl10ServerImpl11tryDispatchEPvm() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("GIniJssLcts", sceIpmiUnknown_GIniJssLcts_nid_no_patch); -int APS5_VABI sceIpmiUnknown_O_Rk6bioKMY_nid_no_patch() { +int APS5_VABI _ZN4IPMI4impl10ServerImpl13createSessionEPPNS_7SessionEPvS5_() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("O+Rk6bioKMY", sceIpmiUnknown_O_Rk6bioKMY_nid_no_patch); -int APS5_VABI sceIpmiUnknown_P17jonIyIB8_nid_no_patch() { +int APS5_VABI _ZN4IPMI4impl10ServerImpl13runDispatcherEPvm() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("P17jonIyIB8", sceIpmiUnknown_P17jonIyIB8_nid_no_patch); -int APS5_VABI sceIpmiUnknown_e_OnyPe42tI_nid_no_patch() { +int APS5_VABI _ZN4IPMI4impl10ServerImpl18shutdownDispatcherEv() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("e-OnyPe42tI", sceIpmiUnknown_e_OnyPe42tI_nid_no_patch); -int APS5_VABI sceIpmiUnknown_dPOHeORqhbw_nid_no_patch() { +int APS5_VABI _ZN4IPMI4impl10ServerImpl7destroyEv() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("dPOHeORqhbw", sceIpmiUnknown_dPOHeORqhbw_nid_no_patch); -int APS5_VABI sceIpmiUnknown_w_zWDsN_0bQ_nid_no_patch() { +int APS5_VABI _ZN4IPMI4impl10ServerImplC1Ev() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("w-zWDsN+0bQ", sceIpmiUnknown_w_zWDsN_0bQ_nid_no_patch); -int APS5_VABI sceIpmiUnknown_Gzah1AMZDS8_nid_no_patch() { +int APS5_VABI _ZN4IPMI4impl10ServerImplC2Ev() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("Gzah1AMZDS8", sceIpmiUnknown_Gzah1AMZDS8_nid_no_patch); -int APS5_VABI sceIpmiUnknown_2VzR2bMe3iU_nid_no_patch() { +int APS5_VABI _ZN4IPMI4impl10ServerImplD0Ev() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("2VzR2bMe3iU", sceIpmiUnknown_2VzR2bMe3iU_nid_no_patch); -int APS5_VABI sceIpmiUnknown_D1A_mAFwjT4_nid_no_patch() { +int APS5_VABI _ZN4IPMI4impl10ServerImplD1Ev() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("D1A-mAFwjT4", sceIpmiUnknown_D1A_mAFwjT4_nid_no_patch); -int APS5_VABI sceIpmiUnknown_uMWav34mMq8_nid_no_patch() { +int APS5_VABI _ZN4IPMI4impl10ServerImplD2Ev() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("uMWav34mMq8", sceIpmiUnknown_uMWav34mMq8_nid_no_patch); -int APS5_VABI sceIpmiUnknown_32j2cyCl3Yo_nid_no_patch() { +int APS5_VABI _ZN4IPMI4impl11SessionImpl10trySendMsgEjPKvm() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("32j2cyCl3Yo", sceIpmiUnknown_32j2cyCl3Yo_nid_no_patch); -int APS5_VABI sceIpmiUnknown_bNAnspllfsc_nid_no_patch() { +int APS5_VABI _ZN4IPMI4impl11SessionImpl11getUserDataEv() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("bNAnspllfsc", sceIpmiUnknown_bNAnspllfsc_nid_no_patch); -int APS5_VABI sceIpmiUnknown_tFY6zzpnlOs_nid_no_patch() { +int APS5_VABI _ZN4IPMI4impl11SessionImpl11tryDispatchEPvm() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("tFY6zzpnlOs", sceIpmiUnknown_tFY6zzpnlOs_nid_no_patch); -int APS5_VABI sceIpmiUnknown_vk71LUV2VVw_nid_no_patch() { +int APS5_VABI _ZN4IPMI4impl11SessionImpl12getClientPidEv() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("vk71LUV2VVw", sceIpmiUnknown_vk71LUV2VVw_nid_no_patch); -int APS5_VABI sceIpmiUnknown__S36x8s8CAw_nid_no_patch() { +int APS5_VABI _ZN4IPMI4impl11SessionImpl12setEventFlagEjm() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("-S36x8s8CAw", sceIpmiUnknown__S36x8s8CAw_nid_no_patch); -int APS5_VABI sceIpmiUnknown_03U1u_RK3vg_nid_no_patch() { +int APS5_VABI _ZN4IPMI4impl11SessionImpl13emptyMsgQueueEj() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("03U1u-RK3vg", sceIpmiUnknown_03U1u_RK3vg_nid_no_patch); -int APS5_VABI sceIpmiUnknown_AYyxvDy_SFU_nid_no_patch() { +int APS5_VABI _ZN4IPMI4impl11SessionImpl26respondToSyncMethodRequestEiPKNS_10BufferInfoEj() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("AYyxvDy-SFU", sceIpmiUnknown_AYyxvDy_SFU_nid_no_patch); -int APS5_VABI sceIpmiUnknown_r0t1NxWUNwI_nid_no_patch() { +int APS5_VABI _ZN4IPMI4impl11SessionImpl26respondToSyncMethodRequestEiPKvm() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("r0t1NxWUNwI", sceIpmiUnknown_r0t1NxWUNwI_nid_no_patch); } diff --git a/core/libs/prx/libSceIpmi/include/Api.hpp b/core/libs/prx/libSceIpmi/include/Api.hpp index 200ce258ea..ae6dfa84ed 100644 --- a/core/libs/prx/libSceIpmi/include/Api.hpp +++ b/core/libs/prx/libSceIpmi/include/Api.hpp @@ -59,4 +59,45 @@ int APS5_VABI _ZN4IPMI6Client6Config26SyncMethodInvocationConfigC2Ev(void); int APS5_VABI _ZN4IPMI6Client6Config14MsgQueueConfigC2Ev(void); int APS5_VABI _ZN4IPMI6Server6ConfigC2Ev(void); +int APS5_VABI _ZN4IPMI4impl10ClientImpl10disconnectEv(); +int APS5_VABI _ZN4IPMI4impl10ClientImpl10initializeEPKNS_6Client6ConfigEPv(); +int APS5_VABI _ZN4IPMI4impl10ClientImpl11getUserDataEv(); +int APS5_VABI _ZN4IPMI4impl10ClientImpl12tryGetResultEjPiPvPmm(); +int APS5_VABI _ZN4IPMI4impl10ClientImpl12tryGetResultEjjPiPNS_10BufferInfoEj(); +int APS5_VABI _ZN4IPMI4impl10ClientImpl13pollEventFlagEjmjPm(); +int APS5_VABI _ZN4IPMI4impl10ClientImpl13waitEventFlagEjmjPmPj(); +int APS5_VABI _ZN4IPMI4impl10ClientImpl16invokeSyncMethodEjPKNS_8DataInfoEjPiPNS_10BufferInfoEj(); +int APS5_VABI _ZN4IPMI4impl10ClientImpl16invokeSyncMethodEjPKvmPiPvPmm(); +int APS5_VABI _ZN4IPMI4impl10ClientImpl17invokeAsyncMethodEjPKNS_8DataInfoEjPjPKNS_6Client12EventNotifeeE(); +int APS5_VABI _ZN4IPMI4impl10ClientImpl17invokeAsyncMethodEjPKvmPjPKNS_6Client12EventNotifeeE(); +int APS5_VABI _ZN4IPMI4impl10ClientImpl19terminateConnectionEv(); +int APS5_VABI _ZN4IPMI4impl10ClientImpl6getMsgEjPvPmmPj(); +int APS5_VABI _ZN4IPMI4impl10ClientImpl7connectEPKvmPi(); +int APS5_VABI _ZN4IPMI4impl10ClientImpl7destroyEv(); +int APS5_VABI _ZN4IPMI4impl10ClientImpl9tryGetMsgEjPvPmm(); +int APS5_VABI _ZN4IPMI4impl10ClientImplC1Ev(); +int APS5_VABI _ZN4IPMI4impl10ClientImplC2Ev(); +int APS5_VABI _ZN4IPMI4impl10ClientImplD0Ev(); +int APS5_VABI _ZN4IPMI4impl10ClientImplD1Ev(); +int APS5_VABI _ZN4IPMI4impl10ClientImplD2Ev(); +int APS5_VABI _ZN4IPMI4impl10ServerImpl11getUserDataEv(); +int APS5_VABI _ZN4IPMI4impl10ServerImpl11tryDispatchEPvm(); +int APS5_VABI _ZN4IPMI4impl10ServerImpl13createSessionEPPNS_7SessionEPvS5_(); +int APS5_VABI _ZN4IPMI4impl10ServerImpl13runDispatcherEPvm(); +int APS5_VABI _ZN4IPMI4impl10ServerImpl18shutdownDispatcherEv(); +int APS5_VABI _ZN4IPMI4impl10ServerImpl7destroyEv(); +int APS5_VABI _ZN4IPMI4impl10ServerImplC1Ev(); +int APS5_VABI _ZN4IPMI4impl10ServerImplC2Ev(); +int APS5_VABI _ZN4IPMI4impl10ServerImplD0Ev(); +int APS5_VABI _ZN4IPMI4impl10ServerImplD1Ev(); +int APS5_VABI _ZN4IPMI4impl10ServerImplD2Ev(); +int APS5_VABI _ZN4IPMI4impl11SessionImpl10trySendMsgEjPKvm(); +int APS5_VABI _ZN4IPMI4impl11SessionImpl11getUserDataEv(); +int APS5_VABI _ZN4IPMI4impl11SessionImpl11tryDispatchEPvm(); +int APS5_VABI _ZN4IPMI4impl11SessionImpl12getClientPidEv(); +int APS5_VABI _ZN4IPMI4impl11SessionImpl12setEventFlagEjm(); +int APS5_VABI _ZN4IPMI4impl11SessionImpl13emptyMsgQueueEj(); +int APS5_VABI _ZN4IPMI4impl11SessionImpl26respondToSyncMethodRequestEiPKNS_10BufferInfoEj(); +int APS5_VABI _ZN4IPMI4impl11SessionImpl26respondToSyncMethodRequestEiPKvm(); + } diff --git a/core/libs/prx/libSceLibcInternal/CMakeLists.txt b/core/libs/prx/libSceLibcInternal/CMakeLists.txt index da60a49d7c..b3bbfa698f 100644 --- a/core/libs/prx/libSceLibcInternal/CMakeLists.txt +++ b/core/libs/prx/libSceLibcInternal/CMakeLists.txt @@ -4,6 +4,19 @@ set_source_files_properties(UserContext.cpp PROPERTIES COMPILE_OPTIONS -fno-omit target_link_libraries(libSceLibcInternal PRIVATE ${CMAKE_DL_LIBS}) target_sources(libSceLibcInternal PRIVATE Directory.cpp) target_sources(libSceLibcInternal PRIVATE InventoryExports.cpp) +target_sources(libSceLibcInternal PRIVATE Pad.cpp) +if(BUILD_TESTING) + add_test_executable(guest_libc_internal_pad_tests ${LIBS_INCLUDE_DIR}/tests/GuestLibcInternalPad.cpp) + target_include_directories(guest_libc_internal_pad_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_libc_internal_pad_tests PRIVATE libSceLibcInternal libkernel libc) + configure_windows_unwind(guest_libc_internal_pad_tests) + add_test(NAME guest_libc_internal_pad COMMAND guest_libc_internal_pad_tests) + set_tests_properties(guest_libc_internal_pad PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_libc_internal_pad PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() target_sources(libSceLibcInternal PRIVATE InitOnce.cpp) target_sources(libSceLibcInternal PRIVATE Mutex.cpp) target_sources(libSceLibcInternal PRIVATE AlignedNew.cpp) @@ -44,6 +57,20 @@ if(BUILD_TESTING) endif() endif() target_sources(libSceLibcInternal PRIVATE Err.cpp) +target_sources(libSceLibcInternal PRIVATE RecoveredInventory.cpp) +target_sources(libSceLibcInternal PRIVATE ExceptionDestructors.cpp) +if(BUILD_TESTING) + add_test_executable(guest_libc_internal_exception_destructors_tests ${LIBS_INCLUDE_DIR}/tests/GuestLibcInternalExceptionDestructors.cpp) + target_include_directories(guest_libc_internal_exception_destructors_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_libc_internal_exception_destructors_tests PRIVATE libSceLibcInternal libkernel libc) + configure_windows_unwind(guest_libc_internal_exception_destructors_tests) + add_test(NAME guest_libc_internal_exception_destructors COMMAND guest_libc_internal_exception_destructors_tests) + set_tests_properties(guest_libc_internal_exception_destructors PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_libc_internal_exception_destructors PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() configure_windows_unwind(libSceLibcInternal) target_sources(libSceLibcInternal PRIVATE FilesystemStatistics.cpp PathStatistics.cpp) target_sources(libSceLibcInternal PRIVATE Inet.cpp) @@ -109,6 +136,15 @@ if(BUILD_TESTING) set_tests_properties(guest_utime PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + if(NOT WIN32) + add_test_executable(guest_posix_spawn_errors_tests ${LIBS_INCLUDE_DIR}/tests/GuestPosixSpawnErrors.cpp) + target_include_directories(guest_posix_spawn_errors_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_posix_spawn_errors_tests PRIVATE libSceLibcInternal libc libkernel) + file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/spawn-errors) + add_test(NAME guest_posix_spawn_errors COMMAND guest_posix_spawn_errors_tests) + set_tests_properties(guest_posix_spawn_errors PROPERTIES TIMEOUT 20 + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/spawn-errors) + endif() add_test_executable(guest_posix_spawn_tests ${LIBS_INCLUDE_DIR}/tests/GuestPosixSpawn.cpp) target_include_directories(guest_posix_spawn_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_posix_spawn_tests PRIVATE libSceLibcInternal libc libkernel) @@ -120,6 +156,19 @@ if(BUILD_TESTING) set_tests_properties(guest_posix_spawn PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_posix_spawn_optional_pid_tests ${LIBS_INCLUDE_DIR}/tests/GuestPosixSpawnOptionalPid.cpp) + target_include_directories(guest_posix_spawn_optional_pid_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_posix_spawn_optional_pid_tests PRIVATE libSceLibcInternal libc libkernel) + configure_windows_unwind(guest_posix_spawn_optional_pid_tests) + file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/spawn-optional-pid) + add_test(NAME guest_posix_spawn_optional_pid COMMAND guest_posix_spawn_optional_pid_tests) + set_tests_properties(guest_posix_spawn_optional_pid PROPERTIES TIMEOUT 20 + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/spawn-optional-pid) + add_asan_test_options(guest_posix_spawn_optional_pid handle_sigbus=0) + if(WIN32) + set_tests_properties(guest_posix_spawn_optional_pid PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_program_name_tests ${LIBS_INCLUDE_DIR}/tests/GuestProgramName.cpp) target_include_directories(guest_program_name_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_program_name_tests PRIVATE libSceLibcInternal libkernel libc) diff --git a/core/libs/prx/libSceLibcInternal/ExceptionDestructors.cpp b/core/libs/prx/libSceLibcInternal/ExceptionDestructors.cpp new file mode 100644 index 0000000000..9419eec401 --- /dev/null +++ b/core/libs/prx/libSceLibcInternal/ExceptionDestructors.cpp @@ -0,0 +1,23 @@ +#include "prx/libSceLibcInternal/include/Api.hpp" +#include "prx/libc/include/Api.hpp" +#include "prx/libc/include/General.hpp" +#include "SceTypes.hpp" + +extern "C" { + +void APS5_VABI sceLibcInternalFutureErrorBaseDestructor_nid_no_patch(void* self) { + _ZNSt12future_errorD1Ev_nid_postfix(static_cast(self)); +} +APS5_EXPORT("XFh0C66aEms", sceLibcInternalFutureErrorBaseDestructor_nid_no_patch); + +void APS5_VABI sceLibcInternalOverflowErrorBaseDestructor_nid_no_patch(void* self) { + _ZNSt14overflow_errorD1Ev_nid_postfix(static_cast(self)); +} +APS5_EXPORT("OypvNf3Uq3c", sceLibcInternalOverflowErrorBaseDestructor_nid_no_patch); + +void APS5_VABI sceLibcInternalUnderflowErrorBaseDestructor_nid_no_patch(void* self) { + _ZNSt15underflow_errorD1Ev_nid_postfix(static_cast(self)); +} +APS5_EXPORT("xiqd+QkuYXc", sceLibcInternalUnderflowErrorBaseDestructor_nid_no_patch); + +} diff --git a/core/libs/prx/libSceLibcInternal/InventoryExports.cpp b/core/libs/prx/libSceLibcInternal/InventoryExports.cpp index 056e657d70..3047b1c7a3 100644 --- a/core/libs/prx/libSceLibcInternal/InventoryExports.cpp +++ b/core/libs/prx/libSceLibcInternal/InventoryExports.cpp @@ -94,11 +94,6 @@ int APS5_VABI libSceLibcInternalInventory_017_nid_no_patch() { } APS5_EXPORT("rNYLEsL7M0k", libSceLibcInternalInventory_017_nid_no_patch); -int APS5_VABI libSceLibcInternalInventory_018_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("XFh0C66aEms", libSceLibcInternalInventory_018_nid_no_patch); - int APS5_VABI libSceLibcInternalInventory_019_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } @@ -224,11 +219,6 @@ int APS5_VABI libSceLibcInternalInventory_043_nid_no_patch() { } APS5_EXPORT("EYZJsnX58DE", libSceLibcInternalInventory_043_nid_no_patch); -int APS5_VABI libSceLibcInternalInventory_044_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("OypvNf3Uq3c", libSceLibcInternalInventory_044_nid_no_patch); - int APS5_VABI libSceLibcInternalInventory_045_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } @@ -259,11 +249,6 @@ int APS5_VABI libSceLibcInternalInventory_050_nid_no_patch() { } APS5_EXPORT("olsoiZsezkk", libSceLibcInternalInventory_050_nid_no_patch); -int APS5_VABI libSceLibcInternalInventory_051_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("xiqd+QkuYXc", libSceLibcInternalInventory_051_nid_no_patch); - int APS5_VABI libSceLibcInternalInventory_052_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } @@ -1444,16 +1429,6 @@ int APS5_VABI libSceLibcInternalInventory_284_nid_no_patch() { } APS5_EXPORT("n8onIBR4Qdk", libSceLibcInternalInventory_284_nid_no_patch); -int APS5_VABI libSceLibcInternalUnknown_BIALMFTZ75I_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("BIALMFTZ75I", libSceLibcInternalUnknown_BIALMFTZ75I_nid_no_patch); - -int APS5_VABI libSceLibcInternalUnknown_fqLrWSWcGHw_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("fqLrWSWcGHw", libSceLibcInternalUnknown_fqLrWSWcGHw_nid_no_patch); - int APS5_VABI libSceLibcInternalInventory_285_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } diff --git a/core/libs/prx/libSceLibcInternal/InventoryExports6.cpp b/core/libs/prx/libSceLibcInternal/InventoryExports6.cpp index a54350a928..25ace47dd5 100644 --- a/core/libs/prx/libSceLibcInternal/InventoryExports6.cpp +++ b/core/libs/prx/libSceLibcInternal/InventoryExports6.cpp @@ -471,12 +471,6 @@ int APS5_VABI sceLibcInternalUnknown6_080_bTQcNwRc8hE_nid_no_patch() { } APS5_EXPORT("bTQcNwRc8hE", sceLibcInternalUnknown6_080_bTQcNwRc8hE_nid_no_patch); -int APS5_VABI sceLibcInternalUnknown6_081_XyJPhPqpzMw_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); - return 0; -} -APS5_EXPORT("XyJPhPqpzMw", sceLibcInternalUnknown6_081_XyJPhPqpzMw_nid_no_patch); - int APS5_VABI sceLibcInternalUnknown6_082_Sopthb9ztZI_nid_no_patch() { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceLibcInternal/Pad.cpp b/core/libs/prx/libSceLibcInternal/Pad.cpp new file mode 100644 index 0000000000..76fe427ebe --- /dev/null +++ b/core/libs/prx/libSceLibcInternal/Pad.cpp @@ -0,0 +1,13 @@ +#include "prx/libSceLibcInternal/include/Api.hpp" +#include "prx/libc/include/Api.hpp" +#include "prx/libc/include/General.hpp" +#include "SceTypes.hpp" + +extern "C" { + +void APS5_VABI sceLibcInternalPadCompleteDestructor_nid_no_patch(void* self) { + _ZNSt4_PadD2Ev_nid_postfix(static_cast(self)); +} +APS5_EXPORT("XyJPhPqpzMw", sceLibcInternalPadCompleteDestructor_nid_no_patch); + +} diff --git a/core/libs/prx/libSceLibcInternal/RecoveredInventory.cpp b/core/libs/prx/libSceLibcInternal/RecoveredInventory.cpp new file mode 100644 index 0000000000..8aa0952286 --- /dev/null +++ b/core/libs/prx/libSceLibcInternal/RecoveredInventory.cpp @@ -0,0 +1,166 @@ +#include "prx/libSceLibcInternal/include/RecoveredInventory.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI __std_minmax_d_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI __std_minmax_element_f_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI __std_bitset_to_string_1_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI __std_max_1i_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI __std_minmax_element_d_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI __std_max_2u_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZNSt8__sce_v215_Thrd_sleep_forEm_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI __std_atomic_notify_multi_direct_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI __std_min_d_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI __std_max_1u_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI __std_min_2i_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI __std_max_element_d_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI __std_min_1u_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI __std_min_element_f_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI __std_minmax_f_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI __std_min_f_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI __std_minmax_1u_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI __truncdfhf2_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI __std_minmax_4i_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI __std_atomic_wait_get_deadline_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI __std_max_2i_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceLibcInternalPrimitiveMallocStatsFast_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI __std_atomic_wait_get_remaining_timeout_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI __std_atomic_wait_direct_8_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI __std_min_1i_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI __std_min_2u_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI __std_minmax_1i_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI __std_minmax_8i_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI __std_max_d_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI __std_max_element_f_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI __std_minmax_4u_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceLibcInternalPrimitiveMallocStats_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZNSt8__sce_v236_Mtx_init_with_default_name_overrideEPP13pthread_mutexiPKc_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI __std_minmax_2u_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI __std_minmax_2i_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI __std_max_f_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI __std_minmax_8u_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI __std_min_element_d_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI __std_atomic_wait_direct_16_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI __std_bitset_to_string_2_nid_postfix() { + NotImplemented_nid_no_patch(__func__); +} + +} diff --git a/core/libs/prx/libSceLibcInternal/Spawn.cpp b/core/libs/prx/libSceLibcInternal/Spawn.cpp index edb0f41a8f..c6aadc3af3 100644 --- a/core/libs/prx/libSceLibcInternal/Spawn.cpp +++ b/core/libs/prx/libSceLibcInternal/Spawn.cpp @@ -4,6 +4,7 @@ #include "prx/libc/include/ChildProcesses.hpp" #include "prx/libc/include/FilesystemError.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/HostErrno.hpp" #ifdef _WIN32 #include "prx/libc/include/WindowsDescriptor.hpp" #endif @@ -382,6 +383,10 @@ int SpawnChild(const char* function, const std::string& guest, PosixSpawnFileAct return 0; } #else +int HostSpawnError(int error) { + return error == 0 ? 0 : GuestErrnoFromHost_nid_no_patch(error); +} + int SpawnChild(const char* function, const std::string& guest, PosixSpawnFileActions* const* actions, PosixSpawnAttributes* const* attributes, std::int32_t* processId, char* const* arguments, char* const* environment) { const auto target = ConvertedExecutableTarget(function, guest); @@ -391,7 +396,7 @@ int SpawnChild(const char* function, const std::string& guest, PosixSpawnFileAct if ((flags & ~SpawnFlagsImplemented) != 0) NotImplemented_nid_no_patch(("posix_spawn: the attribute flags " + HexFlag(flags)).c_str()); if (flags != 0) { - if (posix_spawnattr_init(&hostAttributes) != 0) return Fault; + if (const int error = HostSpawnError(posix_spawnattr_init(&hostAttributes))) return error; usedAttributes = &hostAttributes; short hostFlags = 0; if ((flags & SpawnFlagResetIds) != 0) hostFlags |= POSIX_SPAWN_RESETIDS; @@ -404,9 +409,9 @@ int SpawnChild(const char* function, const std::string& guest, PosixSpawnFileAct posix_spawn_file_actions_t hostActions; posix_spawn_file_actions_t* usedActions = nullptr; if (actions != nullptr && !(*actions)->actions.empty()) { - if (posix_spawn_file_actions_init(&hostActions) != 0) { + if (const int error = HostSpawnError(posix_spawn_file_actions_init(&hostActions))) { if (usedAttributes != nullptr) posix_spawnattr_destroy(usedAttributes); - return Fault; + return error; } usedActions = &hostActions; } @@ -415,17 +420,17 @@ int SpawnChild(const char* function, const std::string& guest, PosixSpawnFileAct for (const auto& action : (*actions)->actions) { switch (action.kind) { case PosixSpawnFileActionKind::Close: - result = posix_spawn_file_actions_addclose(usedActions, action.descriptor); + result = HostSpawnError(posix_spawn_file_actions_addclose(usedActions, action.descriptor)); break; case PosixSpawnFileActionKind::Duplicate: - result = posix_spawn_file_actions_adddup2(usedActions, action.descriptor, action.newDescriptor); + result = HostSpawnError(posix_spawn_file_actions_adddup2(usedActions, action.descriptor, action.newDescriptor)); break; case PosixSpawnFileActionKind::Open: { std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(action.path.c_str(), native)) result = error; else { try { - result = posix_spawn_file_actions_addopen(usedActions, action.descriptor, native.c_str(), HostOpenFlags(action.openFlags), action.mode); + result = HostSpawnError(posix_spawn_file_actions_addopen(usedActions, action.descriptor, native.c_str(), HostOpenFlags(action.openFlags), action.mode)); } catch (const std::invalid_argument&) { result = InvalidArgument; } @@ -443,7 +448,7 @@ int SpawnChild(const char* function, const std::string& guest, PosixSpawnFileAct if (hostEnvironment == nullptr) hostEnvironment = *_NSGetEnviron(); #endif pid_t childPid = -1; - result = posix_spawn(&childPid, target.c_str(), usedActions, usedAttributes, arguments, hostEnvironment); + result = HostSpawnError(posix_spawn(&childPid, target.c_str(), usedActions, usedAttributes, arguments, hostEnvironment)); if (result == 0 && processId != nullptr) *processId = static_cast(childPid); } if (usedActions != nullptr) posix_spawn_file_actions_destroy(usedActions); @@ -590,7 +595,8 @@ int SpawnProgram(const char* function, const char* file, bool searchesPath, Posi std::string found; if (!ProgramFound(file, searchesPath, found)) return NoSuchFile; if (arguments == nullptr) return Fault; - return SpawnChild(function, found, actions, attributes, processId, arguments, environment); + std::int32_t ignoredProcessId = 0; + return SpawnChild(function, found, actions, attributes, processId != nullptr ? processId : &ignoredProcessId, arguments, environment); } int APS5_VABI posix_spawn_nid_postfix(std::int32_t* processId, const char* path, PosixSpawnFileActions* const* actions, PosixSpawnAttributes* const* attributes, char* const* arguments, char* const* environment) { diff --git a/core/libs/prx/libSceLibcInternal/include/Api.hpp b/core/libs/prx/libSceLibcInternal/include/Api.hpp index 97e30f397e..d2be46cc9b 100644 --- a/core/libs/prx/libSceLibcInternal/include/Api.hpp +++ b/core/libs/prx/libSceLibcInternal/include/Api.hpp @@ -94,6 +94,7 @@ std::uint64_t APS5_VABI _ZNSt10filesystem10_File_sizeEPKcPi_nid_postfix(const ch int APS5_VABI _ZNSt10filesystem16_Last_write_timeEPKcPlS2__nid_postfix(const char* path, std::int64_t* seconds, std::int64_t* nanoseconds); int APS5_VABI _ZNSt10filesystem9_Make_dirEPKcS1__nid_postfix(const char* path, const char* existing); bool APS5_VABI _ZNSt10filesystem11_Remove_dirEPKc_nid_postfix(const char* path); +void APS5_VABI sceLibcInternalPadCompleteDestructor_nid_no_patch(void* self); int APS5_VABI _ZNSt10filesystem7_UnlinkEPKc_nid_postfix(const char* path); int APS5_VABI _ZNSt10filesystem6_ChmodEPKcNS_5permsE_nid_postfix(const char* path, int perms); bool APS5_VABI _ZNSt10filesystem12_Current_getERA260_c_nid_postfix(char* target); @@ -109,6 +110,9 @@ int APS5_VABI _Mtxunlock_nid_postfix(PthreadMutex* mutex); int APS5_VABI _Mtxdst_nid_postfix(PthreadMutex* mutex); void* APS5_VABI sceLibcInternalAlignedArrayNewNothrow_nid_no_patch(std::size_t size, std::size_t alignment, const void* tag) noexcept; void APS5_VABI sceLibcInternalAlignedArrayDelete_nid_no_patch(void* pointer, std::size_t alignment); +void APS5_VABI sceLibcInternalFutureErrorBaseDestructor_nid_no_patch(void* self); +void APS5_VABI sceLibcInternalOverflowErrorBaseDestructor_nid_no_patch(void* self); +void APS5_VABI sceLibcInternalUnderflowErrorBaseDestructor_nid_no_patch(void* self); int APS5_VABI sceLibcMspaceSetMallocCallback(); int APS5_VABI sceLibcPafMspaceReportMemoryBlocks(); diff --git a/core/libs/prx/libSceLibcInternal/include/InventoryExports.hpp b/core/libs/prx/libSceLibcInternal/include/InventoryExports.hpp index f513e0d264..83ea0a2f25 100644 --- a/core/libs/prx/libSceLibcInternal/include/InventoryExports.hpp +++ b/core/libs/prx/libSceLibcInternal/include/InventoryExports.hpp @@ -22,7 +22,6 @@ int APS5_VABI libSceLibcInternalInventory_014_nid_no_patch(); int APS5_VABI libSceLibcInternalInventory_015_nid_no_patch(); int APS5_VABI libSceLibcInternalInventory_016_nid_no_patch(); int APS5_VABI libSceLibcInternalInventory_017_nid_no_patch(); -int APS5_VABI libSceLibcInternalInventory_018_nid_no_patch(); int APS5_VABI libSceLibcInternalInventory_019_nid_no_patch(); int APS5_VABI libSceLibcInternalInventory_020_nid_no_patch(); int APS5_VABI libSceLibcInternalInventory_021_nid_no_patch(); @@ -48,14 +47,12 @@ int APS5_VABI libSceLibcInternalInventory_040_nid_no_patch(); int APS5_VABI libSceLibcInternalInventory_041_nid_no_patch(); int APS5_VABI libSceLibcInternalInventory_042_nid_no_patch(); int APS5_VABI libSceLibcInternalInventory_043_nid_no_patch(); -int APS5_VABI libSceLibcInternalInventory_044_nid_no_patch(); int APS5_VABI libSceLibcInternalInventory_045_nid_no_patch(); int APS5_VABI libSceLibcInternalInventory_046_nid_no_patch(); int APS5_VABI libSceLibcInternalInventory_047_nid_no_patch(); int APS5_VABI libSceLibcInternalInventory_048_nid_no_patch(); int APS5_VABI libSceLibcInternalInventory_049_nid_no_patch(); int APS5_VABI libSceLibcInternalInventory_050_nid_no_patch(); -int APS5_VABI libSceLibcInternalInventory_051_nid_no_patch(); int APS5_VABI libSceLibcInternalInventory_052_nid_no_patch(); int APS5_VABI libSceLibcInternalInventory_053_nid_no_patch(); int APS5_VABI libSceLibcInternalInventory_054_nid_no_patch(); @@ -292,8 +289,6 @@ int APS5_VABI libSceLibcInternalInventory_281_nid_no_patch(); int APS5_VABI libSceLibcInternalInventory_282_nid_no_patch(); int APS5_VABI libSceLibcInternalInventory_283_nid_no_patch(); int APS5_VABI libSceLibcInternalInventory_284_nid_no_patch(); -int APS5_VABI libSceLibcInternalUnknown_BIALMFTZ75I_nid_no_patch(); -int APS5_VABI libSceLibcInternalUnknown_fqLrWSWcGHw_nid_no_patch(); int APS5_VABI libSceLibcInternalInventory_285_nid_no_patch(); int APS5_VABI libSceLibcInternalUnknown_GG6441JdYkA_nid_no_patch(); int APS5_VABI libSceLibcInternalUnknown_QZ9YgTk_plus_yrE_nid_no_patch(); diff --git a/core/libs/prx/libSceLibcInternal/include/RecoveredInventory.hpp b/core/libs/prx/libSceLibcInternal/include/RecoveredInventory.hpp new file mode 100644 index 0000000000..9a381429bf --- /dev/null +++ b/core/libs/prx/libSceLibcInternal/include/RecoveredInventory.hpp @@ -0,0 +1,48 @@ +#pragma once + +#include "SceTypes.hpp" + +extern "C" { + +int APS5_VABI __std_minmax_d_nid_postfix(); +int APS5_VABI __std_minmax_element_f_nid_postfix(); +int APS5_VABI __std_bitset_to_string_1_nid_postfix(); +int APS5_VABI __std_max_1i_nid_postfix(); +int APS5_VABI __std_minmax_element_d_nid_postfix(); +int APS5_VABI __std_max_2u_nid_postfix(); +int APS5_VABI _ZNSt8__sce_v215_Thrd_sleep_forEm_nid_postfix(); +int APS5_VABI __std_atomic_notify_multi_direct_nid_postfix(); +int APS5_VABI __std_min_d_nid_postfix(); +int APS5_VABI __std_max_1u_nid_postfix(); +int APS5_VABI __std_min_2i_nid_postfix(); +int APS5_VABI __std_max_element_d_nid_postfix(); +int APS5_VABI __std_min_1u_nid_postfix(); +int APS5_VABI __std_min_element_f_nid_postfix(); +int APS5_VABI __std_minmax_f_nid_postfix(); +int APS5_VABI __std_min_f_nid_postfix(); +int APS5_VABI __std_minmax_1u_nid_postfix(); +int APS5_VABI __truncdfhf2_nid_postfix(); +int APS5_VABI __std_minmax_4i_nid_postfix(); +int APS5_VABI __std_atomic_wait_get_deadline_nid_postfix(); +int APS5_VABI __std_max_2i_nid_postfix(); +int APS5_VABI sceLibcInternalPrimitiveMallocStatsFast_nid_postfix(); +int APS5_VABI __std_atomic_wait_get_remaining_timeout_nid_postfix(); +int APS5_VABI __std_atomic_wait_direct_8_nid_postfix(); +int APS5_VABI __std_min_1i_nid_postfix(); +int APS5_VABI __std_min_2u_nid_postfix(); +int APS5_VABI __std_minmax_1i_nid_postfix(); +int APS5_VABI __std_minmax_8i_nid_postfix(); +int APS5_VABI __std_max_d_nid_postfix(); +int APS5_VABI __std_max_element_f_nid_postfix(); +int APS5_VABI __std_minmax_4u_nid_postfix(); +int APS5_VABI sceLibcInternalPrimitiveMallocStats_nid_postfix(); +int APS5_VABI _ZNSt8__sce_v236_Mtx_init_with_default_name_overrideEPP13pthread_mutexiPKc_nid_postfix(); +int APS5_VABI __std_minmax_2u_nid_postfix(); +int APS5_VABI __std_minmax_2i_nid_postfix(); +int APS5_VABI __std_max_f_nid_postfix(); +int APS5_VABI __std_minmax_8u_nid_postfix(); +int APS5_VABI __std_min_element_d_nid_postfix(); +int APS5_VABI __std_atomic_wait_direct_16_nid_postfix(); +int APS5_VABI __std_bitset_to_string_2_nid_postfix(); + +} diff --git a/core/libs/prx/libSceLibcInternal/libSceLibcInternal.hpp b/core/libs/prx/libSceLibcInternal/libSceLibcInternal.hpp index 0dc3722539..8fb6498e98 100644 --- a/core/libs/prx/libSceLibcInternal/libSceLibcInternal.hpp +++ b/core/libs/prx/libSceLibcInternal/libSceLibcInternal.hpp @@ -1,5 +1,6 @@ #pragma once +#include "prx/libSceLibcInternal/include/RecoveredInventory.hpp" #include "prx/libSceLibcInternal/include/RuntimeInternals.hpp" #include "prx/libSceLibcInternal/include/Api.hpp" #include "prx/libSceLibcInternal/include/MathInternals.hpp" diff --git a/core/libs/prx/libSceLibreSsl/CMakeLists.txt b/core/libs/prx/libSceLibreSsl/CMakeLists.txt index 4b1c4b65d0..ebaa00d309 100644 --- a/core/libs/prx/libSceLibreSsl/CMakeLists.txt +++ b/core/libs/prx/libSceLibreSsl/CMakeLists.txt @@ -1,10 +1,21 @@ -add_library(libSceLibreSsl SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Digest.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Runtime.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Aes.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Error.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Ocsp.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Stack.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Ui.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Rsa.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/X509StoreContext.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Pkey.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/EvpCipherCtx.cpp) +add_library(libSceLibreSsl SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Bignum.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Digest.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Runtime.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Aes.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Error.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Ocsp.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Stack.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Ui.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Rsa.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/X509StoreContext.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Pkey.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/EvpCipherCtx.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Evp.cpp) target_include_directories(libSceLibreSsl PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(libSceLibreSsl PRIVATE mbedcrypto) configure_windows_unwind(libSceLibreSsl) target_sources(libSceLibreSsl PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/Asn1.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/ExData.cpp) if(BUILD_TESTING) + add_test_executable(guest_libressl_bn_tests ${LIBS_INCLUDE_DIR}/tests/GuestLibreSslBn.cpp) + target_include_directories(guest_libressl_bn_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_libressl_bn_tests PRIVATE libSceLibreSsl libc) + configure_windows_unwind(guest_libressl_bn_tests) + add_test(NAME guest_libressl_bn COMMAND guest_libressl_bn_tests) + set_tests_properties(guest_libressl_bn PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_libressl_bn PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_libressl_x509_errors_tests ${LIBS_INCLUDE_DIR}/tests/GuestLibreSslX509Errors.cpp) target_include_directories(guest_libressl_x509_errors_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_libressl_x509_errors_tests PRIVATE libSceLibreSsl libc) @@ -360,3 +371,16 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endif() + +if(BUILD_TESTING) + add_test_executable(guest_libressl_evp_tests ${LIBS_INCLUDE_DIR}/tests/GuestLibreSslEvp.cpp) + target_include_directories(guest_libressl_evp_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_libressl_evp_tests PRIVATE libSceLibreSsl libc) + configure_windows_unwind(guest_libressl_evp_tests) + add_test(NAME guest_libressl_evp COMMAND guest_libressl_evp_tests) + set_tests_properties(guest_libressl_evp PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_libressl_evp PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceLibreSsl/Cmac.cmake b/core/libs/prx/libSceLibreSsl/Cmac.cmake new file mode 100644 index 0000000000..e7b8cf205c --- /dev/null +++ b/core/libs/prx/libSceLibreSsl/Cmac.cmake @@ -0,0 +1,32 @@ +target_sources(libSceLibreSsl PRIVATE ${CMAKE_CURRENT_LIST_DIR}/src/Cmac.cpp) +target_link_libraries(libSceLibreSsl PRIVATE mbedcrypto) +if(MINGW OR CMAKE_SYSTEM_NAME STREQUAL "Linux") + target_link_options(libSceLibreSsl PRIVATE LINKER:--exclude-libs,ALL) +endif() + +if(BUILD_TESTING) + add_test_executable(guest_libressl_cmac_tests ${LIBS_INCLUDE_DIR}/tests/GuestLibreSslCmac.cpp) + target_include_directories(guest_libressl_cmac_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_libressl_cmac_tests PRIVATE libSceLibreSsl libc mbedcrypto) + configure_windows_unwind(guest_libressl_cmac_tests) + add_test(NAME guest_libressl_cmac COMMAND guest_libressl_cmac_tests) + + add_test_executable(guest_libressl_cmac_patched_tests ${LIBS_INCLUDE_DIR}/tests/GuestLibreSslCmac.cpp) + target_include_directories(guest_libressl_cmac_patched_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_compile_definitions(guest_libressl_cmac_patched_tests PRIVATE ANYPS5_CMAC_DYNAMIC) + target_link_libraries(guest_libressl_cmac_patched_tests PRIVATE mbedcrypto ${CMAKE_DL_LIBS}) + configure_windows_unwind(guest_libressl_cmac_patched_tests) + if(WIN32) + target_link_libraries(guest_libressl_cmac_patched_tests PRIVATE libc) + endif() + add_custom_target(guest_libressl_cmac_patch DEPENDS ${PATCHED_DIR}/libSceLibreSsl.prx ${PATCHED_DIR}/libc.prx) + add_dependencies(guest_libressl_cmac_patched_tests guest_libressl_cmac_patch) + add_test(NAME guest_libressl_cmac_patched COMMAND guest_libressl_cmac_patched_tests ${PATCHED_DIR}) + set_tests_properties(guest_libressl_cmac guest_libressl_cmac_patched PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_libressl_cmac PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + set_tests_properties(guest_libressl_cmac_patched PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:${PATCHED_DIR}") + endif() +endif() diff --git a/core/libs/prx/libSceLibreSsl/Export.cpp b/core/libs/prx/libSceLibreSsl/Export.cpp index 75096fdd45..0f80ff0573 100644 --- a/core/libs/prx/libSceLibreSsl/Export.cpp +++ b/core/libs/prx/libSceLibreSsl/Export.cpp @@ -8,6 +8,7 @@ #include #include #include "SceTypes.hpp" +#include "prx/libSceLibreSsl/include/Bio.hpp" #include "prx/libc/include/General.hpp" #include "mbedtls/des.h" @@ -48,36 +49,16 @@ int APS5_VABI ASN1_TIME_print_nid_postfix(void) { return 0; } -int APS5_VABI BIO_ctrl_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI BIO_f_ssl_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI BIO_free_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI BIO_new_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI BIO_new_fp_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI BIO_new_mem_buf_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI BIO_new_socket_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -88,89 +69,13 @@ int APS5_VABI BIO_printf_nid_postfix(void) { return 0; } -int APS5_VABI BIO_puts_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI BIO_s_file_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI BIO_s_mem_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI BIO_write_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI BN_CTX_end_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI BN_CTX_free_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI BN_CTX_new_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI BN_CTX_start_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI BN_add_word_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI BN_bin2bn_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI BN_bn2bin_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI BN_free_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI BN_hex2bn_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI BN_mod_inverse_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI BN_mul_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI BN_new_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI BN_num_bits_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI BIO_write_nid_postfix(LibreSsl::Bio* bio, const void* source, int size) { + return LibreSslBioWrite_nid_no_patch(bio, source, size); } int APS5_VABI BN_print_nid_postfix(void) { @@ -178,41 +83,6 @@ int APS5_VABI BN_print_nid_postfix(void) { return 0; } -int APS5_VABI BN_set_word_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI BN_sub_word_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI CMAC_CTX_free_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI CMAC_CTX_new_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI CMAC_Final_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI CMAC_Init_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI CMAC_Update_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI CONF_modules_free_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -510,26 +380,6 @@ int APS5_VABI EVP_DecryptUpdate_nid_postfix(void) { return 0; } -int APS5_VABI EVP_Digest_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI EVP_DigestFinal_ex_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI EVP_DigestInit_ex_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI EVP_DigestUpdate_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI EVP_DigestVerifyFinal_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -555,31 +405,6 @@ int APS5_VABI EVP_EncryptUpdate_nid_postfix(void) { return 0; } -int APS5_VABI EVP_MD_CTX_cleanup_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI EVP_MD_CTX_create_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI EVP_MD_CTX_destroy_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI EVP_MD_CTX_init_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI EVP_MD_size_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI EVP_PKEY_copy_parameters_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -615,10 +440,6 @@ int APS5_VABI EVP_PKEY_set1_RSA_nid_postfix(void) { return 0; } -int APS5_VABI EVP_aes_128_cbc_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} int APS5_VABI EVP_aes_128_cfb8_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); @@ -635,11 +456,6 @@ int APS5_VABI EVP_aes_128_gcm_nid_postfix(void) { return 0; } -int APS5_VABI EVP_aes_192_cbc_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI EVP_aes_192_cfb8_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -655,11 +471,6 @@ int APS5_VABI EVP_aes_192_gcm_nid_postfix(void) { return 0; } -int APS5_VABI EVP_aes_256_cbc_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI EVP_aes_256_cfb8_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -680,61 +491,6 @@ int APS5_VABI EVP_cleanup_nid_postfix(void) { return 0; } -int APS5_VABI EVP_md5_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI EVP_sha256_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI EVP_sha384_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI EVP_sha512_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI HMAC_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI HMAC_CTX_cleanup_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI HMAC_CTX_init_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI HMAC_Final_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI HMAC_Init_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI HMAC_Init_ex_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI HMAC_Update_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI OBJ_nid2sn_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceLibreSsl/MemoryBio.cmake b/core/libs/prx/libSceLibreSsl/MemoryBio.cmake new file mode 100644 index 0000000000..a9ed5d36c8 --- /dev/null +++ b/core/libs/prx/libSceLibreSsl/MemoryBio.cmake @@ -0,0 +1,32 @@ +target_sources(libSceLibreSsl PRIVATE ${CMAKE_CURRENT_LIST_DIR}/src/Bio.cpp) + +if(BUILD_TESTING) + add_test_executable(guest_libressl_bio_tests ${LIBS_INCLUDE_DIR}/tests/GuestLibreSslBio.cpp) + target_include_directories(guest_libressl_bio_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_libressl_bio_tests PRIVATE libSceLibreSsl libc) + configure_windows_unwind(guest_libressl_bio_tests) + add_test(NAME guest_libressl_bio COMMAND guest_libressl_bio_tests) + set_tests_properties(guest_libressl_bio PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_libressl_bio PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + + add_test_executable(guest_libressl_bio_patched_tests ${LIBS_INCLUDE_DIR}/tests/GuestLibreSslBio.cpp) + target_include_directories(guest_libressl_bio_patched_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_compile_definitions(guest_libressl_bio_patched_tests PRIVATE ANYPS5_BIO_DYNAMIC) + target_link_libraries(guest_libressl_bio_patched_tests PRIVATE ${CMAKE_DL_LIBS}) + configure_windows_unwind(guest_libressl_bio_patched_tests) + if(WIN32) + target_link_libraries(guest_libressl_bio_patched_tests PRIVATE libc) + endif() + add_custom_target(libressl_bio_patched_libraries DEPENDS + ${PATCHED_DIR}/libSceLibreSsl.prx ${PATCHED_DIR}/libc.prx ${PATCHED_DIR}/libkernel.prx) + add_dependencies(guest_libressl_bio_patched_tests libressl_bio_patched_libraries) + add_test(NAME guest_libressl_bio_patched COMMAND guest_libressl_bio_patched_tests ${PATCHED_DIR}) + set_tests_properties(guest_libressl_bio_patched PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_libressl_bio_patched PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:${PATCHED_DIR}") + endif() +endif() diff --git a/core/libs/prx/libSceLibreSsl/include/Api.hpp b/core/libs/prx/libSceLibreSsl/include/Api.hpp index edc3330651..78ebb73e80 100644 --- a/core/libs/prx/libSceLibreSsl/include/Api.hpp +++ b/core/libs/prx/libSceLibreSsl/include/Api.hpp @@ -1,6 +1,9 @@ #pragma once +#include #include "SceTypes.hpp" +#include "Bio.hpp" +#include "Cmac.hpp" struct Asn1String { std::int32_t length; @@ -131,6 +134,18 @@ struct Sha512Context { std::uint32_t num, mdLength; }; +struct Bignum { + std::uint64_t* d; + int top; + int dmax; + int neg; + int flags; +}; + +struct BignumContext { + std::vector marks; +}; + using StackCompare = int (APS5_VABI*)(const void*, const void*); using StackElementFree = void (APS5_VABI*)(void*); @@ -250,6 +265,46 @@ struct LibreSslX509StoreContext { void* exData; }; +struct EvpMdContext; + +struct EvpMd { + std::int32_t type; + std::int32_t pkeyType; + std::int32_t mdSize; + std::uint64_t flags; + int (APS5_VABI* init)(EvpMdContext* context); + int (APS5_VABI* update)(EvpMdContext* context, const void* data, std::size_t length); + int (APS5_VABI* finalize)(EvpMdContext* context, unsigned char* digest); + int (APS5_VABI* copy)(EvpMdContext* to, const EvpMdContext* from); + int (APS5_VABI* cleanup)(EvpMdContext* context); + void* sign; + void* verify; + std::int32_t requiredPkeyType[5]; + std::int32_t blockSize; + std::int32_t contextSize; + int (APS5_VABI* control)(EvpMdContext* context, int command, int argument, void* pointer); +}; + +struct EvpMdContext { + const EvpMd* digest; + void* engine; + std::uint64_t flags; + void* mdData; + void* pkeyContext; + int (APS5_VABI* update)(EvpMdContext* context, const void* data, std::size_t length); +}; + +inline constexpr std::size_t HmacMaxBlock = 128; + +struct HmacContext { + const EvpMd* md; + EvpMdContext mdContext; + EvpMdContext innerContext; + EvpMdContext outerContext; + std::uint32_t keyLength; + unsigned char key[HmacMaxBlock]; +}; + struct LibreSslEvpCipherCtx { const void* cipher; void* engine; @@ -277,39 +332,39 @@ int APS5_VABI ASN1_STRING_print_nid_postfix(void); int APS5_VABI ASN1_STRING_to_UTF8_nid_postfix(unsigned char** output, Asn1String* value); int APS5_VABI ASN1_STRING_type_nid_postfix(Asn1String* value); int APS5_VABI ASN1_TIME_print_nid_postfix(void); -int APS5_VABI BIO_ctrl_nid_postfix(void); +std::int64_t APS5_VABI BIO_ctrl_nid_postfix(LibreSsl::Bio* bio, int command, std::int64_t argument, void* pointer); int APS5_VABI BIO_f_ssl_nid_postfix(void); -int APS5_VABI BIO_free_nid_postfix(void); -int APS5_VABI BIO_new_nid_postfix(void); +int APS5_VABI BIO_free_nid_postfix(LibreSsl::Bio* bio); +LibreSsl::Bio* APS5_VABI BIO_new_nid_postfix(const LibreSsl::BioMethod* method); int APS5_VABI BIO_new_fp_nid_postfix(void); -int APS5_VABI BIO_new_mem_buf_nid_postfix(void); +LibreSsl::Bio* APS5_VABI BIO_new_mem_buf_nid_postfix(const void* source, int size); int APS5_VABI BIO_new_socket_nid_postfix(void); int APS5_VABI BIO_printf_nid_postfix(void); -int APS5_VABI BIO_puts_nid_postfix(void); +int APS5_VABI BIO_puts_nid_postfix(LibreSsl::Bio* bio, const char* source); int APS5_VABI BIO_s_file_nid_postfix(void); -int APS5_VABI BIO_s_mem_nid_postfix(void); -int APS5_VABI BIO_write_nid_postfix(void); -int APS5_VABI BN_CTX_end_nid_postfix(void); -int APS5_VABI BN_CTX_free_nid_postfix(void); -int APS5_VABI BN_CTX_new_nid_postfix(void); -int APS5_VABI BN_CTX_start_nid_postfix(void); -int APS5_VABI BN_add_word_nid_postfix(void); -int APS5_VABI BN_bin2bn_nid_postfix(void); -int APS5_VABI BN_bn2bin_nid_postfix(void); -int APS5_VABI BN_free_nid_postfix(void); -int APS5_VABI BN_hex2bn_nid_postfix(void); -int APS5_VABI BN_mod_inverse_nid_postfix(void); -int APS5_VABI BN_mul_nid_postfix(void); -int APS5_VABI BN_new_nid_postfix(void); -int APS5_VABI BN_num_bits_nid_postfix(void); +const LibreSsl::BioMethod* APS5_VABI BIO_s_mem_nid_postfix(); +int APS5_VABI BIO_write_nid_postfix(LibreSsl::Bio* bio, const void* source, int size); +void APS5_VABI BN_CTX_end_nid_postfix(BignumContext* ctx); +void APS5_VABI BN_CTX_free_nid_postfix(BignumContext* ctx); +BignumContext* APS5_VABI BN_CTX_new_nid_postfix(void); +void APS5_VABI BN_CTX_start_nid_postfix(BignumContext* ctx); +int APS5_VABI BN_add_word_nid_postfix(Bignum* a, std::uint64_t w); +Bignum* APS5_VABI BN_bin2bn_nid_postfix(const unsigned char* s, int len, Bignum* ret); +int APS5_VABI BN_bn2bin_nid_postfix(const Bignum* a, unsigned char* to); +void APS5_VABI BN_free_nid_postfix(Bignum* a); +int APS5_VABI BN_hex2bn_nid_postfix(Bignum** a, const char* str); +Bignum* APS5_VABI BN_mod_inverse_nid_postfix(Bignum* r, const Bignum* a, const Bignum* n, BignumContext*); +int APS5_VABI BN_mul_nid_postfix(Bignum* r, const Bignum* a, const Bignum* b, BignumContext*); +Bignum* APS5_VABI BN_new_nid_postfix(void); +int APS5_VABI BN_num_bits_nid_postfix(const Bignum* a); int APS5_VABI BN_print_nid_postfix(void); -int APS5_VABI BN_set_word_nid_postfix(void); -int APS5_VABI BN_sub_word_nid_postfix(void); -int APS5_VABI CMAC_CTX_free_nid_postfix(void); -int APS5_VABI CMAC_CTX_new_nid_postfix(void); -int APS5_VABI CMAC_Final_nid_postfix(void); -int APS5_VABI CMAC_Init_nid_postfix(void); -int APS5_VABI CMAC_Update_nid_postfix(void); +int APS5_VABI BN_set_word_nid_postfix(Bignum* a, std::uint64_t w); +int APS5_VABI BN_sub_word_nid_postfix(Bignum* a, std::uint64_t w); +void APS5_VABI CMAC_CTX_free_nid_postfix(LibreSslCmac* context); +LibreSslCmac* APS5_VABI CMAC_CTX_new_nid_postfix(); +int APS5_VABI CMAC_Final_nid_postfix(LibreSslCmac* context, unsigned char* output, std::size_t* length); +int APS5_VABI CMAC_Init_nid_postfix(LibreSslCmac* context, const void* key, std::size_t keyLength, const LibreSslCipher* cipher, void* engine); +int APS5_VABI CMAC_Update_nid_postfix(LibreSslCmac* context, const void* data, std::size_t length); int APS5_VABI CONF_modules_free_nid_postfix(void); int APS5_VABI CONF_modules_load_file_nid_postfix(void); int APS5_VABI CRYPTO_THREADID_set_callback_nid_postfix(void* callback); @@ -359,13 +414,13 @@ int APS5_VABI ENGINE_init_nid_postfix(void); int APS5_VABI ENGINE_load_builtin_engines_nid_postfix(void); int APS5_VABI ENGINE_load_private_key_nid_postfix(void); int APS5_VABI ENGINE_set_default_nid_postfix(void); -void APS5_VABI ERR_clear_error_nid_postfix(void); +void APS5_VABI ERR_clear_error_nid_postfix(); char* APS5_VABI ERR_error_string_nid_postfix(std::uint64_t error, char* buffer); void APS5_VABI ERR_error_string_n_nid_postfix(std::uint64_t error, char* buffer, std::size_t length); void APS5_VABI ERR_free_strings_nid_postfix(); -std::uint64_t APS5_VABI ERR_get_error_nid_postfix(void); -std::uint64_t APS5_VABI ERR_peek_error_nid_postfix(void); -void APS5_VABI ERR_remove_thread_state_nid_postfix(const void* threadId); +std::uint64_t APS5_VABI ERR_get_error_nid_postfix(); +std::uint64_t APS5_VABI ERR_peek_error_nid_postfix(); +void APS5_VABI ERR_remove_thread_state_nid_postfix(const void* id); int APS5_VABI EVP_CIPHER_CTX_cleanup_nid_postfix(void); int APS5_VABI EVP_CIPHER_CTX_ctrl_nid_postfix(void); int APS5_VABI EVP_CIPHER_CTX_free_nid_postfix(void); @@ -379,20 +434,20 @@ int APS5_VABI EVP_CipherUpdate_nid_postfix(void); int APS5_VABI EVP_DecryptFinal_ex_nid_postfix(void); int APS5_VABI EVP_DecryptInit_ex_nid_postfix(void); int APS5_VABI EVP_DecryptUpdate_nid_postfix(void); -int APS5_VABI EVP_Digest_nid_postfix(void); -int APS5_VABI EVP_DigestFinal_ex_nid_postfix(void); -int APS5_VABI EVP_DigestInit_ex_nid_postfix(void); -int APS5_VABI EVP_DigestUpdate_nid_postfix(void); +int APS5_VABI EVP_Digest_nid_postfix(const void* data, std::size_t length, unsigned char* digest, std::uint32_t* size, const EvpMd* type, void* engine); +int APS5_VABI EVP_DigestFinal_ex_nid_postfix(EvpMdContext* context, unsigned char* digest, std::uint32_t* size); +int APS5_VABI EVP_DigestInit_ex_nid_postfix(EvpMdContext* context, const EvpMd* type, void* engine); +int APS5_VABI EVP_DigestUpdate_nid_postfix(EvpMdContext* context, const void* data, std::size_t length); int APS5_VABI EVP_DigestVerifyFinal_nid_postfix(void); int APS5_VABI EVP_DigestVerifyInit_nid_postfix(void); int APS5_VABI EVP_EncryptFinal_ex_nid_postfix(void); int APS5_VABI EVP_EncryptInit_ex_nid_postfix(void); int APS5_VABI EVP_EncryptUpdate_nid_postfix(void); -int APS5_VABI EVP_MD_CTX_cleanup_nid_postfix(void); -int APS5_VABI EVP_MD_CTX_create_nid_postfix(void); -int APS5_VABI EVP_MD_CTX_destroy_nid_postfix(void); -int APS5_VABI EVP_MD_CTX_init_nid_postfix(void); -int APS5_VABI EVP_MD_size_nid_postfix(void); +int APS5_VABI EVP_MD_CTX_cleanup_nid_postfix(EvpMdContext* context); +EvpMdContext* APS5_VABI EVP_MD_CTX_create_nid_postfix(void); +void APS5_VABI EVP_MD_CTX_destroy_nid_postfix(EvpMdContext* context); +void APS5_VABI EVP_MD_CTX_init_nid_postfix(EvpMdContext* context); +int APS5_VABI EVP_MD_size_nid_postfix(const EvpMd* md); int APS5_VABI EVP_PKEY_copy_parameters_nid_postfix(void); int APS5_VABI EVP_PKEY_free_nid_postfix(void); int APS5_VABI EVP_PKEY_get1_EC_KEY_nid_postfix(void); @@ -401,32 +456,32 @@ int APS5_VABI EVP_PKEY_id_nid_postfix(void); int APS5_VABI EVP_PKEY_new_nid_postfix(void); int APS5_VABI EVP_PKEY_set1_RSA_nid_postfix(void); int APS5_VABI EVP_PKEY_type_nid_postfix(int type); -int APS5_VABI EVP_aes_128_cbc_nid_postfix(void); +const LibreSslCipher* APS5_VABI EVP_aes_128_cbc_nid_postfix(); int APS5_VABI EVP_aes_128_cfb8_nid_postfix(void); int APS5_VABI EVP_aes_128_ctr_nid_postfix(void); int APS5_VABI EVP_aes_128_gcm_nid_postfix(void); -int APS5_VABI EVP_aes_192_cbc_nid_postfix(void); +const LibreSslCipher* APS5_VABI EVP_aes_192_cbc_nid_postfix(); int APS5_VABI EVP_aes_192_cfb8_nid_postfix(void); int APS5_VABI EVP_aes_192_ctr_nid_postfix(void); int APS5_VABI EVP_aes_192_gcm_nid_postfix(void); -int APS5_VABI EVP_aes_256_cbc_nid_postfix(void); +const LibreSslCipher* APS5_VABI EVP_aes_256_cbc_nid_postfix(); int APS5_VABI EVP_aes_256_cfb8_nid_postfix(void); int APS5_VABI EVP_aes_256_ctr_nid_postfix(void); int APS5_VABI EVP_aes_256_gcm_nid_postfix(void); int APS5_VABI EVP_cleanup_nid_postfix(void); -int APS5_VABI EVP_md5_nid_postfix(void); -int APS5_VABI EVP_sha256_nid_postfix(void); -int APS5_VABI EVP_sha384_nid_postfix(void); -int APS5_VABI EVP_sha512_nid_postfix(void); +const EvpMd* APS5_VABI EVP_md5_nid_postfix(void); +const EvpMd* APS5_VABI EVP_sha256_nid_postfix(void); +const EvpMd* APS5_VABI EVP_sha384_nid_postfix(void); +const EvpMd* APS5_VABI EVP_sha512_nid_postfix(void); void APS5_VABI GENERAL_NAMES_free_nid_postfix(LibreSslStack* names); void APS5_VABI GENERAL_NAME_free_nid_postfix(LibreSslGeneralName* name); -int APS5_VABI HMAC_nid_postfix(void); -int APS5_VABI HMAC_CTX_cleanup_nid_postfix(void); -int APS5_VABI HMAC_CTX_init_nid_postfix(void); -int APS5_VABI HMAC_Final_nid_postfix(void); -int APS5_VABI HMAC_Init_nid_postfix(void); -int APS5_VABI HMAC_Init_ex_nid_postfix(void); -int APS5_VABI HMAC_Update_nid_postfix(void); +unsigned char* APS5_VABI HMAC_nid_postfix(const EvpMd* md, const void* key, int keyLength, const unsigned char* data, std::size_t length, unsigned char* digest, std::uint32_t* digestLength); +void APS5_VABI HMAC_CTX_cleanup_nid_postfix(HmacContext* context); +void APS5_VABI HMAC_CTX_init_nid_postfix(HmacContext* context); +int APS5_VABI HMAC_Final_nid_postfix(HmacContext* context, unsigned char* digest, std::uint32_t* length); +int APS5_VABI HMAC_Init_nid_postfix(HmacContext* context, const void* key, int length, const EvpMd* md); +int APS5_VABI HMAC_Init_ex_nid_postfix(HmacContext* context, const void* key, int length, const EvpMd* md, void* engine); +int APS5_VABI HMAC_Update_nid_postfix(HmacContext* context, const unsigned char* data, std::size_t length); void APS5_VABI OBJ_cleanup_nid_postfix(void); int APS5_VABI OBJ_create_nid_postfix(const char* oid, const char* shortName, const char* longName); int APS5_VABI OBJ_nid2sn_nid_postfix(void); diff --git a/core/libs/prx/libSceLibreSsl/include/Bio.hpp b/core/libs/prx/libSceLibreSsl/include/Bio.hpp new file mode 100644 index 0000000000..63613ce1d7 --- /dev/null +++ b/core/libs/prx/libSceLibreSsl/include/Bio.hpp @@ -0,0 +1,72 @@ +#ifndef CORE_LIBS_PRX_LIBSCELIBRESSL_INCLUDE_BIO_HPP +#define CORE_LIBS_PRX_LIBSCELIBRESSL_INCLUDE_BIO_HPP + +#include +#include +#include "prx/libc/include/general/VabiMacros.hpp" + +namespace LibreSsl { + +struct Bio; +using BioCallback = std::int64_t (APS5_VABI *)(Bio*, int, const char*, int, std::int64_t, std::int64_t); +using BioInfoCallback = void (APS5_VABI *)(Bio*, int, const char*, int, std::int64_t, std::int64_t); + +struct BioMethod { + int type; + const char* name; + int (APS5_VABI *write)(Bio*, const char*, int); + int (APS5_VABI *read)(Bio*, char*, int); + int (APS5_VABI *puts)(Bio*, const char*); + int (APS5_VABI *gets)(Bio*, char*, int); + std::int64_t (APS5_VABI *control)(Bio*, int, std::int64_t, void*); + int (APS5_VABI *create)(Bio*); + int (APS5_VABI *destroy)(Bio*); + std::int64_t (APS5_VABI *callbackControl)(Bio*, int, BioInfoCallback); +}; + +struct Bio { + const BioMethod* method; + BioCallback callback; + char* callbackArgument; + int initialized; + int shutdown; + int flags; + int retryReason; + int number; + void* data; + Bio* next; + Bio* previous; + int references; + std::uint64_t bytesRead; + std::uint64_t bytesWritten; + void* extraData; +}; + +struct BioBuffer { + std::size_t length; + char* data; + std::size_t capacity; +}; + +static_assert(sizeof(BioMethod) == 80); +static_assert(offsetof(BioMethod, write) == 16); +static_assert(offsetof(BioMethod, control) == 48); +static_assert(offsetof(BioMethod, callbackControl) == 72); +static_assert(sizeof(Bio) == 104); +static_assert(offsetof(Bio, initialized) == 24); +static_assert(offsetof(Bio, flags) == 32); +static_assert(offsetof(Bio, number) == 40); +static_assert(offsetof(Bio, data) == 48); +static_assert(offsetof(Bio, references) == 72); +static_assert(offsetof(Bio, bytesRead) == 80); +static_assert(offsetof(Bio, bytesWritten) == 88); +static_assert(offsetof(Bio, extraData) == 96); +static_assert(sizeof(BioBuffer) == 24); +static_assert(offsetof(BioBuffer, data) == 8); +static_assert(offsetof(BioBuffer, capacity) == 16); + +} // namespace LibreSsl + +extern "C" int LibreSslBioWrite_nid_no_patch(LibreSsl::Bio* bio, const void* source, int size); + +#endif diff --git a/core/libs/prx/libSceLibreSsl/include/Cmac.hpp b/core/libs/prx/libSceLibreSsl/include/Cmac.hpp new file mode 100644 index 0000000000..caf9698952 --- /dev/null +++ b/core/libs/prx/libSceLibreSsl/include/Cmac.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include +#include +#include "prx/libc/include/general/VabiMacros.hpp" + +struct LibreSslCipherContext; + +struct LibreSslCipher { + std::int32_t nid; + std::int32_t blockSize; + std::int32_t keyLength; + std::int32_t ivLength; + std::uint64_t flags; + int (APS5_VABI* init)(LibreSslCipherContext*, const unsigned char*, const unsigned char*, int); + int (APS5_VABI* cipher)(LibreSslCipherContext*, unsigned char*, const unsigned char*, std::size_t); + int (APS5_VABI* cleanup)(LibreSslCipherContext*); + std::int32_t contextSize; + int (APS5_VABI* setAsn1Parameters)(LibreSslCipherContext*, void*); + int (APS5_VABI* getAsn1Parameters)(LibreSslCipherContext*, void*); + int (APS5_VABI* control)(LibreSslCipherContext*, int, int, void*); + void* applicationData; +}; + +struct LibreSslCipherContext { + const LibreSslCipher* cipher; + void* engine; + std::int32_t encrypt; + std::int32_t bufferLength; + unsigned char originalIv[16]; + unsigned char iv[16]; + unsigned char buffer[32]; + std::int32_t position; + void* applicationData; + std::int32_t keyLength; + std::uint64_t flags; + void* cipherData; + std::int32_t finalUsed; + std::int32_t blockMask; + unsigned char final[32]; +}; + +static_assert(sizeof(LibreSslCipher) == 88 && offsetof(LibreSslCipher, init) == 24 && offsetof(LibreSslCipher, contextSize) == 48); +static_assert(sizeof(LibreSslCipherContext) == 168 && offsetof(LibreSslCipherContext, iv) == 40 && offsetof(LibreSslCipherContext, flags) == 112 && offsetof(LibreSslCipherContext, cipherData) == 120); + +struct LibreSslCmac; diff --git a/core/libs/prx/libSceLibreSsl/src/Bignum.cpp b/core/libs/prx/libSceLibreSsl/src/Bignum.cpp new file mode 100644 index 0000000000..27ce2ec72e --- /dev/null +++ b/core/libs/prx/libSceLibreSsl/src/Bignum.cpp @@ -0,0 +1,378 @@ +#include "prx/libSceLibreSsl/libSceLibreSsl.hpp" +#include "prx/libc/include/Api.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include "prx/libc/include/General.hpp" + +namespace { + +static_assert(sizeof(Bignum) == 24); +static_assert(offsetof(Bignum, d) == 0); +static_assert(offsetof(Bignum, top) == 8); +static_assert(offsetof(Bignum, dmax) == 12); +static_assert(offsetof(Bignum, neg) == 16); +static_assert(offsetof(Bignum, flags) == 20); + +constexpr int FlagMalloced = 1; +constexpr int FlagStaticData = 2; + +using Mag = std::vector; + +struct Int { + Mag mag; + bool neg = false; +}; + +void Trim(Mag& m) { + while (!m.empty() && m.back() == 0) m.pop_back(); +} + +int Compare(const Mag& a, const Mag& b) { + if (a.size() != b.size()) return a.size() < b.size() ? -1 : 1; + for (std::size_t i = a.size(); i-- > 0;) { + if (a[i] != b[i]) return a[i] < b[i] ? -1 : 1; + } + return 0; +} + +Mag AddMag(const Mag& a, const Mag& b) { + const std::size_t count = a.size() > b.size() ? a.size() : b.size(); + Mag r; + r.reserve(count + 1); + std::uint64_t carry = 0; + for (std::size_t i = 0; i < count; i++) { + const unsigned __int128 sum = static_cast(i < a.size() ? a[i] : 0) + (i < b.size() ? b[i] : 0) + carry; + r.push_back(static_cast(sum)); + carry = static_cast(sum >> 64); + } + if (carry) r.push_back(carry); + return r; +} + +Mag SubMag(const Mag& a, const Mag& b) { + Mag r(a.size()); + std::uint64_t borrow = 0; + for (std::size_t i = 0; i < a.size(); i++) { + const std::uint64_t x = a[i]; + const std::uint64_t y = i < b.size() ? b[i] : 0; + const std::uint64_t diff = x - y; + const std::uint64_t first = x < y; + r[i] = diff - borrow; + borrow = first | (diff < borrow); + } + Trim(r); + return r; +} + +Mag MulMag(const Mag& a, const Mag& b) { + if (a.empty() || b.empty()) return {}; + Mag r(a.size() + b.size(), 0); + for (std::size_t i = 0; i < a.size(); i++) { + std::uint64_t carry = 0; + for (std::size_t j = 0; j < b.size(); j++) { + const unsigned __int128 product = static_cast(a[i]) * b[j] + r[i + j] + carry; + r[i + j] = static_cast(product); + carry = static_cast(product >> 64); + } + r[i + b.size()] = carry; + } + Trim(r); + return r; +} + +std::size_t BitLength(const Mag& m) { + if (m.empty()) return 0; + return (m.size() - 1) * 64 + std::bit_width(m.back()); +} + +Mag ShiftLeft(const Mag& m, std::size_t bits) { + if (m.empty()) return {}; + const std::size_t words = bits / 64; + const unsigned rest = static_cast(bits % 64); + Mag r(m.size() + words + 1, 0); + for (std::size_t i = 0; i < m.size(); i++) { + r[i + words] |= m[i] << rest; + if (rest) r[i + words + 1] |= m[i] >> (64 - rest); + } + Trim(r); + return r; +} + +void ShiftRightOne(Mag& m) { + for (std::size_t i = 0; i < m.size(); i++) { + m[i] >>= 1; + if (i + 1 < m.size()) m[i] |= m[i + 1] << 63; + } + Trim(m); +} + +void DivMod(const Mag& a, const Mag& b, Mag& quotient, Mag& remainder) { + remainder = a; + quotient.clear(); + const std::size_t aBits = BitLength(a); + const std::size_t bBits = BitLength(b); + if (aBits < bBits) return; + const std::size_t shift = aBits - bBits; + Mag divisor = ShiftLeft(b, shift); + quotient.assign(shift / 64 + 1, 0); + for (std::size_t s = shift + 1; s-- > 0;) { + if (Compare(remainder, divisor) >= 0) { + remainder = SubMag(remainder, divisor); + quotient[s / 64] |= std::uint64_t(1) << (s % 64); + } + ShiftRightOne(divisor); + } + Trim(quotient); +} + +Int AddSigned(const Int& x, const Int& y) { + Int r; + if (x.neg == y.neg) { + r.mag = AddMag(x.mag, y.mag); + r.neg = x.neg; + } else { + const int order = Compare(x.mag, y.mag); + if (order > 0) { + r.mag = SubMag(x.mag, y.mag); + r.neg = x.neg; + } else if (order < 0) { + r.mag = SubMag(y.mag, x.mag); + r.neg = y.neg; + } + } + if (r.mag.empty()) r.neg = false; + return r; +} + +bool InverseMod(const Int& a, const Mag& n, Mag& out) { + if (n.size() == 1 && n[0] == 1) { + out.clear(); + return true; + } + Mag quotient; + Mag r1; + DivMod(a.mag, n, quotient, r1); + if (a.neg && !r1.empty()) r1 = SubMag(n, r1); + Mag r0 = n; + Int t0; + Int t1; + t1.mag = {1}; + while (!r1.empty()) { + Mag remainder; + DivMod(r0, r1, quotient, remainder); + Int scaled; + scaled.mag = MulMag(quotient, t1.mag); + scaled.neg = !t1.neg && !scaled.mag.empty(); + Int t2 = AddSigned(t0, scaled); + r0 = std::move(r1); + r1 = std::move(remainder); + t0 = std::move(t1); + t1 = std::move(t2); + } + if (r0.size() != 1 || r0[0] != 1) return false; + out = t0.neg ? SubMag(n, t0.mag) : t0.mag; + return true; +} + +Bignum* Checked(Bignum* value, const char* function) { + if (!value) throw std::invalid_argument(std::string(function) + ": null BIGNUM"); + return value; +} + +const Bignum* Checked(const Bignum* value, const char* function) { + if (!value) throw std::invalid_argument(std::string(function) + ": null BIGNUM"); + return value; +} + +Int Load(const Bignum* bn) { + Int r; + if (bn->top > 0) r.mag.assign(bn->d, bn->d + bn->top); + Trim(r.mag); + r.neg = bn->neg && !r.mag.empty(); + return r; +} + +bool Store(Bignum* bn, const Mag& m, bool neg) { + if (m.empty()) { + bn->top = 0; + bn->neg = 0; + return true; + } + const std::size_t count = m.size(); + if (!bn->d || static_cast(bn->dmax) < count) { + if (bn->flags & FlagStaticData) return false; + if (count > 0x7FFFFFFF) return false; + auto* grown = static_cast(calloc_nid_postfix(count, sizeof(std::uint64_t))); + if (!grown) return false; + if (bn->d) free_nid_postfix(bn->d); + bn->d = grown; + bn->dmax = static_cast(count); + } + for (std::size_t i = 0; i < count; i++) bn->d[i] = m[i]; + for (std::size_t i = count; i < static_cast(bn->dmax); i++) bn->d[i] = 0; + bn->top = static_cast(count); + bn->neg = neg ? 1 : 0; + return true; +} + +Bignum* NewBignum() { + auto* bn = static_cast(calloc_nid_postfix(1, sizeof(Bignum))); + if (bn) bn->flags = FlagMalloced; + return bn; +} + +void FreeBignum(Bignum* bn) { + if (!bn) return; + if (bn->d && !(bn->flags & FlagStaticData)) free_nid_postfix(bn->d); + if (bn->flags & FlagMalloced) { + free_nid_postfix(bn); + } else { + bn->d = nullptr; + } +} + +int HexValue(char c) { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + return -1; +} + +bool ApplyWord(Bignum* a, std::uint64_t w, bool subtract) { + Int word; + if (w) word.mag = {w}; + word.neg = subtract && w; + const Int sum = AddSigned(Load(a), word); + return Store(a, sum.mag, sum.neg); +} +} + +extern "C" { +Bignum* APS5_VABI BN_new_nid_postfix(void) { + return NewBignum(); +} + +void APS5_VABI BN_free_nid_postfix(Bignum* a) { + FreeBignum(a); +} + +BignumContext* APS5_VABI BN_CTX_new_nid_postfix(void) { + return new (std::nothrow) BignumContext(); +} + +void APS5_VABI BN_CTX_free_nid_postfix(BignumContext* ctx) { + delete ctx; +} + +void APS5_VABI BN_CTX_start_nid_postfix(BignumContext* ctx) { + if (!ctx) throw std::invalid_argument(std::string(__func__) + ": null context"); + ctx->marks.push_back(0); +} + +void APS5_VABI BN_CTX_end_nid_postfix(BignumContext* ctx) { + if (!ctx) throw std::invalid_argument(std::string(__func__) + ": null context"); + if (!ctx->marks.empty()) ctx->marks.pop_back(); +} + +Bignum* APS5_VABI BN_bin2bn_nid_postfix(const unsigned char* s, int len, Bignum* ret) { + if (len < 0) return nullptr; + if (len > 0 && !s) throw std::invalid_argument(std::string(__func__) + ": null input"); + Bignum* target = ret ? ret : NewBignum(); + if (!target) return nullptr; + Mag m((static_cast(len) + 7) / 8, 0); + for (int i = 0; i < len; i++) { + const std::size_t bit = static_cast(len - 1 - i); + m[bit / 8] |= static_cast(s[i]) << (bit % 8 * 8); + } + Trim(m); + if (!Store(target, m, false)) { + if (!ret) FreeBignum(target); + return nullptr; + } + return target; +} + +int APS5_VABI BN_num_bits_nid_postfix(const Bignum* a) { + return static_cast(BitLength(Load(Checked(a, __func__)).mag)); +} + +int APS5_VABI BN_bn2bin_nid_postfix(const Bignum* a, unsigned char* to) { + const Mag m = Load(Checked(a, __func__)).mag; + const std::size_t length = (BitLength(m) + 7) / 8; + if (length && !to) throw std::invalid_argument(std::string(__func__) + ": null output"); + for (std::size_t i = 0; i < length; i++) { + const std::size_t bit = length - 1 - i; + to[i] = static_cast(m[bit / 8] >> (bit % 8 * 8)); + } + return static_cast(length); +} + +int APS5_VABI BN_hex2bn_nid_postfix(Bignum** a, const char* str) { + if (!str || !*str) return 0; + const bool negative = *str == '-'; + const char* digits = str + negative; + std::size_t count = 0; + while (HexValue(digits[count]) >= 0) count++; + if (count == 0 || count > 0x7FFFFFFF / 4) return 0; + const int consumed = static_cast(count + negative); + if (!a) return consumed; + Mag m((count + 15) / 16, 0); + for (std::size_t i = 0; i < count; i++) { + const std::size_t nibble = count - 1 - i; + m[nibble / 16] |= static_cast(HexValue(digits[i])) << (nibble % 16 * 4); + } + Trim(m); + Bignum* target = *a ? *a : NewBignum(); + if (!target) return 0; + if (!Store(target, m, negative)) { + if (!*a) FreeBignum(target); + return 0; + } + *a = target; + return consumed; +} + +int APS5_VABI BN_set_word_nid_postfix(Bignum* a, std::uint64_t w) { + Checked(a, __func__); + Mag m; + if (w) m = {w}; + return Store(a, m, false); +} + +int APS5_VABI BN_add_word_nid_postfix(Bignum* a, std::uint64_t w) { + return ApplyWord(Checked(a, __func__), w, false); +} + +int APS5_VABI BN_sub_word_nid_postfix(Bignum* a, std::uint64_t w) { + return ApplyWord(Checked(a, __func__), w, true); +} + +int APS5_VABI BN_mul_nid_postfix(Bignum* r, const Bignum* a, const Bignum* b, BignumContext*) { + Checked(r, __func__); + const Int x = Load(Checked(a, __func__)); + const Int y = Load(Checked(b, __func__)); + const Mag product = MulMag(x.mag, y.mag); + return Store(r, product, x.neg != y.neg); +} + +Bignum* APS5_VABI BN_mod_inverse_nid_postfix(Bignum* r, const Bignum* a, const Bignum* n, BignumContext*) { + const Int value = Load(Checked(a, __func__)); + const Int modulus = Load(Checked(n, __func__)); + if (modulus.mag.empty()) return nullptr; + Mag inverse; + if (!InverseMod(value, modulus.mag, inverse)) return nullptr; + Bignum* target = r ? r : NewBignum(); + if (!target) return nullptr; + if (!Store(target, inverse, false)) { + if (!r) FreeBignum(target); + return nullptr; + } + return target; +} +} diff --git a/core/libs/prx/libSceLibreSsl/src/Bio.cpp b/core/libs/prx/libSceLibreSsl/src/Bio.cpp new file mode 100644 index 0000000000..98f88e4985 --- /dev/null +++ b/core/libs/prx/libSceLibreSsl/src/Bio.cpp @@ -0,0 +1,284 @@ +#include "prx/libSceLibreSsl/libSceLibreSsl.hpp" +#include "prx/libc/include/Api.hpp" +#include "prx/libSceLibreSsl/ErrorQueue.hpp" +#include "prx/libc/include/General.hpp" +#include +#include +#include +#include +#include + + +namespace { + +using LibreSsl::Bio; +using LibreSsl::BioBuffer; +using LibreSsl::BioMethod; + +constexpr int ReadOnly = 0x200; +constexpr int RetryFlags = 0xf; +constexpr int RetryRead = 0x9; +constexpr std::size_t MaximumGrowth = 0x5ffffffc; +constexpr int BioLibrary = 32; +constexpr int BufferLibrary = 7; +constexpr int AllocationFailure = 65; +constexpr int NullParameter = 115; +constexpr int Uninitialized = 120; +constexpr int UnsupportedMethod = 121; +constexpr int ReadOnlyWrite = 126; + +int queueError(int library, int reason, int result) { + LibreSslPutError_nid_no_patch(library, 0xfff, reason, __FILE__, __LINE__); + return result; +} + +void clearBuffer(BioBuffer& buffer) { + auto* bytes = reinterpret_cast(buffer.data); + for (std::size_t i = 0; i < buffer.capacity; ++i) bytes[i] = 0; +} + +BioBuffer& memoryBuffer(Bio* bio) { + if (!bio || !bio->data) throw std::invalid_argument("BIO: missing memory buffer"); + return *static_cast(bio->data); +} + +int APS5_VABI createMemory(Bio* bio) { + auto* buffer = static_cast(calloc_nid_postfix(1, sizeof(BioBuffer))); + if (!buffer) return queueError(BufferLibrary, AllocationFailure, 0); + bio->data = buffer; + bio->initialized = 1; + bio->shutdown = 1; + bio->number = -1; + return 1; +} + +int APS5_VABI destroyMemory(Bio* bio) { + if (!bio) return 0; + if (bio->shutdown && bio->initialized && bio->data) { + auto* buffer = static_cast(bio->data); + if (!(bio->flags & ReadOnly)) { + clearBuffer(*buffer); + free_nid_postfix(buffer->data); + } + free_nid_postfix(buffer); + bio->data = nullptr; + } + return 1; +} + +int APS5_VABI readMemory(Bio* bio, char* destination, int size) { + auto& buffer = memoryBuffer(bio); + bio->flags &= ~RetryFlags; + const int count = size >= 0 ? static_cast(std::min(size, buffer.length)) : size; + if (destination && count > 0) { + std::memcpy(destination, buffer.data, count); + buffer.length -= count; + if (bio->flags & ReadOnly) buffer.data += count; + else std::memmove(buffer.data, buffer.data + count, buffer.length); + return count; + } + if (buffer.length != 0) return count; + if (bio->number != 0) bio->flags |= RetryRead; + return bio->number; +} + +int APS5_VABI writeMemory(Bio* bio, const char* source, int size) { + auto& buffer = memoryBuffer(bio); + if (!source) return queueError(BioLibrary, NullParameter, -1); + if (bio->flags & ReadOnly) return queueError(BioLibrary, ReadOnlyWrite, -1); + if (size < 0) throw std::invalid_argument("BIO: negative memory write size"); + bio->flags &= ~RetryFlags; + const auto count = static_cast(size); + if (buffer.length > std::numeric_limits::max() - count) NotImplemented_nid_no_patch(__func__); + const std::size_t length = buffer.length + count; + if (length > buffer.capacity) { + if (length > MaximumGrowth) return queueError(BufferLibrary, AllocationFailure, -1); + const std::size_t capacity = (length + 3) / 3 * 4; + auto* data = static_cast(calloc_nid_postfix(capacity, 1)); + if (!data) return queueError(BufferLibrary, AllocationFailure, -1); + if (buffer.length != 0) std::memcpy(data, buffer.data, buffer.length); + if (count != 0) std::memcpy(data + buffer.length, source, count); + clearBuffer(buffer); + free_nid_postfix(buffer.data); + buffer.data = data; + buffer.capacity = capacity; + } else if (count != 0) { + std::memmove(buffer.data + buffer.length, source, count); + } + buffer.length = length; + return size; +} + +int APS5_VABI putsMemory(Bio* bio, const char* source) { + if (!source) throw std::invalid_argument("BIO_puts: null string"); + const std::size_t size = std::strlen(source); + if (size > static_cast(std::numeric_limits::max())) NotImplemented_nid_no_patch(__func__); + return writeMemory(bio, source, static_cast(size)); +} + +int APS5_VABI getsMemory(Bio* bio, char* destination, int size) { + auto& buffer = memoryBuffer(bio); + if (!destination || size == std::numeric_limits::min()) throw std::invalid_argument("BIO: invalid line buffer"); + bio->flags &= ~RetryFlags; + if (size <= 1 || buffer.length == 0) { + *destination = '\0'; + return 0; + } + const auto limit = std::min(size - 1, buffer.length); + const void* newline = std::memchr(buffer.data, '\n', limit); + const std::size_t count = newline ? static_cast(newline) - buffer.data + 1 : limit; + const int result = readMemory(bio, destination, static_cast(count)); + destination[result] = '\0'; + return result; +} + +std::int64_t APS5_VABI controlMemory(Bio* bio, int command, std::int64_t argument, void* pointer) { + if (!bio) throw std::invalid_argument("BIO: null memory BIO"); + switch (command) { + case 1: { + auto& buffer = memoryBuffer(bio); + if (buffer.data) { + if (bio->flags & ReadOnly) { + if (buffer.capacity < buffer.length) throw std::invalid_argument("BIO: invalid read-only buffer length"); + buffer.data -= buffer.capacity - buffer.length; + buffer.length = buffer.capacity; + } else { + clearBuffer(buffer); + buffer.length = 0; + } + } + return 1; + } + case 2: + return memoryBuffer(bio).length == 0; + case 3: + if (pointer) *static_cast(pointer) = memoryBuffer(bio).data; + return static_cast(memoryBuffer(bio).length); + case 8: + return bio->shutdown; + case 9: + bio->shutdown = static_cast(argument); + return 1; + case 10: + return static_cast(memoryBuffer(bio).length); + case 11: + case 12: + return 1; + case 13: + return 0; + case 114: + destroyMemory(bio); + bio->data = pointer; + bio->shutdown = static_cast(argument); + return 1; + case 115: + if (pointer) std::memcpy(pointer, &bio->data, sizeof(bio->data)); + return 1; + case 130: + bio->number = static_cast(argument); + return 1; + default: + return 0; + } +} + +const BioMethod MemoryMethod{0x401, "memory buffer", writeMemory, readMemory, putsMemory, getsMemory, controlMemory, createMemory, destroyMemory, nullptr}; + +} // namespace + +extern "C" { + +const LibreSsl::BioMethod* APS5_VABI BIO_s_mem_nid_postfix() { + return &MemoryMethod; +} + +LibreSsl::Bio* APS5_VABI BIO_new_nid_postfix(const LibreSsl::BioMethod* method) { + if (!method) throw std::invalid_argument("BIO_new: null method"); + auto* bio = static_cast(calloc_nid_postfix(1, sizeof(Bio))); + if (!bio) { + queueError(BioLibrary, AllocationFailure, 0); + return nullptr; + } + bio->method = method; + bio->shutdown = 1; + bio->references = 1; + try { + if (!method->create || method->create(bio)) return bio; + } catch (...) { + free_nid_postfix(bio); + throw; + } + free_nid_postfix(bio); + return nullptr; +} + +LibreSsl::Bio* APS5_VABI BIO_new_mem_buf_nid_postfix(const void* source, int size) { + if (!source) { + queueError(BioLibrary, NullParameter, 0); + return nullptr; + } + auto* bio = BIO_new_nid_postfix(&MemoryMethod); + if (!bio) return nullptr; + auto& buffer = memoryBuffer(bio); + buffer.data = const_cast(static_cast(source)); + buffer.length = size < 0 ? std::strlen(buffer.data) : static_cast(size); + buffer.capacity = buffer.length; + bio->flags |= ReadOnly; + bio->number = 0; + return bio; +} + +int APS5_VABI BIO_free_nid_postfix(LibreSsl::Bio* bio) { + if (!bio) return 0; + if (bio->extraData) NotImplemented_nid_no_patch(__func__); + if (std::atomic_ref(bio->references).fetch_sub(1) > 1) return 1; + if (bio->callback) { + const int result = static_cast(bio->callback(bio, 1, nullptr, 0, 0, 1)); + if (result <= 0) return result; + } + if (bio->extraData) NotImplemented_nid_no_patch(__func__); + if (bio->method && bio->method->destroy) bio->method->destroy(bio); + free_nid_postfix(bio); + return 1; +} + +std::int64_t APS5_VABI BIO_ctrl_nid_postfix(LibreSsl::Bio* bio, int command, std::int64_t argument, void* pointer) { + if (!bio) return 0; + if (!bio->method || !bio->method->control) return queueError(BioLibrary, UnsupportedMethod, -2); + const auto callback = bio->callback; + if (callback) { + const auto result = callback(bio, 6, static_cast(pointer), command, argument, 1); + if (result <= 0) return result; + } + const auto result = bio->method->control(bio, command, argument, pointer); + return callback ? callback(bio, 0x86, static_cast(pointer), command, argument, result) : result; +} + +int LibreSslBioWrite_nid_no_patch(LibreSsl::Bio* bio, const void* source, int size) { + if (!bio || size < 0) return 0; + if (!bio->method || !bio->method->write) return queueError(BioLibrary, UnsupportedMethod, -2); + const auto callback = bio->callback; + if (callback) { + const int result = static_cast(callback(bio, 3, static_cast(source), size, 0, 1)); + if (result <= 0) return result; + } + if (!bio->initialized) return queueError(BioLibrary, Uninitialized, -2); + const int result = bio->method->write(bio, static_cast(source), size); + if (result > 0) bio->bytesWritten += static_cast(result); + return callback ? static_cast(callback(bio, 0x83, static_cast(source), size, 0, result)) : result; +} + +int APS5_VABI BIO_puts_nid_postfix(LibreSsl::Bio* bio, const char* source) { + if (!bio || !bio->method || !bio->method->puts) return queueError(BioLibrary, UnsupportedMethod, -2); + const auto callback = bio->callback; + if (callback) { + const int result = static_cast(callback(bio, 4, source, 0, 0, 1)); + if (result <= 0) return result; + } + if (!bio->initialized) return queueError(BioLibrary, Uninitialized, -2); + const int result = bio->method->puts(bio, source); + if (result > 0) bio->bytesWritten += static_cast(result); + return callback ? static_cast(callback(bio, 0x84, source, 0, 0, result)) : result; +} + +} diff --git a/core/libs/prx/libSceLibreSsl/src/Cmac.cpp b/core/libs/prx/libSceLibreSsl/src/Cmac.cpp new file mode 100644 index 0000000000..8f9967a383 --- /dev/null +++ b/core/libs/prx/libSceLibreSsl/src/Cmac.cpp @@ -0,0 +1,271 @@ +#include "prx/libSceLibreSsl/libSceLibreSsl.hpp" +#include "prx/libc/include/Api.hpp" +#include "prx/libSceLibreSsl/include/Cmac.hpp" +#include "prx/libSceLibreSsl/include/Error.hpp" +#include "prx/libc/include/General.hpp" +#include +#include +#include +#include +#include + +struct LibreSslCmac { + LibreSslCipherContext cipherContext; + unsigned char firstSubkey[32]; + unsigned char secondSubkey[32]; + unsigned char previousBlock[32]; + unsigned char lastBlock[32]; + std::int32_t lastLength; +}; + +static_assert(sizeof(LibreSslCmac) == 304); + +namespace { + +void Cleanse(void* data, std::size_t length) { + auto* bytes = static_cast(data); + while (length-- != 0) *bytes++ = 0; +} + +int Error(int reason) { + LibreSslPutError_nid_no_patch(6, 0xfff, reason, __FILE__, __LINE__); + return 0; +} + +void RequireContext(const LibreSslCmac* context) { + if (context == nullptr) throw std::invalid_argument("CMAC: null context"); +} + +int Cleanup(LibreSslCipherContext& context) { + if (context.engine != nullptr) NotImplemented_nid_no_patch(__func__); + if (context.cipher != nullptr) { + if (context.cipher->cleanup != nullptr && !context.cipher->cleanup(&context)) return 0; + if (context.cipherData != nullptr) Cleanse(context.cipherData, context.cipher->contextSize); + } + free_nid_postfix(context.cipherData); + Cleanse(&context, sizeof(context)); + return 1; +} + +int Control(LibreSslCipherContext& context, int command, int argument) { + if (context.cipher == nullptr) return Error(131); + if (context.cipher->control == nullptr) return Error(132); + const int result = context.cipher->control(&context, command, argument, nullptr); + return result == -1 ? Error(133) : result; +} + +int InitializeCipher(LibreSslCipherContext& context, const LibreSslCipher* cipher, const unsigned char* key, const unsigned char* iv, void* engine) { + context.encrypt = 1; + if (context.engine != nullptr || (cipher != nullptr && engine != nullptr)) NotImplemented_nid_no_patch(__func__); + if (cipher != nullptr) { + if (cipher->contextSize < 0 || cipher->ivLength < 0) throw std::invalid_argument("CMAC: invalid cipher dimensions"); + if (context.cipher != nullptr) { + const auto flags = context.flags; + Cleanup(context); + context.encrypt = 1; + context.flags = flags; + } + context.cipher = cipher; + context.cipherData = nullptr; + if (cipher->contextSize != 0) { + context.cipherData = calloc_nid_postfix(1, cipher->contextSize); + if (context.cipherData == nullptr) return Error(65); + } + context.keyLength = cipher->keyLength; + context.flags &= 1; + if ((cipher->flags & 0x40) != 0 && !Control(context, 0, 0)) return Error(134); + } + if (context.cipher == nullptr) return Error(131); + const auto& method = *context.cipher; + if (method.blockSize != 1 && method.blockSize != 8 && method.blockSize != 16) return Error(136); + const auto mode = method.flags & 0xf0007; + if ((context.flags & 1) == 0 && mode == 0x10002) return Error(170); + if ((method.flags & 0x10) == 0) { + if (mode == 2 || mode == 3 || mode == 4 || mode == 5) { + if (method.ivLength > 16) return Error(102); + if (mode != 2) context.position = 0; + if (mode == 5) { + if (iv != nullptr) std::memcpy(context.iv, iv, method.ivLength); + } else { + if (iv != nullptr) std::memcpy(context.originalIv, iv, method.ivLength); + std::memcpy(context.iv, context.originalIv, method.ivLength); + } + } else if (mode != 0 && mode != 1) { + return 0; + } + } + if (key != nullptr || (method.flags & 0x20) != 0) { + if (method.init == nullptr) NotImplemented_nid_no_patch(__func__); + if (!method.init(&context, key, iv, 1)) return 0; + } + context.bufferLength = 0; + context.finalUsed = 0; + context.blockMask = method.blockSize - 1; + return 1; +} + +int SetKeyLength(LibreSslCipherContext& context, std::size_t length) { + const int narrowed = std::bit_cast(static_cast(length)); + if ((context.cipher->flags & 0x80) != 0) return Control(context, 1, narrowed); + if (context.keyLength == narrowed) return 1; + if (narrowed > 0 && (context.cipher->flags & 8) != 0) { + context.keyLength = narrowed; + return 1; + } + return Error(130); +} + +int EncryptBlock(LibreSslCmac& context, unsigned char* output, const unsigned char* input) { + if (context.cipherContext.cipher->cipher == nullptr) NotImplemented_nid_no_patch(__func__); + return context.cipherContext.cipher->cipher(&context.cipherContext, output, input, context.cipherContext.cipher->blockSize); +} + +void DoubleBlock(unsigned char* output, const unsigned char* input, int length) { + unsigned char carry = 0; + for (int index = length; index-- > 0;) { + const auto value = input[index]; + output[index] = static_cast((value << 1) | carry); + carry = value >> 7; + } + output[length - 1] ^= static_cast((length == 16 ? 0x87 : 0x1b) & -static_cast(carry)); +} + +int APS5_VABI AesInit(LibreSslCipherContext* context, const unsigned char* key, const unsigned char*, int encrypt) { + if (key == nullptr || context->cipherData == nullptr) throw std::invalid_argument("CMAC AES: missing key storage"); + if (context->keyLength != 16 && context->keyLength != 24 && context->keyLength != 32) return Error(143); + auto* aes = static_cast(context->cipherData); + mbedtls_aes_init(aes); + const int result = encrypt ? mbedtls_aes_setkey_enc(aes, key, context->keyLength * 8) : mbedtls_aes_setkey_dec(aes, key, context->keyLength * 8); + if (result != 0) throw std::runtime_error("CMAC AES: backend key setup failed"); + return 1; +} + +int APS5_VABI AesCipher(LibreSslCipherContext* context, unsigned char* output, const unsigned char* input, std::size_t length) { + if (context->cipherData == nullptr || (length != 0 && (output == nullptr || input == nullptr))) throw std::invalid_argument("CMAC AES: null data"); + if (length == 0) return 1; + auto* aes = static_cast(context->cipherData); + const int mode = context->encrypt ? MBEDTLS_AES_ENCRYPT : MBEDTLS_AES_DECRYPT; + const auto complete = length - length % 16; + if (complete != 0 && mbedtls_aes_crypt_cbc(aes, mode, complete, context->iv, input, output) != 0) + throw std::runtime_error("CMAC AES: backend encryption failed"); + const auto remaining = length - complete; + if (remaining == 0) return 1; + input += complete; + output += complete; + unsigned char block[16]; + unsigned char nextIv[16]; + if (context->encrypt) { + std::memcpy(block, context->iv, 16); + for (std::size_t index = 0; index < remaining; ++index) block[index] ^= input[index]; + if (mbedtls_aes_crypt_ecb(aes, mode, block, output) != 0) throw std::runtime_error("CMAC AES: backend encryption failed"); + std::memcpy(context->iv, output, 16); + } else { + std::memcpy(nextIv, input, 16); + if (mbedtls_aes_crypt_ecb(aes, mode, input, block) != 0) throw std::runtime_error("CMAC AES: backend decryption failed"); + for (std::size_t index = 0; index < remaining; ++index) output[index] = block[index] ^ context->iv[index]; + std::memcpy(context->iv, nextIv, 16); + } + Cleanse(block, sizeof(block)); + Cleanse(nextIv, sizeof(nextIv)); + return 1; +} + +const LibreSslCipher aes128{419, 16, 16, 16, 0x5002, AesInit, AesCipher, nullptr, sizeof(mbedtls_aes_context), nullptr, nullptr, nullptr, nullptr}; +const LibreSslCipher aes192{423, 16, 24, 16, 0x5002, AesInit, AesCipher, nullptr, sizeof(mbedtls_aes_context), nullptr, nullptr, nullptr, nullptr}; +const LibreSslCipher aes256{427, 16, 32, 16, 0x5002, AesInit, AesCipher, nullptr, sizeof(mbedtls_aes_context), nullptr, nullptr, nullptr, nullptr}; + +} + +extern "C" { + +LibreSslCmac* APS5_VABI CMAC_CTX_new_nid_postfix() { + auto* context = static_cast(malloc_nid_postfix(sizeof(LibreSslCmac))); + if (context != nullptr) { + context->cipherContext = {}; + context->lastLength = -1; + } + return context; +} + +void APS5_VABI CMAC_CTX_free_nid_postfix(LibreSslCmac* context) { + if (context == nullptr) return; + Cleanup(context->cipherContext); + Cleanse(context, sizeof(*context)); + free_nid_postfix(context); +} + +int APS5_VABI CMAC_Init_nid_postfix(LibreSslCmac* context, const void* key, std::size_t keyLength, const LibreSslCipher* cipher, void* engine) { + RequireContext(context); + const unsigned char zero[32]{}; + if (key == nullptr && keyLength == 0 && cipher == nullptr && engine == nullptr) { + if (context->lastLength == -1) return 0; + if (!InitializeCipher(context->cipherContext, nullptr, nullptr, zero, nullptr)) return 0; + std::memset(context->previousBlock, 0, context->cipherContext.cipher->blockSize); + context->lastLength = 0; + return 1; + } + if (cipher != nullptr && !InitializeCipher(context->cipherContext, cipher, nullptr, nullptr, engine)) return 0; + if (key == nullptr) return 1; + if (context->cipherContext.cipher == nullptr) return 0; + if (!SetKeyLength(context->cipherContext, keyLength)) return 0; + if (!InitializeCipher(context->cipherContext, nullptr, static_cast(key), zero, nullptr)) return 0; + if (!EncryptBlock(*context, context->previousBlock, zero)) return 0; + const auto blockSize = context->cipherContext.cipher->blockSize; + DoubleBlock(context->firstSubkey, context->previousBlock, blockSize); + DoubleBlock(context->secondSubkey, context->firstSubkey, blockSize); + Cleanse(context->previousBlock, blockSize); + if (!InitializeCipher(context->cipherContext, nullptr, nullptr, zero, nullptr)) return 0; + context->lastLength = 0; + return 1; +} + +int APS5_VABI CMAC_Update_nid_postfix(LibreSslCmac* context, const void* data, std::size_t length) { + RequireContext(context); + if (context->lastLength == -1) return 0; + if (length == 0) return 1; + if (data == nullptr) throw std::invalid_argument("CMAC_Update: null input"); + const auto* bytes = static_cast(data); + const auto blockSize = context->cipherContext.cipher->blockSize; + if (context->lastLength != 0) { + const auto take = std::min(length, static_cast(blockSize - context->lastLength)); + std::memcpy(context->lastBlock + context->lastLength, bytes, take); + context->lastLength += static_cast(take); + bytes += take; + length -= take; + if (length == 0) return 1; + if (!EncryptBlock(*context, context->previousBlock, context->lastBlock)) return 0; + } + while (length > static_cast(blockSize)) { + if (!EncryptBlock(*context, context->previousBlock, bytes)) return 0; + bytes += blockSize; + length -= blockSize; + } + std::memcpy(context->lastBlock, bytes, length); + context->lastLength = static_cast(length); + return 1; +} + +int APS5_VABI CMAC_Final_nid_postfix(LibreSslCmac* context, unsigned char* output, std::size_t* length) { + RequireContext(context); + if (context->lastLength == -1) return 0; + if (length == nullptr) throw std::invalid_argument("CMAC_Final: null output length"); + const auto blockSize = context->cipherContext.cipher->blockSize; + *length = blockSize; + if (output == nullptr) return 1; + const bool full = context->lastLength == blockSize; + if (!full) { + std::memset(context->lastBlock + context->lastLength, 0, blockSize - context->lastLength); + context->lastBlock[context->lastLength] = 0x80; + } + const auto* subkey = full ? context->firstSubkey : context->secondSubkey; + for (int index = 0; index < blockSize; ++index) output[index] = context->lastBlock[index] ^ subkey[index]; + if (EncryptBlock(*context, output, output)) return 1; + Cleanse(output, blockSize); + return 0; +} + +const LibreSslCipher* APS5_VABI EVP_aes_128_cbc_nid_postfix() { return &aes128; } +const LibreSslCipher* APS5_VABI EVP_aes_192_cbc_nid_postfix() { return &aes192; } +const LibreSslCipher* APS5_VABI EVP_aes_256_cbc_nid_postfix() { return &aes256; } + +} diff --git a/core/libs/prx/libSceLibreSsl/src/Evp.cpp b/core/libs/prx/libSceLibreSsl/src/Evp.cpp new file mode 100644 index 0000000000..a611047347 --- /dev/null +++ b/core/libs/prx/libSceLibreSsl/src/Evp.cpp @@ -0,0 +1,529 @@ +/* $OpenBSD: digest.c,v 1.27 2017/01/29 17:49:23 beck Exp $ */ +/* Copyright (C) 1995-1998 Eric Young (eay@cryptsoft.com) + * All rights reserved. + * + * This package is an SSL implementation written + * by Eric Young (eay@cryptsoft.com). + * The implementation was written so as to conform with Netscapes SSL. + * + * This library is free for commercial and non-commercial use as long as + * the following conditions are aheared to. The following conditions + * apply to all code found in this distribution, be it the RC4, RSA, + * lhash, DES, etc., code; not just the SSL code. The SSL documentation + * included with this distribution is covered by the same copyright terms + * except that the holder is Tim Hudson (tjh@cryptsoft.com). + * + * Copyright remains Eric Young's, and as such any Copyright notices in + * the code are not to be removed. + * If this package is used in a product, Eric Young should be given attribution + * as the author of the parts of the library used. + * This can be in the form of a textual message at program startup or + * in documentation (online or textual) provided with the package. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. All advertising materials mentioning features or use of this software + * must display the following acknowledgement: + * "This product includes cryptographic software written by + * Eric Young (eay@cryptsoft.com)" + * The word 'cryptographic' can be left out if the rouines from the library + * being used are not cryptographic related :-). + * 4. If you include any Windows specific code (or a derivative thereof) from + * the apps directory (application code) you must include an acknowledgement: + * "This product includes software written by Tim Hudson (tjh@cryptsoft.com)" + * + * THIS SOFTWARE IS PROVIDED BY ERIC YOUNG ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * + * The licence and distribution terms for any publically available version or + * derivative of this code cannot be changed. i.e. this code cannot simply be + * copied and put under another distribution licence + * [including the GNU Public Licence.] + */ +/* ==================================================================== + * Copyright (c) 1998-2001 The OpenSSL Project. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. All advertising materials mentioning features or use of this + * software must display the following acknowledgment: + * "This product includes software developed by the OpenSSL Project + * for use in the OpenSSL Toolkit. (http://www.openssl.org/)" + * + * 4. The names "OpenSSL Toolkit" and "OpenSSL Project" must not be used to + * endorse or promote products derived from this software without + * prior written permission. For written permission, please contact + * openssl-core@openssl.org. + * + * 5. Products derived from this software may not be called "OpenSSL" + * nor may "OpenSSL" appear in their names without prior written + * permission of the OpenSSL Project. + * + * 6. Redistributions of any form whatsoever must retain the following + * acknowledgment: + * "This product includes software developed by the OpenSSL Project + * for use in the OpenSSL Toolkit (http://www.openssl.org/)" + * + * THIS SOFTWARE IS PROVIDED BY THE OpenSSL PROJECT ``AS IS'' AND ANY + * EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE OpenSSL PROJECT OR + * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * ==================================================================== + * + * This product includes cryptographic software written by Eric Young + * (eay@cryptsoft.com). This product includes software written by Tim + * Hudson (tjh@cryptsoft.com). + * + */ +/* $OpenBSD: hmac.c,v 1.24 2017/03/03 10:39:07 inoguchi Exp $ */ +/* Copyright (C) 1995-1998 Eric Young (eay@cryptsoft.com) + * All rights reserved. + * + * This package is an SSL implementation written + * by Eric Young (eay@cryptsoft.com). + * The implementation was written so as to conform with Netscapes SSL. + * + * This library is free for commercial and non-commercial use as long as + * the following conditions are aheared to. The following conditions + * apply to all code found in this distribution, be it the RC4, RSA, + * lhash, DES, etc., code; not just the SSL code. The SSL documentation + * included with this distribution is covered by the same copyright terms + * except that the holder is Tim Hudson (tjh@cryptsoft.com). + * + * Copyright remains Eric Young's, and as such any Copyright notices in + * the code are not to be removed. + * If this package is used in a product, Eric Young should be given attribution + * as the author of the parts of the library used. + * This can be in the form of a textual message at program startup or + * in documentation (online or textual) provided with the package. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. All advertising materials mentioning features or use of this software + * must display the following acknowledgement: + * "This product includes cryptographic software written by + * Eric Young (eay@cryptsoft.com)" + * The word 'cryptographic' can be left out if the rouines from the library + * being used are not cryptographic related :-). + * 4. If you include any Windows specific code (or a derivative thereof) from + * the apps directory (application code) you must include an acknowledgement: + * "This product includes software written by Tim Hudson (tjh@cryptsoft.com)" + * + * THIS SOFTWARE IS PROVIDED BY ERIC YOUNG ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * + * The licence and distribution terms for any publically available version or + * derivative of this code cannot be changed. i.e. this code cannot simply be + * copied and put under another distribution licence + * [including the GNU Public Licence.] + */ + +#include "prx/libc/include/Api.hpp" +#include "prx/libSceLibreSsl/include/Api.hpp" +#include +#include +#include +#include +#include +#include "prx/libc/include/General.hpp" + +namespace { +static_assert(sizeof(EvpMd) == 120); +static_assert(offsetof(EvpMd, flags) == 16); +static_assert(offsetof(EvpMd, sign) == 64); +static_assert(offsetof(EvpMd, requiredPkeyType) == 80); +static_assert(offsetof(EvpMd, blockSize) == 100); +static_assert(offsetof(EvpMd, control) == 112); +static_assert(sizeof(EvpMdContext) == 48); +static_assert(offsetof(EvpMdContext, update) == 40); +static_assert(sizeof(HmacContext) == 288); +static_assert(offsetof(HmacContext, keyLength) == 152); +static_assert(offsetof(HmacContext, key) == 156); + +constexpr std::uint64_t FlagOneShot = 0x1; +constexpr std::uint64_t FlagCleaned = 0x2; +constexpr std::uint64_t FlagReuse = 0x4; +constexpr std::uint64_t FlagNoInit = 0x100; +constexpr std::size_t MaxDigestSize = 64; +constexpr std::uint64_t PkeyMethodSignature = 0x4; +constexpr std::uint64_t DigestAlgorithmIdAbsent = 0x8; +constexpr std::int32_t PkeyRsa = 6; +constexpr std::int32_t PkeyRsa2 = 19; +constexpr std::int32_t PointerSize = 8; + +int APS5_VABI Md5Init(EvpMdContext* context) { + return MD5_Init_nid_postfix(static_cast(context->mdData)); +} + +int APS5_VABI Md5Update(EvpMdContext* context, const void* data, std::size_t length) { + return MD5_Update_nid_postfix(static_cast(context->mdData), data, length); +} + +int APS5_VABI Md5Final(EvpMdContext* context, unsigned char* digest) { + return MD5_Final_nid_postfix(digest, static_cast(context->mdData)); +} + +int APS5_VABI Sha256Init(EvpMdContext* context) { + return SHA256_Init_nid_postfix(static_cast(context->mdData)); +} + +int APS5_VABI Sha256Update(EvpMdContext* context, const void* data, std::size_t length) { + return SHA256_Update_nid_postfix(static_cast(context->mdData), data, length); +} + +int APS5_VABI Sha256Final(EvpMdContext* context, unsigned char* digest) { + return SHA256_Final_nid_postfix(digest, static_cast(context->mdData)); +} + +int APS5_VABI Sha384Init(EvpMdContext* context) { + return SHA384_Init_nid_postfix(static_cast(context->mdData)); +} + +int APS5_VABI Sha512Init(EvpMdContext* context) { + return SHA512_Init_nid_postfix(static_cast(context->mdData)); +} + +int APS5_VABI Sha512Update(EvpMdContext* context, const void* data, std::size_t length) { + return SHA512_Update_nid_postfix(static_cast(context->mdData), data, length); +} + +int APS5_VABI Sha512Final(EvpMdContext* context, unsigned char* digest) { + return SHA512_Final_nid_postfix(digest, static_cast(context->mdData)); +} + +constexpr EvpMd Md5 { + 4, 8, 16, 0, Md5Init, Md5Update, Md5Final, nullptr, nullptr, nullptr, nullptr, + {PkeyRsa, PkeyRsa2, 0, 0, 0}, 64, PointerSize + 92, nullptr, +}; + +constexpr EvpMd Sha256 { + 672, 668, 32, PkeyMethodSignature | DigestAlgorithmIdAbsent, Sha256Init, Sha256Update, Sha256Final, nullptr, nullptr, nullptr, nullptr, + {PkeyRsa, PkeyRsa2, 0, 0, 0}, 64, PointerSize + 112, nullptr, +}; + +constexpr EvpMd Sha384 { + 673, 669, 48, PkeyMethodSignature | DigestAlgorithmIdAbsent, Sha384Init, Sha512Update, Sha512Final, nullptr, nullptr, nullptr, nullptr, + {PkeyRsa, PkeyRsa2, 0, 0, 0}, 128, PointerSize + 216, nullptr, +}; + +constexpr EvpMd Sha512 { + 674, 670, 64, PkeyMethodSignature | DigestAlgorithmIdAbsent, Sha512Init, Sha512Update, Sha512Final, nullptr, nullptr, nullptr, nullptr, + {PkeyRsa, PkeyRsa2, 0, 0, 0}, 128, PointerSize + 216, nullptr, +}; + +template TContext& RequireContext(TContext* context, const char* function) { + if (!context) throw std::invalid_argument(std::string(function) + ": null context"); + return *context; +} + +void RequireNoEngineOrKey(const EvpMdContext& context, const char* function) { + if (context.engine) NotImplemented_nid_no_patch((std::string(function) + ": ENGINE").c_str()); + if (context.pkeyContext) NotImplemented_nid_no_patch((std::string(function) + ": EVP_PKEY_CTX").c_str()); +} + +void ReleaseData(EvpMdContext& context) { + if (!context.digest || context.digest->contextSize <= 0 || !context.mdData || (context.flags & FlagReuse)) return; + std::memset(context.mdData, 0, static_cast(context.digest->contextSize)); + free_nid_postfix(context.mdData); + context.mdData = nullptr; +} + +int CleanupContext(EvpMdContext& context, const char* function) { + RequireNoEngineOrKey(context, function); + if (context.digest && context.digest->cleanup && !(context.flags & FlagCleaned)) context.digest->cleanup(&context); + ReleaseData(context); + std::memset(&context, 0, sizeof(context)); + return 1; +} + +int DigestInit(EvpMdContext& context, const EvpMd* type, void* engine, const char* function) { + RequireNoEngineOrKey(context, function); + if (engine) NotImplemented_nid_no_patch((std::string(function) + ": ENGINE").c_str()); + context.flags &= ~FlagCleaned; + if (!type) { + if (!context.digest) return 0; + throw std::invalid_argument(std::string(function) + ": null digest type for a context that has one"); + } + if (context.digest != type) { + ReleaseData(context); + if (!(context.flags & FlagNoInit) && type->contextSize) { + if (type->contextSize < 0) throw std::invalid_argument(std::string(function) + ": negative digest context size"); + context.mdData = malloc_nid_postfix(static_cast(type->contextSize)); + if (!context.mdData) { + context.digest = nullptr; + context.update = nullptr; + return 0; + } + context.update = type->update; + } + context.digest = type; + } + if (context.flags & FlagNoInit) return 1; + if (!context.digest->init) throw std::invalid_argument(std::string(function) + ": digest without an init function"); + return context.digest->init(&context); +} + +int DigestUpdate(EvpMdContext& context, const void* data, std::size_t length, const char* function) { + if (!context.update) throw std::invalid_argument(std::string(function) + ": context without an update function"); + return context.update(&context, data, length); +} + +int DigestFinal(EvpMdContext& context, unsigned char* digest, std::uint32_t* size, const char* function) { + if (!context.digest) throw std::invalid_argument(std::string(function) + ": context without a digest"); + const auto* type = context.digest; + if (static_cast(type->mdSize) > MaxDigestSize) return 0; + if (!type->finalize) throw std::invalid_argument(std::string(function) + ": digest without a final function"); + const int result = type->finalize(&context, digest); + if (size) *size = static_cast(type->mdSize); + if (type->cleanup) { + type->cleanup(&context); + context.flags |= FlagCleaned; + } + if (type->contextSize) { + if (!context.mdData || type->contextSize < 0) throw std::invalid_argument(std::string(function) + ": context without digest data"); + std::memset(context.mdData, 0, static_cast(type->contextSize)); + } + return result; +} + +int CopyContext(EvpMdContext& to, const EvpMdContext& from, const char* function) { + if (!from.digest) return 0; + RequireNoEngineOrKey(from, function); + void* reused = nullptr; + if (to.digest == from.digest) { + reused = to.mdData; + to.flags |= FlagReuse; + } + CleanupContext(to, function); + std::memcpy(&to, &from, sizeof(to)); + if (from.mdData && to.digest->contextSize) { + if (to.digest->contextSize < 0) throw std::invalid_argument(std::string(function) + ": negative digest context size"); + const auto size = static_cast(to.digest->contextSize); + to.mdData = reused ? reused : malloc_nid_postfix(size); + if (!to.mdData) { + to.digest = nullptr; + to.update = nullptr; + return 0; + } + std::memmove(to.mdData, from.mdData, size); + } + to.update = from.update; + if (to.digest->copy) return to.digest->copy(&to, &from); + return 1; +} + +void HmacContextInit(HmacContext& context) { + std::memset(&context.innerContext, 0, sizeof(context.innerContext)); + std::memset(&context.outerContext, 0, sizeof(context.outerContext)); + std::memset(&context.mdContext, 0, sizeof(context.mdContext)); + context.md = nullptr; +} + +void HmacContextCleanup(HmacContext& context, const char* function) { + CleanupContext(context.innerContext, function); + CleanupContext(context.outerContext, function); + CleanupContext(context.mdContext, function); + std::memset(&context, 0, sizeof(context)); +} + +int HmacInit(HmacContext& context, const void* key, int length, const EvpMd* md, void* engine, const char* function) { + if (md && md != context.md && (!key || length < 0)) return 0; + bool reset = false; + if (md) { + reset = true; + context.md = md; + } else if (context.md) { + md = context.md; + } else { + return 0; + } + if (key) { + reset = true; + const int block = md->blockSize; + if (static_cast(block) > sizeof(context.key)) return 0; + if (block < length) { + if (!DigestInit(context.mdContext, md, engine, function)) return 0; + if (!DigestUpdate(context.mdContext, key, static_cast(length), function)) return 0; + if (!DigestFinal(context.mdContext, context.key, &context.keyLength, function)) return 0; + } else { + if (length < 0 || static_cast(length) > sizeof(context.key)) return 0; + std::memcpy(context.key, key, static_cast(length)); + context.keyLength = static_cast(length); + } + if (context.keyLength != HmacMaxBlock) std::memset(context.key + context.keyLength, 0, HmacMaxBlock - context.keyLength); + } + if (reset) { + unsigned char pad[HmacMaxBlock]; + const auto block = static_cast(md->blockSize); + for (std::size_t i = 0; i < HmacMaxBlock; ++i) pad[i] = 0x36 ^ context.key[i]; + if (!DigestInit(context.innerContext, md, engine, function)) return 0; + if (!DigestUpdate(context.innerContext, pad, block, function)) return 0; + for (std::size_t i = 0; i < HmacMaxBlock; ++i) pad[i] = 0x5c ^ context.key[i]; + if (!DigestInit(context.outerContext, md, engine, function)) return 0; + if (!DigestUpdate(context.outerContext, pad, block, function)) return 0; + } + return CopyContext(context.mdContext, context.innerContext, function) ? 1 : 0; +} + +int HmacUpdate(HmacContext& context, const unsigned char* data, std::size_t length, const char* function) { + if (!context.md) return 0; + return DigestUpdate(context.mdContext, data, length, function); +} + +int HmacFinal(HmacContext& context, unsigned char* digest, std::uint32_t* length, const char* function) { + if (!context.md) return 0; + unsigned char inner[MaxDigestSize]; + std::uint32_t innerLength = 0; + if (!DigestFinal(context.mdContext, inner, &innerLength, function)) return 0; + if (!CopyContext(context.mdContext, context.outerContext, function)) return 0; + if (!DigestUpdate(context.mdContext, inner, innerLength, function)) return 0; + if (!DigestFinal(context.mdContext, digest, length, function)) return 0; + return 1; +} +} + +extern "C" { + +const EvpMd* APS5_VABI EVP_md5_nid_postfix(void) { + return &Md5; +} + +const EvpMd* APS5_VABI EVP_sha256_nid_postfix(void) { + return &Sha256; +} + +const EvpMd* APS5_VABI EVP_sha384_nid_postfix(void) { + return &Sha384; +} + +const EvpMd* APS5_VABI EVP_sha512_nid_postfix(void) { + return &Sha512; +} + +int APS5_VABI EVP_MD_size_nid_postfix(const EvpMd* md) { + return md ? md->mdSize : -1; +} + +void APS5_VABI EVP_MD_CTX_init_nid_postfix(EvpMdContext* context) { + std::memset(&RequireContext(context, __func__), 0, sizeof(EvpMdContext)); +} + +EvpMdContext* APS5_VABI EVP_MD_CTX_create_nid_postfix(void) { + return static_cast(calloc_nid_postfix(1, sizeof(EvpMdContext))); +} + +int APS5_VABI EVP_MD_CTX_cleanup_nid_postfix(EvpMdContext* context) { + return CleanupContext(RequireContext(context, __func__), __func__); +} + +void APS5_VABI EVP_MD_CTX_destroy_nid_postfix(EvpMdContext* context) { + if (!context) return; + CleanupContext(*context, __func__); + free_nid_postfix(context); +} + +int APS5_VABI EVP_DigestInit_ex_nid_postfix(EvpMdContext* context, const EvpMd* type, void* engine) { + return DigestInit(RequireContext(context, __func__), type, engine, __func__); +} + +int APS5_VABI EVP_DigestUpdate_nid_postfix(EvpMdContext* context, const void* data, std::size_t length) { + return DigestUpdate(RequireContext(context, __func__), data, length, __func__); +} + +int APS5_VABI EVP_DigestFinal_ex_nid_postfix(EvpMdContext* context, unsigned char* digest, std::uint32_t* size) { + return DigestFinal(RequireContext(context, __func__), digest, size, __func__); +} + +int APS5_VABI EVP_Digest_nid_postfix(const void* data, std::size_t length, unsigned char* digest, std::uint32_t* size, const EvpMd* type, void* engine) { + EvpMdContext context {}; + context.flags = FlagOneShot; + const int result = DigestInit(context, type, engine, __func__) && DigestUpdate(context, data, length, __func__) && DigestFinal(context, digest, size, __func__); + CleanupContext(context, __func__); + return result; +} + +void APS5_VABI HMAC_CTX_init_nid_postfix(HmacContext* context) { + HmacContextInit(RequireContext(context, __func__)); +} + +void APS5_VABI HMAC_CTX_cleanup_nid_postfix(HmacContext* context) { + HmacContextCleanup(RequireContext(context, __func__), __func__); +} + +int APS5_VABI HMAC_Init_ex_nid_postfix(HmacContext* context, const void* key, int length, const EvpMd* md, void* engine) { + return HmacInit(RequireContext(context, __func__), key, length, md, engine, __func__); +} + +int APS5_VABI HMAC_Init_nid_postfix(HmacContext* context, const void* key, int length, const EvpMd* md) { + auto& c = RequireContext(context, __func__); + if (key && md) HmacContextInit(c); + return HmacInit(c, key, length, md, nullptr, __func__); +} + +int APS5_VABI HMAC_Update_nid_postfix(HmacContext* context, const unsigned char* data, std::size_t length) { + return HmacUpdate(RequireContext(context, __func__), data, length, __func__); +} + +int APS5_VABI HMAC_Final_nid_postfix(HmacContext* context, unsigned char* digest, std::uint32_t* length) { + return HmacFinal(RequireContext(context, __func__), digest, length, __func__); +} + +unsigned char* APS5_VABI HMAC_nid_postfix(const EvpMd* md, const void* key, int keyLength, const unsigned char* data, std::size_t length, unsigned char* digest, std::uint32_t* digestLength) { + static unsigned char buffer[MaxDigestSize]; + if (!digest) digest = buffer; + HmacContext context; + HmacContextInit(context); + const bool ok = HMAC_Init_nid_postfix(&context, key, keyLength, md) && HmacUpdate(context, data, length, __func__) && HmacFinal(context, digest, digestLength, __func__); + HmacContextCleanup(context, __func__); + return ok ? digest : nullptr; +} + +} diff --git a/core/libs/prx/libSceLibreSsl3/AdditionalExports.cpp b/core/libs/prx/libSceLibreSsl3/AdditionalExports.cpp index 06c5ffe3a4..df0619e169 100644 --- a/core/libs/prx/libSceLibreSsl3/AdditionalExports.cpp +++ b/core/libs/prx/libSceLibreSsl3/AdditionalExports.cpp @@ -685,10 +685,6 @@ int APS5_VABI ASN1_TYPE_free_nid_postfix() { NotImplemented_nid_no_patch(__func__); } -int APS5_VABI ASN1_TIME_diff_nid_postfix() { - NotImplemented_nid_no_patch(__func__); -} - int APS5_VABI ECDSA_SIG_set0_nid_postfix() { NotImplemented_nid_no_patch(__func__); } diff --git a/core/libs/prx/libSceLibreSsl3/CMakeLists.txt b/core/libs/prx/libSceLibreSsl3/CMakeLists.txt index 79c5a1a91d..d6060cd2c0 100644 --- a/core/libs/prx/libSceLibreSsl3/CMakeLists.txt +++ b/core/libs/prx/libSceLibreSsl3/CMakeLists.txt @@ -12,8 +12,19 @@ if(WIN32) target_sources(libSceLibreSsl3 PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/ParentExports.cpp) endif() target_sources(libSceLibreSsl3 PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/PkeyGetters.cpp) +target_sources(libSceLibreSsl3 PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/Time.cpp) if(BUILD_TESTING) + add_test_executable(guest_libressl3_time_tests ${CMAKE_CURRENT_SOURCE_DIR}/tests/Time.cpp) + target_include_directories(guest_libressl3_time_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_libressl3_time_tests PRIVATE libSceLibreSsl3) + configure_windows_unwind(guest_libressl3_time_tests) + add_test(NAME guest_libressl3_time COMMAND guest_libressl3_time_tests) + set_tests_properties(guest_libressl3_time PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_libressl3_time PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_libressl3_pkey_getters_tests ${CMAKE_CURRENT_SOURCE_DIR}/tests/PkeyGetters.cpp) target_include_directories(guest_libressl3_pkey_getters_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_libressl3_pkey_getters_tests PRIVATE libSceLibreSsl3 libc) @@ -87,8 +98,14 @@ if(BUILD_TESTING) configure_windows_unwind(guest_libressl3_key_reference_tests) add_test(NAME guest_libressl3_key_reference COMMAND guest_libressl3_key_reference_tests) set_tests_properties(guest_libressl3_key_reference PROPERTIES TIMEOUT 20) + add_test_executable(guest_libressl3_version_tests ${CMAKE_CURRENT_SOURCE_DIR}/tests/OpenSslVersion.cpp) + target_include_directories(guest_libressl3_version_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_libressl3_version_tests PRIVATE libSceLibreSsl3 libc) + configure_windows_unwind(guest_libressl3_version_tests) + add_test(NAME guest_libressl3_version COMMAND guest_libressl3_version_tests) + set_tests_properties(guest_libressl3_version PROPERTIES TIMEOUT 30) if(WIN32) - set_tests_properties(guest_crypto_memcmp guest_libressl3_digest guest_libressl3_asn1 guest_libressl3_key_reference PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_crypto_memcmp guest_libressl3_digest guest_libressl3_asn1 guest_libressl3_key_reference guest_libressl3_version 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 3b94e55781..d327cfb245 100644 --- a/core/libs/prx/libSceLibreSsl3/Export.cpp +++ b/core/libs/prx/libSceLibreSsl3/Export.cpp @@ -219,9 +219,16 @@ int APS5_VABI OCSP_resp_find_status_nid_postfix(void) { return 0; } -int APS5_VABI OpenSSL_version_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +const char* APS5_VABI OpenSSL_version_nid_postfix(int type) { + switch (type) { + case 0: return "LibreSSL 4.0.0"; + case 1: return "compiler: information not available"; + case 2: return "built on: date not available"; + case 3: return "platform: information not available"; + case 4: return "OPENSSLDIR: \"/etc/ssl\""; + case 5: return "ENGINESDIR: N/A"; + default: return "not available"; + } } int APS5_VABI PKCS7_free_nid_postfix(void) { diff --git a/core/libs/prx/libSceLibreSsl3/include/Api.hpp b/core/libs/prx/libSceLibreSsl3/include/Api.hpp index 9b3150e1b9..5aa440134d 100644 --- a/core/libs/prx/libSceLibreSsl3/include/Api.hpp +++ b/core/libs/prx/libSceLibreSsl3/include/Api.hpp @@ -54,7 +54,7 @@ int APS5_VABI OBJ_txt2obj_nid_postfix(void); int APS5_VABI OCSP_CERTID_free_nid_postfix(void); int APS5_VABI OCSP_cert_to_id_nid_postfix(void); int APS5_VABI OCSP_resp_find_status_nid_postfix(void); -int APS5_VABI OpenSSL_version_nid_postfix(void); +const char* APS5_VABI OpenSSL_version_nid_postfix(int type); int APS5_VABI PKCS7_free_nid_postfix(void); int APS5_VABI PKCS8_PRIV_KEY_INFO_free_nid_postfix(void); int APS5_VABI RSA_check_key_nid_postfix(void); @@ -270,7 +270,7 @@ int APS5_VABI tls_config_parse_protocols_nid_postfix(); int APS5_VABI SSL_CTX_set_post_handshake_auth_nid_postfix(); int APS5_VABI tls_config_insecure_noverifyname_nid_postfix(); int APS5_VABI ASN1_TYPE_free_nid_postfix(); -int APS5_VABI ASN1_TIME_diff_nid_postfix(); +int APS5_VABI ASN1_TIME_diff_nid_postfix(int* days, int* seconds, const Asn1String* from, const Asn1String* to); int APS5_VABI ECDSA_SIG_set0_nid_postfix(); int APS5_VABI OBJ_obj2txt_nid_postfix(); int APS5_VABI SSL_set_quic_transport_params_nid_postfix(); diff --git a/core/libs/prx/libSceLibreSsl3/src/Time.cpp b/core/libs/prx/libSceLibreSsl3/src/Time.cpp new file mode 100644 index 0000000000..4d6a8e0cf8 --- /dev/null +++ b/core/libs/prx/libSceLibreSsl3/src/Time.cpp @@ -0,0 +1,51 @@ +#include "prx/libSceLibreSsl3/libSceLibreSsl3.hpp" + +#include +#include +#include + +namespace { + +static_assert(sizeof(Asn1String) == 24); +static_assert(offsetof(Asn1String, data) == 8); + +bool ReadTime(const Asn1String* value, std::int64_t& seconds) { + if (!value) { + seconds = std::time(nullptr); + return seconds >= -62167219200LL && seconds <= 253402300799LL; + } + if (!value->data || (value->length != 13 && value->length != 15)) return false; + const auto* data = value->data; + if (data[value->length - 1] != 'Z') return false; + for (int i = 0; i < value->length - 1; ++i) + if (data[i] < '0' || data[i] > '9') return false; + const int yearDigits = value->length - 11; + int year = 0; + for (int i = 0; i < yearDigits; ++i) year = year * 10 + data[i] - '0'; + if (yearDigits == 2) year += year < 50 ? 2000 : 1900; + unsigned fields[5]; + for (int i = 0; i < 5; ++i) { + const int position = yearDigits + i * 2; + fields[i] = (data[position] - '0') * 10 + data[position + 1] - '0'; + } + const std::chrono::year_month_day date{std::chrono::year{year}, + std::chrono::month{fields[0]}, std::chrono::day{fields[1]}}; + if (!date.ok() || fields[2] > 23 || fields[3] > 59 || fields[4] > 59) return false; + seconds = static_cast(std::chrono::sys_days{date}.time_since_epoch().count()) * 86400 + + fields[2] * 3600 + fields[3] * 60 + fields[4]; + return true; +} + +} + +extern "C" int APS5_VABI ASN1_TIME_diff_nid_postfix(int* days, int* seconds, + const Asn1String* from, const Asn1String* to) { + std::int64_t start; + std::int64_t end; + if (!ReadTime(from, start) || !ReadTime(to, end)) return 0; + if (!days || !seconds) throw std::invalid_argument("ASN1_TIME_diff: null output"); + const auto difference = end - start; + *seconds = static_cast(difference % 86400); + *days = static_cast(difference / 86400); + return 1; +} diff --git a/core/libs/prx/libSceLibreSsl3/tests/OpenSslVersion.cpp b/core/libs/prx/libSceLibreSsl3/tests/OpenSslVersion.cpp new file mode 100644 index 0000000000..721f50eb9c --- /dev/null +++ b/core/libs/prx/libSceLibreSsl3/tests/OpenSslVersion.cpp @@ -0,0 +1,36 @@ +#include +#include +#include +#include +#include "prx/libSceLibreSsl3/include/Api.hpp" + +#ifdef LIBRESSL3_STANDALONE_TEST +extern "C" [[noreturn]] void NotImplemented_nid_no_patch(const char* function) { + throw std::runtime_error(function); +} +#endif + +int main() { + constexpr std::array expected { + "LibreSSL 4.0.0", + "compiler: information not available", + "built on: date not available", + "platform: information not available", + "OPENSSLDIR: \"/etc/ssl\"", + "ENGINESDIR: N/A", + }; + + for (int type = 0; type < static_cast(expected.size()); ++type) { + const char* first = OpenSSL_version_nid_postfix(type); + const char* second = OpenSSL_version_nid_postfix(type); + if (!first || std::strcmp(first, expected[type]) != 0 || first != second) return EXIT_FAILURE; + } + + constexpr std::array unsupported {-1, 6, 42, 0x7fffffff}; + for (int type : unsupported) { + const char* value = OpenSSL_version_nid_postfix(type); + if (!value || std::strcmp(value, "not available") != 0) return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +} diff --git a/core/libs/prx/libSceLibreSsl3/tests/Time.cpp b/core/libs/prx/libSceLibreSsl3/tests/Time.cpp new file mode 100644 index 0000000000..4214ffefe9 --- /dev/null +++ b/core/libs/prx/libSceLibreSsl3/tests/Time.cpp @@ -0,0 +1,128 @@ +#include "prx/libSceLibreSsl3/libSceLibreSsl3.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void Require(bool condition, std::source_location location = std::source_location::current()) { + if (condition) return; + std::fprintf(stderr, "LibreSSL3 time check failed at line %u\n", location.line()); + std::abort(); +} + +Asn1String Time(std::string_view text, int type = 24) { + return {static_cast(text.size()), type, + reinterpret_cast(const_cast(text.data())), 0}; +} + +void Check(std::string_view from, std::string_view to, int days, int seconds) { + auto start = Time(from); + auto end = Time(to); + std::array output{17, 18, 19, 20}; + Require(ASN1_TIME_diff_nid_postfix(&output[1], &output[2], &start, &end) == 1); + Require(output == std::array{17, days, seconds, 20}); + Require(ASN1_TIME_diff_nid_postfix(&output[1], &output[2], &end, &start) == 1); + Require(output == std::array{17, -days, -seconds, 20}); + Require(ASN1_TIME_diff_nid_postfix(&output[1], &output[1], &start, &end) == 1); + Require(output[1] == days); +} + +void Invalid(const Asn1String& invalid) { + auto valid = Time("20000101000000Z"); + int days = 123; + int seconds = 456; + Require(ASN1_TIME_diff_nid_postfix(&days, &seconds, &invalid, &valid) == 0); + Require(days == 123 && seconds == 456); + Require(ASN1_TIME_diff_nid_postfix(&days, &seconds, &valid, &invalid) == 0); + Require(days == 123 && seconds == 456); +} + +void CurrentTime() { + auto epoch = Time("19700101000000Z"); + int days; + int seconds; + auto before = std::time(nullptr); + Require(ASN1_TIME_diff_nid_postfix(&days, &seconds, &epoch, nullptr) == 1); + auto after = std::time(nullptr); + const std::int64_t forward = static_cast(days) * 86400 + seconds; + Require(forward >= before && forward <= after); + before = std::time(nullptr); + Require(ASN1_TIME_diff_nid_postfix(&days, &seconds, nullptr, &epoch) == 1); + after = std::time(nullptr); + const std::int64_t backward = static_cast(days) * 86400 + seconds; + Require(-backward >= before && -backward <= after); + before = std::time(nullptr); + Require(ASN1_TIME_diff_nid_postfix(&days, &seconds, nullptr, nullptr) == 1); + after = std::time(nullptr); + const std::int64_t elapsed = static_cast(days) * 86400 + seconds; + Require(elapsed >= 0 && elapsed <= after - before); +} + +} + +int main() { + Check("19700101000000Z", "19700101000000Z", 0, 0); + Check("19700101235959Z", "19700102000000Z", 0, 1); + Check("19991231235959Z", "20000102000001Z", 1, 2); + Check("19700101000000Z", "19700101235959Z", 0, 86399); + Check("00000101000000Z", "99991231235959Z", 3652424, 86399); + Check("00000228000000Z", "00000301000000Z", 2, 0); + Check("500101000000Z", "19500101000000Z", 0, 0); + Check("491231235959Z", "20500101000000Z", 0, 1); + Check("000229000000Z", "20000301000000Z", 1, 0); + for (int year = 0; year < 10000; ++year) { + char from[32]; + char to[32]; + std::snprintf(from, sizeof(from), "%04d0228000000Z", year); + std::snprintf(to, sizeof(to), "%04d0301000000Z", year); + const bool leap = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0); + Check(from, to, leap ? 2 : 1, 0); + } + for (const auto text : {"", "200001010000Z", "20000101000000", "20000101000000z", + "20000101000000+0000", "20000101000000.0Z", "20000101000000ZZ", + "20000001000000Z", "20001301000000Z", "20000100000000Z", "20000132000000Z", + "20000431000000Z", "19000229000000Z", "21000229000000Z", "20010229000000Z", + "20000101240000Z", "20000101006000Z", "20000101000060Z", "200001010000/0Z"}) + Invalid(Time(text)); + std::array bytes{}; + std::memcpy(bytes.data(), "20000101000000Z", bytes.size()); + Asn1String bounded{15, -100, bytes.data(), -1}; + int days; + int seconds; + auto same = Time("000101000000Z", 99); + Require(ASN1_TIME_diff_nid_postfix(&days, &seconds, &bounded, &same) == 1); + Require(days == 0 && seconds == 0); + for (std::size_t i = 0; i < bytes.size(); ++i) { + const auto saved = bytes[i]; + for (const auto replacement : {0, 0x80, 0xff}) { + bytes[i] = static_cast(replacement); + Invalid(bounded); + } + bytes[i] = saved; + } + bounded.length = -1; + Invalid(bounded); + bounded.length = 15; + bounded.data = nullptr; + Invalid(bounded); + for (bool missingDays : {false, true}) { + bool threw = false; + days = 123; + seconds = 456; + try { + ASN1_TIME_diff_nid_postfix(missingDays ? nullptr : &days, + missingDays ? &seconds : nullptr, &same, &same); + } catch (const std::invalid_argument&) { + threw = true; + } + Require(threw && days == 123 && seconds == 456); + } + CurrentTime(); +} diff --git a/core/libs/prx/libSceLoginService/Export.cpp b/core/libs/prx/libSceLoginService/Export.cpp index e3a24fd796..b37bf2c7fe 100644 --- a/core/libs/prx/libSceLoginService/Export.cpp +++ b/core/libs/prx/libSceLoginService/Export.cpp @@ -1,20 +1,37 @@ #include "prx/libSceLoginService/include/Api.hpp" +#include #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +namespace { + +constexpr int SCE_LOGIN_SERVICE_ERROR_NOT_INITIALIZED = static_cast(0x813a0001u); +constexpr int SCE_LOGIN_SERVICE_ERROR_INVALID_ARGUMENT = static_cast(0x813a0003u); + +std::atomic g_initialized{false}; + +} + extern "C" { int APS5_VABI sceLoginServiceInitialize(void) { + g_initialized.store(true); return 0; } int APS5_VABI sceLoginServiceTerminate(void) { + g_initialized.store(false); return 0; } int APS5_VABI sceLoginServiceRequestDevices(const LoginServiceDeviceRequest* param) { - (void)param; + if (param == nullptr) { + return SCE_LOGIN_SERVICE_ERROR_INVALID_ARGUMENT; + } + if (!g_initialized.load()) { + return SCE_LOGIN_SERVICE_ERROR_NOT_INITIALIZED; + } NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceMbus/PendingExports.cpp b/core/libs/prx/libSceMbus/PendingExports.cpp index c696940b17..f2310b6e67 100644 --- a/core/libs/prx/libSceMbus/PendingExports.cpp +++ b/core/libs/prx/libSceMbus/PendingExports.cpp @@ -335,7 +335,7 @@ int APS5_VABI sceMbusEventBusStatusChangeUnsubscribe(std::uint64_t unusedSlot, s } void APS5_VABI sceMbusEventCallbackFuncsInit_(MbusEventCallbacks* callbacks, std::uint32_t size) { - if (size != 0) std::memset(callbacks, 0, size); + if (callbacks != nullptr) std::memset(callbacks, 0, size); } int APS5_VABI sceMbusEventCreate_(std::uint32_t flags, MbusEvent* buffer, std::uint32_t capacity, const MbusEventCallbacks* callbacks, void* userData) { diff --git a/core/libs/prx/libSceMouse/CMakeLists.txt b/core/libs/prx/libSceMouse/CMakeLists.txt index cf563cd6a7..9d75d16ebd 100644 --- a/core/libs/prx/libSceMouse/CMakeLists.txt +++ b/core/libs/prx/libSceMouse/CMakeLists.txt @@ -7,6 +7,18 @@ target_include_directories(libSceMouse PRIVATE ${LIBS_INCLUDE_DIR}) set_target_properties(libSceMouse PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(libSceMouse) +if(BUILD_TESTING) + add_test_executable(mouse_absolute_tests ${CMAKE_CURRENT_SOURCE_DIR}/tests/MouseAbsolute.cpp) + target_include_directories(mouse_absolute_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(mouse_absolute_tests PRIVATE libSceMouse libkernel libc ${CMAKE_DL_LIBS}) + configure_windows_unwind(mouse_absolute_tests) + add_test(NAME mouse_absolute_position COMMAND mouse_absolute_tests) + if(WIN32) + set_tests_properties(mouse_absolute_position PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + if(BUILD_TESTING) add_test_executable(mouse_tests ${CMAKE_CURRENT_SOURCE_DIR}/tests/Mouse.cpp diff --git a/core/libs/prx/libSceMouse/Export.cpp b/core/libs/prx/libSceMouse/Export.cpp index e7a4202cef..7fc6bae2e4 100644 --- a/core/libs/prx/libSceMouse/Export.cpp +++ b/core/libs/prx/libSceMouse/Export.cpp @@ -82,19 +82,16 @@ int APS5_VABI sceMouseSetProcessPrivilege() { return 0; } -int APS5_VABI sceMouseReadAbsolutePosition() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceMouseReadAbsolutePosition(std::int32_t handle, MouseAbsoluteData* data, std::int32_t num) { + return Mouse::ReadAbsolutePosition(handle, data, num); } -int APS5_VABI sceMouseSetAbsoluteHighPrecisionMode() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceMouseSetAbsoluteHighPrecisionMode(std::int32_t handle, std::int32_t mode) { + return Mouse::SetAbsoluteHighPrecisionMode(handle, mode); } -int APS5_VABI sceMouseSetAbsoluteVirtualScreenSize() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceMouseSetAbsoluteVirtualScreenSize(std::int32_t handle, std::int32_t size) { + return Mouse::SetAbsoluteVirtualScreenSize(handle, size); } } diff --git a/core/libs/prx/libSceMouse/include/Api.hpp b/core/libs/prx/libSceMouse/include/Api.hpp index 1e30e9a3fc..6ed1540333 100644 --- a/core/libs/prx/libSceMouse/include/Api.hpp +++ b/core/libs/prx/libSceMouse/include/Api.hpp @@ -1,6 +1,7 @@ #pragma once #include "SceTypes.hpp" +#include "prx/libSceMouse/include/mouse_structs.h" extern "C" { @@ -19,8 +20,8 @@ int APS5_VABI sceMouseMbusInit(); int APS5_VABI sceMouseSetHandType(); int APS5_VABI sceMouseSetPointerSpeed(); int APS5_VABI sceMouseSetProcessPrivilege(); -int APS5_VABI sceMouseReadAbsolutePosition(); -int APS5_VABI sceMouseSetAbsoluteHighPrecisionMode(); -int APS5_VABI sceMouseSetAbsoluteVirtualScreenSize(); +int APS5_VABI sceMouseReadAbsolutePosition(std::int32_t handle, MouseAbsoluteData* data, std::int32_t num); +int APS5_VABI sceMouseSetAbsoluteHighPrecisionMode(std::int32_t handle, std::int32_t mode); +int APS5_VABI sceMouseSetAbsoluteVirtualScreenSize(std::int32_t handle, std::int32_t size); } diff --git a/core/libs/prx/libSceMouse/include/MouseState.hpp b/core/libs/prx/libSceMouse/include/MouseState.hpp index 390cf33968..b6a161f68d 100644 --- a/core/libs/prx/libSceMouse/include/MouseState.hpp +++ b/core/libs/prx/libSceMouse/include/MouseState.hpp @@ -6,6 +6,8 @@ struct MouseInputEvent { std::int32_t x = 0; std::int32_t y = 0; + std::int32_t countsX = 0; + std::int32_t countsY = 0; std::int32_t wheel = 0; std::int32_t tilt = 0; std::uint32_t button = 0; @@ -22,6 +24,9 @@ int Open(int userId, std::int32_t type, std::int32_t index, const MouseOpenParam int Close(std::int32_t handle); int Read(std::int32_t handle, MouseData* data, std::int32_t num); int ResetAbsolutePosition(std::int32_t handle, std::uint16_t x, std::uint16_t y); +int SetAbsoluteVirtualScreenSize(std::int32_t handle, std::int32_t size); +int SetAbsoluteHighPrecisionMode(std::int32_t handle, std::int32_t mode); +int ReadAbsolutePosition(std::int32_t handle, MouseAbsoluteData* data, std::int32_t num); void Publish(const MouseInputEvent& event); } diff --git a/core/libs/prx/libSceMouse/include/mouse_structs.h b/core/libs/prx/libSceMouse/include/mouse_structs.h index 30c7d3a8f9..a1da9bb48d 100644 --- a/core/libs/prx/libSceMouse/include/mouse_structs.h +++ b/core/libs/prx/libSceMouse/include/mouse_structs.h @@ -10,6 +10,18 @@ struct MouseOpenParam { std::uint8_t reserved[7]; }; +struct MouseAbsoluteData { + std::uint64_t timestamp; + std::uint8_t connected; + std::uint8_t padding[3]; + std::uint32_t buttons; + std::uint16_t x; + std::uint16_t y; + std::int32_t wheel; + std::int32_t tilt; + std::uint8_t reserved[12]; +}; + constexpr int MOUSE_OK = 0; constexpr int MOUSE_ERROR_INVALID_ARG = static_cast(0x80df0001u); constexpr int MOUSE_ERROR_INVALID_HANDLE = static_cast(0x80df0003u); @@ -20,6 +32,13 @@ constexpr int MOUSE_HANDLE = 1; constexpr std::uint8_t MOUSE_OPEN_PARAM_MERGED = 1; static_assert(sizeof(MouseOpenParam) == 8); +static_assert(sizeof(MouseAbsoluteData) == 40); +static_assert(offsetof(MouseAbsoluteData, connected) == 8); +static_assert(offsetof(MouseAbsoluteData, buttons) == 12); +static_assert(offsetof(MouseAbsoluteData, x) == 16); +static_assert(offsetof(MouseAbsoluteData, y) == 18); +static_assert(offsetof(MouseAbsoluteData, wheel) == 20); +static_assert(offsetof(MouseAbsoluteData, tilt) == 24); static_assert(sizeof(MouseData) == 40); static_assert(alignof(MouseData) == 8); static_assert(offsetof(MouseData, timestamp) == 0); diff --git a/core/libs/prx/libSceMouse/src/mouse_impl.cpp b/core/libs/prx/libSceMouse/src/mouse_impl.cpp index b4df78375a..ac1b19e9d8 100644 --- a/core/libs/prx/libSceMouse/src/mouse_impl.cpp +++ b/core/libs/prx/libSceMouse/src/mouse_impl.cpp @@ -3,6 +3,8 @@ #include #include +#include +#include #include namespace { @@ -14,6 +16,74 @@ bool initialized = false; bool opened = false; bool connected = true; std::uint32_t buttons = 0; +constexpr std::array SCREEN_WIDTHS{1280, 1920, 2560, 3840}; +constexpr std::array SCREEN_HEIGHTS{720, 1080, 1440, 2160}; +constexpr std::array CURVE_SPEEDS{0.0f, 0.4300079345703125f, 1.25f, 3.8600006103515625f, 40.0f}; +constexpr std::array CURVE_MOVES{0.0f, 1.0702667236328125f, 4.140625f, 18.984375f, 443.75f}; +struct AbsoluteSample { + std::uint64_t timestamp; + std::uint32_t buttons; + std::int32_t x; + std::int32_t y; + std::int32_t wheel; + std::int32_t tilt; + bool connected; +}; + +std::array absoluteHistory{}; +int absoluteHead = 0; +int absoluteCount = 0; +std::uint32_t screenSize = 1; +std::int64_t absoluteX = 0; +std::int64_t absoluteY = 0; +std::uint8_t highPrecision = 0; +float fractionX = 0.0f; +float fractionY = 0.0f; + +float curve(float speed) { + std::size_t segment = 1; + while (segment + 1 < CURVE_SPEEDS.size() && speed > CURVE_SPEEDS[segment]) ++segment; + const float lowSpeed = CURVE_SPEEDS[segment - 1]; + const float lowMove = CURVE_MOVES[segment - 1]; + return lowMove + (speed - lowSpeed) * (CURVE_MOVES[segment] - lowMove) / (CURVE_SPEEDS[segment] - lowSpeed); +} + +std::int64_t step(float& fraction, float delta, float scale) { + const float total = delta * scale + fraction; + const float whole = std::trunc(total); + fraction = total - whole; + return static_cast(whole); +} + +void moveAbsolute(std::int32_t dx, std::int32_t dy) { + std::int64_t moveX = dx; + std::int64_t moveY = dy; + if (highPrecision != 0) { + moveX = 0; + moveY = 0; + const float length = std::sqrt(static_cast(dx) * static_cast(dx) + static_cast(dy) * static_cast(dy)); + if (length != 0.0f) { + const float scale = curve(length * 120.0f / 420.0f) * 96.0f / 120.0f / length; + moveX = step(fractionX, static_cast(dx), scale); + moveY = step(fractionY, static_cast(dy), scale); + } + } + absoluteX = std::clamp(absoluteX + moveX, 0, SCREEN_WIDTHS[screenSize] - 1); + absoluteY = std::clamp(absoluteY + moveY, 0, SCREEN_HEIGHTS[screenSize] - 1); +} + +void enqueueAbsolute(const AbsoluteSample& sample) { + if (absoluteCount == MOUSE_MAX_DATA_NUM) { + absoluteHead = (absoluteHead + 1) % MOUSE_MAX_DATA_NUM; + --absoluteCount; + } + absoluteHistory[(absoluteHead + absoluteCount) % MOUSE_MAX_DATA_NUM] = sample; + ++absoluteCount; +} + +bool sharesRecord(const AbsoluteSample& first, const AbsoluteSample& second) { + return first.connected && second.connected && first.buttons == second.buttons; +} void enqueue(const MouseData& data) { if (count == MOUSE_MAX_DATA_NUM) { @@ -23,6 +93,24 @@ void enqueue(const MouseData& data) { history[(head + count) % MOUSE_MAX_DATA_NUM] = data; ++count; } + +bool motionOnly(const MouseData& data) { + return data.wheel == 0 && data.tilt == 0 && (data.x_axis != 0 || data.y_axis != 0); +} + +std::int32_t saturatedSum(std::int32_t a, std::int32_t b) { + return static_cast(std::clamp(static_cast(a) + b, INT32_MIN, INT32_MAX)); +} + +bool mergeMotion(const MouseData& data) { + if (count == 0 || !motionOnly(data)) return false; + MouseData& tail = history[(head + count - 1) % MOUSE_MAX_DATA_NUM]; + if (!motionOnly(tail) || tail.buttons != data.buttons || tail.connected != data.connected) return false; + tail.timestamp = data.timestamp; + tail.x_axis = saturatedSum(tail.x_axis, data.x_axis); + tail.y_axis = saturatedSum(tail.y_axis, data.y_axis); + return true; +} } namespace Mouse { @@ -50,10 +138,19 @@ int Open(int userId, std::int32_t type, std::int32_t index, const MouseOpenParam buttons = 0; head = 0; count = 0; + screenSize = 1; + absoluteX = 0; + absoluteY = 0; + highPrecision = 0; + fractionX = 0.0f; + fractionY = 0.0f; MouseData initial{}; initial.timestamp = sceKernelGetProcessTime(); initial.connected = connected; enqueue(initial); + absoluteHead = 0; + absoluteCount = 0; + enqueueAbsolute(AbsoluteSample{initial.timestamp, 0, 0, 0, 0, 0, connected}); return MOUSE_HANDLE; } @@ -65,6 +162,8 @@ int Close(std::int32_t handle) { buttons = 0; head = 0; count = 0; + absoluteHead = 0; + absoluteCount = 0; return MOUSE_OK; } @@ -82,11 +181,87 @@ int Read(std::int32_t handle, MouseData* data, std::int32_t num) { return available; } +int SetAbsoluteVirtualScreenSize(std::int32_t handle, std::int32_t size) { + std::lock_guard lock(mouseMutex); + if (!initialized) return MOUSE_ERROR_NOT_INITIALIZED; + const auto next = static_cast(size); + if (next >= SCREEN_WIDTHS.size()) return MOUSE_ERROR_INVALID_ARG; + if (handle != MOUSE_HANDLE || !opened) return MOUSE_ERROR_INVALID_HANDLE; + absoluteX = absoluteX * SCREEN_WIDTHS[next] / SCREEN_WIDTHS[screenSize]; + absoluteY = absoluteY * SCREEN_HEIGHTS[next] / SCREEN_HEIGHTS[screenSize]; + screenSize = next; + return MOUSE_OK; +} + +int SetAbsoluteHighPrecisionMode(std::int32_t handle, std::int32_t mode) { + std::lock_guard lock(mouseMutex); + if (!initialized) return MOUSE_ERROR_NOT_INITIALIZED; + if (handle != MOUSE_HANDLE || !opened) return MOUSE_ERROR_INVALID_HANDLE; + highPrecision = static_cast(mode); + return MOUSE_OK; +} + +int ReadAbsolutePosition(std::int32_t handle, MouseAbsoluteData* data, std::int32_t num) { + std::lock_guard lock(mouseMutex); + if (!initialized) return MOUSE_ERROR_NOT_INITIALIZED; + if (data == nullptr || num <= 0 || num > MOUSE_MAX_DATA_NUM) return MOUSE_ERROR_INVALID_ARG; + if (handle != MOUSE_HANDLE || !opened) return MOUSE_ERROR_INVALID_HANDLE; + if (!connected) { + data[0] = MouseAbsoluteData{}; + return 1; + } + std::array samples{}; + std::array endsRecord{}; + const int pending = absoluteCount; + for (int i = 0; i < pending; ++i) { + samples[i] = absoluteHistory[(absoluteHead + i) % MOUSE_MAX_DATA_NUM]; + endsRecord[i] = true; + } + absoluteHead = 0; + absoluteCount = 0; + int records = pending; + for (int i = 0; i + 1 < pending && records > num; ++i) { + if (!sharesRecord(samples[i], samples[i + 1])) continue; + endsRecord[i] = false; + --records; + } + int first = pending; + for (int kept = 0; first > 0 && kept < num;) { + --first; + if (endsRecord[first]) ++kept; + while (first > 0 && !endsRecord[first - 1]) --first; + } + int written = 0; + std::int32_t wheel = 0; + std::int32_t tilt = 0; + for (int i = first; i < pending; ++i) { + const AbsoluteSample& sample = samples[i]; + if (!sample.connected) continue; + moveAbsolute(sample.x, sample.y); + wheel += sample.wheel; + tilt += sample.tilt; + if (!endsRecord[i]) continue; + MouseAbsoluteData& record = data[written++]; + record.timestamp = sample.timestamp; + record.connected = 1; + record.buttons = sample.buttons; + record.x = static_cast(absoluteX); + record.y = static_cast(absoluteY); + record.wheel = wheel; + record.tilt = tilt; + wheel = 0; + tilt = 0; + } + return written; +} + int ResetAbsolutePosition(std::int32_t handle, std::uint16_t x, std::uint16_t y) { std::lock_guard lock(mouseMutex); if (!initialized) return MOUSE_ERROR_NOT_INITIALIZED; if (handle != MOUSE_HANDLE || !opened) return MOUSE_ERROR_INVALID_HANDLE; - if (x >= 1920 || y >= 1080) return MOUSE_ERROR_INVALID_ARG; + if (x >= SCREEN_WIDTHS[screenSize] || y >= SCREEN_HEIGHTS[screenSize]) return MOUSE_ERROR_INVALID_ARG; + absoluteX = x; + absoluteY = y; return MOUSE_OK; } @@ -100,19 +275,26 @@ void Publish(const MouseInputEvent& event) { return; } if (!opened) return; + const bool countsGiven = event.countsX != 0 || event.countsY != 0; + const bool noReport = event.button == 0 && !event.resetButtons && !event.connectionChange && event.wheel == 0 && event.tilt == 0; + if (noReport && event.x == 0 && event.y == 0 && !countsGiven) return; if (event.resetButtons) buttons = 0; if (event.button != 0) { if (event.pressed) buttons |= event.button; else buttons &= ~event.button; } + const std::uint64_t timestamp = sceKernelGetProcessTime(); + enqueueAbsolute(AbsoluteSample{timestamp, buttons, countsGiven ? event.countsX : event.x, countsGiven ? event.countsY : event.y, event.wheel, event.tilt, connected}); + if (noReport && event.x == 0 && event.y == 0) return; MouseData data{}; - data.timestamp = sceKernelGetProcessTime(); + data.timestamp = timestamp; data.connected = connected; data.buttons = buttons; data.x_axis = event.x; data.y_axis = event.y; data.wheel = event.wheel; data.tilt = event.tilt; + if (event.button == 0 && !event.resetButtons && mergeMotion(data)) return; enqueue(data); } diff --git a/core/libs/prx/libSceMouse/tests/Mouse.cpp b/core/libs/prx/libSceMouse/tests/Mouse.cpp index 27c99cc622..16eb9d3477 100644 --- a/core/libs/prx/libSceMouse/tests/Mouse.cpp +++ b/core/libs/prx/libSceMouse/tests/Mouse.cpp @@ -5,7 +5,9 @@ #include "SDL_events.h" #include "SDL_mouse.h" +#include #include +#include extern "C" { bool MouseIsOpen_nid_no_patch(); @@ -14,6 +16,18 @@ bool MouseIsOpen_nid_no_patch(); static void Require(bool value) { if (!value) std::abort(); } +struct AbsoluteRecord { + std::uint64_t timestamp; + std::uint8_t connected; + std::uint8_t padding[3]; + std::uint32_t buttons; + std::uint16_t x; + std::uint16_t y; + std::int32_t wheel; + std::int32_t tilt; + std::uint8_t reserved[12]; +}; + int main() { MouseData data[64]{}; Require(!MouseIsOpen_nid_no_patch()); @@ -97,13 +111,87 @@ int main() { input.HandleEvent(event, 7); Require(sceMouseRead(MOUSE_HANDLE, data, 1) == 1 && data[0].connected); - for (int i = 0; i < 70; ++i) { + for (int i = 0; i < 300; ++i) { MouseInputEvent motion{}; - motion.x = i; + motion.x = -1; + motion.y = i % 2; MousePublishInput_nid_no_patch(motion); } + Require(sceMouseRead(MOUSE_HANDLE, data, 8) == 1); + Require(data[0].x_axis == -300 && data[0].y_axis == 150 && data[0].buttons == 0); + Require(sceMouseRead(MOUSE_HANDLE, data, 1) == 0); + + MouseInputEvent still{}; + MousePublishInput_nid_no_patch(still); + Require(sceMouseRead(MOUSE_HANDLE, data, 1) == 0); + for (int i = 0; i < 300; ++i) { + MouseInputEvent alternating{}; + alternating.x = i % 2; + MousePublishInput_nid_no_patch(alternating); + } + Require(sceMouseRead(MOUSE_HANDLE, data, 8) == 1); + Require(data[0].x_axis == 150 && data[0].y_axis == 0); + Require(sceMouseRead(MOUSE_HANDLE, data, 1) == 0); + + MouseInputEvent zero{}; + MouseInputEvent two{}; + two.x = 2; + MouseInputEvent three{}; + three.x = 3; + MouseInputEvent down{}; + down.button = 1; + down.pressed = true; + MouseInputEvent up{}; + up.button = 1; + for (const MouseInputEvent& report : {two, zero, down, zero, three, zero, up, zero}) MousePublishInput_nid_no_patch(report); + Require(sceMouseRead(MOUSE_HANDLE, data, 64) == 4); + Require(data[0].x_axis == 2 && data[0].buttons == 0); + Require(data[1].x_axis == 0 && data[1].buttons == 1); + Require(data[2].x_axis == 3 && data[2].buttons == 1); + Require(data[3].x_axis == 0 && data[3].buttons == 0); + + MouseInputEvent step{}; + step.x = 3; + MousePublishInput_nid_no_patch(step); + MousePublishInput_nid_no_patch(step); + MouseInputEvent press{}; + press.button = 1; + press.pressed = true; + MousePublishInput_nid_no_patch(press); + MousePublishInput_nid_no_patch(step); + MousePublishInput_nid_no_patch(step); + MouseInputEvent wheel{}; + wheel.wheel = 1; + MousePublishInput_nid_no_patch(wheel); + MousePublishInput_nid_no_patch(step); + MouseInputEvent release{}; + release.button = 1; + MousePublishInput_nid_no_patch(release); + MousePublishInput_nid_no_patch(step); + Require(sceMouseRead(MOUSE_HANDLE, data, 64) == 7); + Require(data[0].x_axis == 6 && data[0].buttons == 0); + Require(data[1].x_axis == 0 && data[1].buttons == 1); + Require(data[2].x_axis == 6 && data[2].buttons == 1); + Require(data[3].wheel == 1 && data[3].x_axis == 0 && data[3].buttons == 1); + Require(data[4].x_axis == 3 && data[4].wheel == 0 && data[4].buttons == 1); + Require(data[5].x_axis == 0 && data[5].buttons == 0); + Require(data[6].x_axis == 3 && data[6].buttons == 0); + Require(data[2].timestamp >= data[1].timestamp && data[6].timestamp >= data[5].timestamp); + + MouseInputEvent large{}; + large.x = INT32_MAX; + MousePublishInput_nid_no_patch(large); + MousePublishInput_nid_no_patch(large); + Require(sceMouseRead(MOUSE_HANDLE, data, 8) == 1 && data[0].x_axis == INT32_MAX); + + for (int i = 0; i < 70; ++i) { + MouseInputEvent click{}; + click.button = 1; + click.pressed = (i % 2) == 0; + MousePublishInput_nid_no_patch(click); + } Require(sceMouseRead(MOUSE_HANDLE, data, 64) == 64); - Require(data[0].x_axis == 6 && data[63].x_axis == 69); + Require(data[0].buttons == 1 && data[63].buttons == 0 && data[62].buttons == 1); Require(sceMouseRead(MOUSE_HANDLE, data, 1) == 0); Require(sceMouseClose(MOUSE_HANDLE) == MOUSE_OK); Require(!MouseIsOpen_nid_no_patch()); @@ -119,4 +207,22 @@ int main() { Require(sceMouseClose(MOUSE_HANDLE) == MOUSE_OK); Require(!MouseIsOpen_nid_no_patch()); Require(sceMouseResetAbsolutePosition(MOUSE_HANDLE, 959, 539) == MOUSE_ERROR_INVALID_HANDLE); + + using ReadAbsolute = int (APS5_VABI*)(std::int32_t, AbsoluteRecord*, std::int32_t); + const auto readAbsolute = reinterpret_cast(reinterpret_cast(&sceMouseReadAbsolutePosition)); + AbsoluteRecord absolute[MOUSE_MAX_DATA_NUM]{}; + Require(sceMouseOpen(1, 0, 0, nullptr) == MOUSE_HANDLE); + Require(sceMouseRead(MOUSE_HANDLE, data, 64) == 1); + Require(readAbsolute(MOUSE_HANDLE, absolute, MOUSE_MAX_DATA_NUM) == 1); + Require(sceMouseResetAbsolutePosition(MOUSE_HANDLE, 100, 100) == MOUSE_OK); + input.SetPresentation(3840, 2160, 1920, 1080); + event = {}; + event.type = SDL_MOUSEMOTION; + event.motion.windowID = 7; + event.motion.xrel = 10; + event.motion.yrel = 5; + input.HandleEvent(event, 7); + Require(sceMouseRead(MOUSE_HANDLE, data, 1) == 1 && data[0].x_axis == 20 && data[0].y_axis == 10); + Require(readAbsolute(MOUSE_HANDLE, absolute, 1) == 1 && absolute[0].x == 110 && absolute[0].y == 105); + Require(sceMouseClose(MOUSE_HANDLE) == MOUSE_OK); } diff --git a/core/libs/prx/libSceMouse/tests/MouseAbsolute.cpp b/core/libs/prx/libSceMouse/tests/MouseAbsolute.cpp new file mode 100644 index 0000000000..5659f4bf73 --- /dev/null +++ b/core/libs/prx/libSceMouse/tests/MouseAbsolute.cpp @@ -0,0 +1,193 @@ +#include "prx/libSceMouse/include/Api.hpp" +#include "prx/libSceMouse/include/MouseState.hpp" +#include "prx/libSceMouse/include/mouse_structs.h" + +#include +#include +#include +#include +#ifdef _WIN32 +#include +#else +#include +#endif + +namespace { + +struct AbsoluteRecord { + std::uint64_t timestamp; + std::uint8_t connected; + std::uint8_t padding[3]; + std::uint32_t buttons; + std::uint16_t x; + std::uint16_t y; + std::int32_t wheel; + std::int32_t tilt; + std::uint8_t reserved[12]; +}; +static_assert(sizeof(AbsoluteRecord) == 40); + +using SetValueFn = int (APS5_VABI*)(std::int32_t, std::int32_t); +using ReadAbsoluteFn = int (APS5_VABI*)(std::int32_t, AbsoluteRecord*, std::int32_t); + +template +Function Resolve(const char* name) { +#ifdef _WIN32 + void* symbol = reinterpret_cast(GetProcAddress(GetModuleHandleA("libSceMouse.prx"), name)); +#else + void* symbol = dlsym(RTLD_DEFAULT, name); +#endif + if (!symbol) { + std::fprintf(stderr, "%s is not exported by libSceMouse\n", name); + std::abort(); + } + return reinterpret_cast(symbol); +} + +void Require(bool value, const char* what) { + if (value) return; + std::fprintf(stderr, "mouse absolute position: %s\n", what); + std::abort(); +} + +void Motion(std::int32_t x, std::int32_t y) { + MouseInputEvent event{}; + event.x = x; + event.y = y; + MousePublishInput_nid_no_patch(event); +} + +void Button(std::uint32_t button, bool pressed) { + MouseInputEvent event{}; + event.button = button; + event.pressed = pressed; + MousePublishInput_nid_no_patch(event); +} + +} + +int main() { + const auto setSize = Resolve("sceMouseSetAbsoluteVirtualScreenSize"); + const auto setMode = Resolve("sceMouseSetAbsoluteHighPrecisionMode"); + const auto readAbsolute = Resolve("sceMouseReadAbsolutePosition"); + AbsoluteRecord records[64]; + + Require(setSize(MOUSE_HANDLE, 9) == MOUSE_ERROR_NOT_INITIALIZED, "the screen size before initialization did not return NOT_INITIALIZED first"); + Require(setMode(MOUSE_HANDLE, 1) == MOUSE_ERROR_NOT_INITIALIZED, "the precision mode before initialization did not return NOT_INITIALIZED"); + Require(readAbsolute(MOUSE_HANDLE, nullptr, 0) == MOUSE_ERROR_NOT_INITIALIZED, "a read before initialization did not return NOT_INITIALIZED first"); + Require(sceMouseInit() == MOUSE_OK, "initialization failed"); + Require(setSize(MOUSE_HANDLE, 4) == MOUSE_ERROR_INVALID_ARG && setSize(MOUSE_HANDLE, -1) == MOUSE_ERROR_INVALID_ARG, "a size past 3 was not refused before the handle"); + Require(setSize(MOUSE_HANDLE, 1) == MOUSE_ERROR_INVALID_HANDLE, "the screen size of a closed handle was set"); + Require(setMode(MOUSE_HANDLE, 1) == MOUSE_ERROR_INVALID_HANDLE, "the precision mode of a closed handle was set"); + Require(readAbsolute(MOUSE_HANDLE, nullptr, 1) == MOUSE_ERROR_INVALID_ARG && readAbsolute(MOUSE_HANDLE, records, 0) == MOUSE_ERROR_INVALID_ARG && readAbsolute(MOUSE_HANDLE, records, 65) == MOUSE_ERROR_INVALID_ARG, "a bad read was not refused before the handle"); + Require(readAbsolute(MOUSE_HANDLE, records, 1) == MOUSE_ERROR_INVALID_HANDLE, "a closed handle was read"); + Require(sceMouseResetAbsolutePosition(MOUSE_HANDLE, 5000, 0) == MOUSE_ERROR_INVALID_HANDLE, "a reset with a bad handle and a bad position did not return INVALID_HANDLE"); + + Require(sceMouseOpen(1, 0, 0, nullptr) == MOUSE_HANDLE, "open failed"); + std::memset(records, 0xA5, sizeof(records)); + Require(readAbsolute(MOUSE_HANDLE, records, 64) == 1, "the sample queued at open was not read"); + Require(records[0].connected == 1 && records[0].x == 0 && records[0].y == 0, "the position does not start at the top-left corner"); + Require(records[0].padding[0] == 0xA5 && records[0].padding[2] == 0xA5 && records[0].reserved[0] == 0xA5 && records[0].reserved[11] == 0xA5, "bytes the console leaves alone were written"); + Require(readAbsolute(MOUSE_HANDLE, records, 1) == 0, "a read with no samples returned a record"); + + Require(sceMouseResetAbsolutePosition(MOUSE_HANDLE, 1920, 0) == MOUSE_ERROR_INVALID_ARG, "x 1920 was taken at 1920x1080"); + Require(sceMouseResetAbsolutePosition(MOUSE_HANDLE, 1000, 500) == MOUSE_OK, "the reset failed"); + Require(setSize(MOUSE_HANDLE, 3) == MOUSE_OK, "the size 3840x2160 was refused"); + Button(1, true); + Require(readAbsolute(MOUSE_HANDLE, records, 1) == 1 && records[0].x == 2000 && records[0].y == 1000 && records[0].buttons == 1, "the position was not scaled to 3840x2160"); + Require(sceMouseResetAbsolutePosition(MOUSE_HANDLE, 3839, 2159) == MOUSE_OK, "the corner of 3840x2160 was refused"); + Require(sceMouseResetAbsolutePosition(MOUSE_HANDLE, 2000, 1000) == MOUSE_OK, "the reset at 3840x2160 failed"); + Require(setSize(MOUSE_HANDLE, 0) == MOUSE_OK, "the size 1280x720 was refused"); + Button(1, false); + Require(readAbsolute(MOUSE_HANDLE, records, 1) == 1 && records[0].x == 666 && records[0].y == 333 && records[0].buttons == 0, "the position was not scaled to 1280x720 with truncation"); + Require(sceMouseResetAbsolutePosition(MOUSE_HANDLE, 1280, 0) == MOUSE_ERROR_INVALID_ARG, "x 1280 was taken at 1280x720"); + + Require(setSize(MOUSE_HANDLE, 1) == MOUSE_OK && sceMouseResetAbsolutePosition(MOUSE_HANDLE, 100, 100) == MOUSE_OK, "returning to 1920x1080 failed"); + Motion(5, -3); + Require(readAbsolute(MOUSE_HANDLE, records, 1) == 1 && records[0].x == 105 && records[0].y == 97, "motion did not move the position one for one"); + Motion(-5000, 5000); + Require(readAbsolute(MOUSE_HANDLE, records, 1) == 1 && records[0].x == 0 && records[0].y == 1079, "the position was not clamped to the screen"); + MouseInputEvent wheel{}; + wheel.wheel = 2; + wheel.tilt = -1; + MousePublishInput_nid_no_patch(wheel); + Require(readAbsolute(MOUSE_HANDLE, records, 1) == 1 && records[0].wheel == 2 && records[0].tilt == -1, "the wheel and tilt were not copied"); + Require(sceMouseResetAbsolutePosition(MOUSE_HANDLE, 100, 100) == MOUSE_OK, "the reset before the merge checks failed"); + Motion(1, 0); + Motion(2, 0); + Motion(4, 0); + Require(readAbsolute(MOUSE_HANDLE, records, 3) == 3 && records[0].x == 101 && records[1].x == 103 && records[2].x == 107, "three samples read with num 3 did not give one record each"); + Motion(1, 0); + Motion(2, 0); + Motion(4, 0); + Require(readAbsolute(MOUSE_HANDLE, records, 1) == 1 && records[0].x == 114, "three samples read with num 1 did not share one record with all their motion"); + + Require(setMode(MOUSE_HANDLE, 0x100) == MOUSE_OK && sceMouseResetAbsolutePosition(MOUSE_HANDLE, 100, 100) == MOUSE_OK, "the precision mode was refused"); + Motion(1, 0); + Require(readAbsolute(MOUSE_HANDLE, records, 1) == 1 && records[0].x == 101, "mode 0x100 did not keep its low byte 0, which is off"); + Require(setMode(MOUSE_HANDLE, 1) == MOUSE_OK && sceMouseResetAbsolutePosition(MOUSE_HANDLE, 100, 100) == MOUSE_OK, "turning high precision on failed"); + Motion(1, 0); + Require(readAbsolute(MOUSE_HANDLE, records, 1) == 1 && records[0].x == 100, "a delta of 1 moved a whole pixel in high precision"); + Motion(1, 0); + Require(readAbsolute(MOUSE_HANDLE, records, 1) == 1 && records[0].x == 101, "the kept fraction was not added to the next delta"); + Motion(10, 0); + Require(readAbsolute(MOUSE_HANDLE, records, 1) == 1 && records[0].x == 111, "a delta of 10 did not follow the curve"); + Motion(100, 0); + Require(readAbsolute(MOUSE_HANDLE, records, 1) == 1 && records[0].x == 359 && records[0].y == 100, "a delta of 100 did not follow the curve"); + + MouseData relative[64]; + const auto reopen = [&](const char* what) { + Require(sceMouseClose(MOUSE_HANDLE) == MOUSE_OK && sceMouseOpen(1, 0, 0, nullptr) == MOUSE_HANDLE, what); + Require(readAbsolute(MOUSE_HANDLE, records, 64) == 1 && sceMouseRead(MOUSE_HANDLE, relative, 64) == 1, what); + }; + reopen("reopening for the queued precision check failed"); + Require(setMode(MOUSE_HANDLE, 1) == MOUSE_OK && sceMouseResetAbsolutePosition(MOUSE_HANDLE, 100, 100) == MOUSE_OK, "turning high precision on failed"); + for (int i = 0; i < 10; ++i) Motion(1, 0); + Require(readAbsolute(MOUSE_HANDLE, records, 1) == 1 && records[0].x == 105, "ten queued deltas of 1 did not each go through the curve"); + reopen("reopening for the incremental precision check failed"); + Require(setMode(MOUSE_HANDLE, 1) == MOUSE_OK && sceMouseResetAbsolutePosition(MOUSE_HANDLE, 100, 100) == MOUSE_OK, "turning high precision on failed"); + for (int i = 0; i < 10; ++i) { + Motion(1, 0); + Require(readAbsolute(MOUSE_HANDLE, records, 1) == 1, "a delta read on its own gave no record"); + } + Require(records[0].x == 105, "ten deltas of 1 read one at a time did not end where the queued ones did"); + + Require(setMode(MOUSE_HANDLE, 0) == MOUSE_OK && sceMouseResetAbsolutePosition(MOUSE_HANDLE, 0, 100) == MOUSE_OK, "the edge reset failed"); + Motion(-5, 0); + Motion(5, 0); + Require(readAbsolute(MOUSE_HANDLE, records, 1) == 1 && records[0].x == 5, "queued motion at the left edge was not clamped per sample"); + Require(sceMouseResetAbsolutePosition(MOUSE_HANDLE, 0, 100) == MOUSE_OK, "the edge reset failed"); + Motion(-5, 0); + Require(readAbsolute(MOUSE_HANDLE, records, 1) == 1 && records[0].x == 0, "motion past the left edge was not clamped"); + Motion(5, 0); + Require(readAbsolute(MOUSE_HANDLE, records, 1) == 1 && records[0].x == 5, "motion read one at a time at the edge did not end where the queued motion did"); + + Require(sceMouseResetAbsolutePosition(MOUSE_HANDLE, 100, 100) == MOUSE_OK, "the reset before the size check failed"); + Motion(10, 0); + Require(setSize(MOUSE_HANDLE, 3) == MOUSE_OK && readAbsolute(MOUSE_HANDLE, records, 1) == 1 && records[0].x == 210 && records[0].y == 200, "a queued sample was not applied at the size in force when it was read"); + Require(setSize(MOUSE_HANDLE, 1) == MOUSE_OK && sceMouseResetAbsolutePosition(MOUSE_HANDLE, 100, 100) == MOUSE_OK, "returning to 1920x1080 failed"); + Motion(10, 0); + Require(sceMouseResetAbsolutePosition(MOUSE_HANDLE, 50, 50) == MOUSE_OK && readAbsolute(MOUSE_HANDLE, records, 1) == 1 && records[0].x == 60 && records[0].y == 50, "a queued sample did not move the position a later reset set"); + + Require(sceMouseRead(MOUSE_HANDLE, relative, 64) >= 0, "draining sceMouseRead failed"); + Motion(7, 0); + Require(sceMouseRead(MOUSE_HANDLE, relative, 1) == 1 && relative[0].x_axis == 7, "sceMouseRead did not get the motion"); + Require(readAbsolute(MOUSE_HANDLE, records, 1) == 1 && records[0].x == 67, "a relative read took the sample from the absolute reads"); + Motion(3, 0); + Require(readAbsolute(MOUSE_HANDLE, records, 1) == 1 && records[0].x == 70, "the absolute read missed the motion"); + Require(sceMouseRead(MOUSE_HANDLE, relative, 1) == 1 && relative[0].x_axis == 3, "an absolute read took the sample from sceMouseRead"); + + MouseInputEvent disconnect{}; + disconnect.connectionChange = true; + disconnect.connected = false; + MousePublishInput_nid_no_patch(disconnect); + std::memset(records, 0xA5, sizeof(records)); + Require(readAbsolute(MOUSE_HANDLE, records, 4) == 1, "a disconnected mouse did not return one record"); + const AbsoluteRecord zero{}; + Require(std::memcmp(&records[0], &zero, sizeof(zero)) == 0 && records[1].connected == 0xA5, "a disconnected mouse did not return one zeroed record"); + + Require(sceMouseClose(MOUSE_HANDLE) == MOUSE_OK, "close failed"); + Require(readAbsolute(MOUSE_HANDLE, records, 1) == MOUSE_ERROR_INVALID_HANDLE, "a closed handle was read"); + std::puts("mouse absolute position tests passed"); + return 0; +} diff --git a/core/libs/prx/libSceMsgDialog.native/Export.cpp b/core/libs/prx/libSceMsgDialog.native/Export.cpp index 71826e1fca..4b015f3115 100644 --- a/core/libs/prx/libSceMsgDialog.native/Export.cpp +++ b/core/libs/prx/libSceMsgDialog.native/Export.cpp @@ -55,9 +55,10 @@ int APS5_VABI sceMsgDialogGetResult(MsgDialogResult* result) { if (status == 0) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; if (result == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; if (status != COMMON_DIALOG_STATUS_FINISHED) return COMMON_DIALOG_ERROR_NOT_FINISHED; - *result = MsgDialogResult{}; - result->result = COMMON_DIALOG_RESULT_OK; - result->button_id = g_buttonId.load(); + MsgDialogResult value{}; + value.result = COMMON_DIALOG_RESULT_OK; + value.button_id = g_buttonId.load(); + if (!GuestCopyOut_nid_no_patch(result, &value, sizeof(value))) return COMMON_DIALOG_ERROR_ARG_NULL; return 0; } diff --git a/core/libs/prx/libSceNKWebKit/CMakeLists.txt b/core/libs/prx/libSceNKWebKit/CMakeLists.txt index 9bad4ef4d0..bc7cef34db 100644 --- a/core/libs/prx/libSceNKWebKit/CMakeLists.txt +++ b/core/libs/prx/libSceNKWebKit/CMakeLists.txt @@ -55,6 +55,10 @@ if(BUILD_TESTING) target_link_libraries(nkwebkit_${testName}_tests PRIVATE libSceNKWebKit libSceJsc libc) configure_windows_unwind(nkwebkit_${testName}_tests) add_test(NAME nkwebkit_${testName} COMMAND nkwebkit_${testName}_tests) + if(ANYPS5_ENABLE_ASAN AND testName STREQUAL "wtf") + set_tests_properties(nkwebkit_${testName} PROPERTIES ENVIRONMENT_MODIFICATION + "ASAN_OPTIONS=path_list_append:allocator_may_return_null=1") + endif() if(WIN32) set_tests_properties(nkwebkit_${testName} PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") diff --git a/core/libs/prx/libSceNKWebKit/Export.cpp b/core/libs/prx/libSceNKWebKit/Export.cpp index 3fefb6614b..41b73e0b27 100644 --- a/core/libs/prx/libSceNKWebKit/Export.cpp +++ b/core/libs/prx/libSceNKWebKit/Export.cpp @@ -18764,10 +18764,6 @@ int APS5_VABI _ZN3WTF12isMainThreadEv() { NotImplemented_nid_no_patch(__func__); } -int APS5_VABI _ZN3WTF13FastBitVector10clearRangeEmm() { - NotImplemented_nid_no_patch(__func__); -} - int APS5_VABI _ZN3WTF13MetaAllocator17addFreshFreeSpaceEPvm() { NotImplemented_nid_no_patch(__func__); } @@ -19408,10 +19404,6 @@ int APS5_VABI _ZN3WTF19isCompilationThreadEv() { NotImplemented_nid_no_patch(__func__); } -int APS5_VABI _ZN3WTF19mimeTypeFromDataURLENS_10StringViewE() { - NotImplemented_nid_no_patch(__func__); -} - int APS5_VABI _ZN3WTF19numGraphemeClustersENS_10StringViewE() { NotImplemented_nid_no_patch(__func__); } @@ -19584,14 +19576,6 @@ int APS5_VABI _ZN3WTF22CrossThreadTaskHandlerD2Ev() { NotImplemented_nid_no_patch(__func__); } -int APS5_VABI _ZN3WTF22FastBitVectorWordOwner10resizeSlowEm() { - NotImplemented_nid_no_patch(__func__); -} - -int APS5_VABI _ZN3WTF22FastBitVectorWordOwner13setEqualsSlowERKS0_() { - NotImplemented_nid_no_patch(__func__); -} - int APS5_VABI _ZN3WTF22TextBreakIteratorCache9singletonEv() { NotImplemented_nid_no_patch(__func__); } @@ -36584,10 +36568,6 @@ int APS5_VABI _ZNK3WTF3URL24strippedForUseAsReferrerEv() { NotImplemented_nid_no_patch(__func__); } -int APS5_VABI _ZNK3WTF3URL30stringCenterEllipsizedToLengthEj() { - NotImplemented_nid_no_patch(__func__); -} - int APS5_VABI _ZNK3WTF3URL4dumpERNS_11PrintStreamE() { NotImplemented_nid_no_patch(__func__); } @@ -39288,10 +39268,6 @@ int APS5_VABI _ZNK7WebCore20RenderBoxModelObject18inlineContinuationEv() { NotImplemented_nid_no_patch(__func__); } -int APS5_VABI _ZNK7WebCore20ResourceResponseBase11httpVersionEv() { - NotImplemented_nid_no_patch(__func__); -} - int APS5_VABI _ZNK7WebCore20ResourceResponseBase12isAttachmentEv() { NotImplemented_nid_no_patch(__func__); } @@ -39308,34 +39284,14 @@ int APS5_VABI _ZNK7WebCore20ResourceResponseBase14httpStatusCodeEv() { NotImplemented_nid_no_patch(__func__); } -int APS5_VABI _ZNK7WebCore20ResourceResponseBase14httpStatusTextEv() { - NotImplemented_nid_no_patch(__func__); -} - -int APS5_VABI _ZNK7WebCore20ResourceResponseBase14isInHTTPFamilyEv() { - NotImplemented_nid_no_patch(__func__); -} - int APS5_VABI _ZNK7WebCore20ResourceResponseBase15httpHeaderFieldENS_14HTTPHeaderNameE() { NotImplemented_nid_no_patch(__func__); } -int APS5_VABI _ZNK7WebCore20ResourceResponseBase16httpHeaderFieldsEv() { - NotImplemented_nid_no_patch(__func__); -} - -int APS5_VABI _ZNK7WebCore20ResourceResponseBase16textEncodingNameEv() { - NotImplemented_nid_no_patch(__func__); -} - int APS5_VABI _ZNK7WebCore20ResourceResponseBase17suggestedFilenameEv() { NotImplemented_nid_no_patch(__func__); } -int APS5_VABI _ZNK7WebCore20ResourceResponseBase21expectedContentLengthEv() { - NotImplemented_nid_no_patch(__func__); -} - int APS5_VABI _ZNK7WebCore20ResourceResponseBase23hasCacheValidatorFieldsEv() { NotImplemented_nid_no_patch(__func__); } @@ -39360,10 +39316,6 @@ int APS5_VABI _ZNK7WebCore20ResourceResponseBase34cacheControlContainsMustRevali NotImplemented_nid_no_patch(__func__); } -int APS5_VABI _ZNK7WebCore20ResourceResponseBase3urlEv() { - NotImplemented_nid_no_patch(__func__); -} - int APS5_VABI _ZNK7WebCore20ResourceResponseBase6sourceEv() { NotImplemented_nid_no_patch(__func__); } @@ -39376,10 +39328,6 @@ int APS5_VABI _ZNK7WebCore20ResourceResponseBase8lazyInitENS0_9InitLevelE() { NotImplemented_nid_no_patch(__func__); } -int APS5_VABI _ZNK7WebCore20ResourceResponseBase8mimeTypeEv() { - NotImplemented_nid_no_patch(__func__); -} - int APS5_VABI _ZNK7WebCore20ScrollingCoordinator36coordinatesScrollingForOverflowLayerERKNS_11RenderLayerE() { NotImplemented_nid_no_patch(__func__); } diff --git a/core/libs/prx/libSceNKWebKit/include/InventoryExports.hpp b/core/libs/prx/libSceNKWebKit/include/InventoryExports.hpp index 91b4326336..b80ce7d366 100644 --- a/core/libs/prx/libSceNKWebKit/include/InventoryExports.hpp +++ b/core/libs/prx/libSceNKWebKit/include/InventoryExports.hpp @@ -1647,7 +1647,6 @@ int APS5_VABI _ZN3WTF11setNonBlockEi(); int APS5_VABI _ZN3WTF12CountingLock10unlockSlowEv(); int APS5_VABI _ZN3WTF12CountingLock8lockSlowEv(); int APS5_VABI _ZN3WTF12isMainThreadEv(); -int APS5_VABI _ZN3WTF13FastBitVector10clearRangeEmm(); int APS5_VABI _ZN3WTF13MetaAllocator17addFreshFreeSpaceEPvm(); int APS5_VABI _ZN3WTF13MetaAllocator17currentStatisticsERKNS_6LockerINS_4LockEEE(); int APS5_VABI _ZN3WTF13MetaAllocator17freeFreeSpaceNodeEPNS0_13FreeSpaceNodeE(); @@ -1821,7 +1820,6 @@ int APS5_VABI _ZN3WTF19ParallelEnvironment7executeEPv(); int APS5_VABI _ZN3WTF19ParallelEnvironmentC1EPFvPvEmi(); int APS5_VABI _ZN3WTF19ParallelEnvironmentC2EPFvPvEmi(); int APS5_VABI _ZN3WTF19isCompilationThreadEv(); -int APS5_VABI _ZN3WTF19mimeTypeFromDataURLENS_10StringViewE(); int APS5_VABI _ZN3WTF19numGraphemeClustersENS_10StringViewE(); int APS5_VABI _ZN3WTF20ConcurrentPtrHashSet15deleteOldTablesEv(); int APS5_VABI _ZN3WTF20ConcurrentPtrHashSet5clearEv(); @@ -1865,8 +1863,6 @@ int APS5_VABI _ZN3WTF22CrossThreadTaskHandlerC2EPKcNS0_25AutodrainedPoolForRunLo int APS5_VABI _ZN3WTF22CrossThreadTaskHandlerD0Ev(); int APS5_VABI _ZN3WTF22CrossThreadTaskHandlerD1Ev(); int APS5_VABI _ZN3WTF22CrossThreadTaskHandlerD2Ev(); -int APS5_VABI _ZN3WTF22FastBitVectorWordOwner10resizeSlowEm(); -int APS5_VABI _ZN3WTF22FastBitVectorWordOwner13setEqualsSlowERKS0_(); int APS5_VABI _ZN3WTF22TextBreakIteratorCache9singletonEv(); int APS5_VABI _ZN3WTF22isMainThreadOrGCThreadEv(); int APS5_VABI _ZN3WTF23callOnMainThreadAndWaitEONS_8FunctionIFvvEEE(); @@ -6140,7 +6136,6 @@ int APS5_VABI _ZNK3WTF3URL17lastPathComponentEv(); int APS5_VABI _ZNK3WTF3URL19protocolHostAndPortEv(); int APS5_VABI _ZNK3WTF3URL21truncatedForUseAsBaseEv(); int APS5_VABI _ZNK3WTF3URL24strippedForUseAsReferrerEv(); -int APS5_VABI _ZNK3WTF3URL30stringCenterEllipsizedToLengthEj(); int APS5_VABI _ZNK3WTF3URL4dumpERNS_11PrintStreamE(); int APS5_VABI _ZNK3WTF3URL4userEv(); int APS5_VABI _ZNK3WTF3URL8passwordEv(); @@ -6825,29 +6820,21 @@ int APS5_VABI _ZNK7WebCore20LowPowerModeNotifier21isLowPowerModeEnabledEv(); int APS5_VABI _ZNK7WebCore20PasteboardCustomData18createSharedBufferEv(); int APS5_VABI _ZNK7WebCore20PasteboardWriterData7isEmptyEv(); int APS5_VABI _ZNK7WebCore20RenderBoxModelObject18inlineContinuationEv(); -int APS5_VABI _ZNK7WebCore20ResourceResponseBase11httpVersionEv(); int APS5_VABI _ZNK7WebCore20ResourceResponseBase12isAttachmentEv(); int APS5_VABI _ZNK7WebCore20ResourceResponseBase12isSuccessfulEv(); int APS5_VABI _ZNK7WebCore20ResourceResponseBase13isRedirectionEv(); int APS5_VABI _ZNK7WebCore20ResourceResponseBase14httpStatusCodeEv(); -int APS5_VABI _ZNK7WebCore20ResourceResponseBase14httpStatusTextEv(); -int APS5_VABI _ZNK7WebCore20ResourceResponseBase14isInHTTPFamilyEv(); int APS5_VABI _ZNK7WebCore20ResourceResponseBase15httpHeaderFieldENS_14HTTPHeaderNameE(); -int APS5_VABI _ZNK7WebCore20ResourceResponseBase16httpHeaderFieldsEv(); -int APS5_VABI _ZNK7WebCore20ResourceResponseBase16textEncodingNameEv(); int APS5_VABI _ZNK7WebCore20ResourceResponseBase17suggestedFilenameEv(); -int APS5_VABI _ZNK7WebCore20ResourceResponseBase21expectedContentLengthEv(); int APS5_VABI _ZNK7WebCore20ResourceResponseBase23hasCacheValidatorFieldsEv(); int APS5_VABI _ZNK7WebCore20ResourceResponseBase24isAttachmentWithFilenameEv(); int APS5_VABI _ZNK7WebCore20ResourceResponseBase27cacheControlContainsNoCacheEv(); int APS5_VABI _ZNK7WebCore20ResourceResponseBase27cacheControlContainsNoStoreEv(); int APS5_VABI _ZNK7WebCore20ResourceResponseBase29cacheControlContainsImmutableEv(); int APS5_VABI _ZNK7WebCore20ResourceResponseBase34cacheControlContainsMustRevalidateEv(); -int APS5_VABI _ZNK7WebCore20ResourceResponseBase3urlEv(); int APS5_VABI _ZNK7WebCore20ResourceResponseBase6sourceEv(); int APS5_VABI _ZNK7WebCore20ResourceResponseBase8isHTTP09Ev(); int APS5_VABI _ZNK7WebCore20ResourceResponseBase8lazyInitENS0_9InitLevelE(); -int APS5_VABI _ZNK7WebCore20ResourceResponseBase8mimeTypeEv(); int APS5_VABI _ZNK7WebCore20ScrollingCoordinator25scrollableContainerNodeIDERKNS_12RenderObjectE(); int APS5_VABI _ZNK7WebCore20ScrollingCoordinator36coordinatesScrollingForOverflowLayerERKNS_11RenderLayerE(); int APS5_VABI _ZNK7WebCore21BackForwardController12forwardCountEv(); diff --git a/core/libs/prx/libSceNKWebKit/include/ResourceResponseExports.hpp b/core/libs/prx/libSceNKWebKit/include/ResourceResponseExports.hpp new file mode 100644 index 0000000000..2356592e09 --- /dev/null +++ b/core/libs/prx/libSceNKWebKit/include/ResourceResponseExports.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include "SceTypes.hpp" +#include "prx/libSceNKWebKit/src/ResourceResponseAbi.hpp" + +extern "C" { + +const NKWebKit::WtfAbi::URL& APS5_VABI _ZNK7WebCore20ResourceResponseBase3urlEv(const NKWebKit::WebCoreAbi::ResourceResponseBase* self); +const NKWebKit::WebCoreAbi::StringSlot& APS5_VABI _ZNK7WebCore20ResourceResponseBase8mimeTypeEv(const NKWebKit::WebCoreAbi::ResourceResponseBase* self); +long long APS5_VABI _ZNK7WebCore20ResourceResponseBase21expectedContentLengthEv(const NKWebKit::WebCoreAbi::ResourceResponseBase* self); +const NKWebKit::WebCoreAbi::StringSlot& APS5_VABI _ZNK7WebCore20ResourceResponseBase16textEncodingNameEv(const NKWebKit::WebCoreAbi::ResourceResponseBase* self); +const NKWebKit::WebCoreAbi::StringSlot& APS5_VABI _ZNK7WebCore20ResourceResponseBase14httpStatusTextEv(const NKWebKit::WebCoreAbi::ResourceResponseBase* self); +const NKWebKit::WebCoreAbi::StringSlot& APS5_VABI _ZNK7WebCore20ResourceResponseBase11httpVersionEv(const NKWebKit::WebCoreAbi::ResourceResponseBase* self); +const NKWebKit::WebCoreAbi::ResponseHeaderStorage& APS5_VABI _ZNK7WebCore20ResourceResponseBase16httpHeaderFieldsEv(const NKWebKit::WebCoreAbi::ResourceResponseBase* self); +bool APS5_VABI _ZNK7WebCore20ResourceResponseBase14isInHTTPFamilyEv(const NKWebKit::WebCoreAbi::ResourceResponseBase* self); + +} diff --git a/core/libs/prx/libSceNKWebKit/include/WtfExports.hpp b/core/libs/prx/libSceNKWebKit/include/WtfExports.hpp index 148aaddf81..63c9dd727e 100644 --- a/core/libs/prx/libSceNKWebKit/include/WtfExports.hpp +++ b/core/libs/prx/libSceNKWebKit/include/WtfExports.hpp @@ -208,6 +208,8 @@ bool APS5_VABI _ZNK3WTF24TimeWithDynamicClockTypegeERKS0_(const WTF::TimeWithDyn bool APS5_VABI _ZNK3WTF24TimeWithDynamicClockTypegtERKS0_(const WTF::TimeWithDynamicClockType* self, const WTF::TimeWithDynamicClockType& other); bool APS5_VABI _ZNK3WTF24TimeWithDynamicClockTypeleERKS0_(const WTF::TimeWithDynamicClockType* self, const WTF::TimeWithDynamicClockType& other); WTF::Seconds APS5_VABI _ZNK3WTF24TimeWithDynamicClockTypemiERKS0_(const WTF::TimeWithDynamicClockType* self, const WTF::TimeWithDynamicClockType& other); +NKWebKit::WtfAbi::String APS5_VABI _ZN3WTF19mimeTypeFromDataURLENS_10StringViewE(NKWebKit::WtfAbi::StringView dataURL); +NKWebKit::WtfAbi::String APS5_VABI _ZNK3WTF3URL30stringCenterEllipsizedToLengthEj(const NKWebKit::WtfAbi::URL* self, unsigned length); bool APS5_VABI _ZNK3WTF3URL10protocolIsENS_10StringViewE(const NKWebKit::WtfAbi::URL* self, NKWebKit::WtfAbi::StringView protocol); NKWebKit::WtfAbi::StringView APS5_VABI _ZNK3WTF3URL11encodedUserEv(const NKWebKit::WtfAbi::URL* self); bool APS5_VABI _ZNK3WTF3URL12isAboutBlankEv(const NKWebKit::WtfAbi::URL* self); diff --git a/core/libs/prx/libSceNKWebKit/include/WtfFastBitVector.hpp b/core/libs/prx/libSceNKWebKit/include/WtfFastBitVector.hpp new file mode 100644 index 0000000000..53852bc137 --- /dev/null +++ b/core/libs/prx/libSceNKWebKit/include/WtfFastBitVector.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include +#include + +namespace WTF { + +struct FastBitVectorWordOwner { + std::uint32_t* words = nullptr; + std::size_t numBits = 0; +}; + +struct FastBitVector { + FastBitVectorWordOwner storage; +}; + +static_assert(std::is_standard_layout_v); +static_assert(sizeof(FastBitVectorWordOwner) == 16); +static_assert(alignof(FastBitVectorWordOwner) == 8); +static_assert(offsetof(FastBitVectorWordOwner, words) == 0); +static_assert(offsetof(FastBitVectorWordOwner, numBits) == 8); +static_assert(std::is_standard_layout_v); +static_assert(sizeof(FastBitVector) == 16); +static_assert(alignof(FastBitVector) == 8); +static_assert(offsetof(FastBitVector, storage) == 0); + +} + +extern "C" { + +void APS5_VABI _ZN3WTF13FastBitVector10clearRangeEmm(WTF::FastBitVector* vector, std::size_t begin, std::size_t end); +void APS5_VABI _ZN3WTF22FastBitVectorWordOwner10resizeSlowEm(WTF::FastBitVectorWordOwner* owner, std::size_t numBits); +void APS5_VABI _ZN3WTF22FastBitVectorWordOwner13setEqualsSlowERKS0_(WTF::FastBitVectorWordOwner* owner, + const WTF::FastBitVectorWordOwner* other); + +} diff --git a/core/libs/prx/libSceNKWebKit/libSceNKWebKit.hpp b/core/libs/prx/libSceNKWebKit/libSceNKWebKit.hpp index 2f12efa557..6a7a41629d 100644 --- a/core/libs/prx/libSceNKWebKit/libSceNKWebKit.hpp +++ b/core/libs/prx/libSceNKWebKit/libSceNKWebKit.hpp @@ -4,8 +4,10 @@ #include "prx/libSceNKWebKit/include/Api.hpp" #include "prx/libSceNKWebKit/include/InventoryExports.hpp" #include "prx/libSceNKWebKit/include/JscExports.hpp" +#include "prx/libSceNKWebKit/include/ResourceResponseExports.hpp" #include "prx/libSceNKWebKit/include/WebCoreExports.hpp" #include "prx/libSceNKWebKit/include/WebKitConfigExports.hpp" #include "prx/libSceNKWebKit/include/WebKitValueExports.hpp" #include "prx/libSceNKWebKit/include/WtfExports.hpp" +#include "prx/libSceNKWebKit/include/WtfFastBitVector.hpp" #include "prx/libSceNKWebKit/src/Preferences.hpp" diff --git a/core/libs/prx/libSceNKWebKit/src/ResourceResponse.cpp b/core/libs/prx/libSceNKWebKit/src/ResourceResponse.cpp new file mode 100644 index 0000000000..12d22b0058 --- /dev/null +++ b/core/libs/prx/libSceNKWebKit/src/ResourceResponse.cpp @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2006, 2008, 2016 Apple Inc. All rights reserved. + * Copyright (C) 2009 Google Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "prx/libSceNKWebKit/include/ResourceResponseExports.hpp" +#include "prx/libSceNKWebKit/include/WtfExports.hpp" + +#include + +namespace { + +using NKWebKit::WebCoreAbi::ResourceResponseBase; + +const ResourceResponseBase& Checked(const ResourceResponseBase* self) { + if (!self) throw std::invalid_argument("null WebCore::ResourceResponseBase"); + return *self; +} + +} + +extern "C" { + +const NKWebKit::WtfAbi::URL& APS5_VABI _ZNK7WebCore20ResourceResponseBase3urlEv(const ResourceResponseBase* self) { + return Checked(self).url; +} + +const NKWebKit::WebCoreAbi::StringSlot& APS5_VABI _ZNK7WebCore20ResourceResponseBase8mimeTypeEv(const ResourceResponseBase* self) { + return Checked(self).mimeType; +} + +long long APS5_VABI _ZNK7WebCore20ResourceResponseBase21expectedContentLengthEv(const ResourceResponseBase* self) { + return Checked(self).expectedContentLength; +} + +const NKWebKit::WebCoreAbi::StringSlot& APS5_VABI _ZNK7WebCore20ResourceResponseBase16textEncodingNameEv(const ResourceResponseBase* self) { + return Checked(self).textEncodingName; +} + +const NKWebKit::WebCoreAbi::StringSlot& APS5_VABI _ZNK7WebCore20ResourceResponseBase14httpStatusTextEv(const ResourceResponseBase* self) { + return Checked(self).httpStatusText; +} + +const NKWebKit::WebCoreAbi::StringSlot& APS5_VABI _ZNK7WebCore20ResourceResponseBase11httpVersionEv(const ResourceResponseBase* self) { + return Checked(self).httpVersion; +} + +const NKWebKit::WebCoreAbi::ResponseHeaderStorage& APS5_VABI _ZNK7WebCore20ResourceResponseBase16httpHeaderFieldsEv(const ResourceResponseBase* self) { + return Checked(self).httpHeaderFields; +} + +bool APS5_VABI _ZNK7WebCore20ResourceResponseBase14isInHTTPFamilyEv(const ResourceResponseBase* self) { + const auto& url = Checked(self).url; + if (!url.impl || !(url.bits & 1)) return false; + NKWebKit::WtfAbi::StringView view{url.impl->data, url.impl->length, !!(url.impl->hashAndFlags & 4)}; + return _ZN3WTF22protocolIsInHTTPFamilyENS_10StringViewE(view); +} + +} diff --git a/core/libs/prx/libSceNKWebKit/src/ResourceResponseAbi.hpp b/core/libs/prx/libSceNKWebKit/src/ResourceResponseAbi.hpp new file mode 100644 index 0000000000..3a67cf5c78 --- /dev/null +++ b/core/libs/prx/libSceNKWebKit/src/ResourceResponseAbi.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include "prx/libSceNKWebKit/src/WtfStringAbi.hpp" + +#include + +namespace NKWebKit::WebCoreAbi { + +struct StringSlot { + WtfAbi::StringImpl* impl; +}; + +struct ResponseHeaderStorage { + alignas(8) unsigned char storage[32]; +}; + +struct ResourceResponseBase { + WtfAbi::URL url; + StringSlot mimeType; + long long expectedContentLength; + StringSlot textEncodingName; + StringSlot httpStatusText; + StringSlot httpVersion; + ResponseHeaderStorage httpHeaderFields; +}; + +static_assert(sizeof(StringSlot) == 8); +static_assert(sizeof(ResponseHeaderStorage) == 32); +static_assert(offsetof(ResourceResponseBase, url) == 0); +static_assert(offsetof(ResourceResponseBase, mimeType) == 40); +static_assert(offsetof(ResourceResponseBase, expectedContentLength) == 48); +static_assert(offsetof(ResourceResponseBase, textEncodingName) == 56); +static_assert(offsetof(ResourceResponseBase, httpStatusText) == 64); +static_assert(offsetof(ResourceResponseBase, httpVersion) == 72); +static_assert(offsetof(ResourceResponseBase, httpHeaderFields) == 80); + +} diff --git a/core/libs/prx/libSceNKWebKit/src/WtfFastBitVector.cpp b/core/libs/prx/libSceNKWebKit/src/WtfFastBitVector.cpp new file mode 100644 index 0000000000..a3eb1e1b61 --- /dev/null +++ b/core/libs/prx/libSceNKWebKit/src/WtfFastBitVector.cpp @@ -0,0 +1,70 @@ +#include "prx/libSceNKWebKit/include/WtfFastBitVector.hpp" +#include "prx/libSceNKWebKit/include/WtfExports.hpp" + +#include +#include +#include +#include + +namespace { + +std::size_t WordCount(std::size_t bits) { + if (bits > std::numeric_limits::max() - 31) + throw std::length_error("FastBitVector: bit count overflows word rounding"); + return (bits + 31) / 32; +} + +std::size_t CheckedWordCount(const WTF::FastBitVectorWordOwner* owner) { + if (!owner) throw std::invalid_argument("FastBitVector: null word owner"); + const auto count = WordCount(owner->numBits); + if (count && !owner->words) throw std::invalid_argument("FastBitVector: null nonempty word storage"); + return count; +} + +} + +extern "C" void APS5_VABI _ZN3WTF13FastBitVector10clearRangeEmm(WTF::FastBitVector* vector, + std::size_t begin, std::size_t end) { + if (!vector) throw std::invalid_argument("FastBitVector::clearRange: null vector"); + auto& storage = vector->storage; + if (begin > end || end > storage.numBits) + throw std::out_of_range("FastBitVector::clearRange: invalid bit range"); + if (begin == end) return; + WordCount(storage.numBits); + if (!storage.words) throw std::invalid_argument("FastBitVector::clearRange: null words"); + const auto firstWord = begin / 32; + const auto lastWord = (end - 1) / 32; + const auto firstMask = std::numeric_limits::max() << (begin % 32); + const auto lastBits = end % 32; + const auto lastMask = lastBits ? (std::uint32_t{1} << lastBits) - 1 : std::numeric_limits::max(); + if (firstWord == lastWord) { + storage.words[firstWord] &= ~(firstMask & lastMask); + return; + } + storage.words[firstWord] &= ~firstMask; + std::fill_n(storage.words + firstWord + 1, lastWord - firstWord - 1, std::uint32_t{0}); + storage.words[lastWord] &= ~lastMask; +} + +extern "C" void APS5_VABI _ZN3WTF22FastBitVectorWordOwner10resizeSlowEm(WTF::FastBitVectorWordOwner* owner, + std::size_t numBits) { + const auto oldCount = CheckedWordCount(owner); + const auto newCount = WordCount(numBits); + if (newCount < oldCount) throw std::invalid_argument("FastBitVector::resizeSlow: shrinking word storage"); + auto* words = static_cast(_ZN3WTF10fastMallocEm(newCount * sizeof(std::uint32_t))); + if (oldCount) std::memcpy(words, owner->words, oldCount * sizeof(std::uint32_t)); + std::fill_n(words + oldCount, newCount - oldCount, std::uint32_t{0}); + _ZN3WTF8fastFreeEPv(owner->words); + owner->words = words; +} + +extern "C" void APS5_VABI _ZN3WTF22FastBitVectorWordOwner13setEqualsSlowERKS0_(WTF::FastBitVectorWordOwner* owner, + const WTF::FastBitVectorWordOwner* other) { + CheckedWordCount(owner); + const auto count = CheckedWordCount(other); + auto* words = static_cast(_ZN3WTF10fastMallocEm(count * sizeof(std::uint32_t))); + if (count) std::memcpy(words, other->words, count * sizeof(std::uint32_t)); + _ZN3WTF8fastFreeEPv(owner->words); + owner->words = words; + owner->numBits = other->numBits; +} diff --git a/core/libs/prx/libSceNKWebKit/src/WtfString.cpp b/core/libs/prx/libSceNKWebKit/src/WtfString.cpp index 11d3aad8c7..0ec203f32c 100644 --- a/core/libs/prx/libSceNKWebKit/src/WtfString.cpp +++ b/core/libs/prx/libSceNKWebKit/src/WtfString.cpp @@ -916,6 +916,41 @@ StringView ViewWithoutFragment(const URL& url) { return Slice(view, 0, std::min(url.queryEnd, view.length)); } +std::size_t FindUnit(StringView view, char16_t value, unsigned start) { + for (unsigned i = start; i < view.length; ++i) { + if (Unit(view, i) == value) return i; + } + return NotFound; +} + +StringView LeftView(StringView view, unsigned length) { + return Slice(view, 0, length); +} + +StringView RightView(StringView view, unsigned length) { + return Slice(view, view.length - length, length); +} + +String ConcatViews(const StringView* parts, unsigned count) { + std::size_t total = 0; + bool is8Bit = true; + for (unsigned i = 0; i < count; ++i) { + total += parts[i].length; + is8Bit &= parts[i].is8Bit; + } + if (total > MaxStringLength) throw std::length_error("WTF string exceeds maximum length"); + void* data; + auto* impl = NewImpl(static_cast(total), is8Bit, data); + unsigned offset = 0; + for (unsigned i = 0; i < count; ++i) { + for (unsigned j = 0; j < parts[i].length; ++j, ++offset) { + if (is8Bit) static_cast(data)[offset] = static_cast(Unit(parts[i], j)); + else static_cast(data)[offset] = Unit(parts[i], j); + } + } + return String(impl); +} + } // namespace extern "C" { @@ -1022,4 +1057,20 @@ bool APS5_VABI _ZN3WTF22protocolIsInHTTPFamilyENS_10StringViewE(StringView view) return Unit(view, 4) == ':' || ((Unit(view, 4) | 32) == 's' && view.length >= 6 && Unit(view, 5) == ':'); } +String APS5_VABI _ZN3WTF19mimeTypeFromDataURLENS_10StringViewE(StringView dataURL) { + auto index = FindUnit(dataURL, u';', 5); + if (index == NotFound) index = FindUnit(dataURL, u',', 5); + if (index == NotFound) return String(EmptyImpl()); + if (index == 5) return String(CopyImpl({"text/plain", 10, true})); + return String(ConvertCase(Slice(dataURL, 5, static_cast(index - 5)), true)); +} + +String APS5_VABI _ZNK3WTF3URL30stringCenterEllipsizedToLengthEj(const URL* self, unsigned length) { + const auto& url = Checked(self); + auto view = View(url.impl); + if (view.length <= length) return String(Retain(url.impl)); + const StringView parts[] = {LeftView(view, length / 2 - 1), {"...", 3, true}, RightView(view, length / 2 - 2)}; + return ConcatViews(parts, 3); +} + } diff --git a/core/libs/prx/libSceNKWebKit/tests/FastBitVector.cpp b/core/libs/prx/libSceNKWebKit/tests/FastBitVector.cpp new file mode 100644 index 0000000000..9fb4588aeb --- /dev/null +++ b/core/libs/prx/libSceNKWebKit/tests/FastBitVector.cpp @@ -0,0 +1,198 @@ +#include "prx/libSceNKWebKit/libSceNKWebKit.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void Require(bool condition, std::source_location location = std::source_location::current()) { + if (!condition) { + std::fprintf(stderr, "FastBitVector check failed at line %u\n", location.line()); + std::abort(); + } +} + +using ClearRange = void (APS5_VABI *)(WTF::FastBitVector*, std::size_t, std::size_t); +static_assert(std::is_same_v); +using Resize = void (APS5_VABI *)(WTF::FastBitVectorWordOwner*, std::size_t); +using Copy = void (APS5_VABI *)(WTF::FastBitVectorWordOwner*, const WTF::FastBitVectorWordOwner*); +static_assert(std::is_same_v); +static_assert(std::is_same_v); + +struct GuardedOwner { + std::uint64_t before = 0x123456789ABCDEF0; + WTF::FastBitVectorWordOwner owner; + std::uint64_t after = 0xFEDCBA9876543210; + + GuardedOwner(std::size_t bits) { + owner.numBits = bits; + const auto count = (bits + 31) / 32; + if (count) { + owner.words = static_cast(_ZN3WTF10fastMallocEm(count * sizeof(std::uint32_t))); + for (std::size_t i = 0; i < count; ++i) owner.words[i] = 0xA5A50000u + static_cast(i); + } + } + + ~GuardedOwner() { + _ZN3WTF8fastFreeEPv(owner.words); + } + + void CheckGuards() const { + Require(before == 0x123456789ABCDEF0 && after == 0xFEDCBA9876543210); + } +}; + +template +void RequireThrows(Function function) { + bool threw = false; + try { + function(); + } catch (const std::exception&) { + threw = true; + } + Require(threw); +} + +void TestClearRange() { + ClearRange clear = &_ZN3WTF13FastBitVector10clearRangeEmm; + std::mt19937 random(13117); + std::size_t cases = 0; + for (std::size_t bits = 0; bits <= 130; ++bits) { + const auto words = (bits + 31) / 32; + for (std::size_t begin = 0; begin <= bits; ++begin) { + for (std::size_t end = begin; end <= bits; ++end) { + std::vector guarded(words + 2); + for (auto& word : guarded) word = random(); + const auto before = guarded; + WTF::FastBitVector vector{{guarded.data() + 1, bits}}; + const auto original = vector; + clear(&vector, begin, end); + Require(vector.storage.words == original.storage.words && vector.storage.numBits == bits); + Require(guarded.front() == before.front() && guarded.back() == before.back()); + for (std::size_t bit = 0; bit < words * 32; ++bit) { + const auto mask = std::uint32_t{1} << (bit % 32); + const bool expected = bit >= begin && bit < end ? false : (before[bit / 32 + 1] & mask) != 0; + Require(((guarded[bit / 32 + 1] & mask) != 0) == expected); + } + ++cases; + } + } + } + WTF::FastBitVector empty; + clear(&empty, 0, 0); + RequireThrows([&] { clear(nullptr, 0, 0); }); + RequireThrows([&] { clear(&empty, 0, 1); }); + RequireThrows([&] { clear(&empty, 1, 0); }); + WTF::FastBitVector missing{{nullptr, 64}}; + RequireThrows([&] { clear(&missing, 0, 64); }); + std::array words{0x12345678, 0xABCDEF01}; + const auto before = words; + WTF::FastBitVector guarded{{words.data(), 63}}; + RequireThrows([&] { clear(&guarded, 0, 64); }); + RequireThrows([&] { clear(&guarded, 32, 31); }); + WTF::FastBitVector overflow{{words.data(), std::numeric_limits::max()}}; + RequireThrows([&] { clear(&overflow, 0, 1); }); + Require(words == before); + std::printf("FastBitVector clearRange: %zu exhaustive interval cases passed\n", cases); +} + +void TestResize() { + Resize resize = &_ZN3WTF22FastBitVectorWordOwner10resizeSlowEm; + std::size_t cases = 0; + for (std::size_t bits = 0; bits <= 130; ++bits) { + const auto oldCount = (bits + 31) / 32; + for (std::size_t newBits = 0; newBits <= 260; ++newBits) { + const auto newCount = (newBits + 31) / 32; + if (newCount < oldCount) continue; + GuardedOwner value(bits); + const auto oldWords = value.owner.words; + resize(&value.owner, newBits); + value.CheckGuards(); + Require(value.owner.numBits == bits && value.owner.words != nullptr); + Require(value.owner.words != oldWords); + Require(_ZN3WTF14fastMallocSizeEPKv(value.owner.words) == newCount * sizeof(std::uint32_t)); + for (std::size_t i = 0; i < oldCount; ++i) + Require(value.owner.words[i] == 0xA5A50000u + static_cast(i)); + for (std::size_t i = oldCount; i < newCount; ++i) Require(value.owner.words[i] == 0); + value.owner.numBits = newBits; + ++cases; + } + } + GuardedOwner value(65); + const auto original = value.owner; + const std::array contents{value.owner.words[0], value.owner.words[1], value.owner.words[2]}; + RequireThrows([&] { resize(nullptr, 0); }); + RequireThrows([&] { resize(&value.owner, 32); }); + RequireThrows([&] { resize(&value.owner, std::numeric_limits::max()); }); + Require(value.owner.words == original.words && value.owner.numBits == original.numBits); + Require(std::memcmp(value.owner.words, contents.data(), sizeof(contents)) == 0); + value.CheckGuards(); + WTF::FastBitVectorWordOwner missing{nullptr, 1}; + RequireThrows([&] { resize(&missing, 64); }); + Require(missing.words == nullptr && missing.numBits == 1); + std::printf("FastBitVector resizeSlow: %zu boundary and growth cases passed\n", cases); +} + +void TestCopy() { + Copy copy = &_ZN3WTF22FastBitVectorWordOwner13setEqualsSlowERKS0_; + std::size_t cases = 0; + for (std::size_t bits = 0; bits <= 130; ++bits) { + for (std::size_t oldBits = 0; oldBits <= 130; ++oldBits) { + GuardedOwner source(bits); + GuardedOwner target(oldBits); + const auto sourcePointer = source.owner.words; + copy(&target.owner, &source.owner); + source.CheckGuards(); + target.CheckGuards(); + Require(target.owner.numBits == bits && target.owner.words != nullptr); + Require(source.owner.words == sourcePointer && source.owner.numBits == bits); + Require(target.owner.words != source.owner.words); + const auto count = (bits + 31) / 32; + Require(_ZN3WTF14fastMallocSizeEPKv(target.owner.words) == count * sizeof(std::uint32_t)); + for (std::size_t i = 0; i < count; ++i) Require(target.owner.words[i] == source.owner.words[i]); + if (count) { + target.owner.words[count - 1] ^= 1; + Require(target.owner.words[count - 1] != source.owner.words[count - 1]); + } + ++cases; + } + GuardedOwner self(bits); + copy(&self.owner, &self.owner); + self.CheckGuards(); + Require(self.owner.numBits == bits); + for (std::size_t i = 0; i < (bits + 31) / 32; ++i) + Require(self.owner.words[i] == 0xA5A50000u + static_cast(i)); + } + GuardedOwner source(33); + GuardedOwner target(65); + const auto original = target.owner; + RequireThrows([&] { copy(nullptr, &source.owner); }); + RequireThrows([&] { copy(&target.owner, nullptr); }); + WTF::FastBitVectorWordOwner missing{nullptr, 1}; + RequireThrows([&] { copy(&target.owner, &missing); }); + WTF::FastBitVectorWordOwner overflow{nullptr, std::numeric_limits::max()}; + RequireThrows([&] { copy(&target.owner, &overflow); }); + Require(target.owner.words == original.words && target.owner.numBits == original.numBits); + for (std::size_t i = 0; i < 3; ++i) + Require(target.owner.words[i] == 0xA5A50000u + static_cast(i)); + target.CheckGuards(); + std::printf("FastBitVector setEqualsSlow: %zu copy cases and 131 self-copies passed\n", cases); +} + +} + +int main() { + TestClearRange(); + TestResize(); + TestCopy(); +} diff --git a/core/libs/prx/libSceNKWebKit/tests/ResourceResponse.cpp b/core/libs/prx/libSceNKWebKit/tests/ResourceResponse.cpp new file mode 100644 index 0000000000..7ba192daf9 --- /dev/null +++ b/core/libs/prx/libSceNKWebKit/tests/ResourceResponse.cpp @@ -0,0 +1,89 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libSceNKWebKit/libSceNKWebKit.hpp" + +#include +#include +#include +#include + +using NKWebKit::WebCoreAbi::ResourceResponseBase; +using NKWebKit::WtfAbi::StringImpl; + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "ResourceResponseBase check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +static StringImpl Make(const char* text) { + return StringImpl{2, static_cast(std::strlen(text)), text, 4}; +} + +static void TestFields() { + auto urlText = Make("http://a/"); + auto mime = Make("text/html"); + auto encoding = Make("utf-8"); + auto status = Make("OK"); + auto version = Make("HTTP/1.1"); + ResourceResponseBase response{}; + response.url.impl = &urlText; + response.url.bits = 1 | (4 << 6); + response.mimeType.impl = &mime; + response.expectedContentLength = 1234567890123LL; + response.textEncodingName.impl = &encoding; + response.httpStatusText.impl = &status; + response.httpVersion.impl = &version; + response.httpHeaderFields.storage[3] = 9; + + Require(&_ZNK7WebCore20ResourceResponseBase3urlEv(&response) == &response.url); + Require(&_ZNK7WebCore20ResourceResponseBase8mimeTypeEv(&response) == &response.mimeType); + Require(_ZNK7WebCore20ResourceResponseBase21expectedContentLengthEv(&response) == 1234567890123LL); + Require(&_ZNK7WebCore20ResourceResponseBase16textEncodingNameEv(&response) == &response.textEncodingName); + Require(&_ZNK7WebCore20ResourceResponseBase14httpStatusTextEv(&response) == &response.httpStatusText); + Require(&_ZNK7WebCore20ResourceResponseBase11httpVersionEv(&response) == &response.httpVersion); + Require(&_ZNK7WebCore20ResourceResponseBase16httpHeaderFieldsEv(&response) == &response.httpHeaderFields); + Require(_ZNK7WebCore20ResourceResponseBase16httpHeaderFieldsEv(&response).storage[3] == 9); + Require(urlText.refCount == 2 && mime.refCount == 2); + response.expectedContentLength = -1; + Require(_ZNK7WebCore20ResourceResponseBase21expectedContentLengthEv(&response) == -1); +} + +static void TestHTTPFamily() { + auto http = Make("http://a/"); + auto https = Make("HTTPS://a/"); + auto file = Make("file:///a"); + auto ftp = Make("ftp://a/"); + ResourceResponseBase response{}; + Require(!_ZNK7WebCore20ResourceResponseBase14isInHTTPFamilyEv(&response)); + response.url.impl = &http; + response.url.bits = 1 | (4 << 6); + Require(_ZNK7WebCore20ResourceResponseBase14isInHTTPFamilyEv(&response)); + response.url.impl = &https; + Require(_ZNK7WebCore20ResourceResponseBase14isInHTTPFamilyEv(&response)); + response.url.impl = &file; + Require(!_ZNK7WebCore20ResourceResponseBase14isInHTTPFamilyEv(&response)); + response.url.impl = &ftp; + Require(!_ZNK7WebCore20ResourceResponseBase14isInHTTPFamilyEv(&response)); + response.url.impl = &http; + response.url.bits = 0; + Require(!_ZNK7WebCore20ResourceResponseBase14isInHTTPFamilyEv(&response)); +} + +static void TestNull() { + bool thrown = false; + try { + _ZNK7WebCore20ResourceResponseBase3urlEv(nullptr); + } catch (const std::invalid_argument&) { + thrown = true; + } + Require(thrown); +} + +int main() { + TestFields(); + TestHTTPFamily(); + TestNull(); + return 0; +} diff --git a/core/libs/prx/libSceNKWebKit/tests/Wtf.cpp b/core/libs/prx/libSceNKWebKit/tests/Wtf.cpp index a3b14714dd..8508298479 100644 --- a/core/libs/prx/libSceNKWebKit/tests/Wtf.cpp +++ b/core/libs/prx/libSceNKWebKit/tests/Wtf.cpp @@ -809,6 +809,80 @@ const auto JSRealloc = &_ZN3WTF14jsValueReallocEPvm; std::string ViewText(StringView view) { return Bytes(ViewUTF8(&view, 1)); } +const auto URLEllipsize = &_ZNK3WTF3URL30stringCenterEllipsizedToLengthEj; +const auto DataURLMime = &_ZN3WTF19mimeTypeFromDataURLENS_10StringViewE; + +bool Is8Bit(const String& value) { return value.impl && (value.impl->hashAndFlags & 4); } + +StringView ViewOf(const String& value) { + return {value.impl->data, value.impl->length, !!(value.impl->hashAndFlags & 4)}; +} + +StringView Literal(const char* text) { return {text, static_cast(std::strlen(text)), true}; } + +void TestURLStringHelpers() { + auto narrow = FromUTF8Z(U8("http://example.com/abcdefghij")); + URL narrowURL{narrow.impl, 0, 0, 0, 0, 0, 0, 0, 0}; + for (unsigned length : {29U, 30U, 40U, 0xFFFFFFFFU}) { + auto same = URLEllipsize(&narrowURL, length); + Check(same.impl == narrow.impl && narrow.impl->refCount == 4); + } + Check(narrow.impl->refCount == 2); + Text(URLEllipsize(&narrowURL, 28), "http://exampl...m/abcdefghij"); + Text(URLEllipsize(&narrowURL, 10), "http...hij"); + Text(URLEllipsize(&narrowURL, 11), "http...hij"); + Text(URLEllipsize(&narrowURL, 6), "ht...j"); + Text(URLEllipsize(&narrowURL, 5), "h..."); + Text(URLEllipsize(&narrowURL, 4), "h..."); + Text(URLEllipsize(&narrowURL, 3), "..."); + Text(URLEllipsize(&narrowURL, 2), "..."); + Text(URLEllipsize(&narrowURL, 1), "http://example.com/abcdefghij..."); + Text(URLEllipsize(&narrowURL, 0), "http://example.com/abcdefghij..."); + Check(Is8Bit(URLEllipsize(&narrowURL, 10)) && Is8Bit(URLEllipsize(&narrowURL, 3))); + Check(URLEllipsize(&narrowURL, 10).impl != narrow.impl); + + auto wide = Create16(u"http://wide.test/path", 21); + URL wideURL{wide.impl, 0, 0, 0, 0, 0, 0, 0, 0}; + Check(URLEllipsize(&wideURL, 21).impl == wide.impl); + auto wideShort = URLEllipsize(&wideURL, 10); + Text(wideShort, "http...ath"); + Check(!Is8Bit(wideShort)); + auto wideDots = URLEllipsize(&wideURL, 3); + Text(wideDots, "..."); + Check(!Is8Bit(wideDots)); + auto wideLong = URLEllipsize(&wideURL, 1); + Text(wideLong, "http://wide.test/path..."); + Check(!Is8Bit(wideLong)); + + URL absent{}; + Check(URLEllipsize(&absent, 0).impl == nullptr && URLEllipsize(&absent, 10).impl == nullptr); + auto empty = Create8(U8(""), 0); + URL emptyURL{empty.impl, 0, 0, 0, 0, 0, 0, 0, 0}; + Check(URLEllipsize(&emptyURL, 0).impl == empty.impl && URLEllipsize(&emptyURL, 5).impl == empty.impl); + Throws([&] { URLEllipsize(nullptr, 10); }); + + Text(DataURLMime(Literal("data:text/HTML;base64,x")), "text/html"); + Text(DataURLMime(Literal("data:Text/X,y")), "text/x"); + Text(DataURLMime(Literal("data:,x")), "text/plain"); + Text(DataURLMime(Literal("data:;base64,x")), "text/plain"); + Check(Is8Bit(DataURLMime(Literal("data:,x")))); + Text(DataURLMime(Literal("data:a,b;c")), "a,b"); + Text(DataURLMime(Literal("data:text/plain")), ""); + Text(DataURLMime(Literal("data:")), ""); + Text(DataURLMime(Literal("data")), ""); + Text(DataURLMime(Literal("")), ""); + Text(DataURLMime(StringView{nullptr, 0, true}), ""); + Text(DataURLMime(Literal("http://a;b")), "//a"); + auto wideData = Create16(u"data:TEXT/Plain;x", 17); + auto wideMime = DataURLMime(ViewOf(wideData)); + Text(wideMime, "text/plain"); + Check(!Is8Bit(wideMime)); + auto accented = Create16(u"data:É/X,", 9); + Text(DataURLMime(ViewOf(accented)), "\xC3\x89/x"); + auto widePlain = Create16(u"data:,x", 7); + Text(DataURLMime(ViewOf(widePlain)), "text/plain"); +} + void TestURLsAndReallocation() { struct PinnedBits { unsigned valid : 1; @@ -967,6 +1041,7 @@ void TestURLsAndReallocation() { memory = static_cast(JSRealloc(memory, 2)); Check(memory[0] == 'h' && memory[1] == 'e'); Check(!JSRealloc(memory, 0)); + TestURLStringHelpers(); } } // namespace Wtf2Tests diff --git a/core/libs/prx/libSceNet/CMakeLists.txt b/core/libs/prx/libSceNet/CMakeLists.txt index 54c026e47c..6076a98df3 100644 --- a/core/libs/prx/libSceNet/CMakeLists.txt +++ b/core/libs/prx/libSceNet/CMakeLists.txt @@ -124,6 +124,17 @@ if(BUILD_TESTING) set_tests_properties(guest_sce_net_keepalive PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_net_kernel_forwards_tests ${LIBS_INCLUDE_DIR}/tests/GuestNetKernelForwards.cpp) + target_include_directories(guest_net_kernel_forwards_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_net_kernel_forwards_tests PRIVATE libSceNet libkernel libc) + configure_windows_unwind(guest_net_kernel_forwards_tests) + add_test(NAME guest_sce_net_kernel_forwards COMMAND guest_net_kernel_forwards_tests) + set_tests_properties(guest_sce_net_kernel_forwards PROPERTIES TIMEOUT 20) + if(WIN32) + target_link_libraries(guest_net_kernel_forwards_tests PRIVATE ws2_32) + set_tests_properties(guest_sce_net_kernel_forwards PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_datagram_receive_tests ${LIBS_INCLUDE_DIR}/tests/GuestDatagramReceive.cpp) target_include_directories(guest_datagram_receive_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_datagram_receive_tests PRIVATE libSceNet libkernel libc) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index faecc58faa..a7f0cd1ed3 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -928,6 +928,63 @@ bool GetSocketOptionForKernel(int descriptor, int level, int option, void* value const bool g_socketOptionGetterRegistered = (KernelSetSocketOptionGetter_nid_no_patch(&GetSocketOptionForKernel), true); +int FamilyOfSocket(int descriptor) { + std::lock_guard lk(g_mutex); + const auto it = g_socks.find(descriptor); + return it == g_socks.end() ? 0 : it->second.family; +} + +bool IsStreamSocket(int descriptor) { + std::lock_guard lk(g_mutex); + const auto it = g_socks.find(descriptor); + return it != g_socks.end() && it->second.type == NET_SOCK_STREAM; +} + +int ErrorOf(std::int64_t result) { + return result < 0 ? *errno_slot() : 0; +} + +constexpr int KernelWaitAll = 0x40; + +std::int64_t ReceiveForKernel(int descriptor, void* buffer, std::uint64_t length, int flags, void* address, std::uint32_t* addressLength) { + const int netFlags = flags & ~KernelWaitAll; + const auto size = static_cast(length); + if (length == 0 && !IsStreamSocket(descriptor)) { + KernelSocketPoll::Entry entry{descriptor, KernelSocketPoll::Readable, 0}; + if (PollSockets(&entry, 1, 0) == 0) return 0; + } + if (netFlags == flags || (flags & NET_MSG_PEEK) != 0 || !IsStreamSocket(descriptor)) return sceNetRecvfrom(descriptor, buffer, size, netFlags, address, addressLength); + size_t received = 0; + for (;;) { + const auto result = sceNetRecvfrom(descriptor, buffer == nullptr ? nullptr : static_cast(buffer) + received, size - received, netFlags, address, addressLength); + if (result < 0) return received > 0 ? static_cast(received) : result; + received += static_cast(result); + if (result == 0 || received == size) return static_cast(received); + } +} + +const KernelSocketPoll::Forwards g_socketForwards{ + FamilyOfSocket, + [](int descriptor, const void* address, std::uint32_t length) { + return ErrorOf(sceNetBind_nid_postfix(descriptor, address, length)); + }, + [](int descriptor, void* address, std::uint32_t* length) { + return ErrorOf(sceNetGetsockname(descriptor, address, length)); + }, + [](int descriptor, const void* buffer, std::uint64_t length, int flags, const void* address, std::uint32_t addressLength, int* error) { + const std::int64_t result = sceNetSendto(descriptor, buffer, static_cast(length), flags, address, addressLength); + *error = ErrorOf(result); + return result; + }, + [](int descriptor, void* buffer, std::uint64_t length, int flags, void* address, std::uint32_t* addressLength, int* error) { + const std::int64_t result = ReceiveForKernel(descriptor, buffer, length, flags, address, addressLength); + *error = ErrorOf(result); + return result; + }, +}; + +const bool g_socketForwardsRegistered = (KernelSetSocketForwards_nid_no_patch(&g_socketForwards), true); + constexpr int GuestFcntlGetStatus = 3; constexpr int GuestFcntlSetStatus = 4; constexpr int GuestReadWrite = 2; @@ -2420,7 +2477,7 @@ int APS5_VABI sceNetGetMacAddress(NetEtherAddr* addr, int flags) { } // Keep guest identity stable without exposing the host adapter address. const std::uint8_t mac[6] = {0x02, 0x50, 0x53, 0x35, 0x00, 0x01}; - std::memcpy(addr->data, mac, 6); + if (!GuestCopyOut_nid_no_patch(addr, mac, sizeof(mac))) return fail(NET_EFAULT); return 0; } @@ -2584,7 +2641,7 @@ int APS5_VABI sceNetResolverGetError(int rid, int* status) { std::lock_guard lk(g_mutex); const auto resolver = g_resolvers.find(rid); if (resolver == g_resolvers.end()) return fail(NET_EBADF); - *status = resolver->second.error; + if (!GuestCopyOut_nid_no_patch(status, &resolver->second.error, sizeof(*status))) return fail(NET_EFAULT); return 0; } @@ -2918,7 +2975,9 @@ int APS5_VABI sceNetIoctl(int socketId, std::uint32_t command, void* argument) { if (command != nonblockingRequest && command != readableRequest) return fail(NET_ENOTTY); if (!argument) return fail(NET_EFAULT); if (command == nonblockingRequest) { - const int enabled = *static_cast(argument) != 0; + int enable = 0; + if (!GuestCopyIn_nid_no_patch(&enable, argument, sizeof(enable))) return fail(NET_EFAULT); + const int enabled = enable != 0; return sceNetSetsockopt(socketId, NET_SOL_SOCKET, NET_SO_NBIO, &enabled, sizeof(enabled)); } #ifdef _WIN32 @@ -2934,7 +2993,8 @@ int APS5_VABI sceNetIoctl(int socketId, std::uint32_t command, void* argument) { int available = 0; if (::ioctl(native->value, FIONREAD, &available) != 0) return fail(native_error()); #endif - *static_cast(argument) = static_cast(available); + const int readable = static_cast(available); + if (!GuestCopyOut_nid_no_patch(argument, &readable, sizeof(readable))) return fail(NET_EFAULT); return 0; } diff --git a/core/libs/prx/libSceNgs2.native/CMakeLists.txt b/core/libs/prx/libSceNgs2.native/CMakeLists.txt index e752e849d2..61220b5529 100644 --- a/core/libs/prx/libSceNgs2.native/CMakeLists.txt +++ b/core/libs/prx/libSceNgs2.native/CMakeLists.txt @@ -22,12 +22,17 @@ if(BUILD_TESTING) target_include_directories(ngs2_${testName}_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/LibAtrac9/C/src) target_link_libraries(ngs2_${testName}_tests PRIVATE libSceNgs2.native atrac9 libkernel libc) configure_windows_unwind(ngs2_${testName}_tests) - add_test(NAME ngs2_${testName} COMMAND ngs2_${testName}_tests) + set(ngs2Arguments) + if(ngs2Test STREQUAL "HandleErrors") + set(ngs2Arguments $ $) + endif() + add_test(NAME ngs2_${testName} COMMAND ngs2_${testName}_tests ${ngs2Arguments}) if(WIN32) set_tests_properties(ngs2_${testName} PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endforeach() + target_link_libraries(ngs2_handleerrors_tests PRIVATE libSceNgs2) add_sce_ngs2_test(libSceNgs2.native ngs2_pcm Ngs2Pcm.cpp) add_test(NAME ngs2_detached_pending COMMAND ngs2_threads_tests detached-pending) set_tests_properties(ngs2_detached_pending PROPERTIES TIMEOUT 20) diff --git a/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp b/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp index 71c988fb9b..8b82aab270 100644 --- a/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp @@ -377,6 +377,29 @@ static int ParseAtrac9(const RiffChunks& chunks, Ngs2WaveformInfo& info) { return ParseAtrac9Loop(chunks, info); } +static void AddPcmBlock(Ngs2WaveformInfo& info, std::uint32_t begin, std::uint32_t samples, std::uint32_t repeats) { + auto& block = info.block[info.num_blocks++]; + block = {info.data_offset + static_cast(begin) * info.audio_frame_size, static_cast(samples) * info.audio_frame_size, repeats, 0, samples, 0, 0}; +} + +static int ParsePcmLoop(const RiffChunks& chunks, Ngs2WaveformInfo& info) { + if (chunks.sampler == nullptr) return SCE_NGS2_OK; + if (chunks.samplerSize < 32) throw std::runtime_error("NGS2: a smpl chunk too short for its loop count is not implemented"); + if (ReadLe32(chunks.sampler + 28) == 0) return SCE_NGS2_OK; + if (chunks.samplerSize < 52) throw std::runtime_error("NGS2: a smpl chunk too short for its first loop is not implemented"); + const std::uint32_t begin = ReadLe32(chunks.sampler + 44); + const std::uint32_t end = ReadLe32(chunks.sampler + 48) + 1u; + info.loop_begin_position = begin; + info.loop_end_position = end; + if (end <= begin) return SCE_NGS2_OK; + if (end > info.num_samples) throw std::runtime_error("NGS2: a waveform loop ending past the last sample is not implemented"); + info.num_blocks = 0; + if (begin != 0) AddPcmBlock(info, 0, begin, 0); + AddPcmBlock(info, begin, end - begin, UINT32_MAX); + if (end != info.num_samples) AddPcmBlock(info, end, info.num_samples - end, 0); + return SCE_NGS2_OK; +} + static std::uint32_t PcmWaveformType(std::uint32_t bits, bool floating) { if (floating) return bits == 32 ? 0x18 : bits == 64 ? 0x1a : 0; return bits == 8 ? 0x11 : bits == 16 ? SCE_NGS2_WAVEFORM_TYPE_PCM_I16L : bits == 24 ? 0x14 : bits == 32 ? 0x16 : 0; @@ -394,7 +417,6 @@ static int ParsePcm(const RiffChunks& chunks, std::size_t size, Ngs2WaveformInfo if (channels == 0 || channels > NGS2_MAX_CHANNELS || sampleRate == 0 || sampleRate > MAX_SAMPLE_RATE || ReadLe16(format + 12) != bytesPerFrame) { return SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT; } - if (chunks.sampler != nullptr && chunks.samplerSize >= 32 && ReadLe32(chunks.sampler + 28) != 0) throw std::runtime_error("NGS2: parsing looped waveforms is not implemented"); if (chunks.dataSize > size - chunks.dataOffset || chunks.dataSize % bytesPerFrame != 0) return SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA; const std::size_t dataSize = chunks.dataSize; info.format = {type, channels, sampleRate, 0, 0, 0}; @@ -410,7 +432,7 @@ static int ParsePcm(const RiffChunks& chunks, std::size_t size, Ngs2WaveformInfo info.block[0].data_offset = chunks.dataOffset; info.block[0].data_size = dataSize; info.block[0].num_samples = info.num_samples; - return SCE_NGS2_OK; + return ParsePcmLoop(chunks, info); } static int ParseWaveform(const RiffChunks& chunks, std::size_t size, Ngs2WaveformInfo& info) { diff --git a/core/libs/prx/libSceNgs2.native/src/Custom.cpp b/core/libs/prx/libSceNgs2.native/src/Custom.cpp index 4b1e6e7037..7bbc9b46e8 100644 --- a/core/libs/prx/libSceNgs2.native/src/Custom.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Custom.cpp @@ -7,6 +7,7 @@ #include #include +#include "prx/libc/include/Api.hpp" #include "prx/libc/include/General.hpp" #include "Ngs2Internal.hpp" @@ -308,7 +309,7 @@ void Ngs2ApplyCustomParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param) { if (index >= voice.customPitchShift.size() || voice.rack->customModuleIds[index] != SCE_NGS2_CUSTOM_MODULE_ID_PITCH_SHIFT) APS5_INVALID_ARG_EX; const auto& pitch = ParamAs(param); auto& state = voice.customPitchShift[index]; - state.ratio = std::exp2(static_cast(std::clamp(pitch.cents, -1200, 1200)) / 1200.0f); + state.ratio = exp2f_nid_postfix(static_cast(std::clamp(pitch.cents, -1200, 1200)) / 1200.0f); state.bypassMask = pitch.bypass_mask; state.changed = true; return; diff --git a/core/libs/prx/libSceNgs2.native/src/Voice.cpp b/core/libs/prx/libSceNgs2.native/src/Voice.cpp index 9fa9136c1e..9724006f00 100644 --- a/core/libs/prx/libSceNgs2.native/src/Voice.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Voice.cpp @@ -542,6 +542,36 @@ static int ControlUserFx2(Ngs2Voice& voice, std::uint32_t index, const Ngs2Custo static Ngs2Voice* PatchDest(const Ngs2Voice& voice, const Ngs2VoicePatchParam& patch); +static void CheckFilter(const Ngs2Voice& voice, const Ngs2SamplerVoiceFilterParam& param) { + if (param.header.size < sizeof(Ngs2SamplerVoiceFilterParam)) throw Ngs2ReturnCode{SCE_NGS2_ERROR_INVALID_VOICE_CONTROL_SIZE}; + if (param.index >= voice.rack->maxFilters) throw Ngs2ReturnCode{PS4_LIBRARY ? SCE_NGS2_ERROR_PS4_INVALID_FILTER_INDEX : SCE_NGS2_ERROR_INVALID_PARAM_ID}; + if (!PS4_LIBRARY && param.type >= 12) throw Ngs2ReturnCode{SCE_NGS2_ERROR_INVALID_FILTER_TYPE}; + if (PS4_LIBRARY ? param.location >= PS4_MAX_FILTER_LOCATIONS : param.location >= std::uint64_t{2} + voice.ports.size()) { + throw Ngs2ReturnCode{PS4_LIBRARY ? SCE_NGS2_ERROR_PS4_INVALID_FILTER_LOCATION : SCE_NGS2_ERROR_INVALID_MODULE_LOCATION}; + } +} + +static Ngs2SamplerVoiceFilterParam SamplerFilterParam(const Ngs2VoiceParamHeader& param) { + if (!PS4_LIBRARY) { + if (param.size < sizeof(Ngs2SamplerVoiceFilterParam)) throw Ngs2ReturnCode{SCE_NGS2_ERROR_INVALID_VOICE_CONTROL_SIZE}; + return reinterpret_cast(param); + } + if (param.size != sizeof(Ngs2Ps4SamplerVoiceFilterParam)) throw Ngs2ReturnCode{SCE_NGS2_ERROR_PS4_INVALID_VOICE_CONTROL_SIZE}; + const auto& ps4 = reinterpret_cast(param); + Ngs2SamplerVoiceFilterParam filter{}; + filter.header = {sizeof(filter), 0, param.id}; + filter.index = ps4.index; + filter.location = ps4.location; + filter.channel_mask = ps4.channel_mask; + filter.type = ps4.type; + filter.frequency = ps4.fcq.frequency; + filter.q = ps4.fcq.q; + filter.level = ps4.fcq.level; + filter.reserved[0] = ps4.fcq.reserved; + filter.reserved[1] = ps4.fcq.reserved2; + return filter; +} + static Ngs2PendingControl CaptureParamControl(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param) { if (param.id == SCE_NGS2_VOICE_PARAM_EVENT) { const auto event = ParamAs(param).event_id; @@ -614,6 +644,14 @@ static Ngs2PendingControl CaptureParamControl(Ngs2Voice& voice, const Ngs2VoiceP ParamAs(param); return CopyParam(param); } + if (param.id == SCE_NGS2_SAMPLER_VOICE_PARAM_FILTER) { + if (voice.rack->rackId == SCE_NGS2_RACK_ID_CUSTOM_SAMPLER) { + throw Ngs2ReturnCode{PS4_LIBRARY ? SCE_NGS2_ERROR_PS4_INVALID_VOICE_CONTROL_ID : SCE_NGS2_ERROR_INVALID_PARAM_ID}; + } + if (voice.rack->rackId != SCE_NGS2_RACK_ID_SAMPLER) throw std::invalid_argument("NGS2: voice param " + Ngs2Hex(param.id) + " belongs to another rack"); + CheckFilter(voice, SamplerFilterParam(param)); + return CopyParam(param); + } if (voice.rack->rackId == SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER && (param.id & ~0x1fu) == SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX) { const auto index = param.id & 0x1fu; if (index >= voice.customUserFx.size() || voice.rack->customModuleIds[index] != SCE_NGS2_CUSTOM_MODULE_ID_USER_FX) APS5_INVALID_ARG_EX; @@ -890,12 +928,7 @@ static void AddWaveformBlocks(Ngs2Voice& voice, const Ngs2SamplerVoiceWaveformBl } void Ngs2SetFilter(Ngs2Voice& voice, const Ngs2SamplerVoiceFilterParam& param) { - if (param.header.size < sizeof(Ngs2SamplerVoiceFilterParam)) throw Ngs2ReturnCode{SCE_NGS2_ERROR_INVALID_VOICE_CONTROL_SIZE}; - if (param.index >= voice.rack->maxFilters) throw Ngs2ReturnCode{PS4_LIBRARY ? SCE_NGS2_ERROR_PS4_INVALID_FILTER_INDEX : SCE_NGS2_ERROR_INVALID_PARAM_ID}; - if (!PS4_LIBRARY && param.type >= 12) throw Ngs2ReturnCode{SCE_NGS2_ERROR_INVALID_FILTER_TYPE}; - if (PS4_LIBRARY ? param.location >= PS4_MAX_FILTER_LOCATIONS : param.location >= std::uint64_t{2} + voice.ports.size()) { - throw Ngs2ReturnCode{PS4_LIBRARY ? SCE_NGS2_ERROR_PS4_INVALID_FILTER_LOCATION : SCE_NGS2_ERROR_INVALID_MODULE_LOCATION}; - } + CheckFilter(voice, param); if (voice.filters.size() <= param.index) voice.filters.resize(param.index + 1); auto& filter = voice.filters[param.index]; if (param.type == 0 || (PS4_LIBRARY && param.type >= PS4_FIRST_UNFILTERED_TYPE)) { @@ -961,26 +994,7 @@ static void ApplySamplerParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& para voice.pitch = std::clamp(ratio, 0.0f, 4.0f); break; } - case SCE_NGS2_SAMPLER_VOICE_PARAM_FILTER: - if (PS4_LIBRARY) { - if (param.size != sizeof(Ngs2Ps4SamplerVoiceFilterParam)) throw Ngs2ReturnCode{SCE_NGS2_ERROR_PS4_INVALID_VOICE_CONTROL_SIZE}; - const auto& ps4 = reinterpret_cast(param); - Ngs2SamplerVoiceFilterParam filter{}; - filter.header = {sizeof(filter), 0, param.id}; - filter.index = ps4.index; - filter.location = ps4.location; - filter.channel_mask = ps4.channel_mask; - filter.type = ps4.type; - filter.frequency = ps4.fcq.frequency; - filter.q = ps4.fcq.q; - filter.level = ps4.fcq.level; - filter.reserved[0] = ps4.fcq.reserved; - filter.reserved[1] = ps4.fcq.reserved2; - Ngs2SetFilter(voice, filter); - } else { - Ngs2SetFilter(voice, reinterpret_cast(param)); - } - break; + case SCE_NGS2_SAMPLER_VOICE_PARAM_FILTER: Ngs2SetFilter(voice, SamplerFilterParam(param)); break; case SCE_NGS2_SAMPLER_VOICE_PARAM_PEAK_METER: SetPeakMeter(voice, ParamAs(param).detect_type); break; default: throw std::runtime_error("NGS2: sampler voice param " + Ngs2Hex(param.id) + " is not implemented"); } diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2HandleErrors.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2HandleErrors.cpp index e0000ba149..8d94523fa5 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 "prx/libSceNgs2.native/include/Api.hpp" +#include "prx/libSceNgs2/include/Api.hpp" +#include "tests/ExportLookup.hpp" #include #include @@ -20,6 +22,45 @@ static void CheckExportLinked() { Require(linked != nullptr); } +static void CheckPendingExports(const char* path) { + struct Export { + int (APS5_VABI* function)(void); + const char* name; + }; + const Export exports[] = { + {sceNgs2ModuleArrayEnumItems, "sceNgs2ModuleArrayEnumItems"}, + {sceNgs2ModuleEnumConfigs, "sceNgs2ModuleEnumConfigs"}, + {sceNgs2ModuleQueueEnumItems, "sceNgs2ModuleQueueEnumItems"}, + {sceNgs2RackRunCommands, "sceNgs2RackRunCommands"}, + {sceNgs2StreamCreate, "sceNgs2StreamCreate"}, + {sceNgs2StreamCreateWithAllocator, "sceNgs2StreamCreateWithAllocator"}, + {sceNgs2StreamDestroy, "sceNgs2StreamDestroy"}, + {sceNgs2StreamQueryBufferSize, "sceNgs2StreamQueryBufferSize"}, + {sceNgs2StreamQueryInfo, "sceNgs2StreamQueryInfo"}, + {sceNgs2StreamResetOption, "sceNgs2StreamResetOption"}, + {sceNgs2StreamRunCommands, "sceNgs2StreamRunCommands"}, + {sceNgs2SystemQueryInfo, "sceNgs2SystemQueryInfo"}, + {sceNgs2SystemRunCommands, "sceNgs2SystemRunCommands"}, + }; + if (!ExportsResolveByName(path, exports, "sceNgs2NotExported")) { + throw std::runtime_error("the pending libSceNgs2.native exports do not all resolve by name"); + } +} + +static void CheckCorePendingExports(const char* path) { + struct Export { + void (*function)(void); + const char* name; + }; + const Export exports[] = { + {reinterpret_cast(&sceNgs2FftInit), "sceNgs2FftInit"}, + {reinterpret_cast(&sceNgs2FftProcess), "sceNgs2FftProcess"}, + }; + if (!ExportsResolveByName(path, exports, "sceNgs2NotExported")) { + throw std::runtime_error("the pending libSceNgs2 exports do not all resolve by name"); + } +} + 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; @@ -433,7 +474,7 @@ static void CheckEnumeration() { Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); } -int main() { +int main(int argc, char** argv) { try { constexpr int systemError = static_cast(0x804A8201u); constexpr int rackError = static_cast(0x804A8202u); @@ -476,6 +517,8 @@ int main() { CheckFftQuerySize(); CheckEnumeration(); CheckExportLinked(); + CheckPendingExports(argc > 1 ? argv[1] : nullptr); + CheckCorePendingExports(argc > 2 ? argv[2] : nullptr); 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/Ngs2JobBuffer.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2JobBuffer.cpp index f87b733051..75b1e1ab6f 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2JobBuffer.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2JobBuffer.cpp @@ -572,6 +572,69 @@ static void TestBufferedWaveformCodes() { ResetBuffers(firstBuffer); } +static Ngs2SamplerVoiceFilterParam LowPass(std::uint32_t index, std::uint32_t location, std::uint32_t type) { + Ngs2SamplerVoiceFilterParam param{}; + param.header = {sizeof(param), 0, SCE_NGS2_SAMPLER_VOICE_PARAM_FILTER}; + param.index = index; + param.location = location; + param.type = type; + param.frequency = 1000.0f; + param.q = 0.707f; + param.level = 1.0f; + return param; +} + +static float RenderEnergy(std::uintptr_t system) { + std::array samples{}; + const Ngs2RenderBufferInfo output{samples.data(), sizeof(samples), SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 1}; + Require(sceNgs2SystemRender(system, &output, 1) == SCE_NGS2_OK); + float energy = 0.0f; + for (const float sample : samples) energy += sample * sample; + return energy; +} + +static void TestBufferedSamplerFilter() { + const auto firstBuffer = usedBuffers; + std::array pcm{}; + for (std::size_t i = 0; i < pcm.size(); ++i) pcm[i] = i % 2 == 0 ? 8000 : -8000; + PeerAction peer; + const auto setup = CreatePeerSystem(peer); + const auto voice = MonoSampler(setup.system); + Require(AddBlocks(voice, pcm.data(), 0, {0, static_cast(sizeof(pcm)), 0, 0, static_cast(pcm.size()), 0, 0}) == SCE_NGS2_OK); + Patch(voice, setup.master); + Event(voice, SCE_NGS2_VOICE_EVENT_PLAY); + const auto direct = MonoSampler(setup.system); + const auto custom = Sampler(setup.system, true); + const auto badType = LowPass(0, 1, 12); + const auto badIndex = LowPass(8, 1, 1); + const auto badLocation = LowPass(0, 1000, 1); + const Ngs2VoiceParamHeader truncated{sizeof(Ngs2VoiceParamHeader), 0, SCE_NGS2_SAMPLER_VOICE_PARAM_FILTER}; + const auto filter = LowPass(0, 1, 1); + const std::array rejected{&badType.header, &badIndex.header, &badLocation.header, &truncated}; + std::array immediate{}; + for (std::size_t i = 0; i < rejected.size(); ++i) { + immediate[i] = sceNgs2VoiceControl(direct, rejected[i]); + Require(immediate[i] != SCE_NGS2_OK); + } + const int customImmediate = sceNgs2VoiceControl(custom, &filter.header); + Require(customImmediate != SCE_NGS2_OK); + unsigned checked = 0; + peer.action = [&](unsigned tick) { + if (tick != 0) return; + for (std::size_t i = 0; i < rejected.size(); ++i) Require(sceNgs2VoiceControl(voice, rejected[i]) == immediate[i]); + Require(sceNgs2VoiceControl(custom, &filter.header) == customImmediate); + Require(sceNgs2VoiceControl(voice, &filter.header) == SCE_NGS2_OK); + ++checked; + }; + const float unfiltered = RenderEnergy(setup.system); + Require(unfiltered > 1.0f); + float filtered = unfiltered; + for (unsigned grain = 0; grain < 6; ++grain) filtered = RenderEnergy(setup.system); + Require(checked == 1 && filtered < unfiltered * 1.0e-3f); + Require(sceNgs2SystemDestroy(setup.system, nullptr) == SCE_NGS2_OK); + ResetBuffers(firstBuffer); +} + static void TestBufferedAppendAfterDrain() { const auto firstBuffer = usedBuffers; std::array pcm{}; @@ -748,7 +811,7 @@ static void TestPortVolumeOver() { int main(int argc, char** argv) { if (argc > 1) { - const std::pair scenarios[] = {{"over", TestPortVolumeOver}, {"validation", TestCaptureValidation}, {"drained", TestBufferedAppendAfterDrain}, + const std::pair scenarios[] = {{"over", TestPortVolumeOver}, {"validation", TestCaptureValidation}, {"filter", TestBufferedSamplerFilter}, {"drained", TestBufferedAppendAfterDrain}, {"emptysetup", TestEmptySetupPort}, {"destroyed", TestPendingPatchDestroyedDest}, {"race", TestBlockQueueRace}}; for (const auto& [name, run] : scenarios) { if (std::strcmp(argv[1], name) != 0) continue; @@ -797,11 +860,12 @@ int main(int argc, char** argv) { ++cases; TestCaptureValidation(); TestBufferedWaveformCodes(); + TestBufferedSamplerFilter(); TestBufferedAppendAfterDrain(); TestEmptySetupPort(); TestPendingPatchDestroyedDest(); TestBlockQueueRace(); - cases += 6; + cases += 7; std::printf("NGS2 worker buffering: %u scenarios passed\n", cases + 1); return 0; } diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Parse.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Parse.cpp index 6e7e2d2694..53cba7e77e 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Parse.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Parse.cpp @@ -4,8 +4,12 @@ #include #include +#include +#include +#include #include #include +#include #include static void Put16(std::vector& out, std::uint32_t value) { @@ -108,6 +112,99 @@ static void TestParsePcm() { Require(sceNgs2ParseWaveformData(noChannels.data(), noChannels.size(), &info) == SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT); } +static void AppendLoops(std::vector& file, std::uint32_t loops, std::uint32_t type, std::uint32_t begin, std::uint32_t lastSample, std::uint32_t plays, + std::uint32_t fraction = 0) { + PutTag(file, "smpl"); + Put32(file, 36 + 24 * loops); + for (int i = 0; i < 7; ++i) Put32(file, 0); + Put32(file, loops); + Put32(file, 0); + for (std::uint32_t loop = 0; loop < loops; ++loop) { + Put32(file, loop); + Put32(file, type); + Put32(file, begin); + Put32(file, lastSample); + Put32(file, fraction); + Put32(file, plays); + } + const auto riffSize = static_cast(file.size() - 8); + std::memcpy(file.data() + 4, &riffSize, sizeof(riffSize)); +} + +static void RequireBlock(const Ngs2WaveformBlock& block, std::uint64_t offset, std::uint64_t size, std::uint32_t repeats, std::uint32_t samples) { + Require(block.data_offset == offset && block.data_size == size && block.num_repeats == repeats && block.num_skip_samples == 0 && block.num_samples == samples); +} + +static void AppendShortSampler(std::vector& file, std::uint32_t size, std::uint32_t loops) { + PutTag(file, "smpl"); + Put32(file, size); + for (std::uint32_t offset = 0; offset < size; offset += 4) Put32(file, offset == 28 ? loops : 0); + const auto riffSize = static_cast(file.size() - 8); + std::memcpy(file.data() + 4, &riffSize, sizeof(riffSize)); +} + +static Ngs2WaveformInfo ParseLooped(std::uint32_t loops, std::uint32_t type, std::uint32_t begin, std::uint32_t lastSample, std::uint32_t plays, std::uint32_t fraction = 0) { + auto file = PcmFile(1, 44100, 16, 100); + AppendLoops(file, loops, type, begin, lastSample, plays, fraction); + Ngs2WaveformInfo info{}; + Require(sceNgs2ParseWaveformData(file.data(), file.size(), &info) == SCE_NGS2_OK); + return info; +} + +static void TestParsePcmLoops() { + for (const std::uint32_t bits : {16u, 24u}) { + const std::uint32_t frame = 2 * bits / 8; + auto looped = PcmFile(2, 48000, bits, 100); + AppendLoops(looped, 1, 0, 10, 59, 0); + Ngs2WaveformInfo info{}; + Require(sceNgs2ParseWaveformData(looped.data(), looped.size(), &info) == SCE_NGS2_OK); + Require(info.num_samples == 100 && info.loop_begin_position == 10 && info.loop_end_position == 60 && info.num_blocks == 3); + RequireBlock(info.block[0], 44, 10 * frame, 0, 10); + RequireBlock(info.block[1], 44 + 10 * frame, 50 * frame, UINT32_MAX, 50); + RequireBlock(info.block[2], 44 + 60 * frame, 40 * frame, 0, 40); + } + + for (const auto& info : {ParseLooped(1, 0, 10, 59, 3), ParseLooped(2, 1, 10, 59, 0), ParseLooped(3, 2, 10, 59, 7, 0x80000000u)}) { + Require(info.loop_begin_position == 10 && info.loop_end_position == 60 && info.num_blocks == 3); + RequireBlock(info.block[0], 44, 20, 0, 10); + RequireBlock(info.block[1], 64, 100, UINT32_MAX, 50); + RequireBlock(info.block[2], 164, 80, 0, 40); + } + + auto info = ParseLooped(1, 0, 0, 99, 3); + Require(info.loop_begin_position == 0 && info.loop_end_position == 100 && info.num_blocks == 1); + RequireBlock(info.block[0], 44, 200, UINT32_MAX, 100); + + info = ParseLooped(1, 0, 0, 49, 0); + Require(info.loop_begin_position == 0 && info.loop_end_position == 50 && info.num_blocks == 2); + RequireBlock(info.block[0], 44, 100, UINT32_MAX, 50); + RequireBlock(info.block[1], 144, 100, 0, 50); + + info = ParseLooped(1, 0, 30, 99, 0); + Require(info.loop_begin_position == 30 && info.loop_end_position == 100 && info.num_blocks == 2); + RequireBlock(info.block[0], 44, 60, 0, 30); + RequireBlock(info.block[1], 104, 140, UINT32_MAX, 70); + + info = ParseLooped(1, 0, 50, 50, 0); + Require(info.loop_begin_position == 50 && info.loop_end_position == 51 && info.num_blocks == 3); + RequireBlock(info.block[1], 144, 2, UINT32_MAX, 1); + RequireBlock(info.block[2], 146, 98, 0, 49); + + for (const auto [begin, lastSample, end] : {std::tuple{50u, 49u, 50u}, {50u, 10u, 11u}, {20u, 0xffffffffu, 0u}}) { + info = ParseLooped(1, 0, begin, lastSample, 0); + Require(info.loop_begin_position == begin && info.loop_end_position == end && info.num_blocks == 1); + RequireBlock(info.block[0], 44, 200, 0, 100); + } + + info = ParseLooped(0, 0, 10, 59, 0); + Require(info.num_blocks == 1 && info.loop_begin_position == 0 && info.loop_end_position == 0); + RequireBlock(info.block[0], 44, 200, 0, 100); + + auto emptySampler = PcmFile(1, 44100, 16, 100); + AppendShortSampler(emptySampler, 36, 0); + Require(sceNgs2ParseWaveformData(emptySampler.data(), emptySampler.size(), &info) == SCE_NGS2_OK && info.num_blocks == 1); +} + static void TestParseUnknownFormats() { const std::uint8_t pcmGuid[16] = {0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71}; const std::uint8_t unknownGuid[16] = {0xaa, 0xbb, 0xcc, 0xdd, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc}; @@ -270,6 +367,7 @@ static void TestParseUser() { int main() { TestParsePcm(); + TestParsePcmLoops(); TestParseUser(); TestParseUnknownFormats(); TestParseFile(); diff --git a/core/libs/prx/libSceNpCommon/CMakeLists.txt b/core/libs/prx/libSceNpCommon/CMakeLists.txt index b5896f2365..00aaab61bb 100644 --- a/core/libs/prx/libSceNpCommon/CMakeLists.txt +++ b/core/libs/prx/libSceNpCommon/CMakeLists.txt @@ -3,6 +3,19 @@ if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp") target_sources(libSceNpCommon PRIVATE Export.cpp) endif() target_sources(libSceNpCommon PRIVATE Json.cpp Runtime.cpp Strings.cpp) +if(BUILD_TESTING) + add_test_executable(guest_np_integer_radices_tests ${LIBS_INCLUDE_DIR}/tests/GuestNpIntegerRadices.cpp) + target_include_directories(guest_np_integer_radices_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_np_integer_radices_tests PRIVATE libSceNpCommon libc) + configure_windows_unwind(guest_np_integer_radices_tests) + add_test(NAME guest_np_integer_radices COMMAND guest_np_integer_radices_tests) + set_tests_properties(guest_np_integer_radices PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_np_integer_radices PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + target_include_directories(libSceNpCommon PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(libSceNpCommon PRIVATE libc libkernel libSceRtc libSceUserService) configure_windows_unwind(libSceNpCommon) diff --git a/core/libs/prx/libSceNpCommon/Export.cpp b/core/libs/prx/libSceNpCommon/Export.cpp index d32ce8ed7c..d26cc3bec1 100644 --- a/core/libs/prx/libSceNpCommon/Export.cpp +++ b/core/libs/prx/libSceNpCommon/Export.cpp @@ -165,6 +165,7 @@ int APS5_VABI sceNpGetSystemClockUsec(std::int64_t* usec) { } void APS5_VABI sceNpGetSdkVersion(char* version) { + std::memset(version, 0, 8); SceKernelSystemSwVersion swVersion{}; swVersion.size = sizeof(swVersion); sceKernelGetSystemSwVersion_nid_postfix(&swVersion); diff --git a/core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp b/core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp index 141809fafa..84627b0063 100644 --- a/core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp +++ b/core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp @@ -11,21 +11,11 @@ int APS5_VABI _ZN3sce2np10CancelableC2Ev(void) { return 0; } -int APS5_VABI sceNpUtilXmlEscape(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI _ZN3sce2np4CondD0Ev(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI _sceNpAllocatorExRealloc(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI _ZN3sce2np8WorkItem6CancelEi(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -81,11 +71,6 @@ int APS5_VABI _ZN3sce2np10CancelLockC2Ev(void) { return 0; } -int APS5_VABI _ZN3sce2np13NpTitleSecretC2ERK16SceNpTitleSecret(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI _ZN3sce2np8WorkItem10SetRunningEv(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -216,11 +201,6 @@ int APS5_VABI _ZN3sce2np9Semaphore6CreateEiiPKc(void) { return 0; } -int APS5_VABI sceNpUtilXmlUnescape(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpXmlParse(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -406,11 +386,6 @@ int APS5_VABI _ZThn16_N3sce2np10MemoryFileD1Ev(void) { return 0; } -int APS5_VABI _sceNpAllocatorExFree(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI _ZN3sce2np3ipc17ServiceIpmiClient7ConnectEPKvm(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -431,11 +406,6 @@ int APS5_VABI _ZN3sce2np9HttpTrans21GetResponseStatusCodeEPNS0_6HandleEPi(void) return 0; } -int APS5_VABI _ZN3sce2np13NpTitleSecretC2ERKS1_(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI _ZN3sce2np14CalloutContextD1Ev(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceNpCommon/Runtime.cpp b/core/libs/prx/libSceNpCommon/Runtime.cpp index 669acaed60..b3f6a2ff8c 100644 --- a/core/libs/prx/libSceNpCommon/Runtime.cpp +++ b/core/libs/prx/libSceNpCommon/Runtime.cpp @@ -134,6 +134,21 @@ bool IsTitleIdDigit(char c) { return c >= '0' && c <= '9'; } +constexpr std::size_t NP_TITLE_SECRET_SIZE = sizeof(NpTitleSecret); + +void* const NpTitleSecretVtable[] = { + nullptr, nullptr, + reinterpret_cast(&_ZN3sce2np13NpTitleSecretD1Ev), reinterpret_cast(&_ZN3sce2np13NpTitleSecretD0Ev), +}; + +bool SameTitleSecret(const NpTitleSecret* lhs, const void* rhs) { + return std::memcmp(lhs->data, rhs, NP_TITLE_SECRET_SIZE) == 0; +} + +int OnlyFailure(int result) { + return result < 0 ? result : 0; +} + int StoreTitleId(NpTitleIdObject* out, const char* text, std::size_t length, std::size_t expected) { if (length != expected) return NP_ERROR_TITLE_ID_INVALID; if (!std::all_of(text, text + 4, IsTitleIdLetter) || !std::all_of(text + 4, text + NP_TITLE_ID_CODE_LENGTH, IsTitleIdDigit)) @@ -719,6 +734,38 @@ NpAllocatorEx* APS5_VABI sceNpGlobalHeapGetAllocatorExPtr(void) { return &g_globalAllocatorEx; } +NpAllocator* APS5_VABI _sceNpAllocatorExConvertAllocator(NpAllocatorEx* allocator) { + return &allocator->allocator; +} + +void* APS5_VABI _sceNpAllocatorExMalloc(NpAllocatorEx* allocator, std::size_t size) { + return allocator->allocator.allocate(size, allocator->allocator.context); +} + +void* APS5_VABI _sceNpAllocatorExRealloc(NpAllocatorEx* allocator, void* pointer, std::size_t size) { + return allocator->allocator.reallocate(pointer, size, allocator->allocator.context); +} + +void APS5_VABI _sceNpAllocatorExFree(NpAllocatorEx* allocator, void* pointer) { + allocator->allocator.release(pointer, allocator->allocator.context); +} + +char* APS5_VABI _sceNpAllocatorExStrdup(NpAllocatorEx* allocator, const char* text) { + const std::size_t size = std::strlen(text) + 1; + auto* copy = static_cast(_sceNpAllocatorExMalloc(allocator, size)); + if (copy != nullptr) std::memcpy(copy, text, size); + return copy; +} + +char* APS5_VABI _sceNpAllocatorExStrndup(NpAllocatorEx* allocator, const char* text, std::size_t maxLength) { + const std::size_t length = strnlen(text, maxLength); + auto* copy = static_cast(_sceNpAllocatorExMalloc(allocator, length + 1)); + if (copy == nullptr) return nullptr; + std::memcpy(copy, text, length); + copy[length] = '\0'; + return copy; +} + void* APS5_VABI _ZN3sce2np6ObjectnwEmR14SceNpAllocator(std::size_t size, NpAllocator* allocator) { return AllocateObject(size, allocator); } @@ -1046,6 +1093,8 @@ int APS5_VABI sceNpCalloutInitCtx(NpCalloutContext* context, const char* name, s void APS5_VABI sceNpCalloutTermCtx(NpCalloutContext* context) { if (context->initialized == 0) return; + if (scePthreadEqual(scePthreadSelf(), context->thread) != 0) + throw std::runtime_error("sceNpCalloutTermCtx: terminating a callout context from one of its own callouts is not implemented"); sceNpMutexLock(&context->mutex); context->stopping = 1; @@ -1359,6 +1408,107 @@ bool APS5_VABI _ZN3sce2npneERKNS0_9NpTitleIdERK12SceNpTitleId(const NpTitleIdObj return !SameTitleId(lhs->titleId.data, rhs->data); } +void APS5_VABI _ZN3sce2np13NpTitleSecretC1Ev(NpTitleSecretObject* self) { + self->vtable = &NpTitleSecretVtable[2]; + self->secret = {}; +} + +void APS5_VABI _ZN3sce2np13NpTitleSecretC2Ev(NpTitleSecretObject* self) { + _ZN3sce2np13NpTitleSecretC1Ev(self); +} + +void APS5_VABI _ZN3sce2np13NpTitleSecretC1ERKS1_(NpTitleSecretObject* self, const NpTitleSecretObject* other) { + self->vtable = &NpTitleSecretVtable[2]; + self->secret = other->secret; +} + +void APS5_VABI _ZN3sce2np13NpTitleSecretC2ERKS1_(NpTitleSecretObject* self, const NpTitleSecretObject* other) { + _ZN3sce2np13NpTitleSecretC1ERKS1_(self, other); +} + +void APS5_VABI _ZN3sce2np13NpTitleSecretC1ERK16SceNpTitleSecret(NpTitleSecretObject* self, const NpTitleSecret* secret) { + self->vtable = &NpTitleSecretVtable[2]; + self->secret = *secret; +} + +void APS5_VABI _ZN3sce2np13NpTitleSecretC2ERK16SceNpTitleSecret(NpTitleSecretObject* self, const NpTitleSecret* secret) { + _ZN3sce2np13NpTitleSecretC1ERK16SceNpTitleSecret(self, secret); +} + +void APS5_VABI _ZN3sce2np13NpTitleSecretC1EPKvm(NpTitleSecretObject* self, const void* data, std::size_t size) { + self->vtable = &NpTitleSecretVtable[2]; + if (size != NP_TITLE_SECRET_SIZE) throw std::invalid_argument("sce::np::NpTitleSecret: the size is not 128"); + std::memcpy(self->secret.data, data, NP_TITLE_SECRET_SIZE); +} + +void APS5_VABI _ZN3sce2np13NpTitleSecretC2EPKvm(NpTitleSecretObject* self, const void* data, std::size_t size) { + _ZN3sce2np13NpTitleSecretC1EPKvm(self, data, size); +} + +void APS5_VABI _ZN3sce2np13NpTitleSecretD1Ev(NpTitleSecretObject*) {} + +void APS5_VABI _ZN3sce2np13NpTitleSecretD2Ev(NpTitleSecretObject*) {} + +void APS5_VABI _ZN3sce2np13NpTitleSecretD0Ev(NpTitleSecretObject* self) { + _ZN3sce2np6ObjectdlEPv(self); +} + +NpTitleSecretObject* APS5_VABI _ZN3sce2np13NpTitleSecretaSERKS1_(NpTitleSecretObject* self, const NpTitleSecretObject* other) { + self->secret = other->secret; + return self; +} + +int APS5_VABI _ZN3sce2np13NpTitleSecret13SetSecretDataEPvm(NpTitleSecretObject* self, const void* data, std::size_t size) { + if (size != NP_TITLE_SECRET_SIZE) return NP_ERROR_TITLE_ID_INVALID; + std::memcpy(self->secret.data, data, NP_TITLE_SECRET_SIZE); + return 0; +} + +bool APS5_VABI _ZNK3sce2np13NpTitleSecret7IsEmptyEv(const NpTitleSecretObject* self) { + const NpTitleSecret empty {}; + return SameTitleSecret(&self->secret, empty.data); +} + +bool APS5_VABI _ZNK3sce2np13NpTitleSecret6EqualsEPvm(const NpTitleSecretObject* self, const void* data, std::size_t size) { + return size == NP_TITLE_SECRET_SIZE && SameTitleSecret(&self->secret, data); +} + +bool APS5_VABI _ZNK3sce2np13NpTitleSecret6EqualsERKS1_(const NpTitleSecretObject* self, const NpTitleSecretObject* other) { + return SameTitleSecret(&self->secret, other->secret.data); +} + +bool APS5_VABI _ZNK3sce2np13NpTitleSecret6EqualsERK16SceNpTitleSecret(const NpTitleSecretObject* self, const NpTitleSecret* secret) { + return SameTitleSecret(&self->secret, secret->data); +} + +int APS5_VABI _ZN3sce2np13NpTitleSecret8ParseHexEPS1_PKcm(NpTitleSecretObject* out, const char* hex, std::size_t length) { + return OnlyFailure(sceNpStrnParseHex(hex, length, out->secret.data, NP_TITLE_SECRET_SIZE)); +} + +int APS5_VABI _ZN3sce2np13NpTitleSecret8BuildHexERKS1_Pcmb(const NpTitleSecretObject* secret, char* hex, std::size_t size, bool uppercase) { + return OnlyFailure(sceNpStrBuildHex(hex, size, secret->secret.data, NP_TITLE_SECRET_SIZE, uppercase)); +} + +bool APS5_VABI _ZN3sce2npeqERKNS0_13NpTitleSecretES3_(const NpTitleSecretObject* lhs, const NpTitleSecretObject* rhs) { + return SameTitleSecret(&lhs->secret, rhs->secret.data); +} + +bool APS5_VABI _ZN3sce2npeqERK16SceNpTitleSecretRKNS0_13NpTitleSecretE(const NpTitleSecret* lhs, const NpTitleSecretObject* rhs) { + return SameTitleSecret(lhs, rhs->secret.data); +} + +bool APS5_VABI _ZN3sce2npeqERKNS0_13NpTitleSecretERK16SceNpTitleSecret(const NpTitleSecretObject* lhs, const NpTitleSecret* rhs) { + return SameTitleSecret(&lhs->secret, rhs->data); +} + +bool APS5_VABI _ZN3sce2npneERKNS0_13NpTitleSecretES3_(const NpTitleSecretObject* lhs, const NpTitleSecretObject* rhs) { + return !SameTitleSecret(&lhs->secret, rhs->secret.data); +} + +bool APS5_VABI _ZN3sce2npneERK16SceNpTitleSecretRKNS0_13NpTitleSecretE(const NpTitleSecret* lhs, const NpTitleSecretObject* rhs) { + return !SameTitleSecret(lhs, rhs->secret.data); +} + int APS5_VABI _ZN3sce2np12StreamReader4ReadEPNS0_6HandleEPNS0_9StreamCtxEPvmPm(NpStreamReader* self, void* handle, NpStreamCtx* ctx, void* out, std::size_t size, std::size_t* read) { int result = StreamRead(self, handle, out, size, read); if (result < 0) return result; diff --git a/core/libs/prx/libSceNpCommon/Strings.cpp b/core/libs/prx/libSceNpCommon/Strings.cpp index d4bbd25053..4f865d4438 100644 --- a/core/libs/prx/libSceNpCommon/Strings.cpp +++ b/core/libs/prx/libSceNpCommon/Strings.cpp @@ -1,4 +1,5 @@ #include "prx/libSceNpCommon/include/Api.hpp" +#include #include #include #include @@ -229,8 +230,8 @@ int ParseInteger(const char* str, std::size_t* parsedLength, Parse parse, bool* int CopyToBuffer(char* dst, std::size_t size, const char* src, std::size_t length) { if (size < length + 1) return NP_ERROR_BUFFER_TOO_SMALL; - std::strncpy(dst, src, size - 1); - dst[size - 1] = '\0'; + std::memcpy(dst, src, length); + std::memset(dst + length, 0, size - length); return 0; } @@ -338,6 +339,27 @@ bool IsAllowedTextByte(unsigned char c) { return (c >= 0x20 && c != 0x7F) || c == '\t' || c == '\n' || c == '\r'; } +struct XmlEntity { + char character; + const char* text; +}; + +constexpr XmlEntity XML_ENTITIES[] = {{'"', """}, {'&', "&"}, {'\'', "'"}, {'<', "<"}, {'>', ">"}}; + +const char* XmlEntityOf(char c) { + for (const XmlEntity& entity : XML_ENTITIES) { + if (entity.character == c) return entity.text; + } + return nullptr; +} + +const XmlEntity* XmlEntityAt(const char* text) { + for (const XmlEntity& entity : XML_ENTITIES) { + if (std::strncmp(text, entity.text, std::strlen(entity.text)) == 0) return &entity; + } + return nullptr; +} + bool IsUrlSafe(unsigned char c) { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' || c == '_' || c == '.' || c == '*'; @@ -627,6 +649,66 @@ int APS5_VABI sceNpUtilJsonGetOneChar(char* dst, std::size_t dstSize, const char return status; } +int APS5_VABI sceNpUtilXmlEscape(char* dst, std::size_t dstSize, const char* src, std::size_t srcLength, std::size_t* required) { + if (src == nullptr || srcLength == 0 || (dst == nullptr && required == nullptr)) return NP_UTIL_ERROR_INVALID_ARGUMENT; + const char* const end = src + srcLength; + std::size_t length = 0; + do { + const auto lead = static_cast(*src); + const char* piece = src; + std::size_t pieceLength = 1; + std::size_t consumed = 1; + if (lead < 0x80) { + if (!IsAllowedTextByte(lead)) return NP_UTIL_ERROR_INVALID_CHARACTER; + if (const char* entity = XmlEntityOf(*src)) { + piece = entity; + pieceLength = std::strlen(entity); + } + } else { + consumed = pieceLength = Utf8SequenceLength(lead); + if (pieceLength == 0 || !IsValidSequence(src, pieceLength)) return NP_UTIL_ERROR_INVALID_CHARACTER; + } + if (dst != nullptr) { + if (length + pieceLength > dstSize) return NP_UTIL_ERROR_BUFFER_TOO_SMALL; + std::memcpy(dst + length, piece, pieceLength); + } + length += pieceLength; + src += consumed; + } while (src < end); + if (dst != nullptr) { + if (length + 1 > dstSize) return NP_UTIL_ERROR_BUFFER_TOO_SMALL; + dst[length] = '\0'; + } + if (required != nullptr) *required = length + 1; + return 0; +} + +int APS5_VABI sceNpUtilXmlUnescape(char* dst, std::size_t dstSize, const char* src, std::size_t srcLength, std::size_t* required) { + if (src == nullptr || srcLength == 0 || (dst == nullptr && required == nullptr)) return NP_UTIL_ERROR_INVALID_ARGUMENT; + const char* const end = src + srcLength; + std::size_t length = 0; + do { + if (dst != nullptr && length + 1 > dstSize) return NP_UTIL_ERROR_BUFFER_TOO_SMALL; + char character = *src; + std::size_t consumed = 1; + if (character == '&') { + if (const XmlEntity* entity = XmlEntityAt(src)) { + character = entity->character; + consumed = std::strlen(entity->text); + } + } + if (dst != nullptr) dst[length] = character; + ++length; + src += consumed; + } while (src < end); + if (dst != nullptr) { + if (length + 1 > dstSize) return NP_UTIL_ERROR_BUFFER_TOO_SMALL; + dst[length] = '\0'; + } + if (required != nullptr) *required = length + 1; + return 0; +} + int APS5_VABI sceNpUtilGetNthChar(std::size_t index, const char* text, const char** character) { if (text == nullptr) return NP_UTIL_ERROR_INVALID_ARGUMENT; const char* position = text; @@ -667,7 +749,7 @@ int APS5_VABI sceNpUtilParseJid(NpJid* jid, const char* text, char* buffer, std: *jid = {}; return NP_UTIL_ERROR_BUFFER_TOO_SMALL; } - std::memcpy(buffer + used, pieces[i].data(), pieces[i].size()); + std::copy(pieces[i].begin(), pieces[i].end(), buffer + used); buffer[used + pieces[i].size()] = '\0'; copies[i] = buffer + used; } diff --git a/core/libs/prx/libSceNpCommon/include/Api.hpp b/core/libs/prx/libSceNpCommon/include/Api.hpp index 5799cb4441..243af24f3c 100644 --- a/core/libs/prx/libSceNpCommon/include/Api.hpp +++ b/core/libs/prx/libSceNpCommon/include/Api.hpp @@ -141,6 +141,11 @@ struct NpTitleIdObject { NpTitleId titleId; }; +struct NpTitleSecretObject { + const void* const* vtable; + NpTitleSecret secret; +}; + extern "C" { int APS5_VABI sceNpGetSandboxMode(); @@ -249,6 +254,12 @@ void APS5_VABI sceNpGlobalHeapGetAllocator(NpAllocator* allocator); void APS5_VABI sceNpGlobalHeapGetAllocatorEx(NpAllocatorEx* allocator); NpAllocator* APS5_VABI sceNpGlobalHeapGetAllocatorPtr(void); NpAllocatorEx* APS5_VABI sceNpGlobalHeapGetAllocatorExPtr(void); +NpAllocator* APS5_VABI _sceNpAllocatorExConvertAllocator(NpAllocatorEx* allocator); +void* APS5_VABI _sceNpAllocatorExMalloc(NpAllocatorEx* allocator, std::size_t size); +void* APS5_VABI _sceNpAllocatorExRealloc(NpAllocatorEx* allocator, void* pointer, std::size_t size); +void APS5_VABI _sceNpAllocatorExFree(NpAllocatorEx* allocator, void* pointer); +char* APS5_VABI _sceNpAllocatorExStrdup(NpAllocatorEx* allocator, const char* text); +char* APS5_VABI _sceNpAllocatorExStrndup(NpAllocatorEx* allocator, const char* text, std::size_t maxLength); void* APS5_VABI _ZN3sce2np6ObjectnwEmR14SceNpAllocator(std::size_t size, NpAllocator* allocator); void* APS5_VABI _ZN3sce2np6ObjectnaEmR14SceNpAllocator(std::size_t size, NpAllocator* allocator); void APS5_VABI _ZN3sce2np6ObjectdlEPvR14SceNpAllocator(void* object, NpAllocator* allocator); @@ -346,9 +357,8 @@ int APS5_VABI sceNpUtilGetNthChar(std::size_t index, const char* text, const cha int APS5_VABI sceNpUtilHttpUrlEncode(char* dst, std::size_t dstSize, const char* src, std::size_t srcLength, std::size_t* dstLength); int APS5_VABI _ZN3sce2np10CancelableC2Ev(void); -int APS5_VABI sceNpUtilXmlEscape(void); +int APS5_VABI sceNpUtilXmlEscape(char* dst, std::size_t dstSize, const char* src, std::size_t srcLength, std::size_t* required); int APS5_VABI _ZN3sce2np4CondD0Ev(void); -int APS5_VABI _sceNpAllocatorExRealloc(void); int APS5_VABI _ZN3sce2np8WorkItem6CancelEi(void); int APS5_VABI _ZN3sce2np9WorkQueue11GetItemByIdEi(void); int APS5_VABI _ZN3sce2np14JsonDocBuilderC2ERKNS0_9JsonValueE(void); @@ -360,7 +370,6 @@ int APS5_VABI _ZN3sce2np4Cond4ctorEv(void); int APS5_VABI _ZN3sce2np16StreamReadBufferD1Ev(void); int APS5_VABI _ZNK3sce2np9EventFlag6IsInitEv(void); int APS5_VABI _ZN3sce2np10CancelLockC2Ev(void); -int APS5_VABI _ZN3sce2np13NpTitleSecretC2ERK16SceNpTitleSecret(void); int APS5_VABI _ZN3sce2np8WorkItem10SetRunningEv(void); int APS5_VABI _ZN3sce2np3ipc17ServiceIpmiClient21CreateRequestForAsyncEPiiPKvm(void); int APS5_VABI _ZN3sce2np18HttpConnectionPool13InvalidateAllEv(void); @@ -388,7 +397,7 @@ int APS5_VABI _ZN3sce2np3ipc10IpmiClient11IsConnectedEv(void); int APS5_VABI _ZN3sce2np12HttpTemplate19SetAuthInfoCallbackEPFii15SceHttpAuthTypePKcPcS5_iPPhPmPiPvESA_(void); int APS5_VABI sceNpUtilParseJid(NpJid* jid, const char* text, char* buffer, std::size_t bufferSize, std::size_t* required); int APS5_VABI _ZN3sce2np9Semaphore6CreateEiiPKc(void); -int APS5_VABI sceNpUtilXmlUnescape(void); +int APS5_VABI sceNpUtilXmlUnescape(char* dst, std::size_t dstSize, const char* src, std::size_t srcLength, std::size_t* required); int APS5_VABI sceNpXmlParse(void); int APS5_VABI _ZN3sce2np9WorkQueue15GetFinishedItemENS0_14WorkItemStatusE(void); int APS5_VABI _ZN3sce2np9EventFlag4PollEmjPm(void); @@ -426,12 +435,10 @@ int APS5_VABI _ZN3sce2np7Callout11CalloutFuncEPv(void); int APS5_VABI _ZThn8_N3sce2np6HandleD1Ev(void); int APS5_VABI _ZN3sce2np6Handle4InitEv(void); int APS5_VABI _ZThn16_N3sce2np10MemoryFileD1Ev(void); -int APS5_VABI _sceNpAllocatorExFree(void); int APS5_VABI _ZN3sce2np3ipc17ServiceIpmiClient7ConnectEPKvm(void); int APS5_VABI _ZN3sce2np7RingBufC1Ev(void); int APS5_VABI _ZN3sce2np10MemoryFileD1Ev(void); int APS5_VABI _ZN3sce2np9HttpTrans21GetResponseStatusCodeEPNS0_6HandleEPi(void); -int APS5_VABI _ZN3sce2np13NpTitleSecretC2ERKS1_(void); int APS5_VABI _ZN3sce2np14CalloutContextD1Ev(void); int APS5_VABI _ZN3sce2np5Mutex7DestroyEv(void); int APS5_VABI _ZN3sce2np14JsonDocBuilderC1ERKNS0_9JsonValueE(void); @@ -507,6 +514,30 @@ bool APS5_VABI _ZN3sce2npneERKNS0_9NpTitleIdES3_(const NpTitleIdObject* lhs, con bool APS5_VABI _ZN3sce2npneERK12SceNpTitleIdRKNS0_9NpTitleIdE(const NpTitleId* lhs, const NpTitleIdObject* rhs); bool APS5_VABI _ZN3sce2npneERKNS0_9NpTitleIdERK12SceNpTitleId(const NpTitleIdObject* lhs, const NpTitleId* rhs); +void APS5_VABI _ZN3sce2np13NpTitleSecretC1Ev(NpTitleSecretObject* self); +void APS5_VABI _ZN3sce2np13NpTitleSecretC2Ev(NpTitleSecretObject* self); +void APS5_VABI _ZN3sce2np13NpTitleSecretC1ERKS1_(NpTitleSecretObject* self, const NpTitleSecretObject* other); +void APS5_VABI _ZN3sce2np13NpTitleSecretC2ERKS1_(NpTitleSecretObject* self, const NpTitleSecretObject* other); +void APS5_VABI _ZN3sce2np13NpTitleSecretC1ERK16SceNpTitleSecret(NpTitleSecretObject* self, const NpTitleSecret* secret); +void APS5_VABI _ZN3sce2np13NpTitleSecretC2ERK16SceNpTitleSecret(NpTitleSecretObject* self, const NpTitleSecret* secret); +void APS5_VABI _ZN3sce2np13NpTitleSecretC1EPKvm(NpTitleSecretObject* self, const void* data, std::size_t size); +void APS5_VABI _ZN3sce2np13NpTitleSecretC2EPKvm(NpTitleSecretObject* self, const void* data, std::size_t size); +void APS5_VABI _ZN3sce2np13NpTitleSecretD1Ev(NpTitleSecretObject* self); +void APS5_VABI _ZN3sce2np13NpTitleSecretD2Ev(NpTitleSecretObject* self); +void APS5_VABI _ZN3sce2np13NpTitleSecretD0Ev(NpTitleSecretObject* self); +NpTitleSecretObject* APS5_VABI _ZN3sce2np13NpTitleSecretaSERKS1_(NpTitleSecretObject* self, const NpTitleSecretObject* other); +int APS5_VABI _ZN3sce2np13NpTitleSecret13SetSecretDataEPvm(NpTitleSecretObject* self, const void* data, std::size_t size); +bool APS5_VABI _ZNK3sce2np13NpTitleSecret7IsEmptyEv(const NpTitleSecretObject* self); +bool APS5_VABI _ZNK3sce2np13NpTitleSecret6EqualsEPvm(const NpTitleSecretObject* self, const void* data, std::size_t size); +bool APS5_VABI _ZNK3sce2np13NpTitleSecret6EqualsERKS1_(const NpTitleSecretObject* self, const NpTitleSecretObject* other); +bool APS5_VABI _ZNK3sce2np13NpTitleSecret6EqualsERK16SceNpTitleSecret(const NpTitleSecretObject* self, const NpTitleSecret* secret); +int APS5_VABI _ZN3sce2np13NpTitleSecret8ParseHexEPS1_PKcm(NpTitleSecretObject* out, const char* hex, std::size_t length); +int APS5_VABI _ZN3sce2np13NpTitleSecret8BuildHexERKS1_Pcmb(const NpTitleSecretObject* secret, char* hex, std::size_t size, bool uppercase); +bool APS5_VABI _ZN3sce2npeqERKNS0_13NpTitleSecretES3_(const NpTitleSecretObject* lhs, const NpTitleSecretObject* rhs); +bool APS5_VABI _ZN3sce2npeqERK16SceNpTitleSecretRKNS0_13NpTitleSecretE(const NpTitleSecret* lhs, const NpTitleSecretObject* rhs); +bool APS5_VABI _ZN3sce2npeqERKNS0_13NpTitleSecretERK16SceNpTitleSecret(const NpTitleSecretObject* lhs, const NpTitleSecret* rhs); +bool APS5_VABI _ZN3sce2npneERKNS0_13NpTitleSecretES3_(const NpTitleSecretObject* lhs, const NpTitleSecretObject* rhs); +bool APS5_VABI _ZN3sce2npneERK16SceNpTitleSecretRKNS0_13NpTitleSecretE(const NpTitleSecret* lhs, const NpTitleSecretObject* rhs); int APS5_VABI _ZN3sce2np12StreamReader4ReadEPNS0_6HandleEPNS0_9StreamCtxEPvmPm(NpStreamReader* self, void* handle, NpStreamCtx* ctx, void* out, std::size_t size, std::size_t* read); int APS5_VABI _ZN3sce2np12StreamReader7ReadAllEPNS0_6HandleEPvmPm(NpStreamReader* self, void* handle, void* out, std::size_t size, std::size_t* read); int APS5_VABI _ZN3sce2np12StreamReader7ReadAllEPNS0_6HandleEPNS0_9StreamCtxEPvmPm(NpStreamReader* self, void* handle, NpStreamCtx* ctx, void* out, std::size_t size, std::size_t* read); diff --git a/core/libs/prx/libSceNpCppWebApi/Export.cpp b/core/libs/prx/libSceNpCppWebApi/Export.cpp index dbdb2d77ba..16b152f9b1 100644 --- a/core/libs/prx/libSceNpCppWebApi/Export.cpp +++ b/core/libs/prx/libSceNpCppWebApi/Export.cpp @@ -1762,6 +1762,16 @@ static int VectorPushBackString(IntrusivePtr* self, const CommonString* element) body->size += 1; return 0; } + +static_assert(sizeof(CommonIterator) == 0x10 && offsetof(CommonIterator, element) == 0x08); + +static CommonString* StringVectorElements(const IntrusivePtr* self, const bool end, const char* function) { + const auto* body = static_cast(self->object); + if (body == nullptr) return nullptr; + if (body->magic != VectorBodyMagic || body->elementKind != 2) NotImplemented_nid_no_patch(function); + auto* elements = static_cast(body->elements); + return end && body->size != 0 ? elements + body->size : elements; +} // -------------------------------------------------------------------------- // SessionManager2V1 and extra Common symbols, from a title dump whose modules lack // libSceNpCppWebApi.prx. @@ -2753,24 +2763,25 @@ int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V120RequestPlayerSession14set return 0; } -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi49ParameterToAddPlayerSessionJoinableSpecifiedUsers10initializeEPNS1_6Common10LibContextEPKcNS6_12IntrusivePtrINS3_60PostPlayerSessionsSessionIdJoinableSpecifiedUsersRequestBodyEEE() { +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi49ParameterToAddPlayerSessionJoinableSpecifiedUsers10initializeEPNS1_6Common10LibContextEPKcNS6_12IntrusivePtrINS3_60PostPlayerSessionsSessionIdJoinableSpecifiedUsersRequestBodyEEE(void* self, const std::uint64_t* context, const char* sessionId, const IntrusivePtr* body) { + return InitializeParameterWithBody(self, context, sessionId != nullptr, body); +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi37ParameterToSetPlayerSessionProperties10initializeEPNS1_6Common10LibContextEPKcNS6_12IntrusivePtrINS3_39PatchPlayerSessionsSessionIdRequestBodyEEE(void* self, const std::uint64_t* context, const char* sessionId, const IntrusivePtr* body) { + return InitializeParameterWithBody(self, context, sessionId != nullptr, body); +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi36ParameterToChangePlayerSessionLeader9terminateEv(void* self) { + return TerminateParameter(self); +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi37ParameterToSetPlayerSessionPropertiesC1Ev(void* self) { + ConstructParameter(self); return 0; } -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi37ParameterToSetPlayerSessionProperties10initializeEPNS1_6Common10LibContextEPKcNS6_12IntrusivePtrINS3_39PatchPlayerSessionsSessionIdRequestBodyEEE() { - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi36ParameterToChangePlayerSessionLeader9terminateEv() { - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi37ParameterToSetPlayerSessionPropertiesC1Ev() { - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi49ParameterToAddPlayerSessionJoinableSpecifiedUsers9terminateEv() { - return 0; +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi49ParameterToAddPlayerSessionJoinableSpecifiedUsers9terminateEv(void* self) { + return TerminateParameter(self); } int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V120RequestPlayerSession14setCustomData1EPKvm() { @@ -2787,12 +2798,13 @@ int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V124GetBlockingUsersResponse9get return 0; } -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi37ParameterToSetPlayerSessionPropertiesD1Ev() { +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi37ParameterToSetPlayerSessionPropertiesD1Ev(void* self) { + (void)self; return 0; } -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi39ParameterToSendPlayerSessionInvitations10initializeEPNS1_6Common10LibContextEPKcNS6_12IntrusivePtrINS3_49PostPlayerSessionsSessionIdInvitationsRequestBodyEEE() { - return 0; +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi39ParameterToSendPlayerSessionInvitations10initializeEPNS1_6Common10LibContextEPKcNS6_12IntrusivePtrINS3_49PostPlayerSessionsSessionIdInvitationsRequestBodyEEE(void* self, const std::uint64_t* context, const char* sessionId, const IntrusivePtr* body) { + return InitializeParameterWithBody(self, context, sessionId != nullptr, body); } int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V146PatchPlayerSessionsSessionIdRequestBodyFactory6createEPNS1_6Common10LibContextEPNS5_12IntrusivePtrINS3_39PatchPlayerSessionsSessionIdRequestBodyEEE() { @@ -2821,15 +2833,16 @@ int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi28Parame return 0; } -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi28ParameterToGetPlayerSessions9terminateEv() { - return 0; +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi28ParameterToGetPlayerSessions9terminateEv(void* self) { + return TerminateParameter(self); } int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi29ParameterToLeavePlayerSession9terminateEv(void* self) { return TerminateParameter(self); } -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi39ParameterToSendPlayerSessionInvitationsD1Ev() { +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi39ParameterToSendPlayerSessionInvitationsD1Ev(void* self) { + (void)self; return 0; } @@ -2873,11 +2886,13 @@ int APS5_VABI _ZNK3sce2Np9CppWebApi14SessionManager2V120PlayerSessionForRead9get return 0; } -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi39ParameterToSendPlayerSessionInvitationsC1Ev() { +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi39ParameterToSendPlayerSessionInvitationsC1Ev(void* self) { + ConstructParameter(self); return 0; } -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi28ParameterToGetPlayerSessionsD1Ev() { +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi28ParameterToGetPlayerSessionsD1Ev(void* self) { + (void)self; return 0; } @@ -2902,7 +2917,8 @@ int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V119JoinableUserFactory6creat return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; } -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi36ParameterToChangePlayerSessionLeaderD1Ev() { +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi36ParameterToChangePlayerSessionLeaderD1Ev(void* self) { + (void)self; return 0; } @@ -2910,15 +2926,16 @@ int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V156PostPlayerSessionsSession return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; } -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi39ParameterToSendPlayerSessionInvitations9terminateEv() { - return 0; +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi39ParameterToSendPlayerSessionInvitations9terminateEv(void* self) { + return TerminateParameter(self); } -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi28ParameterToGetPlayerSessions10initializeEPNS1_6Common10LibContextEPKc() { - return 0; +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi28ParameterToGetPlayerSessions10initializeEPNS1_6Common10LibContextEPKc(void* self, const std::uint64_t* context, const char* sessionIds) { + return InitializeParameter(self, context, sessionIds != nullptr); } -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi28ParameterToGetPlayerSessionsC1Ev() { +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi28ParameterToGetPlayerSessionsC1Ev(void* self) { + ConstructParameter(self); return 0; } @@ -2940,24 +2957,26 @@ int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V19ToFactory6createEPNS1_6Com return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; } -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi49ParameterToAddPlayerSessionJoinableSpecifiedUsersD1Ev() { +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi49ParameterToAddPlayerSessionJoinableSpecifiedUsersD1Ev(void* self) { + (void)self; return 0; } -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi36ParameterToChangePlayerSessionLeader10initializeEPNS1_6Common10LibContextEPKcNS6_12IntrusivePtrINS3_43PutPlayerSessionsSessionIdLeaderRequestBodyEEE() { - return 0; +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi36ParameterToChangePlayerSessionLeader10initializeEPNS1_6Common10LibContextEPKcNS6_12IntrusivePtrINS3_43PutPlayerSessionsSessionIdLeaderRequestBodyEEE(void* self, const std::uint64_t* context, const char* sessionId, const IntrusivePtr* body) { + return InitializeParameterWithBody(self, context, sessionId != nullptr, body); } int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V167PostPlayerSessionsSessionIdJoinableSpecifiedUsersRequestBodyFactory6createEPNS1_6Common10LibContextERKNS5_6VectorINS5_12IntrusivePtrINS3_12JoinableUserEEEEEPNS9_INS3_60PostPlayerSessionsSessionIdJoinableSpecifiedUsersRequestBodyEEE() { return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; } -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi36ParameterToChangePlayerSessionLeaderC1Ev() { +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi36ParameterToChangePlayerSessionLeaderC1Ev(void* self) { + ConstructParameter(self); return 0; } -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi37ParameterToSetPlayerSessionProperties9terminateEv() { - return 0; +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi37ParameterToSetPlayerSessionProperties9terminateEv(void* self) { + return TerminateParameter(self); } int APS5_VABI _ZNK3sce2Np9CppWebApi14SessionManager2V118LeaderWithOnlineId12getAccountIdEv() { @@ -2974,7 +2993,8 @@ int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V129GetPlayerSessionsResponse return 0; } -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi49ParameterToAddPlayerSessionJoinableSpecifiedUsersC1Ev() { +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi49ParameterToAddPlayerSessionJoinableSpecifiedUsersC1Ev(void* self) { + ConstructParameter(self); return 0; } @@ -3758,6 +3778,592 @@ int APS5_VABI _ZNK3sce2Np9CppWebApi13InGameCatalog2V53Sku10labelIsSetEv() { return 0; } +struct MatchesParameter { + void* base; + void* implementation; +}; + +static constexpr int MatchesOutOfMemory = static_cast(0x80553504); + +static CommonString* MatchesStringAt(void* implementation, const std::size_t offset) { + return reinterpret_cast(static_cast(implementation) + offset); +} + +static void ConstructMatchesString(CommonString* self, void* libContext) { + self->referenceCount = 0; + self->buffer = nullptr; + self->bufferSize = 0; + self->libContext = libContext; +} + +static void DestroyMatchesString(CommonString* self) { + std::free(self->buffer); + self->buffer = nullptr; + self->bufferSize = 0; +} + +static int AssignMatchesString(CommonString* self, const char* text) { + if (text == nullptr) return ParameterInvalidArgument; + const std::size_t length = std::strlen(text); + char* const oldBuffer = self->buffer; + char* const buffer = static_cast(std::malloc(length + 1)); + if (buffer == nullptr) { + self->buffer = oldBuffer; + return MatchesOutOfMemory; + } + std::memcpy(buffer, text, length + 1); + self->buffer = buffer; + self->bufferSize = length + 1; + std::free(oldBuffer); + return 0; +} + +static int InitializeMatchesParameter(void* self, const std::uint64_t* context, const bool hasText, const char* text, const std::size_t implementationSize, const std::size_t bodyOffset, const std::size_t contextOffset, const IntrusivePtr* body) { + if (context == nullptr) return ParameterInvalidArgument; + if (*context == 0) return ParameterNotInitialized; + auto* parameter = static_cast(self); + if (parameter->implementation != nullptr) return ParameterAlreadyInitialized; + void* implementation = std::calloc(1, implementationSize); + if (implementation == nullptr) return MatchesOutOfMemory; + void* libContext = const_cast(context); + if (hasText) ConstructMatchesString(MatchesStringAt(implementation, 0), libContext); + *reinterpret_cast(static_cast(implementation) + contextOffset) = libContext; + if (hasText) { + const int result = AssignMatchesString(MatchesStringAt(implementation, 0), text); + if (result < 0) { + DestroyMatchesString(MatchesStringAt(implementation, 0)); + std::free(implementation); + return result; + } + } + if (body == nullptr || body->object == nullptr) { + if (hasText) DestroyMatchesString(MatchesStringAt(implementation, 0)); + std::free(implementation); + return ParameterBodyMissing; + } + auto* destination = reinterpret_cast(static_cast(implementation) + bodyOffset); + *destination = *body; + ReferenceCount(destination->object, ReferenceCountOffset).fetch_add(1); + parameter->implementation = implementation; + return 0; +} + +static int TerminateMatchesParameter(void* self, const bool hasText, const std::size_t bodyOffset) { + auto* parameter = static_cast(self); + if (parameter->implementation == nullptr) return ParameterTerminatedUninitialized; + void* implementation = parameter->implementation; + if (hasText) DestroyMatchesString(MatchesStringAt(implementation, 0)); + ReleaseIntrusivePtr(reinterpret_cast(static_cast(implementation) + bodyOffset), ReferenceCountOffset, __func__); + std::free(implementation); + parameter->implementation = nullptr; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V111AddedPlayer12setAccountIdEPKc(void* self, const char* value) { + const int result = AssignMatchesString(MatchesStringAt(self, 96), value); + if (result < 0) return result; + *reinterpret_cast(static_cast(self) + 128) = 1; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V111AddedPlayer13setPlayerNameEPKc(void* self, const char* value) { + const int result = AssignMatchesString(MatchesStringAt(self, 56), value); + if (result < 0) return result; + *reinterpret_cast(static_cast(self) + 88) = 1; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V111AddedPlayer9setTeamIdEPKc(void* self, const char* value) { + const int result = AssignMatchesString(MatchesStringAt(self, 136), value); + if (result < 0) return result; + *reinterpret_cast(static_cast(self) + 168) = 1; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V113RemovedPlayer9setReasonERKNS3_11LeaveReasonE(void* self, const int* value) { + *reinterpret_cast(static_cast(self) + 48) = *value; + *reinterpret_cast(static_cast(self) + 52) = 1; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V116JoinMatchRequest17setNpServiceLabelERKi(void* self, const int* value) { + *reinterpret_cast(static_cast(self) + 16) = *value; + *reinterpret_cast(static_cast(self) + 20) = 1; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V116RequestMatchTeam10setMembersERKNS1_6Common6VectorINS5_12IntrusivePtrINS3_13RequestMemberEEEEE(void* self, const IntrusivePtr* value) { + if (value == nullptr) return ParameterBodyMissing; + AssignIntrusivePtr(reinterpret_cast(static_cast(self) + 88), value, VectorReferenceCountOffset, __func__); + *reinterpret_cast(static_cast(self) + 112) = 1; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V116RequestMatchTeam11setTeamNameEPKc(void* self, const char* value) { + const int result = AssignMatchesString(MatchesStringAt(self, 48), value); + if (result < 0) return result; + *reinterpret_cast(static_cast(self) + 80) = 1; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V117LeaveMatchRequest17setNpServiceLabelERKi(void* self, const int* value) { + *reinterpret_cast(static_cast(self) + 16) = *value; + *reinterpret_cast(static_cast(self) + 20) = 1; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V118AddedPlayerFactory6createEPNS1_6Common10LibContextEPKcNS3_10PlayerTypeEPNS5_12IntrusivePtrINS3_11AddedPlayerEEE(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V118CreateMatchRequest15setInGameRosterERKNS1_6Common12IntrusivePtrINS3_19RequestInGameRosterEEE(void* self, const IntrusivePtr* value) { + if (value == nullptr || value->object == nullptr) return ParameterBodyMissing; + AssignIntrusivePtr(reinterpret_cast(static_cast(self) + 152), value, ReferenceCountOffset, __func__); + *reinterpret_cast(static_cast(self) + 176) = 1; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V118CreateMatchRequest17setExpirationTimeERKi(void* self, const int* value) { + *reinterpret_cast(static_cast(self) + 92) = *value; + *reinterpret_cast(static_cast(self) + 96) = 1; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V118CreateMatchRequest17setNpServiceLabelERKi(void* self, const int* value) { + *reinterpret_cast(static_cast(self) + 16) = *value; + *reinterpret_cast(static_cast(self) + 20) = 1; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V118CreateMatchRequest9setZoneIdEPKc(void* self, const char* value) { + const int result = AssignMatchesString(MatchesStringAt(self, 56), value); + if (result < 0) return result; + *reinterpret_cast(static_cast(self) + 88) = 1; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V118RequestMatchPlayer12setAccountIdEPKc(void* self, const char* value) { + const int result = AssignMatchesString(MatchesStringAt(self, 96), value); + if (result < 0) return result; + *reinterpret_cast(static_cast(self) + 128) = 1; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V118RequestMatchPlayer13setPlayerNameEPKc(void* self, const char* value) { + const int result = AssignMatchesString(MatchesStringAt(self, 56), value); + if (result < 0) return result; + *reinterpret_cast(static_cast(self) + 88) = 1; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V118RequestTeamResults20setTeamMemberResultsERKNS1_6Common6VectorINS5_12IntrusivePtrINS3_23RequestTeamMemberResultEEEEE(void* self, const IntrusivePtr* value) { + if (value == nullptr) return ParameterBodyMissing; + AssignIntrusivePtr(reinterpret_cast(static_cast(self) + 72), value, VectorReferenceCountOffset, __func__); + *reinterpret_cast(static_cast(self) + 96) = 1; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V118RequestTeamResults8setScoreERKd(void* self, const double* value) { + *reinterpret_cast(static_cast(self) + 56) = *value; + *reinterpret_cast(static_cast(self) + 64) = 1; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V119RequestInGameRoster10setPlayersERKNS1_6Common6VectorINS5_12IntrusivePtrINS3_18RequestMatchPlayerEEEEE(void* self, const IntrusivePtr* value) { + if (value == nullptr) return ParameterBodyMissing; + AssignIntrusivePtr(reinterpret_cast(static_cast(self) + 16), value, VectorReferenceCountOffset, __func__); + *reinterpret_cast(static_cast(self) + 40) = 1; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V119RequestInGameRoster8setTeamsERKNS1_6Common6VectorINS5_12IntrusivePtrINS3_16RequestMatchTeamEEEEE(void* self, const IntrusivePtr* value) { + if (value == nullptr) return ParameterBodyMissing; + AssignIntrusivePtr(reinterpret_cast(static_cast(self) + 48), value, VectorReferenceCountOffset, __func__); + *reinterpret_cast(static_cast(self) + 72) = 1; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V119RequestMatchResults20setCompetitiveResultERKNS1_6Common12IntrusivePtrINS3_24RequestCompetitiveResultEEE(void* self, const IntrusivePtr* value) { + if (value == nullptr || value->object == nullptr) return ParameterBodyMissing; + AssignIntrusivePtr(reinterpret_cast(static_cast(self) + 32), value, ReferenceCountOffset, __func__); + *reinterpret_cast(static_cast(self) + 56) = 1; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V119RequestMatchResults20setCooperativeResultERKNS3_24RequestCooperativeResultE(void* self, const int* value) { + *reinterpret_cast(static_cast(self) + 20) = *value; + *reinterpret_cast(static_cast(self) + 24) = 1; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V120RemovedPlayerFactory6createEPNS1_6Common10LibContextEPKcPNS5_12IntrusivePtrINS3_13RemovedPlayerEEE(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V120ReportResultsRequest17setNpServiceLabelERKi(void* self, const int* value) { + *reinterpret_cast(static_cast(self) + 16) = *value; + *reinterpret_cast(static_cast(self) + 20) = 1; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V120ReportResultsRequest18setMatchStatisticsERKNS1_6Common12IntrusivePtrINS3_22RequestMatchStatisticsEEE(void* self, const IntrusivePtr* value) { + if (value == nullptr || value->object == nullptr) return ParameterBodyMissing; + AssignIntrusivePtr(reinterpret_cast(static_cast(self) + 48), value, ReferenceCountOffset, __func__); + *reinterpret_cast(static_cast(self) + 72) = 1; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V120RequestMemberFactory6createEPNS1_6Common10LibContextEPKcPNS5_12IntrusivePtrINS3_13RequestMemberEEE(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V120RequestPlayerResults8setScoreERKd(void* self, const double* value) { + *reinterpret_cast(static_cast(self) + 56) = *value; + *reinterpret_cast(static_cast(self) + 64) = 1; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V120RequestTeamStatistic23setTeamMemberStatisticsERKNS1_6Common6VectorINS5_12IntrusivePtrINS3_26RequestTeamMemberStatisticEEEEE(void* self, const IntrusivePtr* value) { + if (value == nullptr) return ParameterBodyMissing; + AssignIntrusivePtr(reinterpret_cast(static_cast(self) + 80), value, VectorReferenceCountOffset, __func__); + *reinterpret_cast(static_cast(self) + 104) = 1; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V120RequestTeamStatistic8setStatsERKNS1_6Common6VectorINS5_12IntrusivePtrINS3_19AdditionalStatisticEEEEE(void* self, const IntrusivePtr* value) { + if (value == nullptr) return ParameterBodyMissing; + AssignIntrusivePtr(reinterpret_cast(static_cast(self) + 48), value, VectorReferenceCountOffset, __func__); + *reinterpret_cast(static_cast(self) + 72) = 1; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V122RequestMatchStatistics17setTeamStatisticsERKNS1_6Common6VectorINS5_12IntrusivePtrINS3_20RequestTeamStatisticEEEEE(void* self, const IntrusivePtr* value) { + if (value == nullptr) return ParameterBodyMissing; + AssignIntrusivePtr(reinterpret_cast(static_cast(self) + 48), value, VectorReferenceCountOffset, __func__); + *reinterpret_cast(static_cast(self) + 72) = 1; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V122RequestMatchStatistics19setPlayerStatisticsERKNS1_6Common6VectorINS5_12IntrusivePtrINS3_22RequestPlayerStatisticEEEEE(void* self, const IntrusivePtr* value) { + if (value == nullptr) return ParameterBodyMissing; + AssignIntrusivePtr(reinterpret_cast(static_cast(self) + 16), value, VectorReferenceCountOffset, __func__); + *reinterpret_cast(static_cast(self) + 40) = 1; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V123JoinMatchRequestFactory6createEPNS1_6Common10LibContextERKNS5_6VectorINS5_12IntrusivePtrINS3_11AddedPlayerEEEEEPNS9_INS3_16JoinMatchRequestEEE(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V123RequestMatchTeamFactory6createEPNS1_6Common10LibContextEPKcPNS5_12IntrusivePtrINS3_16RequestMatchTeamEEE(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V124LeaveMatchRequestFactory6createEPNS1_6Common10LibContextERKNS5_6VectorINS5_12IntrusivePtrINS3_13RemovedPlayerEEEEEPNS9_INS3_17LeaveMatchRequestEEE(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V124RequestCompetitiveResult14setTeamResultsERKNS1_6Common6VectorINS5_12IntrusivePtrINS3_18RequestTeamResultsEEEEE(void* self, const IntrusivePtr* value) { + if (value == nullptr) return ParameterBodyMissing; + AssignIntrusivePtr(reinterpret_cast(static_cast(self) + 48), value, VectorReferenceCountOffset, __func__); + *reinterpret_cast(static_cast(self) + 72) = 1; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V124RequestCompetitiveResult16setPlayerResultsERKNS1_6Common6VectorINS5_12IntrusivePtrINS3_20RequestPlayerResultsEEEEE(void* self, const IntrusivePtr* value) { + if (value == nullptr) return ParameterBodyMissing; + AssignIntrusivePtr(reinterpret_cast(static_cast(self) + 16), value, VectorReferenceCountOffset, __func__); + *reinterpret_cast(static_cast(self) + 40) = 1; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V124UpdateMatchDetailRequest15setInGameRosterERKNS1_6Common12IntrusivePtrINS3_19RequestInGameRosterEEE(void* self, const IntrusivePtr* value) { + if (value == nullptr || value->object == nullptr) return ParameterBodyMissing; + AssignIntrusivePtr(reinterpret_cast(static_cast(self) + 112), value, ReferenceCountOffset, __func__); + *reinterpret_cast(static_cast(self) + 136) = 1; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V124UpdateMatchDetailRequest17setExpirationTimeERKi(void* self, const int* value) { + *reinterpret_cast(static_cast(self) + 60) = *value; + *reinterpret_cast(static_cast(self) + 64) = 1; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V124UpdateMatchDetailRequest17setNpServiceLabelERKi(void* self, const int* value) { + *reinterpret_cast(static_cast(self) + 16) = *value; + *reinterpret_cast(static_cast(self) + 20) = 1; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V124UpdateMatchDetailRequest9setZoneIdEPKc(void* self, const char* value) { + const int result = AssignMatchesString(MatchesStringAt(self, 24), value); + if (result < 0) return result; + *reinterpret_cast(static_cast(self) + 56) = 1; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V124UpdateMatchStatusRequest17setNpServiceLabelERKi(void* self, const int* value) { + *reinterpret_cast(static_cast(self) + 16) = *value; + *reinterpret_cast(static_cast(self) + 20) = 1; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V125CreateMatchRequestFactory6createEPNS1_6Common10LibContextEPKcPNS5_12IntrusivePtrINS3_18CreateMatchRequestEEE(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V125RequestMatchPlayerFactory6createEPNS1_6Common10LibContextEPKcNS3_10PlayerTypeEPNS5_12IntrusivePtrINS3_18RequestMatchPlayerEEE(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V125RequestTeamResultsFactory6createEPNS1_6Common10LibContextEPKciPNS5_12IntrusivePtrINS3_18RequestTeamResultsEEE(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V126AdditionalStatisticFactory6createEPNS1_6Common10LibContextEPKcS9_PNS5_12IntrusivePtrINS3_19AdditionalStatisticEEE(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V126RequestInGameRosterFactory6createEPNS1_6Common10LibContextEPNS5_12IntrusivePtrINS3_19RequestInGameRosterEEE(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V126RequestMatchResultsFactory6createEPNS1_6Common10LibContextENS3_14ResultsVersionEPNS5_12IntrusivePtrINS3_19RequestMatchResultsEEE(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V127ReportResultsRequestFactory6createEPNS1_6Common10LibContextENS5_12IntrusivePtrINS3_19RequestMatchResultsEEEPNS8_INS3_20ReportResultsRequestEEE(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V127RequestPlayerResultsFactory6createEPNS1_6Common10LibContextEPKciPNS5_12IntrusivePtrINS3_20RequestPlayerResultsEEE(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V127RequestTeamStatisticFactory6createEPNS1_6Common10LibContextEPKcPNS5_12IntrusivePtrINS3_20RequestTeamStatisticEEE(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V129RequestMatchStatisticsFactory6createEPNS1_6Common10LibContextEPNS5_12IntrusivePtrINS3_22RequestMatchStatisticsEEE(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V129RequestPlayerStatisticFactory6createEPNS1_6Common10LibContextEPKcRKNS5_6VectorINS5_12IntrusivePtrINS3_19AdditionalStatisticEEEEEPNSB_INS3_22RequestPlayerStatisticEEE(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V130RequestTeamMemberResultFactory6createEPNS1_6Common10LibContextEPKcdPNS5_12IntrusivePtrINS3_23RequestTeamMemberResultEEE(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V131RequestCompetitiveResultFactory6createEPNS1_6Common10LibContextEPNS5_12IntrusivePtrINS3_24RequestCompetitiveResultEEE(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V131UpdateMatchDetailRequestFactory6createEPNS1_6Common10LibContextEPNS5_12IntrusivePtrINS3_24UpdateMatchDetailRequestEEE(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V131UpdateMatchStatusRequestFactory6createEPNS1_6Common10LibContextENS3_12UpdateStatusEPNS5_12IntrusivePtrINS3_24UpdateMatchStatusRequestEEE(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V133RequestTeamMemberStatisticFactory6createEPNS1_6Common10LibContextEPKcRKNS5_6VectorINS5_12IntrusivePtrINS3_19AdditionalStatisticEEEEEPNSB_INS3_26RequestTeamMemberStatisticEEE(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V18MatchApi10leaveMatchEiRKNS4_21ParameterToLeaveMatchERNS1_6Common11TransactionINS8_15DefaultResponseENS8_12IntrusivePtrINS8_18ResponseHeaderBaseEEEEE(void* self) { + (void)self; + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V18MatchApi11createMatchEiRKNS4_22ParameterToCreateMatchERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_19CreateMatchResponseEEENSA_INS8_18ResponseHeaderBaseEEEEE(void* self) { + (void)self; + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V18MatchApi13reportResultsEiRKNS4_24ParameterToReportResultsERNS1_6Common11TransactionINS8_15DefaultResponseENS8_12IntrusivePtrINS8_18ResponseHeaderBaseEEEEE(void* self) { + (void)self; + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V18MatchApi17updateMatchDetailEiRKNS4_28ParameterToUpdateMatchDetailERNS1_6Common11TransactionINS8_15DefaultResponseENS8_12IntrusivePtrINS8_18ResponseHeaderBaseEEEEE(void* self) { + (void)self; + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V18MatchApi17updateMatchStatusEiRKNS4_28ParameterToUpdateMatchStatusERNS1_6Common11TransactionINS8_15DefaultResponseENS8_12IntrusivePtrINS8_18ResponseHeaderBaseEEEEE(void* self) { + (void)self; + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V18MatchApi20ParameterToJoinMatch10initializeEPNS1_6Common10LibContextEPKcNS6_12IntrusivePtrINS3_16JoinMatchRequestEEE(void* self, const std::uint64_t* context, const char* text, const IntrusivePtr* body) { + return InitializeMatchesParameter(self, context, true, text, 0x40, 0x20, 0x38, body); +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V18MatchApi20ParameterToJoinMatch9terminateEv(void* self) { + return TerminateMatchesParameter(self, true, 0x20); +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V18MatchApi20ParameterToJoinMatchC1Ev(void* self) { + *static_cast(self) = {}; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V18MatchApi20ParameterToJoinMatchD1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V18MatchApi21ParameterToLeaveMatch10initializeEPNS1_6Common10LibContextEPKcNS6_12IntrusivePtrINS3_17LeaveMatchRequestEEE(void* self, const std::uint64_t* context, const char* text, const IntrusivePtr* body) { + return InitializeMatchesParameter(self, context, true, text, 0x40, 0x20, 0x38, body); +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V18MatchApi21ParameterToLeaveMatch9terminateEv(void* self) { + return TerminateMatchesParameter(self, true, 0x20); +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V18MatchApi21ParameterToLeaveMatchC1Ev(void* self) { + *static_cast(self) = {}; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V18MatchApi21ParameterToLeaveMatchD1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V18MatchApi22ParameterToCreateMatch10initializeEPNS1_6Common10LibContextENS6_12IntrusivePtrINS3_18CreateMatchRequestEEE(void* self, const std::uint64_t* context, const IntrusivePtr* body) { + return InitializeMatchesParameter(self, context, false, nullptr, 0x20, 0x00, 0x18, body); +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V18MatchApi22ParameterToCreateMatch9terminateEv(void* self) { + return TerminateMatchesParameter(self, false, 0x00); +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V18MatchApi22ParameterToCreateMatchC1Ev(void* self) { + *static_cast(self) = {}; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V18MatchApi22ParameterToCreateMatchD1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V18MatchApi24ParameterToReportResults10initializeEPNS1_6Common10LibContextEPKcNS6_12IntrusivePtrINS3_20ReportResultsRequestEEE(void* self, const std::uint64_t* context, const char* text, const IntrusivePtr* body) { + return InitializeMatchesParameter(self, context, true, text, 0x40, 0x20, 0x38, body); +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V18MatchApi24ParameterToReportResults9terminateEv(void* self) { + return TerminateMatchesParameter(self, true, 0x20); +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V18MatchApi24ParameterToReportResultsC1Ev(void* self) { + *static_cast(self) = {}; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V18MatchApi24ParameterToReportResultsD1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V18MatchApi28ParameterToUpdateMatchDetail10initializeEPNS1_6Common10LibContextEPKcNS6_12IntrusivePtrINS3_24UpdateMatchDetailRequestEEE(void* self, const std::uint64_t* context, const char* text, const IntrusivePtr* body) { + return InitializeMatchesParameter(self, context, true, text, 0x40, 0x20, 0x38, body); +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V18MatchApi28ParameterToUpdateMatchDetailC1Ev(void* self) { + *static_cast(self) = {}; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V18MatchApi28ParameterToUpdateMatchDetailD1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V18MatchApi28ParameterToUpdateMatchStatus10initializeEPNS1_6Common10LibContextEPKcNS6_12IntrusivePtrINS3_24UpdateMatchStatusRequestEEE(void* self, const std::uint64_t* context, const char* text, const IntrusivePtr* body) { + return InitializeMatchesParameter(self, context, true, text, 0x40, 0x20, 0x38, body); +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V18MatchApi28ParameterToUpdateMatchStatus9terminateEv(void* self) { + return TerminateMatchesParameter(self, true, 0x20); +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V18MatchApi28ParameterToUpdateMatchStatusC1Ev(void* self) { + *static_cast(self) = {}; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V18MatchApi28ParameterToUpdateMatchStatusD1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V18MatchApi9joinMatchEiRKNS4_20ParameterToJoinMatchERNS1_6Common11TransactionINS8_15DefaultResponseENS8_12IntrusivePtrINS8_18ResponseHeaderBaseEEEEE(void* self) { + (void)self; + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +const void* APS5_VABI _ZNK3sce2Np9CppWebApi7Matches2V119CreateMatchResponse10getMatchIdEv(const void* self) { + return static_cast(self) + 0x30; +} + +bool APS5_VABI _ZNK3sce2Np9CppWebApi7Matches2V119RequestInGameRoster10teamsIsSetEv(const void* self) { + return *reinterpret_cast(static_cast(self) + 0x48) != 0; +} + +bool APS5_VABI _ZNK3sce2Np9CppWebApi7Matches2V119RequestInGameRoster12playersIsSetEv(const void* self) { + return *reinterpret_cast(static_cast(self) + 0x28) != 0; +} + + int APS5_VABI _ZNK3sce2Np9CppWebApi13InGameCatalog2V53Sku10priceIsSetEv() { NotImplemented_nid_no_patch(__func__); return 0; @@ -3958,9 +4564,9 @@ void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_13 AssignIntrusivePtr(self, source, 0x30, __func__); } -int APS5_VABI _ZN3sce2Np9CppWebApi6Common8IteratorINS2_6StringEEppEv() { - NotImplemented_nid_no_patch(__func__); - return 0; +CommonIterator* APS5_VABI _ZN3sce2Np9CppWebApi6Common8IteratorINS2_6StringEEppEv(CommonIterator* self) { + self->element = static_cast(self->element) + 1; + return self; } void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorImEEEC1Ev(IntrusivePtr* self) { @@ -3981,9 +4587,8 @@ void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorImEEE3getE return self->object; } -int APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorINS2_6StringEEneERKS5_() { - NotImplemented_nid_no_patch(__func__); - return 0; +bool APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorINS2_6StringEEneERKS5_(const CommonIterator* self, const CommonIterator* other) { + return self->element != other->element; } int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_15DefaultResponseENS2_12IntrusivePtrINS2_18ResponseHeaderBaseEEEE11hasResponseEv() { @@ -3994,9 +4599,10 @@ void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS2_6Stri return self->object; } -int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_6StringEE3endEv() { - NotImplemented_nid_no_patch(__func__); - return 0; +CommonIterator* APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_6StringEE3endEv(CommonIterator* iterator, const IntrusivePtr* self) { + iterator->referenceCount = 0; + iterator->element = StringVectorElements(self, true, __func__); + return iterator; } int APS5_VABI _ZN3sce2Np9CppWebApi6Common6StringeqEPKc(const CommonString* self, const char* other) { @@ -4011,9 +4617,10 @@ int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_15DefaultResponse return TransactionNotStarted; } -int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_6StringEE5beginEv() { - NotImplemented_nid_no_patch(__func__); - return 0; +CommonIterator* APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_6StringEE5beginEv(CommonIterator* iterator, const IntrusivePtr* self) { + iterator->referenceCount = 0; + iterator->element = StringVectorElements(self, false, __func__); + return iterator; } @@ -4021,9 +4628,8 @@ void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS2_6Stri return self->object; } -int APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorINS2_6StringEEdeEv() { - NotImplemented_nid_no_patch(__func__); - return 0; +CommonString* APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorINS2_6StringEEdeEv(const CommonIterator* self) { + return static_cast(self->element); } // -------------------------------------------------------------------------- @@ -6012,6 +6618,27 @@ std::size_t APS5_VABI _ZNK3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1 } +static constexpr int StringInvalidArgument = static_cast(0x80553503); +static constexpr int StringOutOfMemory = static_cast(0x80553504); + +static void ResetString(CommonString* self) { + std::free(self->buffer); + self->buffer = nullptr; + self->bufferSize = 0; +} + +static int AssignString(CommonString* self, const char* text) { + if (text == nullptr) return StringInvalidArgument; + const std::size_t length = std::strlen(text) + 1; + char* buffer = static_cast(std::malloc(length)); + if (buffer == nullptr) return StringOutOfMemory; + std::memcpy(buffer, text, length); + std::free(self->buffer); + self->buffer = buffer; + self->bufferSize = length; + return 0; +} + void APS5_VABI _ZN3sce2Np9CppWebApi6Common6StringC1EPNS2_10LibContextE(CommonString* self, void* libContext) { self->referenceCount = 0; self->buffer = nullptr; @@ -6019,10 +6646,62 @@ void APS5_VABI _ZN3sce2Np9CppWebApi6Common6StringC1EPNS2_10LibContextE(CommonStr self->libContext = libContext; } +void APS5_VABI _ZN3sce2Np9CppWebApi6Common6StringC1Ev(CommonString* self) { + _ZN3sce2Np9CppWebApi6Common6StringC1EPNS2_10LibContextE(self, nullptr); +} + void APS5_VABI _ZN3sce2Np9CppWebApi6Common6StringD1Ev(CommonString* self) { - std::free(self->buffer); - self->buffer = nullptr; - self->bufferSize = 0; + ResetString(self); +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common6StringD2Ev(CommonString* self) { + ResetString(self); +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6String8copyFromEPKc(CommonString* self, const char* text) { + return AssignString(self, text); +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6String8copyFromERKS3_(CommonString* self, const CommonString* source) { + const bool hadContext = self->libContext != nullptr; + if (!hadContext) self->libContext = source->libContext; + if (source->bufferSize == 0) { + ResetString(self); + return 0; + } + const int result = AssignString(self, source->buffer != nullptr ? source->buffer : ""); + if (!hadContext && result != 0) self->libContext = nullptr; + return result; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common6String5clearEv(CommonString* self) { + ResetString(self); +} + +std::uint64_t APS5_VABI _ZNK3sce2Np9CppWebApi6Common6String4sizeEv(const CommonString* self) { + return self->buffer != nullptr ? static_cast(std::strlen(self->buffer)) : 0; +} + +std::uint64_t APS5_VABI _ZNK3sce2Np9CppWebApi6Common6String6lengthEv(const CommonString* self) { + return self->buffer != nullptr ? static_cast(std::strlen(self->buffer)) : 0; +} + +bool APS5_VABI _ZN3sce2Np9CppWebApi6Common6StringeqERKS3_(const CommonString* self, const CommonString* other) { + const char* left = self->buffer != nullptr ? self->buffer : ""; + const char* right = other->buffer != nullptr ? other->buffer : ""; + return std::strncmp(left, right, std::strlen(left) + 1) == 0; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common6String10setContextEPNS2_10LibContextE(CommonString* self, void* libContext) { + self->libContext = libContext; +} + +std::int32_t APS5_VABI _ZN3sce2Np9CppWebApi6Common6String21intrusive_ptr_add_refEPS3_(CommonString* self) { + return std::atomic_ref(self->referenceCount).fetch_add(1) + 1; +} + +std::int32_t APS5_VABI _ZN3sce2Np9CppWebApi6Common6String21intrusive_ptr_sub_refEPS3_(CommonString* self) { + return std::atomic_ref(self->referenceCount).fetch_sub(1); } const char* APS5_VABI _ZNK3sce2Np9CppWebApi6Common6String5c_strEv(const CommonString* self) { @@ -6030,26 +6709,26 @@ const char* APS5_VABI _ZNK3sce2Np9CppWebApi6Common6String5c_strEv(const CommonSt } int APS5_VABI _ZN3sce2Np9CppWebApi6Common6String6appendEPKc(CommonString* self, const char* text) { - if (text == nullptr) return 0; + if (text == nullptr) return StringInvalidArgument; + if (*text == '\0') return 0; + const std::size_t oldLength = self->buffer != nullptr && self->bufferSize != 0 + ? static_cast(self->bufferSize - 1) : 0; const std::size_t textLength = std::strlen(text); - const std::size_t oldLength = self->buffer != nullptr ? std::strlen(self->buffer) : 0; - const std::size_t needed = oldLength + textLength + 1; - if (needed > self->bufferSize) { - std::size_t capacity = self->bufferSize != 0 ? static_cast(self->bufferSize) : 16; - while (capacity < needed) capacity *= 2; - char* buffer = static_cast(std::malloc(capacity)); - if (buffer == nullptr) return -1; - if (oldLength != 0) std::memcpy(buffer, self->buffer, oldLength); - std::memmove(buffer + oldLength, text, textLength + 1); - std::free(self->buffer); - self->buffer = buffer; - self->bufferSize = capacity; - return 0; - } - std::memmove(self->buffer + oldLength, text, textLength + 1); + const std::size_t newSize = oldLength + textLength + 1; + char* buffer = static_cast(std::malloc(newSize)); + if (buffer == nullptr) return StringOutOfMemory; + if (oldLength != 0) std::memcpy(buffer, self->buffer, oldLength); + std::memcpy(buffer + oldLength, text, textLength + 1); + std::free(self->buffer); + self->buffer = buffer; + self->bufferSize = newSize; return 0; } +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6String6appendERKS3_(CommonString* self, const CommonString* source) { + return _ZN3sce2Np9CppWebApi6Common6String6appendEPKc(self, source->buffer != nullptr ? source->buffer : ""); +} + void APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V126RequestPlayerSessionPlayerEEEED1Ev(IntrusivePtr* self) { ReleaseIntrusivePtr(self, 0x30, __func__); } @@ -6079,9 +6758,8 @@ void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS2_6String ReleaseIntrusivePtr(self, StringVectorReferenceCountOffset, __func__); } -int APS5_VABI _ZNK3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_11UserProfile2V113BasicPresenceEEEE4sizeEv() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::size_t APS5_VABI _ZNK3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_11UserProfile2V113BasicPresenceEEEE4sizeEv(const void* self) { + return HostVectorSize(self, __func__); } void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorImEEED1Ev(IntrusivePtr* self) { @@ -6095,4 +6773,90 @@ void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorImEEEaSERS6_ int APS5_VABI _ZNK3sce2Np9CppWebApi6Common6String5emptyEv(const CommonString* self) { return self->buffer == nullptr || self->buffer[0] == '\0'; } + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V115BasicProfileApi16getPublicProfileEiRKNS4_27ParameterToGetPublicProfileERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_24GetPublicProfileResponseEEENSA_INS8_18ResponseHeaderBaseEEEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V115BasicProfileApi27ParameterToGetPublicProfile10initializeEPNS1_6Common10LibContextEPKc() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V115BasicProfileApi27ParameterToGetPublicProfile9terminateEv() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V115BasicProfileApi27ParameterToGetPublicProfileC1Ev() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V115BasicProfileApi27ParameterToGetPublicProfileD1Ev() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V124GetPublicProfileResponseEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V124GetPublicProfileResponseEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(void* self) { + (void)self; + return 0; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V114PersonalDetailEEC1Ev(IntrusivePtr* self) { + *self = {}; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V124GetPublicProfileResponseEEC1Ev(IntrusivePtr* self) { + *self = {}; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V124GetPublicProfileResponseEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, 0x00, __func__); +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_11UserProfile2V16AvatarEEEEEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, 0x30, __func__); +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V124GetPublicProfileResponseEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V125GetPublicProfilesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE28setResponseInformationOptionEP37SceNpWebApi2ResponseInformationOption() { + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V124GetPublicProfileResponseEEENS4_INS2_18ResponseHeaderBaseEEEE11getResponseERS8_() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +bool APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V112BasicProfileEEcvbEv(const IntrusivePtr* self) { + return self->object != nullptr; +} + +bool APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V113BasicPresenceEEcvbEv(const IntrusivePtr* self) { + return self->object != nullptr; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V124GetPublicProfileResponseEEptEv(const IntrusivePtr* self) { + return self->object; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V16AvatarEEptEv(const IntrusivePtr* self) { + return self->object; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_11UserProfile2V112BasicProfileEEEEEEdeEv(const IntrusivePtr* self) { + return self->object; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_11UserProfile2V16AvatarEEEEEEdeEv(const IntrusivePtr* self) { + return self->object; +} + +std::size_t APS5_VABI _ZNK3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_11UserProfile2V112BasicProfileEEEE4sizeEv(const void* self) { + return HostVectorSize(self, __func__); +} } diff --git a/core/libs/prx/libSceNpCppWebApi/include/Api.hpp b/core/libs/prx/libSceNpCppWebApi/include/Api.hpp index c0479d51ef..a648f82777 100644 --- a/core/libs/prx/libSceNpCppWebApi/include/Api.hpp +++ b/core/libs/prx/libSceNpCppWebApi/include/Api.hpp @@ -15,6 +15,11 @@ struct IntrusivePtr { void* libContext; }; +struct CommonIterator { + std::int32_t referenceCount; + void* element; +}; + extern "C" { int APS5_VABI _ZN3sce2Np9CppWebApi10Activities2V114UserActivities11ActivityApi28ParameterToGetUserActivitiesC1Ev(void* self); int APS5_VABI _ZN3sce2Np9CppWebApi10Activities2V114UserActivities11ActivityApi28ParameterToGetUserActivitiesC2Ev(void* self); @@ -526,25 +531,25 @@ int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V149PostPlayerSessionsSession int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi28sendPlayerSessionInvitationsEiRKNS4_39ParameterToSendPlayerSessionInvitationsERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_50PostPlayerSessionsSessionIdInvitationsResponseBodyEEENSA_INS8_18ResponseHeaderBaseEEEEE(); int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi17getPlayerSessionsEiRKNS4_28ParameterToGetPlayerSessionsERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_29GetPlayerSessionsResponseBodyEEENSA_INS8_18ResponseHeaderBaseEEEEE(); int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V120RequestPlayerSession14setCustomData2EPKvm(); -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi49ParameterToAddPlayerSessionJoinableSpecifiedUsers10initializeEPNS1_6Common10LibContextEPKcNS6_12IntrusivePtrINS3_60PostPlayerSessionsSessionIdJoinableSpecifiedUsersRequestBodyEEE(); -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi37ParameterToSetPlayerSessionProperties10initializeEPNS1_6Common10LibContextEPKcNS6_12IntrusivePtrINS3_39PatchPlayerSessionsSessionIdRequestBodyEEE(); -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi36ParameterToChangePlayerSessionLeader9terminateEv(); -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi37ParameterToSetPlayerSessionPropertiesC1Ev(); -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi49ParameterToAddPlayerSessionJoinableSpecifiedUsers9terminateEv(); +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi49ParameterToAddPlayerSessionJoinableSpecifiedUsers10initializeEPNS1_6Common10LibContextEPKcNS6_12IntrusivePtrINS3_60PostPlayerSessionsSessionIdJoinableSpecifiedUsersRequestBodyEEE(void* self, const std::uint64_t* context, const char* sessionId, const IntrusivePtr* body); +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi37ParameterToSetPlayerSessionProperties10initializeEPNS1_6Common10LibContextEPKcNS6_12IntrusivePtrINS3_39PatchPlayerSessionsSessionIdRequestBodyEEE(void* self, const std::uint64_t* context, const char* sessionId, const IntrusivePtr* body); +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi36ParameterToChangePlayerSessionLeader9terminateEv(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi37ParameterToSetPlayerSessionPropertiesC1Ev(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi49ParameterToAddPlayerSessionJoinableSpecifiedUsers9terminateEv(void* self); int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V120RequestPlayerSession14setCustomData1EPKvm(); int APS5_VABI _ZNK3sce2Np9CppWebApi14SessionManager2V120PlayerSessionForRead16customData2IsSetEv(); int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V124GetBlockingUsersResponse9getBlocksEv(); -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi37ParameterToSetPlayerSessionPropertiesD1Ev(); -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi39ParameterToSendPlayerSessionInvitations10initializeEPNS1_6Common10LibContextEPKcNS6_12IntrusivePtrINS3_49PostPlayerSessionsSessionIdInvitationsRequestBodyEEE(); +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi37ParameterToSetPlayerSessionPropertiesD1Ev(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi39ParameterToSendPlayerSessionInvitations10initializeEPNS1_6Common10LibContextEPKcNS6_12IntrusivePtrINS3_49PostPlayerSessionsSessionIdInvitationsRequestBodyEEE(void* self, const std::uint64_t* context, const char* sessionId, const IntrusivePtr* body); int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V146PatchPlayerSessionsSessionIdRequestBodyFactory6createEPNS1_6Common10LibContextEPNS5_12IntrusivePtrINS3_39PatchPlayerSessionsSessionIdRequestBodyEEE(); int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V137RequestPlayerSessionInvitationFactory6createEPNS1_6Common10LibContextENS5_12IntrusivePtrINS3_2ToEEEPNS8_INS3_30RequestPlayerSessionInvitationEEE(); int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V139PatchPlayerSessionsSessionIdRequestBody19setJoinableUserTypeERKNS3_22CustomJoinableUserTypeE(); int APS5_VABI _ZNK3sce2Np9CppWebApi14SessionManager2V127ResponsePlayerSessionPlayer11getPlatformEv(); int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V125GetBasicPresencesResponse17getBasicPresencesEv(); int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi28ParameterToGetPlayerSessions9setfieldsENS1_6Common12IntrusivePtrINS6_6VectorINS6_6StringEEEEE(); -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi28ParameterToGetPlayerSessions9terminateEv(); +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi28ParameterToGetPlayerSessions9terminateEv(void* self); int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi29ParameterToLeavePlayerSession9terminateEv(void* self); -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi39ParameterToSendPlayerSessionInvitationsD1Ev(); +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi39ParameterToSendPlayerSessionInvitationsD1Ev(void* self); int APS5_VABI _ZNK3sce2Np9CppWebApi14SessionManager2V120PlayerSessionForRead14getCustomData1Ev(); int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V19BlocksApi16getBlockingUsersEiRKNS4_27ParameterToGetBlockingUsersERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_24GetBlockingUsersResponseEEENSA_INS8_18ResponseHeaderBaseEEEEE(); int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V19BlocksApi27ParameterToGetBlockingUsers10initializeEPNS1_6Common10LibContextE(void* self, const std::uint64_t* context); @@ -554,31 +559,31 @@ int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi31Parame int APS5_VABI _ZNK3sce2Np9CppWebApi14SessionManager2V120PlayerSessionForRead16customData1IsSetEv(); int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi38addPlayerSessionJoinableSpecifiedUsersEiRKNS4_49ParameterToAddPlayerSessionJoinableSpecifiedUsersERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_61PostPlayerSessionsSessionIdJoinableSpecifiedUsersResponseBodyEEENSA_INS8_18ResponseHeaderBaseEEEEE(); int APS5_VABI _ZNK3sce2Np9CppWebApi14SessionManager2V120PlayerSessionForRead9getLeaderEv(); -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi39ParameterToSendPlayerSessionInvitationsC1Ev(); -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi28ParameterToGetPlayerSessionsD1Ev(); +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi39ParameterToSendPlayerSessionInvitationsC1Ev(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi28ParameterToGetPlayerSessionsD1Ev(void* self); int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V120PlayerSessionForRead28getExclusiveLeaderPrivilegesEv(); int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi26setPlayerSessionPropertiesEiRKNS4_37ParameterToSetPlayerSessionPropertiesERNS1_6Common11TransactionINS8_15DefaultResponseENS8_12IntrusivePtrINS8_18ResponseHeaderBaseEEEEE(); int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi25changePlayerSessionLeaderEiRKNS4_36ParameterToChangePlayerSessionLeaderERNS1_6Common11TransactionINS8_15DefaultResponseENS8_12IntrusivePtrINS8_18ResponseHeaderBaseEEEEE(); int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V150PutPlayerSessionsSessionIdLeaderRequestBodyFactory6createEPNS1_6Common10LibContextERKNS_4Json5ValueEPNS5_12IntrusivePtrINS3_43PutPlayerSessionsSessionIdLeaderRequestBodyEEE(); int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V119JoinableUserFactory6createEPNS1_6Common10LibContextERKmPNS5_12IntrusivePtrINS3_12JoinableUserEEE(); -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi36ParameterToChangePlayerSessionLeaderD1Ev(); +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi36ParameterToChangePlayerSessionLeaderD1Ev(void* self); int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V156PostPlayerSessionsSessionIdInvitationsRequestBodyFactory6createEPNS1_6Common10LibContextEPNS5_12IntrusivePtrINS3_49PostPlayerSessionsSessionIdInvitationsRequestBodyEEE(); -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi39ParameterToSendPlayerSessionInvitations9terminateEv(); -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi28ParameterToGetPlayerSessions10initializeEPNS1_6Common10LibContextEPKc(); -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi28ParameterToGetPlayerSessionsC1Ev(); +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi39ParameterToSendPlayerSessionInvitations9terminateEv(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi28ParameterToGetPlayerSessions10initializeEPNS1_6Common10LibContextEPKc(void* self, const std::uint64_t* context, const char* sessionIds); +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi28ParameterToGetPlayerSessionsC1Ev(void* self); int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V133ResponsePlayerSessionMemberPlayer10getPlayersEv(); int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V139PatchPlayerSessionsSessionIdRequestBody28setExclusiveLeaderPrivilegesERKNS1_6Common6VectorINS5_6StringEEE(); int APS5_VABI _ZNK3sce2Np9CppWebApi14SessionManager2V113PlayerSession9getMemberEv(); int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V19ToFactory6createEPNS1_6Common10LibContextERKmPNS5_12IntrusivePtrINS3_2ToEEE(); -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi49ParameterToAddPlayerSessionJoinableSpecifiedUsersD1Ev(); -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi36ParameterToChangePlayerSessionLeader10initializeEPNS1_6Common10LibContextEPKcNS6_12IntrusivePtrINS3_43PutPlayerSessionsSessionIdLeaderRequestBodyEEE(); +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi49ParameterToAddPlayerSessionJoinableSpecifiedUsersD1Ev(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi36ParameterToChangePlayerSessionLeader10initializeEPNS1_6Common10LibContextEPKcNS6_12IntrusivePtrINS3_43PutPlayerSessionsSessionIdLeaderRequestBodyEEE(void* self, const std::uint64_t* context, const char* sessionId, const IntrusivePtr* body); int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V167PostPlayerSessionsSessionIdJoinableSpecifiedUsersRequestBodyFactory6createEPNS1_6Common10LibContextERKNS5_6VectorINS5_12IntrusivePtrINS3_12JoinableUserEEEEEPNS9_INS3_60PostPlayerSessionsSessionIdJoinableSpecifiedUsersRequestBodyEEE(); -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi36ParameterToChangePlayerSessionLeaderC1Ev(); -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi37ParameterToSetPlayerSessionProperties9terminateEv(); +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi36ParameterToChangePlayerSessionLeaderC1Ev(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi37ParameterToSetPlayerSessionProperties9terminateEv(void* self); int APS5_VABI _ZNK3sce2Np9CppWebApi14SessionManager2V118LeaderWithOnlineId12getAccountIdEv(); int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V139PatchPlayerSessionsSessionIdRequestBody20setInvitableUserTypeERKNS3_23CustomInvitableUserTypeE(); int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V129GetPlayerSessionsResponseBody17getPlayerSessionsEv(); -int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi49ParameterToAddPlayerSessionJoinableSpecifiedUsersC1Ev(); +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi49ParameterToAddPlayerSessionJoinableSpecifiedUsersC1Ev(void* self); int APS5_VABI _ZNK3sce2Np9CppWebApi14SessionManager2V120PlayerSessionForRead14getCustomData2Ev(); int APS5_VABI _ZNK3sce2Np9CppWebApi14SessionManager2V127ResponsePlayerSessionPlayer12getAccountIdEv(); int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_14SessionManager2V130PostPlayerSessionsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE11hasResponseEv(); @@ -796,21 +801,21 @@ void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_ int APS5_VABI _ZN3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_13InGameCatalog2V53SkuEEEEppEv(); int APS5_VABI _ZNK3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_13InGameCatalog2V59ContainerEEEE5emptyEv(const IntrusivePtr* self); void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_13InGameCatalog2V59ContainerEEEEEEaSERKSA_(IntrusivePtr* self, const IntrusivePtr* source); -int APS5_VABI _ZN3sce2Np9CppWebApi6Common8IteratorINS2_6StringEEppEv(); +CommonIterator* APS5_VABI _ZN3sce2Np9CppWebApi6Common8IteratorINS2_6StringEEppEv(CommonIterator* self); void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorImEEEC1Ev(IntrusivePtr* self); int APS5_VABI _ZN3sce2Np9CppWebApi6Common13ConstIteratorINS2_6StringEED2Ev(void* self); int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_6StringEE8copyFromERKS5_(); void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorImEEE3getEv(const IntrusivePtr* self); -int APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorINS2_6StringEEneERKS5_(); +bool APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorINS2_6StringEEneERKS5_(const CommonIterator* self, const CommonIterator* other); int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_15DefaultResponseENS2_12IntrusivePtrINS2_18ResponseHeaderBaseEEEE11hasResponseEv(); void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS2_6StringEEEEptEv(const IntrusivePtr* self); -int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_6StringEE3endEv(); +CommonIterator* APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_6StringEE3endEv(CommonIterator* iterator, const IntrusivePtr* self); int APS5_VABI _ZN3sce2Np9CppWebApi6Common6StringeqEPKc(const CommonString* self, const char* other); std::int64_t APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_15DefaultResponseENS2_12IntrusivePtrINS2_18ResponseHeaderBaseEEEE5getIdEv(); int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_15DefaultResponseENS2_12IntrusivePtrINS2_18ResponseHeaderBaseEEEE5abortEv(); -int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_6StringEE5beginEv(); +CommonIterator* APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_6StringEE5beginEv(CommonIterator* iterator, const IntrusivePtr* self); void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS2_6StringEEEE3getEv(const IntrusivePtr* self); -int APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorINS2_6StringEEdeEv(); +CommonString* APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorINS2_6StringEEdeEv(const CommonIterator* self); int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V115BasicProfileApi28ParameterToGetPublicProfilesD1Ev(); int APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_11UserProfile2V112BasicProfileEEEEdeEv(); @@ -829,6 +834,95 @@ void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_11 void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEED1Ev(IntrusivePtr* self); int APS5_VABI _ZNK3sce2Np9CppWebApi11UserProfile2V112BasicProfile17getPersonalDetailEv(); int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEE11getResponseERS8_(); + +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V111AddedPlayer12setAccountIdEPKc(void* self, const char* value); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V111AddedPlayer13setPlayerNameEPKc(void* self, const char* value); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V111AddedPlayer9setTeamIdEPKc(void* self, const char* value); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V113RemovedPlayer9setReasonERKNS3_11LeaveReasonE(void* self, const int* value); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V116JoinMatchRequest17setNpServiceLabelERKi(void* self, const int* value); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V116RequestMatchTeam10setMembersERKNS1_6Common6VectorINS5_12IntrusivePtrINS3_13RequestMemberEEEEE(void* self, const IntrusivePtr* value); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V116RequestMatchTeam11setTeamNameEPKc(void* self, const char* value); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V117LeaveMatchRequest17setNpServiceLabelERKi(void* self, const int* value); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V118AddedPlayerFactory6createEPNS1_6Common10LibContextEPKcNS3_10PlayerTypeEPNS5_12IntrusivePtrINS3_11AddedPlayerEEE(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V118CreateMatchRequest15setInGameRosterERKNS1_6Common12IntrusivePtrINS3_19RequestInGameRosterEEE(void* self, const IntrusivePtr* value); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V118CreateMatchRequest17setExpirationTimeERKi(void* self, const int* value); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V118CreateMatchRequest17setNpServiceLabelERKi(void* self, const int* value); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V118CreateMatchRequest9setZoneIdEPKc(void* self, const char* value); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V118RequestMatchPlayer12setAccountIdEPKc(void* self, const char* value); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V118RequestMatchPlayer13setPlayerNameEPKc(void* self, const char* value); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V118RequestTeamResults20setTeamMemberResultsERKNS1_6Common6VectorINS5_12IntrusivePtrINS3_23RequestTeamMemberResultEEEEE(void* self, const IntrusivePtr* value); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V118RequestTeamResults8setScoreERKd(void* self, const double* value); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V119RequestInGameRoster10setPlayersERKNS1_6Common6VectorINS5_12IntrusivePtrINS3_18RequestMatchPlayerEEEEE(void* self, const IntrusivePtr* value); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V119RequestInGameRoster8setTeamsERKNS1_6Common6VectorINS5_12IntrusivePtrINS3_16RequestMatchTeamEEEEE(void* self, const IntrusivePtr* value); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V119RequestMatchResults20setCompetitiveResultERKNS1_6Common12IntrusivePtrINS3_24RequestCompetitiveResultEEE(void* self, const IntrusivePtr* value); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V119RequestMatchResults20setCooperativeResultERKNS3_24RequestCooperativeResultE(void* self, const int* value); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V120RemovedPlayerFactory6createEPNS1_6Common10LibContextEPKcPNS5_12IntrusivePtrINS3_13RemovedPlayerEEE(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V120ReportResultsRequest17setNpServiceLabelERKi(void* self, const int* value); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V120ReportResultsRequest18setMatchStatisticsERKNS1_6Common12IntrusivePtrINS3_22RequestMatchStatisticsEEE(void* self, const IntrusivePtr* value); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V120RequestMemberFactory6createEPNS1_6Common10LibContextEPKcPNS5_12IntrusivePtrINS3_13RequestMemberEEE(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V120RequestPlayerResults8setScoreERKd(void* self, const double* value); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V120RequestTeamStatistic23setTeamMemberStatisticsERKNS1_6Common6VectorINS5_12IntrusivePtrINS3_26RequestTeamMemberStatisticEEEEE(void* self, const IntrusivePtr* value); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V120RequestTeamStatistic8setStatsERKNS1_6Common6VectorINS5_12IntrusivePtrINS3_19AdditionalStatisticEEEEE(void* self, const IntrusivePtr* value); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V122RequestMatchStatistics17setTeamStatisticsERKNS1_6Common6VectorINS5_12IntrusivePtrINS3_20RequestTeamStatisticEEEEE(void* self, const IntrusivePtr* value); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V122RequestMatchStatistics19setPlayerStatisticsERKNS1_6Common6VectorINS5_12IntrusivePtrINS3_22RequestPlayerStatisticEEEEE(void* self, const IntrusivePtr* value); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V123JoinMatchRequestFactory6createEPNS1_6Common10LibContextERKNS5_6VectorINS5_12IntrusivePtrINS3_11AddedPlayerEEEEEPNS9_INS3_16JoinMatchRequestEEE(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V123RequestMatchTeamFactory6createEPNS1_6Common10LibContextEPKcPNS5_12IntrusivePtrINS3_16RequestMatchTeamEEE(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V124LeaveMatchRequestFactory6createEPNS1_6Common10LibContextERKNS5_6VectorINS5_12IntrusivePtrINS3_13RemovedPlayerEEEEEPNS9_INS3_17LeaveMatchRequestEEE(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V124RequestCompetitiveResult14setTeamResultsERKNS1_6Common6VectorINS5_12IntrusivePtrINS3_18RequestTeamResultsEEEEE(void* self, const IntrusivePtr* value); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V124RequestCompetitiveResult16setPlayerResultsERKNS1_6Common6VectorINS5_12IntrusivePtrINS3_20RequestPlayerResultsEEEEE(void* self, const IntrusivePtr* value); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V124UpdateMatchDetailRequest15setInGameRosterERKNS1_6Common12IntrusivePtrINS3_19RequestInGameRosterEEE(void* self, const IntrusivePtr* value); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V124UpdateMatchDetailRequest17setExpirationTimeERKi(void* self, const int* value); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V124UpdateMatchDetailRequest17setNpServiceLabelERKi(void* self, const int* value); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V124UpdateMatchDetailRequest9setZoneIdEPKc(void* self, const char* value); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V124UpdateMatchStatusRequest17setNpServiceLabelERKi(void* self, const int* value); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V125CreateMatchRequestFactory6createEPNS1_6Common10LibContextEPKcPNS5_12IntrusivePtrINS3_18CreateMatchRequestEEE(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V125RequestMatchPlayerFactory6createEPNS1_6Common10LibContextEPKcNS3_10PlayerTypeEPNS5_12IntrusivePtrINS3_18RequestMatchPlayerEEE(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V125RequestTeamResultsFactory6createEPNS1_6Common10LibContextEPKciPNS5_12IntrusivePtrINS3_18RequestTeamResultsEEE(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V126AdditionalStatisticFactory6createEPNS1_6Common10LibContextEPKcS9_PNS5_12IntrusivePtrINS3_19AdditionalStatisticEEE(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V126RequestInGameRosterFactory6createEPNS1_6Common10LibContextEPNS5_12IntrusivePtrINS3_19RequestInGameRosterEEE(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V126RequestMatchResultsFactory6createEPNS1_6Common10LibContextENS3_14ResultsVersionEPNS5_12IntrusivePtrINS3_19RequestMatchResultsEEE(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V127ReportResultsRequestFactory6createEPNS1_6Common10LibContextENS5_12IntrusivePtrINS3_19RequestMatchResultsEEEPNS8_INS3_20ReportResultsRequestEEE(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V127RequestPlayerResultsFactory6createEPNS1_6Common10LibContextEPKciPNS5_12IntrusivePtrINS3_20RequestPlayerResultsEEE(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V127RequestTeamStatisticFactory6createEPNS1_6Common10LibContextEPKcPNS5_12IntrusivePtrINS3_20RequestTeamStatisticEEE(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V129RequestMatchStatisticsFactory6createEPNS1_6Common10LibContextEPNS5_12IntrusivePtrINS3_22RequestMatchStatisticsEEE(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V129RequestPlayerStatisticFactory6createEPNS1_6Common10LibContextEPKcRKNS5_6VectorINS5_12IntrusivePtrINS3_19AdditionalStatisticEEEEEPNSB_INS3_22RequestPlayerStatisticEEE(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V130RequestTeamMemberResultFactory6createEPNS1_6Common10LibContextEPKcdPNS5_12IntrusivePtrINS3_23RequestTeamMemberResultEEE(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V131RequestCompetitiveResultFactory6createEPNS1_6Common10LibContextEPNS5_12IntrusivePtrINS3_24RequestCompetitiveResultEEE(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V131UpdateMatchDetailRequestFactory6createEPNS1_6Common10LibContextEPNS5_12IntrusivePtrINS3_24UpdateMatchDetailRequestEEE(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V131UpdateMatchStatusRequestFactory6createEPNS1_6Common10LibContextENS3_12UpdateStatusEPNS5_12IntrusivePtrINS3_24UpdateMatchStatusRequestEEE(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V133RequestTeamMemberStatisticFactory6createEPNS1_6Common10LibContextEPKcRKNS5_6VectorINS5_12IntrusivePtrINS3_19AdditionalStatisticEEEEEPNSB_INS3_26RequestTeamMemberStatisticEEE(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V18MatchApi10leaveMatchEiRKNS4_21ParameterToLeaveMatchERNS1_6Common11TransactionINS8_15DefaultResponseENS8_12IntrusivePtrINS8_18ResponseHeaderBaseEEEEE(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V18MatchApi11createMatchEiRKNS4_22ParameterToCreateMatchERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_19CreateMatchResponseEEENSA_INS8_18ResponseHeaderBaseEEEEE(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V18MatchApi13reportResultsEiRKNS4_24ParameterToReportResultsERNS1_6Common11TransactionINS8_15DefaultResponseENS8_12IntrusivePtrINS8_18ResponseHeaderBaseEEEEE(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V18MatchApi17updateMatchDetailEiRKNS4_28ParameterToUpdateMatchDetailERNS1_6Common11TransactionINS8_15DefaultResponseENS8_12IntrusivePtrINS8_18ResponseHeaderBaseEEEEE(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V18MatchApi17updateMatchStatusEiRKNS4_28ParameterToUpdateMatchStatusERNS1_6Common11TransactionINS8_15DefaultResponseENS8_12IntrusivePtrINS8_18ResponseHeaderBaseEEEEE(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V18MatchApi20ParameterToJoinMatch10initializeEPNS1_6Common10LibContextEPKcNS6_12IntrusivePtrINS3_16JoinMatchRequestEEE(void* self, const std::uint64_t* context, const char* text, const IntrusivePtr* body); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V18MatchApi20ParameterToJoinMatch9terminateEv(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V18MatchApi20ParameterToJoinMatchC1Ev(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V18MatchApi20ParameterToJoinMatchD1Ev(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V18MatchApi21ParameterToLeaveMatch10initializeEPNS1_6Common10LibContextEPKcNS6_12IntrusivePtrINS3_17LeaveMatchRequestEEE(void* self, const std::uint64_t* context, const char* text, const IntrusivePtr* body); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V18MatchApi21ParameterToLeaveMatch9terminateEv(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V18MatchApi21ParameterToLeaveMatchC1Ev(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V18MatchApi21ParameterToLeaveMatchD1Ev(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V18MatchApi22ParameterToCreateMatch10initializeEPNS1_6Common10LibContextENS6_12IntrusivePtrINS3_18CreateMatchRequestEEE(void* self, const std::uint64_t* context, const IntrusivePtr* body); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V18MatchApi22ParameterToCreateMatch9terminateEv(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V18MatchApi22ParameterToCreateMatchC1Ev(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V18MatchApi22ParameterToCreateMatchD1Ev(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V18MatchApi24ParameterToReportResults10initializeEPNS1_6Common10LibContextEPKcNS6_12IntrusivePtrINS3_20ReportResultsRequestEEE(void* self, const std::uint64_t* context, const char* text, const IntrusivePtr* body); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V18MatchApi24ParameterToReportResults9terminateEv(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V18MatchApi24ParameterToReportResultsC1Ev(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V18MatchApi24ParameterToReportResultsD1Ev(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V18MatchApi28ParameterToUpdateMatchDetail10initializeEPNS1_6Common10LibContextEPKcNS6_12IntrusivePtrINS3_24UpdateMatchDetailRequestEEE(void* self, const std::uint64_t* context, const char* text, const IntrusivePtr* body); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V18MatchApi28ParameterToUpdateMatchDetailC1Ev(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V18MatchApi28ParameterToUpdateMatchDetailD1Ev(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V18MatchApi28ParameterToUpdateMatchStatus10initializeEPNS1_6Common10LibContextEPKcNS6_12IntrusivePtrINS3_24UpdateMatchStatusRequestEEE(void* self, const std::uint64_t* context, const char* text, const IntrusivePtr* body); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V18MatchApi28ParameterToUpdateMatchStatus9terminateEv(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V18MatchApi28ParameterToUpdateMatchStatusC1Ev(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V18MatchApi28ParameterToUpdateMatchStatusD1Ev(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi7Matches2V18MatchApi9joinMatchEiRKNS4_20ParameterToJoinMatchERNS1_6Common11TransactionINS8_15DefaultResponseENS8_12IntrusivePtrINS8_18ResponseHeaderBaseEEEEE(void* self); +const void* APS5_VABI _ZNK3sce2Np9CppWebApi7Matches2V119CreateMatchResponse10getMatchIdEv(const void* self); +bool APS5_VABI _ZNK3sce2Np9CppWebApi7Matches2V119RequestInGameRoster10teamsIsSetEv(const void* self); +bool APS5_VABI _ZNK3sce2Np9CppWebApi7Matches2V119RequestInGameRoster12playersIsSetEv(const void* self); int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V125GetPublicProfilesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE(); int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V115BasicProfileApi17getPublicProfilesEiRKNS4_28ParameterToGetPublicProfilesERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_25GetPublicProfilesResponseEEENSA_INS8_18ResponseHeaderBaseEEEEE(); int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V113BasicPresence12setInContextERKb(); @@ -1257,8 +1351,41 @@ void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS2_6String void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS2_6StringEEEEC1EPS6_PFvS8_EPNS2_10LibContextE(IntrusivePtr* self, void* body, void (APS5_VABI* deleter)(void*), void* libContext); void APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V126RequestPlayerSessionPlayerEEEEC1EPNS2_10LibContextE(IntrusivePtr* self, void* libContext); void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS2_6StringEEEED1Ev(IntrusivePtr* self); -int APS5_VABI _ZNK3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_11UserProfile2V113BasicPresenceEEEE4sizeEv(); +std::size_t APS5_VABI _ZNK3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_11UserProfile2V113BasicPresenceEEEE4sizeEv(const void* self); void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorImEEED1Ev(IntrusivePtr* self); void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorImEEEaSERS6_(IntrusivePtr* self, const IntrusivePtr* source); int APS5_VABI _ZNK3sce2Np9CppWebApi6Common6String5emptyEv(const CommonString* self); +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V115BasicProfileApi16getPublicProfileEiRKNS4_27ParameterToGetPublicProfileERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_24GetPublicProfileResponseEEENSA_INS8_18ResponseHeaderBaseEEEEE(); +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V115BasicProfileApi27ParameterToGetPublicProfile10initializeEPNS1_6Common10LibContextEPKc(); +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V115BasicProfileApi27ParameterToGetPublicProfile9terminateEv(); +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V115BasicProfileApi27ParameterToGetPublicProfileC1Ev(); +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V115BasicProfileApi27ParameterToGetPublicProfileD1Ev(); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V124GetPublicProfileResponseEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V124GetPublicProfileResponseEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(void* self); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V114PersonalDetailEEC1Ev(IntrusivePtr* self); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V124GetPublicProfileResponseEEC1Ev(IntrusivePtr* self); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V124GetPublicProfileResponseEED1Ev(IntrusivePtr* self); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_11UserProfile2V16AvatarEEEEEED1Ev(IntrusivePtr* self); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V124GetPublicProfileResponseEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE(); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V125GetPublicProfilesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE28setResponseInformationOptionEP37SceNpWebApi2ResponseInformationOption(); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V124GetPublicProfileResponseEEENS4_INS2_18ResponseHeaderBaseEEEE11getResponseERS8_(); +bool APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V112BasicProfileEEcvbEv(const IntrusivePtr* self); +bool APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V113BasicPresenceEEcvbEv(const IntrusivePtr* self); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V124GetPublicProfileResponseEEptEv(const IntrusivePtr* self); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V16AvatarEEptEv(const IntrusivePtr* self); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_11UserProfile2V112BasicProfileEEEEEEdeEv(const IntrusivePtr* self); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_11UserProfile2V16AvatarEEEEEEdeEv(const IntrusivePtr* self); +std::size_t APS5_VABI _ZNK3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_11UserProfile2V112BasicProfileEEEE4sizeEv(const void* self); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common6StringC1Ev(CommonString* self); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common6StringD2Ev(CommonString* self); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6String8copyFromEPKc(CommonString* self, const char* text); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6String8copyFromERKS3_(CommonString* self, const CommonString* source); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common6String5clearEv(CommonString* self); +std::uint64_t APS5_VABI _ZNK3sce2Np9CppWebApi6Common6String4sizeEv(const CommonString* self); +std::uint64_t APS5_VABI _ZNK3sce2Np9CppWebApi6Common6String6lengthEv(const CommonString* self); +bool APS5_VABI _ZN3sce2Np9CppWebApi6Common6StringeqERKS3_(const CommonString* self, const CommonString* other); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common6String10setContextEPNS2_10LibContextE(CommonString* self, void* libContext); +std::int32_t APS5_VABI _ZN3sce2Np9CppWebApi6Common6String21intrusive_ptr_add_refEPS3_(CommonString* self); +std::int32_t APS5_VABI _ZN3sce2Np9CppWebApi6Common6String21intrusive_ptr_sub_refEPS3_(CommonString* self); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6String6appendERKS3_(CommonString* self, const CommonString* source); } diff --git a/core/libs/prx/libSceNpEntitlementAccess/Export.cpp b/core/libs/prx/libSceNpEntitlementAccess/Export.cpp index bb2e86cb72..5d6b2ecf93 100644 --- a/core/libs/prx/libSceNpEntitlementAccess/Export.cpp +++ b/core/libs/prx/libSceNpEntitlementAccess/Export.cpp @@ -4,10 +4,13 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include +#include #include #include #include #include +#include +#include #include #include #include @@ -79,6 +82,41 @@ const std::vector& OwnedAddons() { return owned; } +std::string PrintableLabel(const std::string& label) { + std::string printable; + for (const unsigned char byte : label) { + if (byte >= 0x20 && byte < 0x7f && byte != '\\') { + printable += static_cast(byte); + continue; + } + char escaped[5]; + std::snprintf(escaped, sizeof(escaped), "\\x%02x", byte); + printable += escaped; + } + return printable; +} + +std::string EntitlementsName() { + if (const auto* configured = ConfiguredEntitlements(); configured != nullptr && configured[0] != 0) return EntitlementsPath().string(); + return "anyps5-entitlements.ini"; +} + +void ReportUnlisted(const std::string& label) { + constexpr std::size_t reportLimit = 64; + static std::mutex reportMutex; + static std::set reported; + static bool capped = false; + std::lock_guard lock(reportMutex); + if (reported.contains(label)) return; + if (reported.size() < reportLimit) { + reported.insert(label); + std::fprintf(stderr, "[entitlements] the title asked for the add-on %s, which %s does not list, so it is reported as not installed\n", PrintableLabel(label).c_str(), EntitlementsName().c_str()); + } else if (!capped) { + capped = true; + std::fprintf(stderr, "[entitlements] the title asked for more than %zu add-ons that %s does not list; further ones are reported as not installed without a line\n", reportLimit, EntitlementsName().c_str()); + } +} + } extern "C" { @@ -92,6 +130,7 @@ int APS5_VABI sceNpEntitlementAccessGetAddcontEntitlementInfo(uint32_t service_l return 0; } } + ReportUnlisted(std::string(entitlement_label->data, strnlen(entitlement_label->data, sizeof(entitlement_label->data)))); return SCE_NP_ENTITLEMENT_ACCESS_ERROR_NOT_FOUND; } diff --git a/core/libs/prx/libSceNpGameIntent/Export.cpp b/core/libs/prx/libSceNpGameIntent/Export.cpp index 7d0ac8f3be..187a1cc748 100644 --- a/core/libs/prx/libSceNpGameIntent/Export.cpp +++ b/core/libs/prx/libSceNpGameIntent/Export.cpp @@ -1,3 +1,4 @@ +#include "prx/libSceNpGameIntent/include/InventoryExports.hpp" #include "prx/libSceNpGameIntent/include/Api.hpp" #include #include @@ -31,6 +32,27 @@ std::atomic_bool& AppLauncherInitialized() { extern "C" { +int APS5_VABI sceNpGameIntentGetPermittedIntentTypes() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceNpGameIntentLaunchAppForPinvoke() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceNpGameIntentLaunchAppRequestToGkp() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceNpGameIntentRemoveLaunchCallback() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceNpGameIntentRequest() { + NotImplemented_nid_no_patch(__func__); +} + + int APS5_VABI sceNpGameIntentGetPropertyValueString(const NpGameIntentData* intentData, const char* key, char* valueBuf, size_t bufSize) { if (!NpGameIntentInitialized()) { return NpGameIntentErrorNotInitialized; diff --git a/core/libs/prx/libSceNpGameIntent/include/InventoryExports.hpp b/core/libs/prx/libSceNpGameIntent/include/InventoryExports.hpp new file mode 100644 index 0000000000..9dd3fc1288 --- /dev/null +++ b/core/libs/prx/libSceNpGameIntent/include/InventoryExports.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include "SceTypes.hpp" + +extern "C" { + +int APS5_VABI sceNpGameIntentGetPermittedIntentTypes(); +int APS5_VABI sceNpGameIntentLaunchAppForPinvoke(); +int APS5_VABI sceNpGameIntentLaunchAppRequestToGkp(); +int APS5_VABI sceNpGameIntentRemoveLaunchCallback(); +int APS5_VABI sceNpGameIntentRequest(); + +} diff --git a/core/libs/prx/libSceNpGameIntent/libSceNpGameIntent.hpp b/core/libs/prx/libSceNpGameIntent/libSceNpGameIntent.hpp index 7c0f3b8b2e..591063b314 100644 --- a/core/libs/prx/libSceNpGameIntent/libSceNpGameIntent.hpp +++ b/core/libs/prx/libSceNpGameIntent/libSceNpGameIntent.hpp @@ -1,3 +1,4 @@ #pragma once #include "prx/libSceNpGameIntent/include/Api.hpp" +#include "prx/libSceNpGameIntent/include/InventoryExports.hpp" diff --git a/core/libs/prx/libSceNpManager/Export.cpp b/core/libs/prx/libSceNpManager/Export.cpp index 9d38ebc1c8..5611a62f1a 100644 --- a/core/libs/prx/libSceNpManager/Export.cpp +++ b/core/libs/prx/libSceNpManager/Export.cpp @@ -1,6 +1,7 @@ #include "prx/libSceNpManager/include/LegacyExports.hpp" #include "prx/libc/include/general/ExportMacros.hpp" #include "prx/libSceNpManager/include/Api.hpp" +#include "prx/libSceNpManager/include/InGameMessageIds.hpp" #include #include #include "SceTypes.hpp" @@ -10,6 +11,8 @@ #include #include #include +#include +#include #include // PSN is not emulated: the user is reported as signed out and online queries fail. @@ -36,6 +39,25 @@ static constexpr uint32_t NP_GAME_PRESENCE_STATUS_OFFLINE = 0; static std::atomic g_nextRequest{1}; +static constexpr int NP_IN_GAME_MESSAGE_ERROR_LIB_CONTEXT_NOT_FOUND = static_cast(0x80552B03); +static constexpr int NP_IN_GAME_MESSAGE_ERROR_HANDLE_NOT_FOUND = static_cast(0x80552B05); + +namespace { +struct InGameMessageContext { + std::set handles; +}; + +std::mutex inGameMessageMutex; +std::map inGameMessageContexts; +std::int32_t nextInGameMessageContext = 1; +std::int32_t nextInGameMessageHandle = 1; + +InGameMessageContext* FindInGameMessageContext(std::int32_t libCtxId) { + const auto found = inGameMessageContexts.find(libCtxId); + return found == inGameMessageContexts.end() ? nullptr : &found->second; +} +} + namespace { std::mutex reachabilityMutex; void* reachabilityCallback = nullptr; @@ -341,9 +363,11 @@ int APS5_VABI sceNpIdMapperOnlineIdToAccountId(int reqId, int userId, const void if (userId == USER_SERVICE_USER_ID_INVALID || !input || !output || value < 1 || value > 1000) return NP_ID_MAPPER_REFUSED_ARGUMENT; return SCE_NP_ERROR_SIGNED_OUT; } -int APS5_VABI sceNpInGameMessageAbortHandle(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpInGameMessageAbortHandle(std::int32_t libCtxId, std::int32_t handleId) { + std::lock_guard lock(inGameMessageMutex); + InGameMessageContext* context = FindInGameMessageContext(libCtxId); + if (!context) return NP_IN_GAME_MESSAGE_ERROR_LIB_CONTEXT_NOT_FOUND; + return context->handles.count(handleId) == 1 ? 0 : NP_IN_GAME_MESSAGE_ERROR_HANDLE_NOT_FOUND; } int APS5_VABI sceNpInGameMessageCheckCallback(void) { NotImplemented_nid_no_patch(__func__); @@ -835,6 +859,12 @@ int APS5_VABI sceNpSetGamePresenceOnline(int req_id, const NpOnlineId* online_id return SCE_NP_ERROR_SIGNED_OUT; } +int APS5_VABI sceNpSetGamePresenceOnlineA(int reqId, int userId, uint32_t presence) { + (void)presence; + if (userId == USER_SERVICE_USER_ID_INVALID) return SCE_NP_ERROR_INVALID_ARGUMENT; + return SignedOutRequest(reqId); +} + int APS5_VABI sceNpGetAccountCountryA(int user_id, void* country_code) { (void)user_id; if (!country_code) return SCE_NP_ERROR_INVALID_ARGUMENT; @@ -1060,19 +1090,29 @@ int APS5_VABI sceNpCheckCallbackForLib() { extern "C" { -int APS5_VABI sceNpInGameMessageCreateHandle() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpInGameMessageCreateHandle(std::int32_t libCtxId) { + std::lock_guard lock(inGameMessageMutex); + InGameMessageContext* context = FindInGameMessageContext(libCtxId); + if (!context) return NP_IN_GAME_MESSAGE_ERROR_LIB_CONTEXT_NOT_FOUND; + const std::int32_t handleId = TakeInGameMessageId(nextInGameMessageHandle, __func__); + context->handles.insert(handleId); + return handleId; } -int APS5_VABI sceNpInGameMessageDeleteHandle() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpInGameMessageDeleteHandle(std::int32_t libCtxId, std::int32_t handleId) { + std::lock_guard lock(inGameMessageMutex); + InGameMessageContext* context = FindInGameMessageContext(libCtxId); + if (!context) return NP_IN_GAME_MESSAGE_ERROR_LIB_CONTEXT_NOT_FOUND; + return context->handles.erase(handleId) == 1 ? 0 : NP_IN_GAME_MESSAGE_ERROR_HANDLE_NOT_FOUND; } -int APS5_VABI sceNpInGameMessageInitialize() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpInGameMessageInitialize(std::size_t poolSize, void* option) { + (void)poolSize; + (void)option; + std::lock_guard lock(inGameMessageMutex); + const std::int32_t libCtxId = TakeInGameMessageId(nextInGameMessageContext, __func__); + inGameMessageContexts.emplace(libCtxId, InGameMessageContext{}); + return libCtxId; } int APS5_VABI sceNpInGameMessagePrepare() { @@ -1085,9 +1125,9 @@ int APS5_VABI sceNpInGameMessageSendData() { return 0; } -int APS5_VABI sceNpInGameMessageTerminate() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpInGameMessageTerminate(std::int32_t libCtxId) { + std::lock_guard lock(inGameMessageMutex); + return inGameMessageContexts.erase(libCtxId) == 1 ? 0 : NP_IN_GAME_MESSAGE_ERROR_LIB_CONTEXT_NOT_FOUND; } int APS5_VABI sceNpSnsFacebookCreateRequest() { diff --git a/core/libs/prx/libSceNpManager/PendingExports.cpp b/core/libs/prx/libSceNpManager/PendingExports.cpp index f8eff6f655..29368447c5 100644 --- a/core/libs/prx/libSceNpManager/PendingExports.cpp +++ b/core/libs/prx/libSceNpManager/PendingExports.cpp @@ -746,10 +746,6 @@ int APS5_VABI sceNpSetAdditionalScope(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNpSetGamePresenceOnlineA(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} int APS5_VABI sceNpSetNpTitleIdVsh(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceNpManager/include/Api.hpp b/core/libs/prx/libSceNpManager/include/Api.hpp index 665c629f42..9090cdaf11 100644 --- a/core/libs/prx/libSceNpManager/include/Api.hpp +++ b/core/libs/prx/libSceNpManager/include/Api.hpp @@ -69,7 +69,7 @@ int APS5_VABI sceNpIdMapperCreateRequest(void); int APS5_VABI sceNpIdMapperDeleteRequest(int reqId); int APS5_VABI sceNpIdMapperNpIdToAccountId(int reqId, int userId, const void* input, void* output, std::uint32_t value, std::uint8_t flag, std::uint64_t extra); int APS5_VABI sceNpIdMapperOnlineIdToAccountId(int reqId, int userId, const void* input, void* output, std::uint32_t value, std::uint8_t flag, std::uint64_t extra); -int APS5_VABI sceNpInGameMessageAbortHandle(void); +int APS5_VABI sceNpInGameMessageAbortHandle(std::int32_t libCtxId, std::int32_t handleId); int APS5_VABI sceNpInGameMessageCheckCallback(void); int APS5_VABI sceNpInGameMessageGetMemoryPoolStatistics(void); int APS5_VABI sceNpInGameMessagePrepareA(void); @@ -176,6 +176,7 @@ int APS5_VABI sceNpCheckPlus(int reqId, const void* param, void* result); int APS5_VABI sceNpNotifyPlusFeature(const void* param); int APS5_VABI sceNpGetAccountId(const NpOnlineId* onlineId, uint64_t* accountId); int APS5_VABI sceNpSetGamePresenceOnline(int reqId, const NpOnlineId* onlineId, uint32_t presence); +int APS5_VABI sceNpSetGamePresenceOnlineA(int reqId, int userId, uint32_t presence); int APS5_VABI sceNpGetAccountCountryA(int userId, void* countryCode); int APS5_VABI sceNpGetAccountIdA(int userId, uint64_t* accountId); int APS5_VABI sceNpGetNpId(int userId, NpId* npId); @@ -206,12 +207,12 @@ int APS5_VABI sceNpGetSandboxInfo(void* sandboxInfo); int APS5_VABI sceNpUnknown_8weiQyIdI3A(int userId, const NpOnlineId* buffer); int APS5_VABI sceNpUnknown_WxLcN76q19o(void); int APS5_VABI sceNpCheckCallbackForLib(); -int APS5_VABI sceNpInGameMessageCreateHandle(); -int APS5_VABI sceNpInGameMessageDeleteHandle(); -int APS5_VABI sceNpInGameMessageInitialize(); +int APS5_VABI sceNpInGameMessageCreateHandle(std::int32_t libCtxId); +int APS5_VABI sceNpInGameMessageDeleteHandle(std::int32_t libCtxId, std::int32_t handleId); +int APS5_VABI sceNpInGameMessageInitialize(std::size_t poolSize, void* option); int APS5_VABI sceNpInGameMessagePrepare(); int APS5_VABI sceNpInGameMessageSendData(); -int APS5_VABI sceNpInGameMessageTerminate(); +int APS5_VABI sceNpInGameMessageTerminate(std::int32_t libCtxId); int APS5_VABI sceNpSnsFacebookCreateRequest(); int APS5_VABI sceNpSnsFacebookDeleteRequest(); int APS5_VABI sceNpSnsFacebookGetAccessToken(); @@ -401,7 +402,6 @@ int APS5_VABI sceNpRegisterStateCallbackForToolkit(void); int APS5_VABI sceNpServiceClientInit(void); int APS5_VABI sceNpServiceClientTerm(void); int APS5_VABI sceNpSetAdditionalScope(void); -int APS5_VABI sceNpSetGamePresenceOnlineA(void); int APS5_VABI sceNpSetNpTitleIdVsh(void); int APS5_VABI sceNpUnregisterGamePresenceCallbackA(void); int APS5_VABI sceNpUnregisterStateCallbackForToolkit(void); diff --git a/core/libs/prx/libSceNpManager/include/InGameMessageIds.hpp b/core/libs/prx/libSceNpManager/include/InGameMessageIds.hpp new file mode 100644 index 0000000000..a87e664318 --- /dev/null +++ b/core/libs/prx/libSceNpManager/include/InGameMessageIds.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include +#include +#include +#include + +inline std::int32_t TakeInGameMessageId(std::int32_t& next, const char* function) { + if (next == std::numeric_limits::max()) throw std::runtime_error(std::string(function) + ": the in-game message ids are exhausted"); + return next++; +} diff --git a/core/libs/prx/libSceNpMatching2/CMakeLists.txt b/core/libs/prx/libSceNpMatching2/CMakeLists.txt index 06978af7ad..843d50f494 100644 --- a/core/libs/prx/libSceNpMatching2/CMakeLists.txt +++ b/core/libs/prx/libSceNpMatching2/CMakeLists.txt @@ -5,7 +5,7 @@ configure_windows_unwind(libSceNpMatching2) if(BUILD_TESTING) add_test_executable(guest_np_matching2_offline_tests ${LIBS_INCLUDE_DIR}/tests/GuestNpMatching2Offline.cpp) target_include_directories(guest_np_matching2_offline_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_np_matching2_offline_tests PRIVATE libSceNpMatching2) + target_link_libraries(guest_np_matching2_offline_tests PRIVATE libSceNpMatching2 ${CMAKE_DL_LIBS}) configure_windows_unwind(guest_np_matching2_offline_tests) add_test(NAME guest_np_matching2_offline COMMAND guest_np_matching2_offline_tests) set_tests_properties(guest_np_matching2_offline PROPERTIES TIMEOUT 20) diff --git a/core/libs/prx/libSceNpMatching2/Export.cpp b/core/libs/prx/libSceNpMatching2/Export.cpp index c6a28b47b5..d6f0266d23 100644 --- a/core/libs/prx/libSceNpMatching2/Export.cpp +++ b/core/libs/prx/libSceNpMatching2/Export.cpp @@ -1,6 +1,7 @@ #include "prx/libSceNpMatching2/include/Api.hpp" #include #include +#include #include #include #include "SceTypes.hpp" @@ -12,10 +13,20 @@ static constexpr int SCE_NP_MATCHING2_ERROR_INVALID_ARGUMENT = static_cast( static constexpr int SCE_NP_MATCHING2_ERROR_CONTEXT_MAX = static_cast(0x80550C04); static constexpr int SCE_NP_MATCHING2_ERROR_CONTEXT_NOT_STARTED = static_cast(0x80550C08); static constexpr int SCE_NP_MATCHING2_ERROR_INVALID_CONTEXT_ID = static_cast(0x80550C0B); +static constexpr int SCE_NP_MATCHING2_ERROR_CONTEXT_NOT_FOUND = static_cast(0x80550C06); +static constexpr int SCE_NP_MATCHING2_ERROR_INVALID_SERVER_ID = static_cast(0x80550C0C); +static constexpr int SCE_NP_MATCHING2_ERROR_INVALID_ROOM_ID = static_cast(0x80550C0F); +static constexpr int SCE_NP_MATCHING2_ERROR_SERVER_NOT_AVAILABLE = static_cast(0x80550C28); +static constexpr int SCE_NP_MATCHING2_ERROR_SESSION_NOT_FOUND = static_cast(0x80550C3A); +static constexpr int SCE_NP_MATCHING2_SIGNALING_ERROR_CTX_NOT_FOUND = static_cast(0x80550E05); +static constexpr int SCE_NP_MATCHING2_SIGNALING_ERROR_INVALID_ARGUMENT = static_cast(0x80550E15); +static constexpr int SCE_NP_MATCHING2_SIGNALING_ERROR_PEER_NOT_FOUND = static_cast(0x80550E19); static constexpr int SCE_NET_ERROR_RESOLVER_ETIMEDOUT = static_cast(0x804101E2); static constexpr std::int32_t SCE_NP_SIGNALING_CONN_STATUS_INACTIVE = 0; static constexpr int MAX_CONTEXTS = 10; +static constexpr std::uint16_t CONTEXT_A_FLAG = 0x100; static constexpr std::uint64_t CREATE_CONTEXT_PARAMETER_SIZE = 0x28; +static constexpr std::uint64_t CREATE_CONTEXT_PARAMETER_A_SIZE = 0x10; namespace { @@ -23,17 +34,64 @@ static_assert(offsetof(NpMatching2InitializeParameter, size) == 0x20); static_assert(sizeof(NpMatching2InitializeParameter) == 0x30); static_assert(sizeof(NpMatching2CreateContextParameter) == CREATE_CONTEXT_PARAMETER_SIZE); +static_assert(sizeof(NpMatching2CreateContextParameterA) == CREATE_CONTEXT_PARAMETER_A_SIZE); std::mutex matching2Mutex; bool matching2Initialized = false; std::unordered_set contexts; int nextContextId = 1; +std::uint16_t signalingPort = 0; + +enum class Leading { ServerId, RoomId }; int CheckContext(std::uint16_t ctxId) { if (!matching2Initialized) return SCE_NP_MATCHING2_ERROR_NOT_INITIALIZED; return contexts.contains(ctxId) ? 0 : SCE_NP_MATCHING2_ERROR_INVALID_CONTEXT_ID; } +int AllocateContext(std::uint16_t* ctxId, std::uint16_t flag) { + for (int tried = 0; tried < MAX_CONTEXTS; ++tried) { + const auto candidate = static_cast((nextContextId - 1 + tried) % MAX_CONTEXTS + 1); + if (contexts.contains(candidate) || contexts.contains(candidate | CONTEXT_A_FLAG)) continue; + contexts.insert(candidate | flag); + nextContextId = candidate % MAX_CONTEXTS + 1; + *ctxId = candidate | flag; + return 0; + } + return SCE_NP_MATCHING2_ERROR_CONTEXT_MAX; +} + +int CheckRequest(const void* request, const std::uint32_t* requestId, Leading leading) { + if (!matching2Initialized) return SCE_NP_MATCHING2_ERROR_NOT_INITIALIZED; + if (!request || !requestId) return SCE_NP_MATCHING2_ERROR_INVALID_ARGUMENT; + if (leading == Leading::ServerId) { + std::uint16_t serverId; + std::memcpy(&serverId, request, sizeof(serverId)); + return serverId == 0 ? SCE_NP_MATCHING2_ERROR_INVALID_SERVER_ID : 0; + } + std::uint64_t roomId; + std::memcpy(&roomId, request, sizeof(roomId)); + return roomId == 0 ? SCE_NP_MATCHING2_ERROR_INVALID_ROOM_ID : 0; +} + +int StartRequest(const char* name, std::uint16_t ctxId, const void* request, const std::uint32_t* requestId, Leading leading) { + std::lock_guard lock(matching2Mutex); + if (const int error = CheckRequest(request, requestId, leading)) return error; + if (!contexts.contains(ctxId)) return SCE_NP_MATCHING2_ERROR_CONTEXT_NOT_FOUND; + NotImplemented_nid_no_patch(name); + return 0; +} + +int CheckLocal(std::uint16_t ctxId) { + return contexts.contains(ctxId) ? SCE_NP_MATCHING2_ERROR_CONTEXT_NOT_STARTED : SCE_NP_MATCHING2_ERROR_CONTEXT_NOT_FOUND; +} + +int ConnectionInfo(std::uint16_t ctxId, const void* info) { + std::lock_guard lock(matching2Mutex); + if (!info) return SCE_NP_MATCHING2_SIGNALING_ERROR_INVALID_ARGUMENT; + return contexts.contains(ctxId) ? SCE_NP_MATCHING2_SIGNALING_ERROR_PEER_NOT_FOUND : SCE_NP_MATCHING2_SIGNALING_ERROR_CTX_NOT_FOUND; +} + } extern "C" { @@ -56,15 +114,7 @@ int APS5_VABI sceNpMatching2CreateContext(const NpMatching2CreateContextParamete std::lock_guard lock(matching2Mutex); if (!matching2Initialized) return SCE_NP_MATCHING2_ERROR_NOT_INITIALIZED; if (!param || param->size != CREATE_CONTEXT_PARAMETER_SIZE || !ctxId) return SCE_NP_MATCHING2_ERROR_INVALID_ARGUMENT; - for (int tried = 0; tried < MAX_CONTEXTS; ++tried) { - const auto candidate = static_cast((nextContextId - 1 + tried) % MAX_CONTEXTS + 1); - if (contexts.contains(candidate)) continue; - contexts.insert(candidate); - nextContextId = candidate % MAX_CONTEXTS + 1; - *ctxId = candidate; - return 0; - } - return SCE_NP_MATCHING2_ERROR_CONTEXT_MAX; + return AllocateContext(ctxId, 0); } int APS5_VABI sceNpMatching2CreateJoinRoom() { @@ -84,9 +134,9 @@ int APS5_VABI sceNpMatching2GetRoomDataExternalList() { return 0; } -int APS5_VABI sceNpMatching2GetRoomDataInternal() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpMatching2GetRoomDataInternal(std::uint16_t ctxId, const void* request, const void* requestOpt, std::uint32_t* requestId) { + (void)requestOpt; + return StartRequest(__func__, ctxId, request, requestId, Leading::RoomId); } int APS5_VABI sceNpMatching2GetRoomMemberDataInternal() { @@ -94,9 +144,11 @@ int APS5_VABI sceNpMatching2GetRoomMemberDataInternal() { return 0; } -int APS5_VABI sceNpMatching2GetServerId() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpMatching2GetServerId(std::uint16_t ctxId, std::uint16_t* serverId) { + std::lock_guard lock(matching2Mutex); + if (!matching2Initialized) return SCE_NP_MATCHING2_ERROR_NOT_INITIALIZED; + if (!serverId) return SCE_NP_MATCHING2_ERROR_INVALID_ARGUMENT; + return CheckLocal(ctxId); } int APS5_VABI sceNpMatching2GetUserInfoList() { @@ -104,9 +156,9 @@ int APS5_VABI sceNpMatching2GetUserInfoList() { return 0; } -int APS5_VABI sceNpMatching2GetWorldInfoList() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpMatching2GetWorldInfoList(std::uint16_t ctxId, const void* request, const void* requestOpt, std::uint32_t* requestId) { + (void)requestOpt; + return StartRequest(__func__, ctxId, request, requestId, Leading::ServerId); } int APS5_VABI sceNpMatching2Initialize(const NpMatching2InitializeParameter* param) { @@ -127,9 +179,11 @@ int APS5_VABI sceNpMatching2KickoutRoomMember() { return 0; } -int APS5_VABI sceNpMatching2LeaveRoom() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpMatching2LeaveRoom(std::uint16_t ctxId, const void* request, const void* requestOpt, std::uint32_t* requestId) { + (void)requestOpt; + std::lock_guard lock(matching2Mutex); + if (const int error = CheckRequest(request, requestId, Leading::RoomId)) return error; + return contexts.contains(ctxId) ? SCE_NP_MATCHING2_ERROR_SESSION_NOT_FOUND : SCE_NP_MATCHING2_ERROR_CONTEXT_NOT_FOUND; } int APS5_VABI sceNpMatching2RegisterContextCallback(void* callback, void* userdata) { @@ -182,14 +236,14 @@ int APS5_VABI sceNpMatching2SetDefaultRequestOptParam(std::uint16_t ctxId, const return CheckContext(ctxId); } -int APS5_VABI sceNpMatching2SetRoomDataExternal() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpMatching2SetRoomDataExternal(std::uint16_t ctxId, const void* request, const void* requestOpt, std::uint32_t* requestId) { + (void)requestOpt; + return StartRequest(__func__, ctxId, request, requestId, Leading::RoomId); } -int APS5_VABI sceNpMatching2SetRoomDataInternal() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpMatching2SetRoomDataInternal(std::uint16_t ctxId, const void* request, const void* requestOpt, std::uint32_t* requestId) { + (void)requestOpt; + return StartRequest(__func__, ctxId, request, requestId, Leading::RoomId); } int APS5_VABI sceNpMatching2SetRoomMemberDataInternal() { @@ -197,14 +251,16 @@ int APS5_VABI sceNpMatching2SetRoomMemberDataInternal() { return 0; } -int APS5_VABI sceNpMatching2SetUserInfo() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpMatching2SetUserInfo(std::uint16_t ctxId, const void* request, const void* requestOpt, std::uint32_t* requestId) { + (void)requestOpt; + return StartRequest(__func__, ctxId, request, requestId, Leading::ServerId); } -int APS5_VABI sceNpMatching2SignalingGetConnectionInfo() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpMatching2SignalingGetConnectionInfo(std::uint16_t ctxId, std::uint64_t roomId, std::uint16_t memberId, std::int32_t infoType, void* info) { + (void)roomId; + (void)memberId; + (void)infoType; + return ConnectionInfo(ctxId, info); } int APS5_VABI sceNpMatching2Terminate(void) { @@ -222,9 +278,11 @@ int APS5_VABI sceNpMatching2AbortContextStart(std::uint16_t ctxId) { return matching2Initialized ? 0 : SCE_NP_MATCHING2_ERROR_NOT_INITIALIZED; } -int APS5_VABI sceNpMatching2CreateContextA(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpMatching2CreateContextA(const NpMatching2CreateContextParameterA* param, std::uint16_t* ctxId) { + std::lock_guard lock(matching2Mutex); + if (!matching2Initialized) return SCE_NP_MATCHING2_ERROR_NOT_INITIALIZED; + if (!param || param->size != CREATE_CONTEXT_PARAMETER_A_SIZE || !ctxId) return SCE_NP_MATCHING2_ERROR_INVALID_ARGUMENT; + return AllocateContext(ctxId, CONTEXT_A_FLAG); } int APS5_VABI sceNpMatching2CreateContextInternal(void) { @@ -257,9 +315,12 @@ int APS5_VABI sceNpMatching2GetMemoryInfo(void) { return 0; } -int APS5_VABI sceNpMatching2GetRoomJoinedSlotMaskLocal(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpMatching2GetRoomJoinedSlotMaskLocal(std::uint16_t ctxId, std::uint64_t roomId, std::uint64_t* joinedSlotMask) { + std::lock_guard lock(matching2Mutex); + if (!matching2Initialized) return SCE_NP_MATCHING2_ERROR_NOT_INITIALIZED; + if (!joinedSlotMask) return SCE_NP_MATCHING2_ERROR_INVALID_ARGUMENT; + if (!contexts.contains(ctxId)) return SCE_NP_MATCHING2_ERROR_CONTEXT_NOT_FOUND; + return roomId == 0 ? SCE_NP_MATCHING2_ERROR_INVALID_SERVER_ID : SCE_NP_MATCHING2_ERROR_CONTEXT_NOT_STARTED; } int APS5_VABI sceNpMatching2GetRoomMemberDataExternalList(void) { @@ -292,9 +353,13 @@ int APS5_VABI sceNpMatching2GetUserInfoListA(void) { return 0; } -int APS5_VABI sceNpMatching2GetWorldIdArrayForAllServers(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpMatching2GetWorldIdArrayForAllServers(std::uint16_t ctxId, void* worldIds, std::uint64_t worldIdsNum, void* resultNum) { + (void)worldIdsNum; + std::lock_guard lock(matching2Mutex); + if (!matching2Initialized) return SCE_NP_MATCHING2_ERROR_NOT_INITIALIZED; + if (!contexts.contains(ctxId)) return SCE_NP_MATCHING2_ERROR_CONTEXT_NOT_FOUND; + if (!worldIds || !resultNum) return SCE_NP_MATCHING2_ERROR_INVALID_ARGUMENT; + return SCE_NP_MATCHING2_ERROR_SERVER_NOT_AVAILABLE; } int APS5_VABI sceNpMatching2GrantRoomOwner(void) { @@ -317,14 +382,18 @@ int APS5_VABI sceNpMatching2LeaveLobby(void) { return 0; } -int APS5_VABI sceNpMatching2RegisterLobbyEventCallback(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpMatching2RegisterLobbyEventCallback(std::uint16_t ctxId, void* callback, void* userdata) { + (void)callback; + (void)userdata; + std::lock_guard lock(matching2Mutex); + return CheckContext(ctxId); } -int APS5_VABI sceNpMatching2RegisterLobbyMessageCallback(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpMatching2RegisterLobbyMessageCallback(std::uint16_t ctxId, void* callback, void* userdata) { + (void)callback; + (void)userdata; + std::lock_guard lock(matching2Mutex); + return CheckContext(ctxId); } int APS5_VABI sceNpMatching2RegisterManualUdpSignalingCallback(void) { @@ -377,9 +446,11 @@ int APS5_VABI sceNpMatching2SignalingEstablishConnection(void) { return 0; } -int APS5_VABI sceNpMatching2SignalingGetConnectionInfoA(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpMatching2SignalingGetConnectionInfoA(std::uint16_t ctxId, std::uint64_t roomId, std::uint16_t memberId, std::int32_t infoType, void* info) { + (void)roomId; + (void)memberId; + (void)infoType; + return ConnectionInfo(ctxId, info); } int APS5_VABI sceNpMatching2SignalingGetConnectionStatus(std::uint16_t ctxId, std::uint64_t roomId, std::uint16_t memberId, std::int32_t* connStatus, void* peerAddr, std::uint16_t* peerPort) { @@ -410,18 +481,20 @@ int APS5_VABI sceNpMatching2SignalingGetPeerNetInfoResult(void) { return 0; } -int APS5_VABI sceNpMatching2SignalingGetPingInfo(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceNpMatching2SignalingGetPingInfo(std::uint16_t ctxId, const void* request, const void* requestOpt, std::uint32_t* requestId) { + (void)requestOpt; + return StartRequest(__func__, ctxId, request, requestId, Leading::RoomId); +} + +int APS5_VABI sceNpMatching2SignalingGetPort(std::uint16_t* port) { + std::lock_guard lock(matching2Mutex); + *port = signalingPort; return 0; } -int APS5_VABI sceNpMatching2SignalingGetPort(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpMatching2SignalingSetPort(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceNpMatching2SignalingSetPort(std::uint16_t port) { + std::lock_guard lock(matching2Mutex); + signalingPort = port; return 0; } diff --git a/core/libs/prx/libSceNpMatching2/include/Api.hpp b/core/libs/prx/libSceNpMatching2/include/Api.hpp index e83dba6539..af9f3bf46f 100644 --- a/core/libs/prx/libSceNpMatching2/include/Api.hpp +++ b/core/libs/prx/libSceNpMatching2/include/Api.hpp @@ -20,6 +20,12 @@ struct NpMatching2CreateContextParameter { std::uint64_t size; }; +struct NpMatching2CreateContextParameterA { + std::int32_t userId; + std::uint32_t serviceLabel; + std::uint64_t size; +}; + extern "C" { int APS5_VABI sceNpMatching2ContextStart(std::uint16_t ctxId, std::uint64_t timeout); @@ -28,15 +34,15 @@ int APS5_VABI sceNpMatching2CreateContext(const NpMatching2CreateContextParamete int APS5_VABI sceNpMatching2CreateJoinRoom(); int APS5_VABI sceNpMatching2DestroyContext(std::uint16_t ctxId); int APS5_VABI sceNpMatching2GetRoomDataExternalList(); -int APS5_VABI sceNpMatching2GetRoomDataInternal(); +int APS5_VABI sceNpMatching2GetRoomDataInternal(std::uint16_t ctxId, const void* request, const void* requestOpt, std::uint32_t* requestId); int APS5_VABI sceNpMatching2GetRoomMemberDataInternal(); -int APS5_VABI sceNpMatching2GetServerId(); +int APS5_VABI sceNpMatching2GetServerId(std::uint16_t ctxId, std::uint16_t* serverId); int APS5_VABI sceNpMatching2GetUserInfoList(); -int APS5_VABI sceNpMatching2GetWorldInfoList(); +int APS5_VABI sceNpMatching2GetWorldInfoList(std::uint16_t ctxId, const void* request, const void* requestOpt, std::uint32_t* requestId); int APS5_VABI sceNpMatching2Initialize(const NpMatching2InitializeParameter* param); int APS5_VABI sceNpMatching2JoinRoom(); int APS5_VABI sceNpMatching2KickoutRoomMember(); -int APS5_VABI sceNpMatching2LeaveRoom(); +int APS5_VABI sceNpMatching2LeaveRoom(std::uint16_t ctxId, const void* request, const void* requestOpt, std::uint32_t* requestId); int APS5_VABI sceNpMatching2RegisterContextCallback(void* callback, void* userdata); int APS5_VABI sceNpMatching2RegisterRoomEventCallback(std::uint16_t ctxId, void* callback, void* userdata); int APS5_VABI sceNpMatching2RegisterRoomMessageCallback(std::uint16_t ctxId, void* callback, void* userdata); @@ -45,35 +51,35 @@ int APS5_VABI sceNpMatching2SearchRoom(); int APS5_VABI sceNpMatching2SendRoomChatMessage(); int APS5_VABI sceNpMatching2SendRoomMessage(); int APS5_VABI sceNpMatching2SetDefaultRequestOptParam(std::uint16_t ctxId, const void* requestOpt); -int APS5_VABI sceNpMatching2SetRoomDataExternal(); -int APS5_VABI sceNpMatching2SetRoomDataInternal(); +int APS5_VABI sceNpMatching2SetRoomDataExternal(std::uint16_t ctxId, const void* request, const void* requestOpt, std::uint32_t* requestId); +int APS5_VABI sceNpMatching2SetRoomDataInternal(std::uint16_t ctxId, const void* request, const void* requestOpt, std::uint32_t* requestId); int APS5_VABI sceNpMatching2SetRoomMemberDataInternal(); -int APS5_VABI sceNpMatching2SetUserInfo(); -int APS5_VABI sceNpMatching2SignalingGetConnectionInfo(); +int APS5_VABI sceNpMatching2SetUserInfo(std::uint16_t ctxId, const void* request, const void* requestOpt, std::uint32_t* requestId); +int APS5_VABI sceNpMatching2SignalingGetConnectionInfo(std::uint16_t ctxId, std::uint64_t roomId, std::uint16_t memberId, std::int32_t infoType, void* info); int APS5_VABI sceNpMatching2Terminate(void); int APS5_VABI sceNpMatching2AbortContextStart(std::uint16_t ctxId); -int APS5_VABI sceNpMatching2CreateContextA(); +int APS5_VABI sceNpMatching2CreateContextA(const NpMatching2CreateContextParameterA* param, std::uint16_t* ctxId); int APS5_VABI sceNpMatching2CreateContextInternal(); int APS5_VABI sceNpMatching2CreateJoinRoomA(); int APS5_VABI sceNpMatching2GetLobbyInfoList(); int APS5_VABI sceNpMatching2GetLobbyMemberDataInternal(); int APS5_VABI sceNpMatching2GetLobbyMemberDataInternalList(); int APS5_VABI sceNpMatching2GetMemoryInfo(); -int APS5_VABI sceNpMatching2GetRoomJoinedSlotMaskLocal(); +int APS5_VABI sceNpMatching2GetRoomJoinedSlotMaskLocal(std::uint16_t ctxId, std::uint64_t roomId, std::uint64_t* joinedSlotMask); int APS5_VABI sceNpMatching2GetRoomMemberDataExternalList(); int APS5_VABI sceNpMatching2GetRoomMemberIdListLocal(); int APS5_VABI sceNpMatching2GetRoomPasswordLocal(); int APS5_VABI sceNpMatching2GetSignalingOptParamLocal(); int APS5_VABI sceNpMatching2GetSslMemoryInfo(); int APS5_VABI sceNpMatching2GetUserInfoListA(); -int APS5_VABI sceNpMatching2GetWorldIdArrayForAllServers(); +int APS5_VABI sceNpMatching2GetWorldIdArrayForAllServers(std::uint16_t ctxId, void* worldIds, std::uint64_t worldIdsNum, void* resultNum); int APS5_VABI sceNpMatching2GrantRoomOwner(); int APS5_VABI sceNpMatching2JoinLobby(); int APS5_VABI sceNpMatching2JoinRoomA(); int APS5_VABI sceNpMatching2LeaveLobby(); -int APS5_VABI sceNpMatching2RegisterLobbyEventCallback(); -int APS5_VABI sceNpMatching2RegisterLobbyMessageCallback(); +int APS5_VABI sceNpMatching2RegisterLobbyEventCallback(std::uint16_t ctxId, void* callback, void* userdata); +int APS5_VABI sceNpMatching2RegisterLobbyMessageCallback(std::uint16_t ctxId, void* callback, void* userdata); int APS5_VABI sceNpMatching2RegisterManualUdpSignalingCallback(); int APS5_VABI sceNpMatching2SendLobbyChatMessage(); int APS5_VABI sceNpMatching2SetExtraInitParam(); @@ -84,13 +90,13 @@ int APS5_VABI sceNpMatching2SignalingAbortConnection(); int APS5_VABI sceNpMatching2SignalingCancelPeerNetInfo(); int APS5_VABI sceNpMatching2SignalingEnableManualUdpMode(); int APS5_VABI sceNpMatching2SignalingEstablishConnection(); -int APS5_VABI sceNpMatching2SignalingGetConnectionInfoA(); +int APS5_VABI sceNpMatching2SignalingGetConnectionInfoA(std::uint16_t ctxId, std::uint64_t roomId, std::uint16_t memberId, std::int32_t infoType, void* info); int APS5_VABI sceNpMatching2SignalingGetConnectionStatus(std::uint16_t ctxId, std::uint64_t roomId, std::uint16_t memberId, std::int32_t* connStatus, void* peerAddr, std::uint16_t* peerPort); int APS5_VABI sceNpMatching2SignalingGetLocalNetInfo(); int APS5_VABI sceNpMatching2SignalingGetPeerNetInfo(); int APS5_VABI sceNpMatching2SignalingGetPeerNetInfoResult(); -int APS5_VABI sceNpMatching2SignalingGetPingInfo(); -int APS5_VABI sceNpMatching2SignalingGetPort(); -int APS5_VABI sceNpMatching2SignalingSetPort(); +int APS5_VABI sceNpMatching2SignalingGetPingInfo(std::uint16_t ctxId, const void* request, const void* requestOpt, std::uint32_t* requestId); +int APS5_VABI sceNpMatching2SignalingGetPort(std::uint16_t* port); +int APS5_VABI sceNpMatching2SignalingSetPort(std::uint16_t port); } diff --git a/core/libs/prx/libSceNpProfileDialog/Export.cpp b/core/libs/prx/libSceNpProfileDialog/Export.cpp index 0cb54f9a1a..523114eaf9 100644 --- a/core/libs/prx/libSceNpProfileDialog/Export.cpp +++ b/core/libs/prx/libSceNpProfileDialog/Export.cpp @@ -1,4 +1,5 @@ #include "prx/libSceNpProfileDialog/include/Api.hpp" +#include #include #include #include @@ -16,6 +17,7 @@ constexpr int COMMON_DIALOG_STATUS_INITIALIZED = 1; constexpr int COMMON_DIALOG_STATUS_FINISHED = 3; 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_ARGUMENT = static_cast(0x80B8000Au); constexpr int COMMON_DIALOG_ERROR_ARG_NULL = static_cast(0x80B8000Du); constexpr int COMMON_DIALOG_RESULT_USER_CANCELED = 1; @@ -137,10 +139,14 @@ int APS5_VABI sceNpProfileDialogGetResult(SceNpProfileDialogResult* result) { return COMMON_DIALOG_ERROR_NOT_FINISHED; } if (result == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; - *result = SceNpProfileDialogResult{}; - result->result = COMMON_DIALOG_OK; - result->userAction = COMMON_DIALOG_RESULT_USER_CANCELED; - result->userData = dialog.userData; + SceNpProfileDialogResult out{}; + out.result = COMMON_DIALOG_OK; + out.userAction = COMMON_DIALOG_RESULT_USER_CANCELED; + out.userData = dialog.userData; + const int savedErrno = errno; + const bool copied = GuestCopyOut_nid_no_patch(result, &out, sizeof(out)); + errno = savedErrno; + if (!copied) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT; return COMMON_DIALOG_OK; } diff --git a/core/libs/prx/libSceNpScoreRanking/CMakeLists.txt b/core/libs/prx/libSceNpScoreRanking/CMakeLists.txt index d73264a441..22e5f023a1 100644 --- a/core/libs/prx/libSceNpScoreRanking/CMakeLists.txt +++ b/core/libs/prx/libSceNpScoreRanking/CMakeLists.txt @@ -5,7 +5,7 @@ configure_windows_unwind(libSceNpScoreRanking) if(BUILD_TESTING) add_test_executable(guest_np_score_offline_tests ${LIBS_INCLUDE_DIR}/tests/GuestNpScoreOffline.cpp) target_include_directories(guest_np_score_offline_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_np_score_offline_tests PRIVATE libSceNpScoreRanking) + target_link_libraries(guest_np_score_offline_tests PRIVATE libSceNpScoreRanking ${CMAKE_DL_LIBS}) configure_windows_unwind(guest_np_score_offline_tests) add_test(NAME guest_np_score_offline COMMAND guest_np_score_offline_tests) set_tests_properties(guest_np_score_offline PROPERTIES TIMEOUT 20) diff --git a/core/libs/prx/libSceNpScoreRanking/Export.cpp b/core/libs/prx/libSceNpScoreRanking/Export.cpp index c52736cc7a..c8caaa6c50 100644 --- a/core/libs/prx/libSceNpScoreRanking/Export.cpp +++ b/core/libs/prx/libSceNpScoreRanking/Export.cpp @@ -1,6 +1,8 @@ #include "prx/libSceNpScoreRanking/include/Api.hpp" #include #include +#include +#include #include #include #include @@ -19,13 +21,22 @@ static constexpr int SCE_NP_COMMUNITY_ERROR_INVALID_ALIGNMENT = static_cast static constexpr int SCE_NP_COMMUNITY_ERROR_TOO_MANY_NPID = static_cast(0x80550719); static constexpr std::uint32_t INVALID_SERVICE_LABEL = 0xFFFFFFFF; static constexpr std::size_t MAX_OBJECTS = 32; +static constexpr int RESOLVE_TIMEOUT_MIN = 1000000; +static constexpr int TRANSFER_TIMEOUT_MIN = 10000000; static constexpr std::uint64_t MAX_RANGE_PER_REQUEST = 100; static constexpr std::uint64_t MAX_NPID_PER_REQUEST = 101; static constexpr std::uint64_t GAME_INFO_MAX_SIZE = 189; static constexpr std::uint64_t NP_ID_SIZE = 36; static constexpr std::uint64_t RANK_DATA_SIZE = 0x80; static constexpr std::uint64_t PLAYER_RANK_DATA_SIZE = 0x88; +static constexpr std::uint64_t RANK_DATA_A_SIZE = 0x90; +static constexpr std::uint64_t PLAYER_RANK_DATA_A_SIZE = 0x98; +static constexpr std::uint64_t ACCOUNT_ID_SIZE = 8; +static constexpr std::uint64_t NP_ID_PC_ID_SIZE = 44; +static constexpr std::uint64_t ACCOUNT_ID_PC_ID_SIZE = 16; static constexpr std::uint64_t COMMENT_SIZE = 64; +static constexpr std::size_t COMMENT_MAX_LENGTH = 255; +static constexpr std::uint32_t SAVE_DATA_OWNER_MODE_MAX = 3; static constexpr std::uint64_t GAME_INFO_SIZE = 200; static constexpr std::uint64_t FRIEND_RANKING_OPTION_SIZE = 24; static constexpr int POLL_FINISHED = 0; @@ -53,13 +64,45 @@ int CheckArrays(std::uint64_t arrayNum, const void* commentArray, std::uint64_t return 0; } -int Offline(int reqId) { +int AddTitleContext() { + std::lock_guard lock(scoreMutex); + if (titleContexts.size() >= MAX_OBJECTS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_OBJECTS; + const int titleCtxId = nextTitleContext++; + titleContexts.insert(titleCtxId); + return titleCtxId; +} + +int Finish(int reqId, int error) { std::lock_guard lock(scoreMutex); const auto request = requests.find(reqId); if (request == requests.end()) return SCE_NP_COMMUNITY_ERROR_INVALID_ID; if (request->second.result == SCE_NP_COMMUNITY_ERROR_ABORTED) return SCE_NP_COMMUNITY_ERROR_ABORTED; - if (!request->second.result) request->second.result = SCE_NP_ERROR_SIGNED_OUT; - return SCE_NP_ERROR_SIGNED_OUT; + if (!request->second.result) request->second.result = error; + return error; +} + +int Offline(int reqId) { + return Finish(reqId, SCE_NP_ERROR_SIGNED_OUT); +} + +bool KnownRequest(int reqId) { + std::lock_guard lock(scoreMutex); + return requests.contains(reqId); +} + +int CheckComment(int reqId, const char* comment, void* option) { + if (option) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (!comment) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (!KnownRequest(reqId)) return SCE_NP_COMMUNITY_ERROR_INVALID_ID; + if (strnlen(comment, COMMENT_MAX_LENGTH + 1) > COMMENT_MAX_LENGTH) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return Offline(reqId); +} + +int RecordGameData(int reqId, const void* data, void* option) { + if (option) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (!data) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (!KnownRequest(reqId)) return SCE_NP_COMMUNITY_ERROR_INVALID_ID; + return Offline(reqId); } int RecordScore(int reqId, const NpScoreGameInfo* gameInfo, void* option) { @@ -68,12 +111,12 @@ int RecordScore(int reqId, const NpScoreGameInfo* gameInfo, void* option) { return Offline(reqId); } -int GetRankingByRange(int reqId, std::uint32_t startSerialRank, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, void* option) { +int GetRankingByRange(int reqId, std::uint32_t startSerialRank, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, void* option, std::uint64_t rankDataSize = RANK_DATA_SIZE, std::uint64_t otherRankDataSize = RANK_DATA_A_SIZE) { if (option) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; if (arrayNum > MAX_RANGE_PER_REQUEST) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; if (startSerialRank == 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; if (!rankArray || arrayNum == 0) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; - if (rankArraySize != arrayNum * RANK_DATA_SIZE) return SCE_NP_COMMUNITY_ERROR_INVALID_ALIGNMENT; + if (rankArraySize != arrayNum * rankDataSize && rankArraySize != arrayNum * otherRankDataSize) return SCE_NP_COMMUNITY_ERROR_INVALID_ALIGNMENT; if (const int error = CheckArrays(arrayNum, commentArray, commentArraySize, infoArray, infoArraySize)) return error; return Offline(reqId); } @@ -87,9 +130,9 @@ int GetRankingByNpId(int reqId, const void* npIdArray, std::uint64_t npIdArraySi return Offline(reqId); } -int GetFriendsRanking(int reqId, int includeSelf, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, const NpScoreFriendRankingOption* option) { +int GetFriendsRanking(int reqId, int includeSelf, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, const NpScoreFriendRankingOption* option, std::uint64_t rankDataSize = RANK_DATA_SIZE) { if (!rankArray) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; - if (rankArraySize != arrayNum * RANK_DATA_SIZE) return SCE_NP_COMMUNITY_ERROR_INVALID_ALIGNMENT; + if (rankArraySize != arrayNum * rankDataSize) return SCE_NP_COMMUNITY_ERROR_INVALID_ALIGNMENT; std::uint32_t startSerialRank = 1; if (includeSelf == 0) { if (option) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; @@ -103,6 +146,38 @@ int GetFriendsRanking(int reqId, int includeSelf, void* rankArray, std::uint64_t if (const int error = CheckArrays(arrayNum, commentArray, commentArraySize, infoArray, infoArraySize)) return error; return Offline(reqId); } + +int GetRankingByAccountId(int reqId, const void* accountIdArray, std::uint64_t accountIdArraySize, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, void* option, std::uint64_t idSize) { + if (option) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayNum > MAX_NPID_PER_REQUEST) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_NPID; + if (arrayNum == 0 || !rankArray || !accountIdArray) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (accountIdArraySize != arrayNum * idSize) return SCE_NP_COMMUNITY_ERROR_INVALID_ALIGNMENT; + if (rankArraySize != arrayNum * PLAYER_RANK_DATA_A_SIZE) return SCE_NP_COMMUNITY_ERROR_INVALID_ALIGNMENT; + if (const int error = CheckArrays(arrayNum, commentArray, commentArraySize, infoArray, infoArraySize)) return error; + return Offline(reqId); +} + +int GetRankingByNpIdPcId(int reqId, const void* idArray, std::uint64_t idArraySize, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, void* option) { + if (option) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (!idArray || arrayNum == 0 || !rankArray) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (arrayNum > MAX_NPID_PER_REQUEST) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_NPID; + if (idArraySize != arrayNum * NP_ID_PC_ID_SIZE || rankArraySize != arrayNum * PLAYER_RANK_DATA_SIZE) return SCE_NP_COMMUNITY_ERROR_INVALID_ALIGNMENT; + if (const int error = CheckArrays(arrayNum, commentArray, commentArraySize, infoArray, infoArraySize)) return error; + return Offline(reqId); +} + +int GetGameData(int reqId, const void* npId, void* option) { + if (!npId) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (*static_cast(npId) == '\0') return Finish(reqId, SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT); + return Offline(reqId); +} + +int GetGameDataByAccountId(int reqId, std::uint64_t accountId, void* option) { + if (option) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (accountId == 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return Offline(reqId); +} } extern "C" { @@ -118,11 +193,7 @@ int APS5_VABI sceNpScoreAbortRequest(int reqId) { int APS5_VABI sceNpScoreCreateNpTitleCtx(std::uint32_t serviceLabel, const void* selfNpId) { if (serviceLabel == INVALID_SERVICE_LABEL) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; if (!selfNpId) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; - std::lock_guard lock(scoreMutex); - if (titleContexts.size() >= MAX_OBJECTS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_OBJECTS; - const int titleCtxId = nextTitleContext++; - titleContexts.insert(titleCtxId); - return titleCtxId; + return AddTitleContext(); } int APS5_VABI sceNpScoreCreateRequest(int titleCtxId) { @@ -222,183 +293,251 @@ int APS5_VABI sceNpScoreRecordScoreAsync(int reqId, std::uint32_t boardId, std:: return RecordScore(reqId, gameInfo, option); } -int APS5_VABI sceNpScoreCensorComment(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceNpScoreCensorComment(int reqId, const char* comment, void* option) { + return CheckComment(reqId, comment, option); +} + +int APS5_VABI sceNpScoreCensorCommentAsync(int reqId, const char* comment, void* option) { + return CheckComment(reqId, comment, option); +} + +int APS5_VABI sceNpScoreChangeModeForOtherSaveDataOwners(int id, std::uint32_t mode) { + if (mode > SAVE_DATA_OWNER_MODE_MAX) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(scoreMutex); + return titleContexts.contains(id) || requests.contains(id) ? 0 : SCE_NP_COMMUNITY_ERROR_INVALID_ID; +} + +int APS5_VABI sceNpScoreCreateNpTitleCtxA(std::uint32_t serviceLabel, int selfId) { + (void)serviceLabel; + (void)selfId; + return SCE_NP_ERROR_SIGNED_OUT; +} + +int APS5_VABI sceNpScoreCreateTitleCtx(const void* titleId, const void* titleSecret, const void* selfNpId) { + std::fputs("sceNpScoreCreateTitleCtx can use only for sp-int, so you can't use this at product.\nPlease use sceNpScoreCreateNpTitleCtx().\nNP Service label is published at NP title's devnet thread.\n", stderr); + if (!titleId || !titleSecret || !selfNpId) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + return AddTitleContext(); +} + +int APS5_VABI sceNpScoreGetBoardInfoAsync(int reqId, std::uint32_t boardId, void* boardInfo, void* option) { + (void)boardId; + if (option) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (!boardInfo) throw std::runtime_error("sceNpScoreGetBoardInfoAsync: the answer for a null board info depends on the title's SDK version"); + return Offline(reqId); +} + +int APS5_VABI sceNpScoreGetFriendsRankingA(int reqId, std::uint32_t boardId, int includeSelf, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, void* lastSortDate, void* totalRecord, const NpScoreFriendRankingOption* option) { + (void)boardId; + (void)lastSortDate; + (void)totalRecord; + return GetFriendsRanking(reqId, includeSelf, rankArray, rankArraySize, commentArray, commentArraySize, infoArray, infoArraySize, arrayNum, option, RANK_DATA_A_SIZE); +} + +int APS5_VABI sceNpScoreGetFriendsRankingAAsync(int reqId, std::uint32_t boardId, int includeSelf, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, void* lastSortDate, void* totalRecord, const NpScoreFriendRankingOption* option) { + (void)boardId; + (void)lastSortDate; + (void)totalRecord; + return GetFriendsRanking(reqId, includeSelf, rankArray, rankArraySize, commentArray, commentArraySize, infoArray, infoArraySize, arrayNum, option, RANK_DATA_A_SIZE); +} + +int APS5_VABI sceNpScoreGetFriendsRankingForCrossSave(int reqId, std::uint32_t boardId, int includeSelf, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, void* lastSortDate, void* totalRecord, const NpScoreFriendRankingOption* option) { + (void)boardId; + (void)lastSortDate; + (void)totalRecord; + return GetFriendsRanking(reqId, includeSelf, rankArray, rankArraySize, commentArray, commentArraySize, infoArray, infoArraySize, arrayNum, option); +} + +int APS5_VABI sceNpScoreGetFriendsRankingForCrossSaveAsync(int reqId, std::uint32_t boardId, int includeSelf, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, void* lastSortDate, void* totalRecord, const NpScoreFriendRankingOption* option) { + (void)boardId; + (void)lastSortDate; + (void)totalRecord; + return GetFriendsRanking(reqId, includeSelf, rankArray, rankArraySize, commentArray, commentArraySize, infoArray, infoArraySize, arrayNum, option); +} + +int APS5_VABI sceNpScoreGetGameData(int reqId, std::uint32_t boardId, const void* npId, std::uint64_t* totalSize, std::uint64_t recvSize, void* data, void* option) { + (void)boardId; + (void)totalSize; + (void)recvSize; + (void)data; + return GetGameData(reqId, npId, option); +} + +int APS5_VABI sceNpScoreGetGameDataAsync(int reqId, std::uint32_t boardId, const void* npId, std::uint64_t* totalSize, std::uint64_t recvSize, void* data, void* option) { + (void)boardId; + (void)totalSize; + (void)recvSize; + (void)data; + return GetGameData(reqId, npId, option); +} + +int APS5_VABI sceNpScoreGetGameDataByAccountId(int reqId, std::uint32_t boardId, std::uint64_t accountId, std::uint64_t* totalSize, std::uint64_t recvSize, void* data, void* option) { + (void)boardId; + (void)totalSize; + (void)recvSize; + (void)data; + return GetGameDataByAccountId(reqId, accountId, option); +} + +int APS5_VABI sceNpScoreGetGameDataByAccountIdAsync(int reqId, std::uint32_t boardId, std::uint64_t accountId, std::uint64_t* totalSize, std::uint64_t recvSize, void* data, void* option) { + (void)boardId; + (void)totalSize; + (void)recvSize; + (void)data; + return GetGameDataByAccountId(reqId, accountId, option); +} + +int APS5_VABI sceNpScoreGetRankingByAccountId(int reqId, std::uint32_t boardId, const std::uint64_t* accountIdArray, std::uint64_t accountIdArraySize, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, void* lastSortDate, void* totalRecord, void* option) { + (void)boardId; + (void)lastSortDate; + (void)totalRecord; + return GetRankingByAccountId(reqId, accountIdArray, accountIdArraySize, rankArray, rankArraySize, commentArray, commentArraySize, infoArray, infoArraySize, arrayNum, option, ACCOUNT_ID_SIZE); +} + +int APS5_VABI sceNpScoreGetRankingByAccountIdAsync(int reqId, std::uint32_t boardId, const std::uint64_t* accountIdArray, std::uint64_t accountIdArraySize, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, void* lastSortDate, void* totalRecord, void* option) { + (void)boardId; + (void)lastSortDate; + (void)totalRecord; + return GetRankingByAccountId(reqId, accountIdArray, accountIdArraySize, rankArray, rankArraySize, commentArray, commentArraySize, infoArray, infoArraySize, arrayNum, option, ACCOUNT_ID_SIZE); +} + +int APS5_VABI sceNpScoreGetRankingByAccountIdForCrossSave(int reqId, std::uint32_t boardId, const std::uint64_t* accountIdArray, std::uint64_t accountIdArraySize, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, void* lastSortDate, void* totalRecord, void* option) { + (void)boardId; + (void)lastSortDate; + (void)totalRecord; + return GetRankingByAccountId(reqId, accountIdArray, accountIdArraySize, rankArray, rankArraySize, commentArray, commentArraySize, infoArray, infoArraySize, arrayNum, option, ACCOUNT_ID_SIZE); +} + +int APS5_VABI sceNpScoreGetRankingByAccountIdForCrossSaveAsync(int reqId, std::uint32_t boardId, const std::uint64_t* accountIdArray, std::uint64_t accountIdArraySize, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, void* lastSortDate, void* totalRecord, void* option) { + (void)boardId; + (void)lastSortDate; + (void)totalRecord; + return GetRankingByAccountId(reqId, accountIdArray, accountIdArraySize, rankArray, rankArraySize, commentArray, commentArraySize, infoArray, infoArraySize, arrayNum, option, ACCOUNT_ID_SIZE); +} + +int APS5_VABI sceNpScoreGetRankingByAccountIdPcId(int reqId, std::uint32_t boardId, const void* idArray, std::uint64_t idArraySize, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, void* lastSortDate, void* totalRecord, void* option) { + (void)boardId; + (void)lastSortDate; + (void)totalRecord; + return GetRankingByAccountId(reqId, idArray, idArraySize, rankArray, rankArraySize, commentArray, commentArraySize, infoArray, infoArraySize, arrayNum, option, ACCOUNT_ID_PC_ID_SIZE); +} + +int APS5_VABI sceNpScoreGetRankingByAccountIdPcIdAsync(int reqId, std::uint32_t boardId, const void* idArray, std::uint64_t idArraySize, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, void* lastSortDate, void* totalRecord, void* option) { + (void)boardId; + (void)lastSortDate; + (void)totalRecord; + return GetRankingByAccountId(reqId, idArray, idArraySize, rankArray, rankArraySize, commentArray, commentArraySize, infoArray, infoArraySize, arrayNum, option, ACCOUNT_ID_PC_ID_SIZE); +} + +int APS5_VABI sceNpScoreGetRankingByAccountIdPcIdForCrossSave(int reqId, std::uint32_t boardId, const void* idArray, std::uint64_t idArraySize, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, void* lastSortDate, void* totalRecord, void* option) { + (void)boardId; + (void)lastSortDate; + (void)totalRecord; + return GetRankingByAccountId(reqId, idArray, idArraySize, rankArray, rankArraySize, commentArray, commentArraySize, infoArray, infoArraySize, arrayNum, option, ACCOUNT_ID_PC_ID_SIZE); +} + +int APS5_VABI sceNpScoreGetRankingByAccountIdPcIdForCrossSaveAsync(int reqId, std::uint32_t boardId, const void* idArray, std::uint64_t idArraySize, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, void* lastSortDate, void* totalRecord, void* option) { + (void)boardId; + (void)lastSortDate; + (void)totalRecord; + return GetRankingByAccountId(reqId, idArray, idArraySize, rankArray, rankArraySize, commentArray, commentArraySize, infoArray, infoArraySize, arrayNum, option, ACCOUNT_ID_PC_ID_SIZE); +} + +int APS5_VABI sceNpScoreGetRankingByNpIdPcId(int reqId, std::uint32_t boardId, const void* idArray, std::uint64_t idArraySize, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, void* lastSortDate, void* totalRecord, void* option) { + (void)boardId; + (void)lastSortDate; + (void)totalRecord; + return GetRankingByNpIdPcId(reqId, idArray, idArraySize, rankArray, rankArraySize, commentArray, commentArraySize, infoArray, infoArraySize, arrayNum, option); +} + +int APS5_VABI sceNpScoreGetRankingByNpIdPcIdAsync(int reqId, std::uint32_t boardId, const void* idArray, std::uint64_t idArraySize, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, void* lastSortDate, void* totalRecord, void* option) { + (void)boardId; + (void)lastSortDate; + (void)totalRecord; + return GetRankingByNpIdPcId(reqId, idArray, idArraySize, rankArray, rankArraySize, commentArray, commentArraySize, infoArray, infoArraySize, arrayNum, option); +} + +int APS5_VABI sceNpScoreGetRankingByRangeA(int reqId, std::uint32_t boardId, std::uint32_t startSerialRank, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, void* lastSortDate, void* totalRecord, void* option) { + (void)boardId; + (void)lastSortDate; + (void)totalRecord; + return GetRankingByRange(reqId, startSerialRank, rankArray, rankArraySize, commentArray, commentArraySize, infoArray, infoArraySize, arrayNum, option, RANK_DATA_A_SIZE); +} + +int APS5_VABI sceNpScoreGetRankingByRangeAAsync(int reqId, std::uint32_t boardId, std::uint32_t startSerialRank, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, void* lastSortDate, void* totalRecord, void* option) { + (void)boardId; + (void)lastSortDate; + (void)totalRecord; + return GetRankingByRange(reqId, startSerialRank, rankArray, rankArraySize, commentArray, commentArraySize, infoArray, infoArraySize, arrayNum, option, RANK_DATA_A_SIZE); +} + +int APS5_VABI sceNpScoreGetRankingByRangeForCrossSave(int reqId, std::uint32_t boardId, std::uint32_t startSerialRank, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, void* lastSortDate, void* totalRecord, void* option) { + (void)boardId; + (void)lastSortDate; + (void)totalRecord; + return GetRankingByRange(reqId, startSerialRank, rankArray, rankArraySize, commentArray, commentArraySize, infoArray, infoArraySize, arrayNum, option); +} + +int APS5_VABI sceNpScoreGetRankingByRangeForCrossSaveAsync(int reqId, std::uint32_t boardId, std::uint32_t startSerialRank, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, void* lastSortDate, void* totalRecord, void* option) { + (void)boardId; + (void)lastSortDate; + (void)totalRecord; + return GetRankingByRange(reqId, startSerialRank, rankArray, rankArraySize, commentArray, commentArraySize, infoArray, infoArraySize, arrayNum, option); +} + +int APS5_VABI sceNpScoreRecordGameData(int reqId, std::uint32_t boardId, std::int64_t score, std::uint64_t totalSize, std::uint64_t sendSize, const void* data, void* option) { + (void)boardId; + (void)score; + (void)totalSize; + (void)sendSize; + return RecordGameData(reqId, data, option); +} + +int APS5_VABI sceNpScoreRecordGameDataAsync(int reqId, std::uint32_t boardId, std::int64_t score, std::uint64_t totalSize, std::uint64_t sendSize, const void* data, void* option) { + (void)boardId; + (void)score; + (void)totalSize; + (void)sendSize; + return RecordGameData(reqId, data, option); +} + +int APS5_VABI sceNpScoreSanitizeComment(int reqId, const char* comment, char* sanitizedComment, void* option) { + (void)sanitizedComment; + return CheckComment(reqId, comment, option); +} + +int APS5_VABI sceNpScoreSanitizeCommentAsync(int reqId, const char* comment, char* sanitizedComment, void* option) { + (void)sanitizedComment; + return CheckComment(reqId, comment, option); +} + +int APS5_VABI sceNpScoreSetPlayerCharacterId(int ctxId, int pcId) { + if (pcId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(scoreMutex); + return titleContexts.contains(ctxId) || requests.contains(ctxId) ? 0 : SCE_NP_COMMUNITY_ERROR_INVALID_ID; +} + +int APS5_VABI sceNpScoreSetThreadParam(int threadPriority, std::uint64_t cpuAffinityMask) { + (void)threadPriority; + (void)cpuAffinityMask; return 0; } -int APS5_VABI sceNpScoreCensorCommentAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpScoreSetTimeout(int id, int resolveRetry, int resolveTimeout, int connTimeout, int sendTimeout, int recvTimeout) { + if (resolveRetry < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (resolveTimeout != 0 && resolveTimeout < RESOLVE_TIMEOUT_MIN) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (connTimeout != 0 && connTimeout < TRANSFER_TIMEOUT_MIN) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (recvTimeout != 0 && recvTimeout < TRANSFER_TIMEOUT_MIN) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (sendTimeout != 0 && sendTimeout < TRANSFER_TIMEOUT_MIN) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(scoreMutex); + return titleContexts.contains(id) || requests.contains(id) ? 0 : SCE_NP_COMMUNITY_ERROR_INVALID_ID; } -int APS5_VABI sceNpScoreChangeModeForOtherSaveDataOwners(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpScoreCreateNpTitleCtxA(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpScoreCreateTitleCtx(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpScoreGetBoardInfoAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpScoreGetFriendsRankingA(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpScoreGetFriendsRankingAAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpScoreGetFriendsRankingForCrossSave(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpScoreGetFriendsRankingForCrossSaveAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpScoreGetGameData(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpScoreGetGameDataAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpScoreGetGameDataByAccountId(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpScoreGetGameDataByAccountIdAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpScoreGetRankingByAccountId(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpScoreGetRankingByAccountIdAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpScoreGetRankingByAccountIdForCrossSave(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpScoreGetRankingByAccountIdForCrossSaveAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpScoreGetRankingByAccountIdPcId(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpScoreGetRankingByAccountIdPcIdAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpScoreGetRankingByAccountIdPcIdForCrossSave(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpScoreGetRankingByAccountIdPcIdForCrossSaveAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpScoreGetRankingByNpIdPcId(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpScoreGetRankingByNpIdPcIdAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpScoreGetRankingByRangeA(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpScoreGetRankingByRangeAAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpScoreGetRankingByRangeForCrossSave(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpScoreGetRankingByRangeForCrossSaveAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpScoreRecordGameData(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpScoreRecordGameDataAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpScoreSanitizeComment(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpScoreSanitizeCommentAsync(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpScoreSetPlayerCharacterId(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpScoreSetThreadParam(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpScoreSetTimeout(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpScoreWaitAsync(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceNpScoreWaitAsync(int reqId, int* result) { + std::lock_guard lock(scoreMutex); + const auto request = requests.find(reqId); + if (request == requests.end()) return SCE_NP_COMMUNITY_ERROR_INVALID_ID; + if (!request->second.result) NotImplemented_nid_no_patch("sceNpScoreWaitAsync (a request with no operation started)"); + if (result) *result = *request->second.result; return 0; } diff --git a/core/libs/prx/libSceNpScoreRanking/include/Api.hpp b/core/libs/prx/libSceNpScoreRanking/include/Api.hpp index 91e525eab3..074f26ade3 100644 --- a/core/libs/prx/libSceNpScoreRanking/include/Api.hpp +++ b/core/libs/prx/libSceNpScoreRanking/include/Api.hpp @@ -32,41 +32,41 @@ int APS5_VABI sceNpScorePollAsync(int reqId, int* result); int APS5_VABI sceNpScoreRecordScore(int reqId, std::uint32_t boardId, std::int64_t score, const void* scoreComment, const NpScoreGameInfo* gameInfo, std::uint32_t* tmpRank, const void* compareDate, void* option); int APS5_VABI sceNpScoreRecordScoreAsync(int reqId, std::uint32_t boardId, std::int64_t score, const void* scoreComment, const NpScoreGameInfo* gameInfo, std::uint32_t* tmpRank, const void* compareDate, void* option); -int APS5_VABI sceNpScoreCensorComment(); -int APS5_VABI sceNpScoreCensorCommentAsync(); -int APS5_VABI sceNpScoreChangeModeForOtherSaveDataOwners(); -int APS5_VABI sceNpScoreCreateNpTitleCtxA(); -int APS5_VABI sceNpScoreCreateTitleCtx(); -int APS5_VABI sceNpScoreGetBoardInfoAsync(); -int APS5_VABI sceNpScoreGetFriendsRankingA(); -int APS5_VABI sceNpScoreGetFriendsRankingAAsync(); -int APS5_VABI sceNpScoreGetFriendsRankingForCrossSave(); -int APS5_VABI sceNpScoreGetFriendsRankingForCrossSaveAsync(); -int APS5_VABI sceNpScoreGetGameData(); -int APS5_VABI sceNpScoreGetGameDataAsync(); -int APS5_VABI sceNpScoreGetGameDataByAccountId(); -int APS5_VABI sceNpScoreGetGameDataByAccountIdAsync(); -int APS5_VABI sceNpScoreGetRankingByAccountId(); -int APS5_VABI sceNpScoreGetRankingByAccountIdAsync(); -int APS5_VABI sceNpScoreGetRankingByAccountIdForCrossSave(); -int APS5_VABI sceNpScoreGetRankingByAccountIdForCrossSaveAsync(); -int APS5_VABI sceNpScoreGetRankingByAccountIdPcId(); -int APS5_VABI sceNpScoreGetRankingByAccountIdPcIdAsync(); -int APS5_VABI sceNpScoreGetRankingByAccountIdPcIdForCrossSave(); -int APS5_VABI sceNpScoreGetRankingByAccountIdPcIdForCrossSaveAsync(); -int APS5_VABI sceNpScoreGetRankingByNpIdPcId(); -int APS5_VABI sceNpScoreGetRankingByNpIdPcIdAsync(); -int APS5_VABI sceNpScoreGetRankingByRangeA(); -int APS5_VABI sceNpScoreGetRankingByRangeAAsync(); -int APS5_VABI sceNpScoreGetRankingByRangeForCrossSave(); -int APS5_VABI sceNpScoreGetRankingByRangeForCrossSaveAsync(); -int APS5_VABI sceNpScoreRecordGameData(); -int APS5_VABI sceNpScoreRecordGameDataAsync(); -int APS5_VABI sceNpScoreSanitizeComment(); -int APS5_VABI sceNpScoreSanitizeCommentAsync(); -int APS5_VABI sceNpScoreSetPlayerCharacterId(); -int APS5_VABI sceNpScoreSetThreadParam(); -int APS5_VABI sceNpScoreSetTimeout(); -int APS5_VABI sceNpScoreWaitAsync(); +int APS5_VABI sceNpScoreCensorComment(int reqId, const char* comment, void* option); +int APS5_VABI sceNpScoreCensorCommentAsync(int reqId, const char* comment, void* option); +int APS5_VABI sceNpScoreChangeModeForOtherSaveDataOwners(int id, std::uint32_t mode); +int APS5_VABI sceNpScoreCreateNpTitleCtxA(std::uint32_t serviceLabel, int selfId); +int APS5_VABI sceNpScoreCreateTitleCtx(const void* titleId, const void* titleSecret, const void* selfNpId); +int APS5_VABI sceNpScoreGetBoardInfoAsync(int reqId, std::uint32_t boardId, void* boardInfo, void* option); +int APS5_VABI sceNpScoreGetFriendsRankingA(int reqId, std::uint32_t boardId, int includeSelf, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, void* lastSortDate, void* totalRecord, const NpScoreFriendRankingOption* option); +int APS5_VABI sceNpScoreGetFriendsRankingAAsync(int reqId, std::uint32_t boardId, int includeSelf, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, void* lastSortDate, void* totalRecord, const NpScoreFriendRankingOption* option); +int APS5_VABI sceNpScoreGetFriendsRankingForCrossSave(int reqId, std::uint32_t boardId, int includeSelf, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, void* lastSortDate, void* totalRecord, const NpScoreFriendRankingOption* option); +int APS5_VABI sceNpScoreGetFriendsRankingForCrossSaveAsync(int reqId, std::uint32_t boardId, int includeSelf, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, void* lastSortDate, void* totalRecord, const NpScoreFriendRankingOption* option); +int APS5_VABI sceNpScoreGetGameData(int reqId, std::uint32_t boardId, const void* npId, std::uint64_t* totalSize, std::uint64_t recvSize, void* data, void* option); +int APS5_VABI sceNpScoreGetGameDataAsync(int reqId, std::uint32_t boardId, const void* npId, std::uint64_t* totalSize, std::uint64_t recvSize, void* data, void* option); +int APS5_VABI sceNpScoreGetGameDataByAccountId(int reqId, std::uint32_t boardId, std::uint64_t accountId, std::uint64_t* totalSize, std::uint64_t recvSize, void* data, void* option); +int APS5_VABI sceNpScoreGetGameDataByAccountIdAsync(int reqId, std::uint32_t boardId, std::uint64_t accountId, std::uint64_t* totalSize, std::uint64_t recvSize, void* data, void* option); +int APS5_VABI sceNpScoreGetRankingByAccountId(int reqId, std::uint32_t boardId, const std::uint64_t* accountIdArray, std::uint64_t accountIdArraySize, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, void* lastSortDate, void* totalRecord, void* option); +int APS5_VABI sceNpScoreGetRankingByAccountIdAsync(int reqId, std::uint32_t boardId, const std::uint64_t* accountIdArray, std::uint64_t accountIdArraySize, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, void* lastSortDate, void* totalRecord, void* option); +int APS5_VABI sceNpScoreGetRankingByAccountIdForCrossSave(int reqId, std::uint32_t boardId, const std::uint64_t* accountIdArray, std::uint64_t accountIdArraySize, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, void* lastSortDate, void* totalRecord, void* option); +int APS5_VABI sceNpScoreGetRankingByAccountIdForCrossSaveAsync(int reqId, std::uint32_t boardId, const std::uint64_t* accountIdArray, std::uint64_t accountIdArraySize, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, void* lastSortDate, void* totalRecord, void* option); +int APS5_VABI sceNpScoreGetRankingByAccountIdPcId(int reqId, std::uint32_t boardId, const void* idArray, std::uint64_t idArraySize, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, void* lastSortDate, void* totalRecord, void* option); +int APS5_VABI sceNpScoreGetRankingByAccountIdPcIdAsync(int reqId, std::uint32_t boardId, const void* idArray, std::uint64_t idArraySize, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, void* lastSortDate, void* totalRecord, void* option); +int APS5_VABI sceNpScoreGetRankingByAccountIdPcIdForCrossSave(int reqId, std::uint32_t boardId, const void* idArray, std::uint64_t idArraySize, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, void* lastSortDate, void* totalRecord, void* option); +int APS5_VABI sceNpScoreGetRankingByAccountIdPcIdForCrossSaveAsync(int reqId, std::uint32_t boardId, const void* idArray, std::uint64_t idArraySize, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, void* lastSortDate, void* totalRecord, void* option); +int APS5_VABI sceNpScoreGetRankingByNpIdPcId(int reqId, std::uint32_t boardId, const void* idArray, std::uint64_t idArraySize, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, void* lastSortDate, void* totalRecord, void* option); +int APS5_VABI sceNpScoreGetRankingByNpIdPcIdAsync(int reqId, std::uint32_t boardId, const void* idArray, std::uint64_t idArraySize, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, void* lastSortDate, void* totalRecord, void* option); +int APS5_VABI sceNpScoreGetRankingByRangeA(int reqId, std::uint32_t boardId, std::uint32_t startSerialRank, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, void* lastSortDate, void* totalRecord, void* option); +int APS5_VABI sceNpScoreGetRankingByRangeAAsync(int reqId, std::uint32_t boardId, std::uint32_t startSerialRank, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, void* lastSortDate, void* totalRecord, void* option); +int APS5_VABI sceNpScoreGetRankingByRangeForCrossSave(int reqId, std::uint32_t boardId, std::uint32_t startSerialRank, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, void* lastSortDate, void* totalRecord, void* option); +int APS5_VABI sceNpScoreGetRankingByRangeForCrossSaveAsync(int reqId, std::uint32_t boardId, std::uint32_t startSerialRank, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, void* lastSortDate, void* totalRecord, void* option); +int APS5_VABI sceNpScoreRecordGameData(int reqId, std::uint32_t boardId, std::int64_t score, std::uint64_t totalSize, std::uint64_t sendSize, const void* data, void* option); +int APS5_VABI sceNpScoreRecordGameDataAsync(int reqId, std::uint32_t boardId, std::int64_t score, std::uint64_t totalSize, std::uint64_t sendSize, const void* data, void* option); +int APS5_VABI sceNpScoreSanitizeComment(int reqId, const char* comment, char* sanitizedComment, void* option); +int APS5_VABI sceNpScoreSanitizeCommentAsync(int reqId, const char* comment, char* sanitizedComment, void* option); +int APS5_VABI sceNpScoreSetPlayerCharacterId(int ctxId, int pcId); +int APS5_VABI sceNpScoreSetThreadParam(int threadPriority, std::uint64_t cpuAffinityMask); +int APS5_VABI sceNpScoreSetTimeout(int id, int resolveRetry, int resolveTimeout, int connTimeout, int sendTimeout, int recvTimeout); +int APS5_VABI sceNpScoreWaitAsync(int reqId, int* result); } diff --git a/core/libs/prx/libSceNpTcs/Export.cpp b/core/libs/prx/libSceNpTcs/Export.cpp index 78dee5e9ae..42025891f3 100644 --- a/core/libs/prx/libSceNpTcs/Export.cpp +++ b/core/libs/prx/libSceNpTcs/Export.cpp @@ -27,44 +27,36 @@ int APS5_VABI sceNpTcsIntUploadDataToNewSlotBegin() { extern "C" { -int APS5_VABI npTcsLegacyUnknown__minus_Bbx0u2xR4Q_nid_no_patch() { +int APS5_VABI sceNpTcsIntUploadDataToNewSlot() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("-Bbx0u2xR4Q", npTcsLegacyUnknown__minus_Bbx0u2xR4Q_nid_no_patch); -int APS5_VABI npTcsLegacyUnknown_3ruFwEv4KQg_nid_no_patch() { +int APS5_VABI sceNpTcsIntCreateContext() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("3ruFwEv4KQg", npTcsLegacyUnknown_3ruFwEv4KQg_nid_no_patch); -int APS5_VABI npTcsLegacyUnknown_Mlt9z_minus_KrFc4_nid_no_patch() { +int APS5_VABI sceNpTcsIntUploadDataToNewSlotEnd() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("Mlt9z-KrFc4", npTcsLegacyUnknown_Mlt9z_minus_KrFc4_nid_no_patch); -int APS5_VABI npTcsLegacyUnknown_ORvFvy4FTvs_nid_no_patch() { +int APS5_VABI sceNpTcsIntDestroyContext() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("ORvFvy4FTvs", npTcsLegacyUnknown_ORvFvy4FTvs_nid_no_patch); -int APS5_VABI npTcsLegacyUnknown_Qx4CWBw5quY_nid_no_patch() { +int APS5_VABI sceNpTcsIntAbortRequest() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("Qx4CWBw5quY", npTcsLegacyUnknown_Qx4CWBw5quY_nid_no_patch); -int APS5_VABI npTcsLegacyUnknown_YTUqdMJrU6Q_nid_no_patch() { +int APS5_VABI sceNpTcsIntDestroyRequest() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("YTUqdMJrU6Q", npTcsLegacyUnknown_YTUqdMJrU6Q_nid_no_patch); -int APS5_VABI npTcsLegacyUnknown_ik_minus_uhY6YvWk_nid_no_patch() { +int APS5_VABI sceNpTcsIntUploadDataToNewSlotWrite() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("ik-uhY6YvWk", npTcsLegacyUnknown_ik_minus_uhY6YvWk_nid_no_patch); -int APS5_VABI npTcsLegacyUnknown_xSse4lLrSNA_nid_no_patch() { +int APS5_VABI sceNpTcsIntCreateRequest() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("xSse4lLrSNA", npTcsLegacyUnknown_xSse4lLrSNA_nid_no_patch); } diff --git a/core/libs/prx/libSceNpTcs/include/LegacyExports.hpp b/core/libs/prx/libSceNpTcs/include/LegacyExports.hpp index 0238e33d45..6f50dce0f6 100644 --- a/core/libs/prx/libSceNpTcs/include/LegacyExports.hpp +++ b/core/libs/prx/libSceNpTcs/include/LegacyExports.hpp @@ -4,13 +4,13 @@ extern "C" { -int APS5_VABI npTcsLegacyUnknown__minus_Bbx0u2xR4Q_nid_no_patch(); -int APS5_VABI npTcsLegacyUnknown_3ruFwEv4KQg_nid_no_patch(); -int APS5_VABI npTcsLegacyUnknown_Mlt9z_minus_KrFc4_nid_no_patch(); -int APS5_VABI npTcsLegacyUnknown_ORvFvy4FTvs_nid_no_patch(); -int APS5_VABI npTcsLegacyUnknown_Qx4CWBw5quY_nid_no_patch(); -int APS5_VABI npTcsLegacyUnknown_YTUqdMJrU6Q_nid_no_patch(); -int APS5_VABI npTcsLegacyUnknown_ik_minus_uhY6YvWk_nid_no_patch(); -int APS5_VABI npTcsLegacyUnknown_xSse4lLrSNA_nid_no_patch(); +int APS5_VABI sceNpTcsIntUploadDataToNewSlot(); +int APS5_VABI sceNpTcsIntCreateContext(); +int APS5_VABI sceNpTcsIntUploadDataToNewSlotEnd(); +int APS5_VABI sceNpTcsIntDestroyContext(); +int APS5_VABI sceNpTcsIntAbortRequest(); +int APS5_VABI sceNpTcsIntDestroyRequest(); +int APS5_VABI sceNpTcsIntUploadDataToNewSlotWrite(); +int APS5_VABI sceNpTcsIntCreateRequest(); } diff --git a/core/libs/prx/libSceNpTrophy/CMakeLists.txt b/core/libs/prx/libSceNpTrophy/CMakeLists.txt index 279849ac16..896b55a5a7 100644 --- a/core/libs/prx/libSceNpTrophy/CMakeLists.txt +++ b/core/libs/prx/libSceNpTrophy/CMakeLists.txt @@ -34,3 +34,16 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endif() + +if(BUILD_TESTING) + add_test_executable(guest_np_trophy_helpers_tests ${LIBS_INCLUDE_DIR}/tests/GuestNpTrophyHelpers.cpp) + target_include_directories(guest_np_trophy_helpers_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_np_trophy_helpers_tests PRIVATE libSceNpTrophy ${CMAKE_DL_LIBS}) + configure_windows_unwind(guest_np_trophy_helpers_tests) + add_test(NAME guest_np_trophy_helpers COMMAND guest_np_trophy_helpers_tests) + set_tests_properties(guest_np_trophy_helpers PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_np_trophy_helpers 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 index 3322573f4d..06adee50e3 100644 --- a/core/libs/prx/libSceNpTrophy/Export.cpp +++ b/core/libs/prx/libSceNpTrophy/Export.cpp @@ -3,6 +3,7 @@ #include #include "prx/libSceNpTrophy/include/Api.hpp" #include +#include #include #include #include @@ -87,18 +88,25 @@ std::uint64_t BigEndian64(const std::uint8_t* bytes) { } std::optional> ReadPackage(const std::filesystem::path& path) { - std::ifstream file(path, std::ios::binary); + std::ifstream file(path, std::ios::binary | std::ios::ate); if (!file) return std::nullopt; + const auto end = file.tellg(); + if (end < static_cast(TrpHeaderBytes)) return std::map{}; + const auto fileSize = static_cast(end); + file.seekg(0); 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{}; + if (count > (fileSize - TrpHeaderBytes) / 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)}; + const TrpEntry contents{BigEndian64(entry.data() + 32), BigEndian64(entry.data() + 40)}; + if (contents.offset > fileSize || contents.size > fileSize - contents.offset) return std::map{}; + entries[name] = contents; } return entries; } @@ -419,14 +427,13 @@ int APS5_VABI sceNpTrophyConfigGetTrophyTitleDetails(void) { return 0; } -int APS5_VABI sceNpTrophyConfigHasGroupFeature(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpTrophyConfigHasGroupFeature(const std::uint8_t* config) { + if (!config) return 0; + return config[0x90] & 1; } -int APS5_VABI sceNpTrophyGroupArrayGetNum(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpTrophyGroupArrayGetNum(const std::uint32_t* groupArray) { + return std::popcount(groupArray[0]); } int APS5_VABI sceNpTrophyIntAbortHandle(void) { @@ -484,19 +491,23 @@ int APS5_VABI sceNpTrophyIntNetSyncTitles(void) { return 0; } -int APS5_VABI sceNpTrophyNumInfoGetTotal(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpTrophyNumInfoGetTotal(const std::uint32_t* numInfo) { + return static_cast(numInfo[0] + numInfo[1] + numInfo[2] + numInfo[3]); } -int APS5_VABI sceNpTrophySetInfoGetTrophyFlagArray(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpTrophySetInfoGetTrophyFlagArray(const std::uint8_t* setInfo, std::uint8_t* flagArray) { + int count = 0; + for (std::size_t index = 0; index < 16; ++index) { + flagArray[index] = setInfo[index] | setInfo[16 + index] | setInfo[32 + index] | setInfo[48 + index]; + count += std::popcount(flagArray[index]); + } + return count; } -int APS5_VABI sceNpTrophySetInfoGetTrophyNum(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpTrophySetInfoGetTrophyNum(const std::uint8_t* setInfo) { + int count = 0; + for (std::size_t index = 0; index < 64; ++index) count += std::popcount(setInfo[index]); + return count; } int APS5_VABI sceNpTrophyShowTrophyList(void) { diff --git a/core/libs/prx/libSceNpTrophy/include/Api.hpp b/core/libs/prx/libSceNpTrophy/include/Api.hpp index 698d7d98db..487ce31811 100644 --- a/core/libs/prx/libSceNpTrophy/include/Api.hpp +++ b/core/libs/prx/libSceNpTrophy/include/Api.hpp @@ -99,8 +99,8 @@ int APS5_VABI sceNpTrophyConfigGetTrophySetInfo(); int APS5_VABI sceNpTrophyConfigGetTrophySetInfoInGroup(); int APS5_VABI sceNpTrophyConfigGetTrophySetVersion(); int APS5_VABI sceNpTrophyConfigGetTrophyTitleDetails(); -int APS5_VABI sceNpTrophyConfigHasGroupFeature(); -int APS5_VABI sceNpTrophyGroupArrayGetNum(); +int APS5_VABI sceNpTrophyConfigHasGroupFeature(const std::uint8_t* config); +int APS5_VABI sceNpTrophyGroupArrayGetNum(const std::uint32_t* groupArray); int APS5_VABI sceNpTrophyIntAbortHandle(); int APS5_VABI sceNpTrophyIntCheckNetSyncTitles(); int APS5_VABI sceNpTrophyIntCreateHandle(); @@ -112,9 +112,9 @@ int APS5_VABI sceNpTrophyIntGetRunningTitles(); int APS5_VABI sceNpTrophyIntGetTrpIconByUri(); int APS5_VABI sceNpTrophyIntNetSyncTitle(); int APS5_VABI sceNpTrophyIntNetSyncTitles(); -int APS5_VABI sceNpTrophyNumInfoGetTotal(); -int APS5_VABI sceNpTrophySetInfoGetTrophyFlagArray(); -int APS5_VABI sceNpTrophySetInfoGetTrophyNum(); +int APS5_VABI sceNpTrophyNumInfoGetTotal(const std::uint32_t* numInfo); +int APS5_VABI sceNpTrophySetInfoGetTrophyFlagArray(const std::uint8_t* setInfo, std::uint8_t* flagArray); +int APS5_VABI sceNpTrophySetInfoGetTrophyNum(const std::uint8_t* setInfo); int APS5_VABI sceNpTrophyShowTrophyList(); int APS5_VABI sceNpTrophySystemAbortHandle(); int APS5_VABI sceNpTrophySystemBuildGroupIconUri(); diff --git a/core/libs/prx/libSceNpTrophy2/CMakeLists.txt b/core/libs/prx/libSceNpTrophy2/CMakeLists.txt index 7f9d3efe3a..d4ea301145 100644 --- a/core/libs/prx/libSceNpTrophy2/CMakeLists.txt +++ b/core/libs/prx/libSceNpTrophy2/CMakeLists.txt @@ -13,7 +13,7 @@ add_library(libSceNpTrophy2 SHARED EXCLUDE_FROM_ALL target_include_directories(libSceNpTrophy2 PRIVATE ${LIBS_INCLUDE_DIR} ) -target_link_libraries(libSceNpTrophy2 PRIVATE libc) +target_link_libraries(libSceNpTrophy2 PRIVATE libc system_service_host_locale) configure_windows_unwind(libSceNpTrophy2) if(BUILD_TESTING) @@ -21,7 +21,7 @@ if(BUILD_TESTING) target_include_directories(guest_nptrophy2_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_nptrophy2_tests PRIVATE libSceNpTrophy2 libc) configure_windows_unwind(guest_nptrophy2_tests) - add_test(NAME guest_nptrophy2 COMMAND guest_nptrophy2_tests) + add_test(NAME guest_nptrophy2 COMMAND guest_nptrophy2_tests $) if(WIN32) set_tests_properties(guest_nptrophy2 PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") diff --git a/core/libs/prx/libSceNpTrophy2/include/TrophyPackage.hpp b/core/libs/prx/libSceNpTrophy2/include/TrophyPackage.hpp index f493e67ea1..d2e011b28d 100644 --- a/core/libs/prx/libSceNpTrophy2/include/TrophyPackage.hpp +++ b/core/libs/prx/libSceNpTrophy2/include/TrophyPackage.hpp @@ -25,6 +25,8 @@ using UcpFiles = std::map>; UcpFiles ReadUcp(std::string_view data); +struct JsonMember; + class Json { public: enum class Kind { Null, Bool, Number, String, Array, Object }; @@ -33,7 +35,7 @@ public: bool boolean = false; std::string text; std::vector items; - std::vector> members; + std::vector members; const Json* Find(std::string_view key) const; bool IsTrue() const; @@ -41,6 +43,11 @@ public: std::optional Id() const; }; +struct JsonMember { + std::string first; + Json second; +}; + Json ParseJson(std::string_view text); inline constexpr std::int32_t GradePlatinum = 1; diff --git a/core/libs/prx/libSceNpTrophy2/src/TrophyStore.cpp b/core/libs/prx/libSceNpTrophy2/src/TrophyStore.cpp index 5091aad105..f3876bd2a5 100644 --- a/core/libs/prx/libSceNpTrophy2/src/TrophyStore.cpp +++ b/core/libs/prx/libSceNpTrophy2/src/TrophyStore.cpp @@ -243,6 +243,10 @@ bool RecordUnlock(std::int32_t userId, std::uint32_t serviceLabel, std::int32_t } extern "C" bool NpTrophy2RecordUnlock_nid_no_patch(std::int32_t userId, std::uint32_t serviceLabel, std::int32_t trophyId) { + if (trophyId < 0) { + std::fprintf(stderr, "[trophy] ignored the unlock of trophy %d for user %d: trophy ids are not negative\n", trophyId, userId); + return false; + } if (const auto* package = NpTrophy2::Package(serviceLabel)) { if (const auto refusal = NpTrophy2::PostedUnlockRefusal(*package, trophyId)) { std::fprintf(stderr, "[trophy] ignored the posted unlock of trophy %d for user %d: %s\n", trophyId, userId, refusal->c_str()); @@ -267,6 +271,5 @@ extern "C" void NpTrophy2RemoveUnlockListener_nid_no_patch(std::uint64_t id) { auto& registry = NpTrophy2::listeners(); std::lock_guard dispatching(registry.dispatch); std::lock_guard lock(registry.mutex); - const auto removed = std::erase_if(registry.entries, [id](const NpTrophy2::Listener& entry) { return entry.id == id; }); - if (removed == 0) throw std::invalid_argument("no trophy unlock listener has id " + std::to_string(id)); + std::erase_if(registry.entries, [id](const NpTrophy2::Listener& entry) { return entry.id == id; }); } diff --git a/core/libs/prx/libSceNpTrophy2/tests/TrophyPackage.cpp b/core/libs/prx/libSceNpTrophy2/tests/TrophyPackage.cpp index 0e7b0e4364..122c2d7195 100644 --- a/core/libs/prx/libSceNpTrophy2/tests/TrophyPackage.cpp +++ b/core/libs/prx/libSceNpTrophy2/tests/TrophyPackage.cpp @@ -1,6 +1,7 @@ #include "prx/libSceNpTrophy2/include/TitleTrophies.hpp" #include "prx/libSceNpTrophy2/include/TrophyPackage.hpp" #include "prx/libSceNpTrophy2/include/TrophyStore.hpp" +#include "prx/libSceSystemService/LanguageLocale.hpp" #include #include @@ -272,6 +273,10 @@ void testPackage() { "a tropconf.json of more than 1048576 values was read"); check(contains(failure([] { NpTrophy2::ParseTrophyPackage("not a package", "en-US"); }), "fewer than the 64-byte header"), "a package that is not a container was read"); check(NpTrophy2::LanguageTag(1) == "en-US" && NpTrophy2::LanguageTag(0) == "ja-JP" && NpTrophy2::LanguageTag(2) == "fr-FR" && NpTrophy2::LanguageTag(29) == "id-ID", "language tags are wrong"); + for (int language = 0; language <= 30; ++language) { + const auto match = SystemServiceLocale::MatchLocale(NpTrophy2::LanguageTag(language)); + check(match.recognised && match.id == language, "the trophy tag of a console language does not map back to it"); + } bool thrown = false; try { NpTrophy2::LanguageTag(31); diff --git a/core/libs/prx/libSceNpTrophy2/tests/TrophyStore.cpp b/core/libs/prx/libSceNpTrophy2/tests/TrophyStore.cpp index 67651be3da..7e4ff8b3d8 100644 --- a/core/libs/prx/libSceNpTrophy2/tests/TrophyStore.cpp +++ b/core/libs/prx/libSceNpTrophy2/tests/TrophyStore.cpp @@ -217,6 +217,8 @@ void testQueries() { check(NpTrophy2RecordUnlock_nid_no_patch(InitialUser, 0, 12), "trophy 12 was not recorded"); check(!NpTrophy2RecordUnlock_nid_no_patch(InitialUser, 0, 12), "trophy 12 was recorded twice"); check(NpTrophy2RecordUnlock_nid_no_patch(InitialUser, 0, 0), "trophy 0 was not recorded"); + check(!throws([] { NpTrophy2RecordUnlock_nid_no_patch(InitialUser, 0, -1); }), "a negative trophy id threw across the C ABI"); + check(!NpTrophy2RecordUnlock_nid_no_patch(InitialUser, 0, -1), "a negative trophy id was recorded"); const auto after = NpTrophy2::CurrentTick(); NpTrophy2RemoveUnlockListener_nid_no_patch(listener); check(NpTrophy2RecordUnlock_nid_no_patch(InitialUser, 0, 5) && seen.unlocks.size() == 2, "the listener did not see each new unlock once"); @@ -236,7 +238,7 @@ void testQueries() { check(!trophyData(other, 12).unlocked && unlockedTrophies(other) == 0, "another user sees the unlocks"); check(read("_trophy/268435456_0.txt").find("12 ") != std::string::npos, "the unlock was not stored in the title folder"); check(throws([] { NpTrophy2AddUnlockListener_nid_no_patch(nullptr, nullptr); }), "a null listener was added"); - check(throws([listener] { NpTrophy2RemoveUnlockListener_nid_no_patch(listener); }), "a removed listener was removed again"); + check(!throws([listener] { NpTrophy2RemoveUnlockListener_nid_no_patch(listener); }), "removing a listener twice threw across the C ABI"); check(sceNpTrophy2DestroyContext(context) == 0 && sceNpTrophy2DestroyContext(other) == 0, "contexts were not destroyed"); } diff --git a/core/libs/prx/libSceNpUniversalDataSystem/CMakeLists.txt b/core/libs/prx/libSceNpUniversalDataSystem/CMakeLists.txt index b74a51652b..a0408bae0b 100644 --- a/core/libs/prx/libSceNpUniversalDataSystem/CMakeLists.txt +++ b/core/libs/prx/libSceNpUniversalDataSystem/CMakeLists.txt @@ -50,7 +50,7 @@ if(BUILD_TESTING) target_include_directories(guest_np_trophy_data_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_np_trophy_data_tests PRIVATE libSceNpUniversalDataSystem libSceNpTrophy2 libc) configure_windows_unwind(guest_np_trophy_data_tests) - add_test(NAME guest_np_trophy_data COMMAND guest_np_trophy_data_tests) + add_test(NAME guest_np_trophy_data COMMAND guest_np_trophy_data_tests $) if(WIN32) set_tests_properties(guest_np_trophy_data PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") diff --git a/core/libs/prx/libSceNpWebApi2/Export.cpp b/core/libs/prx/libSceNpWebApi2/Export.cpp index 1ede183689..3f8d5528e1 100644 --- a/core/libs/prx/libSceNpWebApi2/Export.cpp +++ b/core/libs/prx/libSceNpWebApi2/Export.cpp @@ -1,3 +1,4 @@ +#include "prx/libSceNpWebApi2/include/InventoryExports.hpp" #include "prx/libSceNpWebApi2/include/Api.hpp" #include #include @@ -42,6 +43,15 @@ static std::unordered_map pushHandles; extern "C" { +int APS5_VABI sceNpWebApi2IntGlobalPush2Initialize() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceNpWebApi2IntGlobalPush2Terminate() { + NotImplemented_nid_no_patch(__func__); +} + + int APS5_VABI sceNpWebApi2AbortRequest(int64_t request_id) { (void)request_id; return 0; diff --git a/core/libs/prx/libSceNpWebApi2/include/InventoryExports.hpp b/core/libs/prx/libSceNpWebApi2/include/InventoryExports.hpp new file mode 100644 index 0000000000..e6e12dc193 --- /dev/null +++ b/core/libs/prx/libSceNpWebApi2/include/InventoryExports.hpp @@ -0,0 +1,10 @@ +#pragma once + +#include "SceTypes.hpp" + +extern "C" { + +int APS5_VABI sceNpWebApi2IntGlobalPush2Initialize(); +int APS5_VABI sceNpWebApi2IntGlobalPush2Terminate(); + +} diff --git a/core/libs/prx/libSceNpWebApi2/libSceNpWebApi2.hpp b/core/libs/prx/libSceNpWebApi2/libSceNpWebApi2.hpp index 6462dce53f..773cf36480 100644 --- a/core/libs/prx/libSceNpWebApi2/libSceNpWebApi2.hpp +++ b/core/libs/prx/libSceNpWebApi2/libSceNpWebApi2.hpp @@ -3,3 +3,4 @@ #include "prx/libSceNpWebApi2/include/Firmware320Exports.hpp" #include "prx/libSceNpWebApi2/include/Api.hpp" +#include "prx/libSceNpWebApi2/include/InventoryExports.hpp" diff --git a/core/libs/prx/libScePad/CMakeLists.txt b/core/libs/prx/libScePad/CMakeLists.txt index 54630c0ffa..f979b4eff4 100644 --- a/core/libs/prx/libScePad/CMakeLists.txt +++ b/core/libs/prx/libScePad/CMakeLists.txt @@ -32,6 +32,39 @@ if(BUILD_TESTING) endif() endif() +if(BUILD_TESTING) + add_test_executable(guest_pad_controller_queries_tests ${LIBS_INCLUDE_DIR}/tests/GuestPadControllerQueries.cpp) + target_include_directories(guest_pad_controller_queries_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_pad_controller_queries_tests PRIVATE libScePad libc) + configure_windows_unwind(guest_pad_controller_queries_tests) + add_test(NAME guest_pad_controller_queries COMMAND guest_pad_controller_queries_tests) + if(WIN32) + set_tests_properties(guest_pad_controller_queries PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_pad_firmware_checks_tests ${LIBS_INCLUDE_DIR}/tests/GuestPadFirmwareChecks.cpp) + target_include_directories(guest_pad_firmware_checks_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_pad_firmware_checks_tests PRIVATE libScePad libc) + configure_windows_unwind(guest_pad_firmware_checks_tests) + add_test(NAME guest_pad_firmware_checks COMMAND guest_pad_firmware_checks_tests) + if(WIN32) + set_tests_properties(guest_pad_firmware_checks PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + +if(BUILD_TESTING) + add_test_executable(guest_pad_audio_route_tests ${LIBS_INCLUDE_DIR}/tests/GuestPadAudioRoute.cpp) + target_include_directories(guest_pad_audio_route_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_pad_audio_route_tests PRIVATE libScePad libc) + configure_windows_unwind(guest_pad_audio_route_tests) + add_test(NAME guest_pad_audio_route COMMAND guest_pad_audio_route_tests) + if(WIN32) + set_tests_properties(guest_pad_audio_route PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + if(BUILD_TESTING) add_test_executable(pad_rumble_intensity_tests tests/PadRumbleIntensity.cpp) target_include_directories(pad_rumble_intensity_tests PRIVATE ${LIBS_INCLUDE_DIR}) diff --git a/core/libs/prx/libScePad/Export.cpp b/core/libs/prx/libScePad/Export.cpp index e0c2fec1f7..d105933e65 100644 --- a/core/libs/prx/libScePad/Export.cpp +++ b/core/libs/prx/libScePad/Export.cpp @@ -26,6 +26,9 @@ static_assert(offsetof(PadVirtualTriggerInternalState, mode) == 8); static_assert(offsetof(PadVirtualTriggerInternalState, flags) == 9); static_assert(sizeof(PadLightBarBlinkingParameter) == 3); +static_assert(sizeof(PadContainerIdInformation) == 0x2004); +static_assert(sizeof(PadVolumeGain) == 4); +static_assert(offsetof(PadVolumeGain, micGain) == 3); static_assert(sizeof(PadButtonRemappingInfo) == 16); static_assert(offsetof(PadButtonRemappingInfo, mode) == 15); @@ -40,11 +43,12 @@ constexpr int PAD_ERROR_INVALID_PORT_INDEX = static_cast(0x809b0003); constexpr int PAD_ERROR_NOT_PERMITTED = static_cast(0x80920101); constexpr int PAD_USER_ID_INVALID = -1; -std::array g_opened{false}; -std::array g_everOpened{false}; -bool g_remoteOpened = false; -std::array g_specialUsers{}; +std::array g_opened{}; +std::array g_everOpened{}; +std::atomic_bool g_remoteOpened{false}; +std::array, PAD_SPECIAL_HANDLE_COUNT> g_specialUsers{}; std::atomic_bool g_initialized{false}; +std::atomic_bool g_particularMode{false}; bool SpecialHandle(int handle) { const int slot = handle - PAD_SPECIAL_HANDLE; @@ -68,12 +72,20 @@ bool KnownHandle(int handle) { return PortOf(handle) >= 0 || SpecialHandle(handle); } +int DeviceOf(int handle) { + if (!KnownHandle(handle)) return PAD_ERROR_INVALID_HANDLE; + if (SpecialHandle(handle)) return PAD_ERROR_DEVICE_NOT_CONNECTED; + return Pad::IsConnected(PortOf(handle)) ? PAD_OK : PAD_ERROR_DEVICE_NOT_CONNECTED; +} + bool OpenHandle(int handle) { const int port = PortOf(handle); return (port >= 0 && g_opened[port]) || SpecialHandle(handle); } constexpr std::size_t PAD_LEONARDO_UNIQUE_DATA_SIZE = 0x18; +constexpr std::uint32_t PAD_HISTORY_BUFFER_SIZE = 0x960; +constexpr std::int32_t PAD_OPEN_EXT2_PARAM_SIZE = 0x10; PadData SpecialData() { PadData data{}; @@ -246,7 +258,45 @@ int APS5_VABI scePadSetProcessPrivilege(int privilege) { } int APS5_VABI scePadSetParticularMode(bool mode) { - (void)mode; + g_particularMode.store(mode, std::memory_order_release); + return PAD_OK; +} + +int APS5_VABI scePadGetParticularMode(void) { + if (!Pad::IsInitialized()) return PAD_ERROR_NOT_INITIALIZED; + return g_particularMode.load(std::memory_order_acquire) ? 1 : 0; +} + +int APS5_VABI scePadGetControllerBusType(int handle, int* busType) { + if (!Pad::IsInitialized()) return PAD_ERROR_NOT_INITIALIZED; + if (busType == nullptr) return PAD_ERROR_INVALID_ARG; + const int device = DeviceOf(handle); + if (device != PAD_OK) return device; + *busType = PAD_CONNECT_TYPE_BLUETOOTH; + return PAD_OK; +} + +int APS5_VABI scePadGetJackState(int handle, int* state) { + if (!Pad::IsInitialized()) return PAD_ERROR_NOT_INITIALIZED; + if (state == nullptr) return PAD_ERROR_INVALID_ARG; + const int device = DeviceOf(handle); + if (device != PAD_OK) return device; + *state = 0; + return PAD_OK; +} + +int APS5_VABI scePadIsControllerUpdateRequired(int handle) { + if (!Pad::IsInitialized()) return PAD_ERROR_NOT_INITIALIZED; + return DeviceOf(handle); +} + +int APS5_VABI scePadGetContainerIdInformation(int handle, PadContainerIdInformation* info) { + if (!Pad::IsInitialized()) return PAD_ERROR_NOT_INITIALIZED; + if (info == nullptr) return PAD_ERROR_INVALID_ARG; + const int device = DeviceOf(handle); + if (device != PAD_OK) return device; + info->size = 0; + info->id[0] = '\0'; return PAD_OK; } @@ -262,15 +312,13 @@ int APS5_VABI scePadOpen_nid_postfix(int userId, int type, int index, const void const bool knownType = type == PAD_PORT_TYPE_STANDARD || type == PAD_PORT_TYPE_SPECIAL || SystemRemote(userId, type); if (!unknownType && !knownType) return PAD_ERROR_INVALID_ARG; if (userId == PAD_USER_ID_INVALID) return PAD_ERROR_INVALID_USER_ID; - if (unknownType) NotImplemented_nid_no_patch(__func__); + if (unknownType) return PAD_ERROR_INVALID_ARG; if (index != 0) return PAD_ERROR_INVALID_PORT_INDEX; if (type == PAD_PORT_TYPE_SPECIAL) { if (SpecialHandleOf(userId) != PAD_ERROR_DEVICE_NO_HANDLE) return PAD_ERROR_ALREADY_OPENED; for (int slot = 0; slot < PAD_SPECIAL_HANDLE_COUNT; ++slot) { - if (g_specialUsers[slot] == 0) { - g_specialUsers[slot] = userId; - return PAD_SPECIAL_HANDLE + slot; - } + int free = 0; + if (g_specialUsers[slot].compare_exchange_strong(free, userId)) return PAD_SPECIAL_HANDLE + slot; } return PAD_ERROR_UNKNOWN_USER; } @@ -408,6 +456,31 @@ int APS5_VABI scePadSetVibrationMode(int handle, int mode) { return PAD_OK; } +int APS5_VABI scePadIsSupportedAudioFunction(int handle) { + if (!Pad::IsInitialized()) return PAD_ERROR_NOT_INITIALIZED; + const int device = DeviceOf(handle); + if (device != PAD_OK) return device; + return 1; +} + +int APS5_VABI scePadSetAudioOutPath(int handle, int path) { + if (!Pad::IsInitialized()) return PAD_ERROR_NOT_INITIALIZED; + if (static_cast(path) > 4u) return PAD_ERROR_INVALID_ARG; + if (!KnownHandle(handle)) return PAD_ERROR_INVALID_HANDLE; + if (SpecialHandle(handle)) return PAD_OK; + Pad::SetAudioOutPath(PortOf(handle), path); + return PAD_OK; +} + +int APS5_VABI scePadSetVolumeGain(int handle, const PadVolumeGain* gain) { + if (!Pad::IsInitialized()) return PAD_ERROR_NOT_INITIALIZED; + if (gain == nullptr) return PAD_ERROR_INVALID_ARG; + if (!KnownHandle(handle)) return PAD_ERROR_INVALID_HANDLE; + if (SpecialHandle(handle)) return PAD_OK; + Pad::SetVolumeGain(PortOf(handle), gain->speakerVolume, gain->headsetVolume, gain->micGain); + return PAD_OK; +} + int APS5_VABI scePadSetVibrationTriggerEffectWeakWhileEmbeddedMicInUse(bool enabled) { (void)enabled; return PAD_OK; @@ -487,8 +560,9 @@ int APS5_VABI scePadLastLotusButtonData(std::int32_t handle, std::uint32_t param } int APS5_VABI scePadReadStateExt(std::int32_t handle, PadDataExtended* data) { + if (!KnownHandle(handle) && !RemoteHandle(handle)) return PAD_ERROR_INVALID_HANDLE; + if (data == nullptr) return PAD_ERROR_INVALID_ARG; NotImplemented_nid_no_patch(__func__); - return 0; } int APS5_VABI scePadSetExtensionReport(std::int32_t handle, const PadExtensionReport* report) { @@ -497,8 +571,10 @@ int APS5_VABI scePadSetExtensionReport(std::int32_t handle, const PadExtensionRe } int APS5_VABI scePadGetExtensionUnitInfo(std::int32_t handle, PadExtensionUnitInfo* info) { + if (!Pad::IsInitialized()) return PAD_ERROR_NOT_INITIALIZED; + if (info == nullptr) return PAD_ERROR_INVALID_ARG; + static_cast(handle); NotImplemented_nid_no_patch(__func__); - return 0; } int APS5_VABI scePadEnableExtensionPort(bool enabled) { @@ -510,9 +586,11 @@ int APS5_VABI scePadEnableSpecificDeviceClass(std::int32_t deviceClass) { return 0; } -int APS5_VABI scePadOpenExt2(std::uint64_t deviceId, std::int32_t type, const void* reserved) { +int APS5_VABI scePadOpenExt2(std::int32_t userId, std::int32_t paramSize, const void* param) { + if (!Pad::IsInitialized()) return PAD_ERROR_NOT_INITIALIZED; + if (paramSize != PAD_OPEN_EXT2_PARAM_SIZE || param == nullptr) return PAD_ERROR_INVALID_ARG; + static_cast(userId); NotImplemented_nid_no_patch(__func__); - return 0; } int APS5_VABI scePadDeviceOpen(std::uint64_t deviceId, std::int32_t type, std::int32_t index) { @@ -556,13 +634,15 @@ int APS5_VABI scePadSetLightBarBlinking(std::int32_t handle, const PadLightBarBl } std::uint64_t APS5_VABI scePadGetIdleCount(std::int32_t userId) { + if (!Pad::IsInitialized()) return 0; + static_cast(userId); NotImplemented_nid_no_patch(__func__); - return 0; } int APS5_VABI scePadGetControllerIdleCount(std::int32_t handle, std::uint64_t* count) { + if (!Pad::IsInitialized()) return PAD_ERROR_NOT_INITIALIZED; + if (handle == 0 || count == nullptr) return PAD_ERROR_INVALID_ARG; NotImplemented_nid_no_patch(__func__); - return 0; } int APS5_VABI scePadSetPowerSavingState(std::int32_t handle, std::uint8_t state) { @@ -601,8 +681,10 @@ int APS5_VABI scePadSetLoginUserNumber(std::int32_t number) { } int APS5_VABI scePadGetCapability(std::int32_t deviceId, std::uint32_t* capability) { + if (!Pad::IsInitialized()) return PAD_ERROR_NOT_INITIALIZED; + if (capability == nullptr) return PAD_ERROR_INVALID_ARG; + static_cast(deviceId); NotImplemented_nid_no_patch(__func__); - return 0; } int APS5_VABI scePadShareOutputData(std::int32_t source, std::int32_t destination) { @@ -626,8 +708,10 @@ int APS5_VABI scePadSetVibrationForce(std::int32_t handle, const PadVibrationPar } int APS5_VABI scePadGetVersionInfo(std::int32_t handle, PadVersionInfo* info) { + if (!Pad::IsInitialized()) return PAD_ERROR_NOT_INITIALIZED; + if (info == nullptr) return PAD_ERROR_INVALID_ARG; + static_cast(handle); NotImplemented_nid_no_patch(__func__); - return 0; } int APS5_VABI scePadGetControllerType(std::int32_t handle, std::int32_t* type) { @@ -911,18 +995,27 @@ int APS5_VABI scePadGetTelemetryInfo(std::int32_t handle, PadTelemetryInfo* info } int APS5_VABI scePadGetActiveTime(std::int32_t handle, std::int32_t type, std::uint64_t* activeTime) { + if (!Pad::IsInitialized()) return PAD_ERROR_NOT_INITIALIZED; + if (activeTime == nullptr) return PAD_ERROR_INVALID_ARG; + static_cast(handle); + static_cast(type); + *activeTime = 0; NotImplemented_nid_no_patch(__func__); - return 0; } int APS5_VABI scePadGetDeviceId(std::int32_t handle, std::int32_t* deviceId) { + if (!Pad::IsInitialized()) return PAD_ERROR_NOT_INITIALIZED; + if (deviceId == nullptr) return PAD_ERROR_INVALID_ARG; + *deviceId = -1; + if (!KnownHandle(handle)) return PAD_ERROR_INVALID_HANDLE; NotImplemented_nid_no_patch(__func__); - return 0; } int APS5_VABI scePadGetInfoByPortType(std::int32_t type, PadPortInfo* info) { + if (info == nullptr) return PAD_ERROR_INVALID_ARG; + if (!Pad::IsInitialized()) return PAD_ERROR_NOT_INITIALIZED; + if ((static_cast(type) & ~2u) != 0) return PAD_ERROR_INVALID_ARG; NotImplemented_nid_no_patch(__func__); - return 0; } int APS5_VABI scePadResetLightBarAll(void) { @@ -1053,8 +1146,10 @@ int APS5_VABI scePadDisconnectDevice(std::int32_t deviceId) { } int APS5_VABI scePadGetDeviceInfo(std::int32_t deviceId, PadDeviceInfo* info) { + if (!Pad::IsInitialized()) return PAD_ERROR_NOT_INITIALIZED; + if (info == nullptr) return PAD_ERROR_INVALID_ARG; + static_cast(deviceId); NotImplemented_nid_no_patch(__func__); - return 0; } int APS5_VABI scePadSetProcessFocus(const PadProcessFocusParameter* parameter) { @@ -1111,8 +1206,9 @@ int APS5_VABI scePadSetShareButtonMaskForRemotePlay(std::uint8_t mask) { } int APS5_VABI scePadSetPS4BcVibrationMode(std::uint8_t mode) { + if (!Pad::IsInitialized()) return PAD_ERROR_NOT_INITIALIZED; + static_cast(mode); NotImplemented_nid_no_patch(__func__); - return 0; } int APS5_VABI scePadSetVirtualDeviceComponentInformation(std::int32_t deviceId, const PadVirtualDeviceComponentInformation* info) { @@ -1130,9 +1226,12 @@ int APS5_VABI scePadGetLicenseControllerInformation(std::int32_t handle, PadLice return 0; } -int APS5_VABI scePadReadHistory(std::int32_t handle, PadHistoryData* data, std::int32_t count) { +int APS5_VABI scePadReadHistory(std::int32_t handle, PadHistoryData* data, std::uint32_t size) { + if (!Pad::IsInitialized()) return PAD_ERROR_NOT_INITIALIZED; + if (size != PAD_HISTORY_BUFFER_SIZE) return PAD_ERROR_INVALID_ARG; + static_cast(handle); + static_cast(data); NotImplemented_nid_no_patch(__func__); - return 0; } int APS5_VABI scePadStartRecording(void) { diff --git a/core/libs/prx/libScePad/include/Pad.hpp b/core/libs/prx/libScePad/include/Pad.hpp index 49154aa264..e45b06fcb1 100644 --- a/core/libs/prx/libScePad/include/Pad.hpp +++ b/core/libs/prx/libScePad/include/Pad.hpp @@ -113,6 +113,18 @@ struct PadVirtualDeviceParameter; struct PadVirtualDeviceId; struct PadVirtualDeviceComponentInformation; +struct PadContainerIdInformation { + std::uint32_t size; + char id[0x2000]; +}; + +struct PadVolumeGain { + std::uint8_t speakerVolume; + std::uint8_t headsetVolume; + std::uint8_t padding; + std::uint8_t micGain; +}; + struct PadVirtualTriggerInternalState { std::uint32_t states[2]; std::uint8_t mode; @@ -155,6 +167,14 @@ int APS5_VABI scePadGetTriggerEffectState(int handle, PadTriggerEffectStateInfor int APS5_VABI scePadGetExtControllerInformation(int handle, void* info); int APS5_VABI scePadSetProcessPrivilege(int privilege); int APS5_VABI scePadSetParticularMode(bool mode); +int APS5_VABI scePadGetParticularMode(void); +int APS5_VABI scePadGetControllerBusType(int handle, int* busType); +int APS5_VABI scePadGetJackState(int handle, int* state); +int APS5_VABI scePadIsControllerUpdateRequired(int handle); +int APS5_VABI scePadGetContainerIdInformation(int handle, PadContainerIdInformation* info); +int APS5_VABI scePadIsSupportedAudioFunction(int handle); +int APS5_VABI scePadSetAudioOutPath(int handle, int path); +int APS5_VABI scePadSetVolumeGain(int handle, const PadVolumeGain* gain); int APS5_VABI scePadInit_nid_postfix(void); int APS5_VABI scePadOpen_nid_postfix(int userId, int type, int index, const void* param); int APS5_VABI scePadOpenExt(int userId, int type, int index, const void* param); @@ -192,7 +212,7 @@ int APS5_VABI scePadSetExtensionReport(std::int32_t handle, const PadExtensionRe int APS5_VABI scePadGetExtensionUnitInfo(std::int32_t handle, PadExtensionUnitInfo* info); int APS5_VABI scePadEnableExtensionPort(bool enabled); int APS5_VABI scePadEnableSpecificDeviceClass(std::int32_t deviceClass); -int APS5_VABI scePadOpenExt2(std::uint64_t deviceId, std::int32_t type, const void* reserved); +int APS5_VABI scePadOpenExt2(std::int32_t userId, std::int32_t paramSize, const void* param); int APS5_VABI scePadDeviceOpen(std::uint64_t deviceId, std::int32_t type, std::int32_t index); int APS5_VABI scePadGetDataInternal(std::uint8_t port, PadData* data); int APS5_VABI scePadSetUserColor(std::int32_t handle, std::uint32_t color); @@ -309,7 +329,7 @@ int APS5_VABI scePadSetPS4BcVibrationMode(std::uint8_t mode); int APS5_VABI scePadSetVirtualDeviceComponentInformation(std::int32_t deviceId, const PadVirtualDeviceComponentInformation* info); int APS5_VABI scePadEnablePs4CompatibleMode(std::int32_t handle, bool enabled); int APS5_VABI scePadGetLicenseControllerInformation(std::int32_t handle, PadLicenseControllerInformation* info); -int APS5_VABI scePadReadHistory(std::int32_t handle, PadHistoryData* data, std::int32_t count); +int APS5_VABI scePadReadHistory(std::int32_t handle, PadHistoryData* data, std::uint32_t size); int APS5_VABI scePadStartRecording(void); int APS5_VABI scePadStopRecording(void); int APS5_VABI scePadRemoteControlInsertData(const std::uint8_t* data); diff --git a/core/libs/prx/libScePad/include/PadState.hpp b/core/libs/prx/libScePad/include/PadState.hpp index 1dfc108fcc..79b1dc6988 100644 --- a/core/libs/prx/libScePad/include/PadState.hpp +++ b/core/libs/prx/libScePad/include/PadState.hpp @@ -52,6 +52,11 @@ struct PadOutputState { bool triggerTouched = false; bool motionEnabled = true; int vibrationMode = 0; + int audioOutPath = 0; + bool volumeGainValid = false; + std::uint8_t speakerVolume = 0; + std::uint8_t headsetVolume = 0; + std::uint8_t micGain = 0; }; namespace Pad { @@ -63,6 +68,8 @@ void SetVibration(int port, std::uint8_t large, std::uint8_t small); void SetVibration(std::uint8_t large, std::uint8_t small); void SetVibrationMode(int port, int mode); void SetVibrationMode(int mode); +void SetAudioOutPath(int port, int path); +void SetVolumeGain(int port, std::uint8_t speaker, std::uint8_t headset, std::uint8_t mic); void SetLightBar(int port, bool valid, std::uint8_t r, std::uint8_t g, std::uint8_t b); void SetLightBar(bool valid, std::uint8_t r, std::uint8_t g, std::uint8_t b); void SetTriggerCommand(int port, int trigger, const std::uint8_t* command); diff --git a/core/libs/prx/libScePad/src/PadState.cpp b/core/libs/prx/libScePad/src/PadState.cpp index ee47ae1d03..2844e77320 100644 --- a/core/libs/prx/libScePad/src/PadState.cpp +++ b/core/libs/prx/libScePad/src/PadState.cpp @@ -374,6 +374,27 @@ void Pad::SetVibrationMode(int mode) { SetVibrationMode(0, mode); } +void Pad::SetAudioOutPath(int port, int path) { + if (port < 0 || port >= PAD_MAX_CONTROLLERS) return; + std::lock_guard lock(stateMutex); + auto& output = slots[port].output; + if (output.audioOutPath == path) return; + output.audioOutPath = path; + ++output.sequence; +} + +void Pad::SetVolumeGain(int port, std::uint8_t speaker, std::uint8_t headset, std::uint8_t mic) { + if (port < 0 || port >= PAD_MAX_CONTROLLERS) return; + std::lock_guard lock(stateMutex); + auto& output = slots[port].output; + if (output.volumeGainValid && output.speakerVolume == speaker && output.headsetVolume == headset && output.micGain == mic) return; + output.volumeGainValid = true; + output.speakerVolume = speaker; + output.headsetVolume = headset; + output.micGain = mic; + ++output.sequence; +} + void Pad::SetLightBar(int port, bool valid, std::uint8_t r, std::uint8_t g, std::uint8_t b) { if (port < 0 || port >= PAD_MAX_CONTROLLERS) return; std::lock_guard lock(stateMutex); diff --git a/core/libs/prx/libScePlayGo/Export.cpp b/core/libs/prx/libScePlayGo/Export.cpp index 574f2938ad..038a6005c6 100644 --- a/core/libs/prx/libScePlayGo/Export.cpp +++ b/core/libs/prx/libScePlayGo/Export.cpp @@ -1,6 +1,7 @@ #include "prx/libScePlayGo/include/Api.hpp" #include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include @@ -75,6 +76,28 @@ static void RejectUnreadablePlayGoFiles() { } } +static bool ReadExplicitChunks(ChunkTable& table) { + const char* text = std::getenv("APS5_PLAYGO_UNLISTED_CHUNKS"); + if (text == nullptr || text[0] == '\0') return false; + const std::string value(text); + if (value == "installed" || value == "absent") return false; + std::size_t start = 0; + while (start <= value.size()) { + const auto end = value.find(',', start); + const auto token = value.substr(start, end == std::string::npos ? std::string::npos : end - start); + std::uint32_t parsed = 0; + const auto result = std::from_chars(token.data(), token.data() + token.size(), parsed); + if (token.empty() || result.ec != std::errc{} || result.ptr != token.data() + token.size() || parsed > 0xFFFF) { + throw std::runtime_error("scePlayGo: APS5_PLAYGO_UNLISTED_CHUNKS must be installed, absent, or a comma-separated list of chunk ids from 0 to 65535, not \"" + value + "\""); + } + table.ids.insert(static_cast(parsed)); + if (end == std::string::npos) break; + start = end + 1; + } + table.defined = true; + return true; +} + static const ChunkTable& Chunks() { static const ChunkTable chunks = [] { ChunkTable table; @@ -82,8 +105,12 @@ static const ChunkTable& Chunks() { if (!file) { if (ReadPackageChunks(table)) return table; table.scenario = std::ifstream(ResolvePath_nid_no_patch("/app0/sce_sys/playgo-scenario.json")).good(); - if (!table.scenario) RejectUnreadablePlayGoFiles(); - else for (uint32_t id = 1; id < PLAYGO_SCENARIO_CHUNK_COUNT; ++id) table.ids.insert(static_cast(id)); + if (table.scenario) { + for (uint32_t id = 1; id < PLAYGO_SCENARIO_CHUNK_COUNT; ++id) table.ids.insert(static_cast(id)); + return table; + } + RejectUnreadablePlayGoFiles(); + ReadExplicitChunks(table); return table; } table.defined = true; @@ -112,7 +139,9 @@ static UnlistedChunkAnswer UnlistedChunks() { const std::string value(text); if (value == "installed") return UnlistedChunkAnswer::Installed; if (value == "absent") return UnlistedChunkAnswer::Absent; - throw std::runtime_error("scePlayGo: APS5_PLAYGO_UNLISTED_CHUNKS must be installed or absent, not \"" + value + "\""); + ChunkTable explicitChunks; + ReadExplicitChunks(explicitChunks); + return UnlistedChunkAnswer::Stop; }(); return answer; } @@ -128,7 +157,7 @@ static bool IsKnownChunk(uint16_t id) { } if (chunks.scenario) throw std::runtime_error("scePlayGo: sce_sys/playgo-chunk.dat is missing and sce_sys/playgo-scenario.json lists no chunks, so chunk ID " + std::to_string(id) + " outside the listed ids 0 to " + std::to_string(PLAYGO_SCENARIO_CHUNK_COUNT - 1) + " cannot be checked; set APS5_PLAYGO_UNLISTED_CHUNKS=installed or =absent to answer for this dump"); - throw std::runtime_error("scePlayGo: sce_sys/playgo-chunk.dat and sce_sys/playgo-scenario.json are missing, so chunk ID " + std::to_string(id) + " cannot be checked; set APS5_PLAYGO_UNLISTED_CHUNKS=installed or =absent to answer for this dump"); + throw std::runtime_error("scePlayGo: sce_sys/playgo-chunk.dat and sce_sys/playgo-scenario.json are missing, so chunk ID " + std::to_string(id) + " cannot be checked; set APS5_PLAYGO_UNLISTED_CHUNKS=installed, =absent, or a comma-separated list of chunk ids to answer for this dump"); } static int ValidateChunks(const uint16_t* chunk_ids, uint32_t number_of_entries) { diff --git a/core/libs/prx/libScePlayReady/Export.cpp b/core/libs/prx/libScePlayReady/Export.cpp index c09e68fee3..e9ba888948 100644 --- a/core/libs/prx/libScePlayReady/Export.cpp +++ b/core/libs/prx/libScePlayReady/Export.cpp @@ -1,4 +1,6 @@ #include "prx/libScePlayReady/include/Api.hpp" +#include +#include #include #include #include @@ -75,7 +77,12 @@ int APS5_VABI scePlayReadyApiStop() { int APS5_VABI scePlayReadyApiVersion(uint32_t index, uint64_t* version) { if (index >= std::size(ApiVersions) || version == nullptr) return PlayReadyInvalidArgument; - std::memcpy(version, ApiVersions[index], sizeof(ApiVersions[index])); + const auto savedErrno = errno; + if (!GuestCopyOut_nid_no_patch(version, ApiVersions[index], sizeof(ApiVersions[index]))) { + errno = savedErrno; + return PlayReadyInvalidArgument; + } + errno = savedErrno; return 0; } @@ -102,7 +109,13 @@ int APS5_VABI scePlayReadyBufferFree() { int APS5_VABI scePlayReadyBufferInit(void* buffer) { if (buffer == nullptr) return PlayReadyInvalidArgument; - std::memset(buffer, 0, BufferSize); + const std::array zeros{}; + const auto savedErrno = errno; + if (!GuestCopyOut_nid_no_patch(buffer, zeros.data(), zeros.size())) { + errno = savedErrno; + return PlayReadyInvalidArgument; + } + errno = savedErrno; return 0; } diff --git a/core/libs/prx/libScePlayerInvitationDialog/Export.cpp b/core/libs/prx/libScePlayerInvitationDialog/Export.cpp deleted file mode 100644 index 0f78112135..0000000000 --- a/core/libs/prx/libScePlayerInvitationDialog/Export.cpp +++ /dev/null @@ -1,17 +0,0 @@ -#include "prx/libScePlayerInvitationDialog/libScePlayerInvitationDialog.h" -#include -#include -#include "SceTypes.hpp" -#include "prx/libc/include/General.hpp" - -extern "C" { - -int APS5_VABI scePlayerInvitationDialogTerminate(void) { - return 0; -} - -int APS5_VABI scePlayerInvitationDialogUpdateStatus(void) { - return 0; -} - -} diff --git a/core/libs/prx/libScePlayerSelectionDialog/Export.cpp b/core/libs/prx/libScePlayerSelectionDialog/Export.cpp index d70f74b93b..8172d5144f 100644 --- a/core/libs/prx/libScePlayerSelectionDialog/Export.cpp +++ b/core/libs/prx/libScePlayerSelectionDialog/Export.cpp @@ -1,6 +1,7 @@ #include "prx/libScePlayerSelectionDialog/include/Api.hpp" #include "prx/libSceUserService/UserService.hpp" #include +#include #include #include #include @@ -75,16 +76,24 @@ int APS5_VABI scePlayerSelectionDialogGetResult(PlayerSelectionDialogResult* res const int status = g_status.load(); if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; if (status != COMMON_DIALOG_STATUS_FINISHED) return COMMON_DIALOG_ERROR_NOT_FINISHED; - result->result = COMMON_DIALOG_RESULT_USER_CANCELED; + const std::int32_t canceled = COMMON_DIALOG_RESULT_USER_CANCELED; + const int savedErrno = errno; + const bool copied = GuestCopyOut_nid_no_patch(&result->result, &canceled, sizeof(canceled)); + errno = savedErrno; + if (!copied) return COMMON_DIALOG_ERROR_ARG_NULL; return COMMON_DIALOG_RESULT_USER_CANCELED; } int APS5_VABI scePlayerSelectionDialogParamInitialize(PlayerSelectionDialogParam* param) { if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; - std::memset(param, 0, sizeof(*param)); - param->base.size = sizeof(param->base); - param->base.magic = static_cast(reinterpret_cast(¶m->base)) - 1060003575u; - param->size = sizeof(*param); + PlayerSelectionDialogParam local{}; + local.base.size = sizeof(local.base); + local.base.magic = static_cast(reinterpret_cast(¶m->base)) - 1060003575u; + local.size = sizeof(local); + const int savedErrno = errno; + const bool copied = GuestCopyOut_nid_no_patch(param, &local, sizeof(local)); + errno = savedErrno; + if (!copied) return COMMON_DIALOG_ERROR_ARG_NULL; return COMMON_DIALOG_OK; } diff --git a/core/libs/prx/libScePosixForWebKit/Resolver.cpp b/core/libs/prx/libScePosixForWebKit/Resolver.cpp index 8d7e1382a6..1ce651d7fe 100644 --- a/core/libs/prx/libScePosixForWebKit/Resolver.cpp +++ b/core/libs/prx/libScePosixForWebKit/Resolver.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #endif #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libc/include/HostThreadLocal.hpp" @@ -281,6 +282,20 @@ int APS5_VABI getaddrinfo_nid_postfix(const char* node, const char* service, } if (rawQuery && service) return 9; if (!Ready()) return 4; + const char* numericName = nullptr; + if (node && hints && (hints->flags & 2)) { + std::array v4{}; + std::array v6{}; + const char* scope = std::strchr(node, '%'); + const std::size_t length = scope ? static_cast(scope - node) : std::strlen(node); + std::array address{}; + bool numericV6 = false; + if (length < address.size()) { + std::memcpy(address.data(), node, length); + numericV6 = inet_pton(AF_INET6, address.data(), v6.data()) == 1; + } + if ((hints->flags & 4) || NumericAddress(node, v4) || numericV6) numericName = node; + } std::array queries{nativeHints, nativeHints}; std::size_t queryCount = 1; if (!nativeHints.ai_socktype && !nativeHints.ai_protocol) { @@ -340,11 +355,11 @@ int APS5_VABI getaddrinfo_nid_postfix(const char* node, const char* service, std::memcpy(bytes + 8, &v6->sin6_addr, 16); std::memcpy(bytes + 24, &v6->sin6_scope_id, 4); } - if (item->ai_canonname) { - const auto size = std::strlen(item->ai_canonname) + 1; + if (const char* canonical = item->ai_canonname ? item->ai_canonname : numericName) { + const auto size = std::strlen(canonical) + 1; converted->canonicalName = static_cast(std::malloc(size)); if (!converted->canonicalName) { failure = 6; break; } - std::memcpy(converted->canonicalName, item->ai_canonname, size); + std::memcpy(converted->canonicalName, canonical, size); } } ::freeaddrinfo(native); diff --git a/core/libs/prx/libScePsm/Export.cpp b/core/libs/prx/libScePsm/Export.cpp index d31d96f41f..f1f80e0be1 100644 --- a/core/libs/prx/libScePsm/Export.cpp +++ b/core/libs/prx/libScePsm/Export.cpp @@ -1,3 +1,5 @@ +#include "prx/libc/include/general/ExportMacros.hpp" +#include "prx/libScePsm/include/InventoryExports.hpp" #include "prx/libScePsm/include/Api.hpp" #include #include @@ -332,3 +334,272 @@ int APS5_VABI scePsmRegisterPInvokeCallTable() { } } + +extern "C" { + +int APS5_VABI libScePsmUnknown__minus_ndbidpphoc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-ndbidpphoc", libScePsmUnknown__minus_ndbidpphoc_nid_no_patch); + +int APS5_VABI libScePsmUnknown_0PIOM2UIUpQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0PIOM2UIUpQ", libScePsmUnknown_0PIOM2UIUpQ_nid_no_patch); + +int APS5_VABI libScePsmUnknown_0Tit25nFAP4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0Tit25nFAP4", libScePsmUnknown_0Tit25nFAP4_nid_no_patch); + +int APS5_VABI libScePsmUnknown_12ZHf6Z7WLg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("12ZHf6Z7WLg", libScePsmUnknown_12ZHf6Z7WLg_nid_no_patch); + +int APS5_VABI libScePsmUnknown_1Z9IoxZ508g_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("1Z9IoxZ508g", libScePsmUnknown_1Z9IoxZ508g_nid_no_patch); + +int APS5_VABI libScePsmUnknown_5DwG5treG0o_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("5DwG5treG0o", libScePsmUnknown_5DwG5treG0o_nid_no_patch); + +int APS5_VABI libScePsmUnknown_5dDsI2vg6dc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("5dDsI2vg6dc", libScePsmUnknown_5dDsI2vg6dc_nid_no_patch); + +int APS5_VABI libScePsmUnknown_6FPQyIqTJi0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("6FPQyIqTJi0", libScePsmUnknown_6FPQyIqTJi0_nid_no_patch); + +int APS5_VABI libScePsmUnknown_74_plus_2xQDzd_minus_c_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("74+2xQDzd-c", libScePsmUnknown_74_plus_2xQDzd_minus_c_nid_no_patch); + +int APS5_VABI libScePsmUnknown_8wWLk0PaYGQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("8wWLk0PaYGQ", libScePsmUnknown_8wWLk0PaYGQ_nid_no_patch); + +int APS5_VABI libScePsmUnknown_BaQ59CqSB34_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("BaQ59CqSB34", libScePsmUnknown_BaQ59CqSB34_nid_no_patch); + +int APS5_VABI libScePsmUnknown_C08zEeeiTBI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("C08zEeeiTBI", libScePsmUnknown_C08zEeeiTBI_nid_no_patch); + +int APS5_VABI libScePsmUnknown_CN90Qhgm32U_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("CN90Qhgm32U", libScePsmUnknown_CN90Qhgm32U_nid_no_patch); + +int APS5_VABI libScePsmUnknown_Ia3_minus_a5X02RA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("Ia3-a5X02RA", libScePsmUnknown_Ia3_minus_a5X02RA_nid_no_patch); + +int APS5_VABI libScePsmUnknown_IkQNwXle96Q_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("IkQNwXle96Q", libScePsmUnknown_IkQNwXle96Q_nid_no_patch); + +int APS5_VABI libScePsmUnknown_IyRVF5CAG48_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("IyRVF5CAG48", libScePsmUnknown_IyRVF5CAG48_nid_no_patch); + +int APS5_VABI libScePsmUnknown_KR9fL93LcnU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("KR9fL93LcnU", libScePsmUnknown_KR9fL93LcnU_nid_no_patch); + +int APS5_VABI libScePsmUnknown_L4SntCPTllw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("L4SntCPTllw", libScePsmUnknown_L4SntCPTllw_nid_no_patch); + +int APS5_VABI libScePsmUnknown_N0_plus_F2yO3hRc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("N0+F2yO3hRc", libScePsmUnknown_N0_plus_F2yO3hRc_nid_no_patch); + +int APS5_VABI libScePsmUnknown_OJ_plus_3wLrf7Yk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("OJ+3wLrf7Yk", libScePsmUnknown_OJ_plus_3wLrf7Yk_nid_no_patch); + +int APS5_VABI libScePsmUnknown_OVqKOEWE_minus_yc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("OVqKOEWE-yc", libScePsmUnknown_OVqKOEWE_minus_yc_nid_no_patch); + +int APS5_VABI libScePsmUnknown_PGLOWFhZTtM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("PGLOWFhZTtM", libScePsmUnknown_PGLOWFhZTtM_nid_no_patch); + +int APS5_VABI libScePsmUnknown_PN1KQl3wnoU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("PN1KQl3wnoU", libScePsmUnknown_PN1KQl3wnoU_nid_no_patch); + +int APS5_VABI libScePsmUnknown_QXGdhj5nfpM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("QXGdhj5nfpM", libScePsmUnknown_QXGdhj5nfpM_nid_no_patch); + +int APS5_VABI libScePsmUnknown_SpdSLM7ONAk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("SpdSLM7ONAk", libScePsmUnknown_SpdSLM7ONAk_nid_no_patch); + +int APS5_VABI libScePsmUnknown_UDGJZZ9jrCE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("UDGJZZ9jrCE", libScePsmUnknown_UDGJZZ9jrCE_nid_no_patch); + +int APS5_VABI libScePsmUnknown_UFkg2lqxKPQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("UFkg2lqxKPQ", libScePsmUnknown_UFkg2lqxKPQ_nid_no_patch); + +int APS5_VABI libScePsmUnknown_W8PzsJgkCGU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("W8PzsJgkCGU", libScePsmUnknown_W8PzsJgkCGU_nid_no_patch); + +int APS5_VABI libScePsmUnknown_WAKyoyoA8Dw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("WAKyoyoA8Dw", libScePsmUnknown_WAKyoyoA8Dw_nid_no_patch); + +int APS5_VABI libScePsmUnknown_XmlFvKZlm7k_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("XmlFvKZlm7k", libScePsmUnknown_XmlFvKZlm7k_nid_no_patch); + +int APS5_VABI libScePsmUnknown_YTcYAXVAW_plus_A_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("YTcYAXVAW+A", libScePsmUnknown_YTcYAXVAW_plus_A_nid_no_patch); + +int APS5_VABI libScePsmUnknown_YnZFBPDyB2A_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("YnZFBPDyB2A", libScePsmUnknown_YnZFBPDyB2A_nid_no_patch); + +int APS5_VABI libScePsmUnknown_bGtKORAU2eE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("bGtKORAU2eE", libScePsmUnknown_bGtKORAU2eE_nid_no_patch); + +int APS5_VABI libScePsmUnknown_c_minus_fS4SiII1Q_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("c-fS4SiII1Q", libScePsmUnknown_c_minus_fS4SiII1Q_nid_no_patch); + +int APS5_VABI libScePsmUnknown_cb_minus_Q6dnF1Uk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("cb-Q6dnF1Uk", libScePsmUnknown_cb_minus_Q6dnF1Uk_nid_no_patch); + +int APS5_VABI libScePsmUnknown_dsbtKMQDhjM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("dsbtKMQDhjM", libScePsmUnknown_dsbtKMQDhjM_nid_no_patch); + +int APS5_VABI libScePsmUnknown_e0WJestaOok_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("e0WJestaOok", libScePsmUnknown_e0WJestaOok_nid_no_patch); + +int APS5_VABI libScePsmUnknown_gW4wqd1GV8E_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("gW4wqd1GV8E", libScePsmUnknown_gW4wqd1GV8E_nid_no_patch); + +int APS5_VABI libScePsmUnknown_iKBWRnPccBs_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("iKBWRnPccBs", libScePsmUnknown_iKBWRnPccBs_nid_no_patch); + +int APS5_VABI libScePsmUnknown_j0tmOKH875I_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("j0tmOKH875I", libScePsmUnknown_j0tmOKH875I_nid_no_patch); + +int APS5_VABI libScePsmUnknown_jU_plus_doPzvPiQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("jU+doPzvPiQ", libScePsmUnknown_jU_plus_doPzvPiQ_nid_no_patch); + +int APS5_VABI libScePsmUnknown_jcyBwXYskmc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("jcyBwXYskmc", libScePsmUnknown_jcyBwXYskmc_nid_no_patch); + +int APS5_VABI libScePsmUnknown_m7QCwDRxED4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("m7QCwDRxED4", libScePsmUnknown_m7QCwDRxED4_nid_no_patch); + +int APS5_VABI libScePsmUnknown_ni7QHsx30nw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("ni7QHsx30nw", libScePsmUnknown_ni7QHsx30nw_nid_no_patch); + +int APS5_VABI libScePsmUnknown_owrMSr1b45c_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("owrMSr1b45c", libScePsmUnknown_owrMSr1b45c_nid_no_patch); + +int APS5_VABI libScePsmUnknown_t3a4oAqrvrI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("t3a4oAqrvrI", libScePsmUnknown_t3a4oAqrvrI_nid_no_patch); + +int APS5_VABI libScePsmUnknown_tFmZ8R28vsE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("tFmZ8R28vsE", libScePsmUnknown_tFmZ8R28vsE_nid_no_patch); + +int APS5_VABI libScePsmUnknown_th1_minus_yYnLsw0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("th1-yYnLsw0", libScePsmUnknown_th1_minus_yYnLsw0_nid_no_patch); + +int APS5_VABI libScePsmUnknown_udthK0YXar0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("udthK0YXar0", libScePsmUnknown_udthK0YXar0_nid_no_patch); + +int APS5_VABI libScePsmUnknown_vClC0x57Nqs_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("vClC0x57Nqs", libScePsmUnknown_vClC0x57Nqs_nid_no_patch); + +int APS5_VABI libScePsmUnknown_wDEygcO128w_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("wDEygcO128w", libScePsmUnknown_wDEygcO128w_nid_no_patch); + +int APS5_VABI libScePsmUnknown_xzd24XYJpb0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("xzd24XYJpb0", libScePsmUnknown_xzd24XYJpb0_nid_no_patch); + +int APS5_VABI libScePsmUnknown_yemgMfc2oW8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("yemgMfc2oW8", libScePsmUnknown_yemgMfc2oW8_nid_no_patch); + +} diff --git a/core/libs/prx/libScePsm/include/InventoryExports.hpp b/core/libs/prx/libScePsm/include/InventoryExports.hpp new file mode 100644 index 0000000000..ea789cdc1b --- /dev/null +++ b/core/libs/prx/libScePsm/include/InventoryExports.hpp @@ -0,0 +1,61 @@ +#pragma once + +#include "SceTypes.hpp" + +extern "C" { + +int APS5_VABI libScePsmUnknown__minus_ndbidpphoc_nid_no_patch(); +int APS5_VABI libScePsmUnknown_0PIOM2UIUpQ_nid_no_patch(); +int APS5_VABI libScePsmUnknown_0Tit25nFAP4_nid_no_patch(); +int APS5_VABI libScePsmUnknown_12ZHf6Z7WLg_nid_no_patch(); +int APS5_VABI libScePsmUnknown_1Z9IoxZ508g_nid_no_patch(); +int APS5_VABI libScePsmUnknown_5DwG5treG0o_nid_no_patch(); +int APS5_VABI libScePsmUnknown_5dDsI2vg6dc_nid_no_patch(); +int APS5_VABI libScePsmUnknown_6FPQyIqTJi0_nid_no_patch(); +int APS5_VABI libScePsmUnknown_74_plus_2xQDzd_minus_c_nid_no_patch(); +int APS5_VABI libScePsmUnknown_8wWLk0PaYGQ_nid_no_patch(); +int APS5_VABI libScePsmUnknown_BaQ59CqSB34_nid_no_patch(); +int APS5_VABI libScePsmUnknown_C08zEeeiTBI_nid_no_patch(); +int APS5_VABI libScePsmUnknown_CN90Qhgm32U_nid_no_patch(); +int APS5_VABI libScePsmUnknown_Ia3_minus_a5X02RA_nid_no_patch(); +int APS5_VABI libScePsmUnknown_IkQNwXle96Q_nid_no_patch(); +int APS5_VABI libScePsmUnknown_IyRVF5CAG48_nid_no_patch(); +int APS5_VABI libScePsmUnknown_KR9fL93LcnU_nid_no_patch(); +int APS5_VABI libScePsmUnknown_L4SntCPTllw_nid_no_patch(); +int APS5_VABI libScePsmUnknown_N0_plus_F2yO3hRc_nid_no_patch(); +int APS5_VABI libScePsmUnknown_OJ_plus_3wLrf7Yk_nid_no_patch(); +int APS5_VABI libScePsmUnknown_OVqKOEWE_minus_yc_nid_no_patch(); +int APS5_VABI libScePsmUnknown_PGLOWFhZTtM_nid_no_patch(); +int APS5_VABI libScePsmUnknown_PN1KQl3wnoU_nid_no_patch(); +int APS5_VABI libScePsmUnknown_QXGdhj5nfpM_nid_no_patch(); +int APS5_VABI libScePsmUnknown_SpdSLM7ONAk_nid_no_patch(); +int APS5_VABI libScePsmUnknown_UDGJZZ9jrCE_nid_no_patch(); +int APS5_VABI libScePsmUnknown_UFkg2lqxKPQ_nid_no_patch(); +int APS5_VABI libScePsmUnknown_W8PzsJgkCGU_nid_no_patch(); +int APS5_VABI libScePsmUnknown_WAKyoyoA8Dw_nid_no_patch(); +int APS5_VABI libScePsmUnknown_XmlFvKZlm7k_nid_no_patch(); +int APS5_VABI libScePsmUnknown_YTcYAXVAW_plus_A_nid_no_patch(); +int APS5_VABI libScePsmUnknown_YnZFBPDyB2A_nid_no_patch(); +int APS5_VABI libScePsmUnknown_bGtKORAU2eE_nid_no_patch(); +int APS5_VABI libScePsmUnknown_c_minus_fS4SiII1Q_nid_no_patch(); +int APS5_VABI libScePsmUnknown_cb_minus_Q6dnF1Uk_nid_no_patch(); +int APS5_VABI libScePsmUnknown_dsbtKMQDhjM_nid_no_patch(); +int APS5_VABI libScePsmUnknown_e0WJestaOok_nid_no_patch(); +int APS5_VABI libScePsmUnknown_gW4wqd1GV8E_nid_no_patch(); +int APS5_VABI libScePsmUnknown_iKBWRnPccBs_nid_no_patch(); +int APS5_VABI libScePsmUnknown_j0tmOKH875I_nid_no_patch(); +int APS5_VABI libScePsmUnknown_jU_plus_doPzvPiQ_nid_no_patch(); +int APS5_VABI libScePsmUnknown_jcyBwXYskmc_nid_no_patch(); +int APS5_VABI libScePsmUnknown_m7QCwDRxED4_nid_no_patch(); +int APS5_VABI libScePsmUnknown_ni7QHsx30nw_nid_no_patch(); +int APS5_VABI libScePsmUnknown_owrMSr1b45c_nid_no_patch(); +int APS5_VABI libScePsmUnknown_t3a4oAqrvrI_nid_no_patch(); +int APS5_VABI libScePsmUnknown_tFmZ8R28vsE_nid_no_patch(); +int APS5_VABI libScePsmUnknown_th1_minus_yYnLsw0_nid_no_patch(); +int APS5_VABI libScePsmUnknown_udthK0YXar0_nid_no_patch(); +int APS5_VABI libScePsmUnknown_vClC0x57Nqs_nid_no_patch(); +int APS5_VABI libScePsmUnknown_wDEygcO128w_nid_no_patch(); +int APS5_VABI libScePsmUnknown_xzd24XYJpb0_nid_no_patch(); +int APS5_VABI libScePsmUnknown_yemgMfc2oW8_nid_no_patch(); + +} diff --git a/core/libs/prx/libScePsm/libScePsm.hpp b/core/libs/prx/libScePsm/libScePsm.hpp index 8ba5c40198..60c94a8516 100644 --- a/core/libs/prx/libScePsm/libScePsm.hpp +++ b/core/libs/prx/libScePsm/libScePsm.hpp @@ -1,3 +1,4 @@ #pragma once #include "prx/libScePsm/include/Api.hpp" +#include "prx/libScePsm/include/InventoryExports.hpp" diff --git a/core/libs/prx/libSceRtc/Export.cpp b/core/libs/prx/libSceRtc/Export.cpp index 4f794fddb8..249c9a8961 100644 --- a/core/libs/prx/libSceRtc/Export.cpp +++ b/core/libs/prx/libSceRtc/Export.cpp @@ -145,9 +145,11 @@ std::uint64_t currentTick(std::size_t clock = 0) { int setCurrentTick(std::size_t clock, const RtcTick* tick) { if (!tick) return SCE_RTC_ERROR_INVALID_POINTER; - if (tick->tick < UNIX_EPOCH_TICK) return SCE_RTC_ERROR_INVALID_VALUE; + RtcTick source{}; + if (!GuestCopyIn_nid_no_patch(&source, tick, sizeof(source))) return SCE_RTC_ERROR_INVALID_POINTER; + if (source.tick < UNIX_EPOCH_TICK) return SCE_RTC_ERROR_INVALID_VALUE; std::lock_guard lock(clockMutex); - clocks[clock] = ClockState{tick->tick, std::chrono::steady_clock::now(), true}; + clocks[clock] = ClockState{source.tick, std::chrono::steady_clock::now(), true}; return 0; } @@ -447,13 +449,15 @@ int APS5_VABI sceRtcConvertTickToDateTime(const RtcTick* tick, RtcDateTime* time int APS5_VABI sceRtcGetCurrentTick(RtcTick* tick) { if (!tick) return SCE_RTC_ERROR_INVALID_POINTER; - tick->tick = currentTick(); + const RtcTick value{currentTick()}; + if (!GuestCopyOut_nid_no_patch(tick, &value, sizeof(value))) return SCE_RTC_ERROR_INVALID_POINTER; return 0; } int APS5_VABI sceRtcGetCurrentNetworkTick(RtcTick* tick) { if (!tick) return SCE_RTC_ERROR_INVALID_POINTER; - tick->tick = currentTick(1); + const RtcTick value{currentTick(1)}; + if (!GuestCopyOut_nid_no_patch(tick, &value, sizeof(value))) return SCE_RTC_ERROR_INVALID_POINTER; return 0; } @@ -461,7 +465,8 @@ int APS5_VABI sceRtcGetCurrentClock(RtcDateTime* time, int time_zone_minutes) { if (!time) return SCE_RTC_ERROR_INVALID_POINTER; RtcTick tick{currentTick()}; if (const int result = addTicks(&tick, &tick, time_zone_minutes, TICKS_PER_MINUTE); result != 0) return result; - *time = fromTick(tick.tick); + const RtcDateTime converted = fromTick(tick.tick); + if (!GuestCopyOut_nid_no_patch(time, &converted, sizeof(converted))) return SCE_RTC_ERROR_INVALID_POINTER; return 0; } @@ -471,7 +476,8 @@ int APS5_VABI sceRtcGetCurrentClockLocalTime(RtcDateTime* time) { const RtcTick utc{currentTick()}; if (utc.tick > MAX_TICK) return SCE_RTC_ERROR_INVALID_VALUE; if (const int result = addTicks(&local, &utc, localOffsetSeconds(utc.tick), TICKS_PER_SECOND); result != 0) return result; - *time = fromTick(local.tick); + const RtcDateTime converted = fromTick(local.tick); + if (!GuestCopyOut_nid_no_patch(time, &converted, sizeof(converted))) return SCE_RTC_ERROR_INVALID_POINTER; return 0; } @@ -580,8 +586,8 @@ int APS5_VABI sceRtcFormatRFC3339(char* dateTime, const RtcTick* utc, int timeZo const auto shift = static_cast(timeZoneMinutes) * TICKS_PER_MINUTE; if (rangeChecked() && (shift < 0 ? utc->tick < static_cast(-shift) : utc->tick > std::numeric_limits::max() - static_cast(shift))) return SCE_RTC_ERROR_INVALID_VALUE; const RtcTick local{utc->tick + static_cast(shift)}; - if (local.tick > MAX_TICK) return SCE_RTC_ERROR_INVALID_YEAR; const RtcDateTime time = fromTick(local.tick); + if (time.year < 1 || time.year > 9999) return SCE_RTC_ERROR_INVALID_YEAR; int written = std::snprintf(dateTime, 23, "%04u-%02u-%02uT%02u:%02u:%02u.%02u", time.year, time.month, time.day, time.hour, time.minute, time.second, time.microsecond / 10000); if (timeZoneMinutes == 0) { std::snprintf(dateTime + written, 2, "Z"); @@ -612,11 +618,11 @@ int APS5_VABI sceRtcFormatRFC2822(char* dateTime, const RtcTick* utc, int timeZo } if (timeZoneMinutes < -1439 || timeZoneMinutes > 1439) return SCE_RTC_ERROR_INVALID_VALUE; const RtcTick local{utc->tick + static_cast(static_cast(timeZoneMinutes) * TICKS_PER_MINUTE)}; - if (static_cast(local.tick) > static_cast(MAX_TICK)) return SCE_RTC_ERROR_INVALID_YEAR; - if (static_cast(utc->tick) < 0 || static_cast(local.tick) < 0) return SCE_RTC_ERROR_INVALID_VALUE; const RtcDateTime time = fromTick(local.tick); + if (static_cast(local.tick) >= 0 && (time.year < 1 || time.year > 9999)) return SCE_RTC_ERROR_INVALID_YEAR; + if (static_cast(utc->tick) < 0 || static_cast(local.tick) < 0) return SCE_RTC_ERROR_INVALID_VALUE; const int offset = timeZoneMinutes < 0 ? -timeZoneMinutes : timeZoneMinutes; - std::snprintf(dateTime, 32, "%s, %02u %s %04u %02u:%02u:%02u %c%02d%02d", WEEKDAY_NAMES[(local.tick / TICKS_PER_DAY + 1) % 7], time.day, + std::snprintf(dateTime, 32, "%s, %02u %s %04u %02u:%02u:%02u %c%02d%02d", WEEKDAY_NAMES[(daysFromCivil(time.year, time.month, time.day) + 1) % 7], time.day, MONTH_NAMES[time.month - 1], time.year, time.hour, time.minute, time.second, timeZoneMinutes < 0 ? '-' : '+', offset / 60, offset % 60); return 0; } @@ -703,19 +709,22 @@ int APS5_VABI sceRtcCompareTick(const RtcTick* first, const RtcTick* second) { int APS5_VABI sceRtcGetCurrentRawNetworkTick(RtcTick* tick) { if (!tick) return SCE_RTC_ERROR_INVALID_POINTER; - tick->tick = currentTick(4); + const RtcTick value{currentTick(4)}; + if (!GuestCopyOut_nid_no_patch(tick, &value, sizeof(value))) return SCE_RTC_ERROR_INVALID_POINTER; return 0; } int APS5_VABI sceRtcGetCurrentAdNetworkTick(RtcTick* tick) { if (!tick) return SCE_RTC_ERROR_INVALID_POINTER; - tick->tick = currentTick(2); + const RtcTick value{currentTick(2)}; + if (!GuestCopyOut_nid_no_patch(tick, &value, sizeof(value))) return SCE_RTC_ERROR_INVALID_POINTER; return 0; } int APS5_VABI sceRtcGetCurrentDebugNetworkTick(RtcTick* tick) { if (!tick) return SCE_RTC_ERROR_INVALID_POINTER; - tick->tick = currentTick(3); + const RtcTick value{currentTick(3)}; + if (!GuestCopyOut_nid_no_patch(tick, &value, sizeof(value))) return SCE_RTC_ERROR_INVALID_POINTER; return 0; } @@ -758,11 +767,11 @@ int APS5_VABI sceRtcFormatRFC3339Precise(char* date_time, const RtcTick* utc, in if (utc->tick < shift) return SCE_RTC_ERROR_INVALID_VALUE; localTick = utc->tick - shift; } else { - if (utc->tick > MAX_TICK || MAX_TICK - utc->tick < shift) return SCE_RTC_ERROR_INVALID_YEAR; + if (utc->tick > std::numeric_limits::max() - shift) return SCE_RTC_ERROR_INVALID_VALUE; localTick = utc->tick + shift; } - if (localTick > MAX_TICK) return SCE_RTC_ERROR_INVALID_YEAR; const RtcDateTime time = fromTick(localTick); + if (time.year < 1 || time.year > 9999) return SCE_RTC_ERROR_INVALID_YEAR; int written = std::snprintf(date_time, 20, "%04u-%02u-%02uT%02u:%02u:%02u", time.year, time.month, time.day, time.hour, time.minute, time.second); if (precision != 0) { std::uint32_t divisor = 1; diff --git a/core/libs/prx/libSceSaveData.native/CMakeLists.txt b/core/libs/prx/libSceSaveData.native/CMakeLists.txt index 1d30e2d536..59a990a35d 100644 --- a/core/libs/prx/libSceSaveData.native/CMakeLists.txt +++ b/core/libs/prx/libSceSaveData.native/CMakeLists.txt @@ -41,9 +41,20 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_test_executable(guest_savedata_event_callback_tests ${LIBS_INCLUDE_DIR}/tests/GuestSaveDataEventCallback.cpp) + target_include_directories(guest_savedata_event_callback_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_savedata_event_callback_tests PRIVATE libSceSaveData.native libkernel libc) + configure_windows_unwind(guest_savedata_event_callback_tests) + add_test(NAME guest_savedata_event_callback COMMAND guest_savedata_event_callback_tests) + set_tests_properties(guest_savedata_event_callback PROPERTIES TIMEOUT 60) + if(WIN32) + set_tests_properties(guest_savedata_event_callback PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_savedata_ps4_tests ${LIBS_INCLUDE_DIR}/tests/GuestSaveDataPs4.cpp) target_include_directories(guest_savedata_ps4_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_savedata_ps4_tests PRIVATE libSceSaveData.native libc) + target_link_libraries(guest_savedata_ps4_tests PRIVATE libSceSaveData.native libc ${CMAKE_DL_LIBS}) configure_windows_unwind(guest_savedata_ps4_tests) add_test(NAME guest_savedata_ps4 COMMAND guest_savedata_ps4_tests) if(WIN32) diff --git a/core/libs/prx/libSceSaveData.native/Export.cpp b/core/libs/prx/libSceSaveData.native/Export.cpp index 80382255ef..c3bc7d2071 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -2,10 +2,13 @@ #include "prx/libSceSaveData/include/Api.hpp" #include #include +#include +#include #include #include #include #include +#include #include #include #include @@ -19,11 +22,18 @@ #include #include #include +#include +#include +#ifndef _WIN32 +#include +#include +#endif #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "prx/libc/include/GuestArena.hpp" #include "prx/libc/include/RemoveTree.hpp" #include "prx/libkernel/AppMetadata/include/AppMetadata.hpp" +#include "prx/libkernel/include/Api.hpp" #include "SaveData.hpp" #include "prx/libSceSaveData.native/include/SaveDataHostSpace.hpp" #include "prx/libSceSaveData/SaveDataFile.hpp" @@ -50,9 +60,110 @@ static std::string save_root() { // Events the title polls with sceSaveDataGetEventResult; a backup completes at once here. static std::mutex g_state_mutex; static std::deque g_events; +constexpr std::uint32_t SAVE_DATA_EVENT_TYPE_UMOUNT_BACKUP = 1; constexpr std::uint32_t SAVE_DATA_EVENT_TYPE_BACKUP = 2; constexpr std::uint32_t SAVE_DATA_EVENT_TYPE_CONVERT = 5; +namespace { + +struct EventDelivery { + SaveDataEvent event; + SaveDataEventCallback callback; + void* userdata; +}; + +struct EventWorker { + Pthread thread = nullptr; + bool stopping = false; +}; + +struct EventCallbacks { + std::recursive_mutex calling; + std::mutex mutex; + std::condition_variable wake; + SaveDataEventCallback callback = nullptr; + void* userdata = nullptr; + std::deque pending; + EventWorker* worker = nullptr; + Pthread delivering = nullptr; +}; + +EventCallbacks& Callbacks() { + static auto* callbacks = new EventCallbacks; + return *callbacks; +} + +void* APS5_VABI DeliverEvents(void* argument) { + auto& callbacks = Callbacks(); + auto* worker = static_cast(argument); + for (;;) { + { + std::unique_lock lock(callbacks.mutex); + callbacks.wake.wait(lock, [&] { return worker->stopping || !callbacks.pending.empty(); }); + if (worker->stopping) return nullptr; + } + std::lock_guard calling(callbacks.calling); + EventDelivery delivery{}; + { + std::lock_guard lock(callbacks.mutex); + if (worker->stopping) return nullptr; + if (callbacks.pending.empty()) continue; + delivery = callbacks.pending.front(); + callbacks.pending.pop_front(); + callbacks.delivering = scePthreadSelf(); + } + delivery.callback(&delivery.event, delivery.userdata); + std::lock_guard lock(callbacks.mutex); + callbacks.delivering = nullptr; + } +} + +void StartEventDelivery() { + auto& callbacks = Callbacks(); + auto worker = std::make_unique(); + std::lock_guard lock(callbacks.mutex); + if (callbacks.worker != nullptr) return; + if (scePthreadCreate(&worker->thread, nullptr, DeliverEvents, worker.get(), "SceSaveDataEventCallback") != 0) { + throw std::runtime_error("sceSaveDataRegisterEventCallback: scePthreadCreate failed for the event callback thread"); + } + callbacks.worker = worker.release(); +} + +EventWorker* DetachEventDelivery() { + auto& callbacks = Callbacks(); + std::lock_guard lock(callbacks.mutex); + callbacks.callback = nullptr; + callbacks.userdata = nullptr; + callbacks.pending.clear(); + auto* worker = std::exchange(callbacks.worker, nullptr); + if (worker != nullptr) worker->stopping = true; + return worker; +} + +void JoinEventDelivery(EventWorker* worker) { + std::unique_ptr owned(worker); + Callbacks().wake.notify_all(); + if (scePthreadJoin(owned->thread, nullptr) != 0) throw std::runtime_error("sceSaveDataTerminate: scePthreadJoin failed for the event callback thread"); +} + +bool InEventCallback() { + auto& callbacks = Callbacks(); + std::lock_guard lock(callbacks.mutex); + return callbacks.delivering != nullptr && scePthreadEqual(scePthreadSelf(), callbacks.delivering) != 0; +} + +void DeliverEvent(const SaveDataEvent& event) { + auto& callbacks = Callbacks(); + { + std::lock_guard lock(callbacks.mutex); + if (callbacks.callback == nullptr || callbacks.worker == nullptr) return; + callbacks.pending.push_back({event, callbacks.callback, callbacks.userdata}); + } + callbacks.wake.notify_all(); +} + +} + static bool dir_name_match(const char* str, const char* pattern) { if (pattern == nullptr || pattern[0] == '\0') { return true; @@ -314,8 +425,11 @@ int APS5_VABI sceSaveDataBackup(const SaveDataBackup* backup) { event.user_id = backup->user_id; if (backup->title_id != nullptr) event.title_id = *backup->title_id; event.dir_name = *backup->dir_name; - std::lock_guard lock(g_state_mutex); - g_events.push_back(event); + { + std::lock_guard lock(g_state_mutex); + g_events.push_back(event); + } + DeliverEvent(event); return SAVE_DATA_OK; } @@ -334,6 +448,10 @@ static std::string blocks_path(const std::string& real_path) { return real_path + ".blocks"; } +static bool isValidMountPoint(const SaveDataMountPoint* mount_point) { + return mount_point != nullptr && std::memchr(mount_point->data, '\0', sizeof(mount_point->data)) != nullptr; +} + static bool isMountedDirectory(const std::string& path, std::error_code& error) { for (const auto& slot : g_slots) { if (!slot.used) continue; @@ -352,16 +470,301 @@ static bool isMountedDirectory(const std::string& path, std::error_code& error) return false; } +static constexpr char BACKUP_DIR[] = "_sd_backup"; +static constexpr char BACKUP_STAGING_DIR[] = "_sd_backup_tmp"; +static constexpr char BACKUP_OLD_DIR[] = "_sd_backup_old"; +static constexpr char RESTORE_STAGING_DIR[] = "_sd_restore_tmp"; +static constexpr char RESTORE_OLD_DIR[] = "_sd_restore_old"; + +static std::atomic g_file_step_hook{nullptr}; + +void SaveDataSetFileStepHook_nid_no_patch(bool (*hook)(const char* step, const char* path)) { + g_file_step_hook.store(hook, std::memory_order_release); +} + +static void file_step(const char* step, const std::string& path) { + const auto hook = g_file_step_hook.load(std::memory_order_acquire); + if (hook != nullptr && !hook(step, path.c_str())) { + throw std::filesystem::filesystem_error(std::string("SaveData: ") + step + " failed", path, std::make_error_code(std::errc::io_error)); + } +} + +static bool is_link(const std::filesystem::path& path, const std::filesystem::file_status& status) { +#ifdef _WIN32 + const DWORD attributes = GetFileAttributesW(path.c_str()); + if (attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0) { + return true; + } +#endif + return std::filesystem::is_symlink(status); +} + +static std::filesystem::file_type plain_type(const std::string& path) { + std::error_code error; + const auto status = std::filesystem::symlink_status(path, error); + if (status.type() == std::filesystem::file_type::not_found) { + return std::filesystem::file_type::not_found; + } + if (error) { + throw std::filesystem::filesystem_error("SaveData: cannot read the type of", path, error); + } + if (is_link(path, status) || (status.type() != std::filesystem::file_type::regular && status.type() != std::filesystem::file_type::directory)) { + throw std::filesystem::filesystem_error("SaveData: not a plain file or directory", path, std::make_error_code(std::errc::operation_not_permitted)); + } + return status.type(); +} + +static void require_plain_tree(const std::string& path) { + if (plain_type(path) != std::filesystem::file_type::directory) { + return; + } + for (const auto& entry : std::filesystem::directory_iterator(path)) { + require_plain_tree(entry.path().string()); + } +} + +static void require_inside(const std::string& root, const std::string& path) { + const auto base = std::filesystem::weakly_canonical(std::filesystem::absolute(root)); + const auto target = std::filesystem::weakly_canonical(std::filesystem::absolute(path)); + const auto [inBase, inTarget] = std::mismatch(base.begin(), base.end(), target.begin(), target.end()); + if (inBase != base.end() || inTarget == target.end()) { + throw std::filesystem::filesystem_error("SaveData: a save path leaves its root", path, root, std::make_error_code(std::errc::operation_not_permitted)); + } +} + +static void copy_file_step(const char* step, const std::string& from, const std::string& to) { + if (plain_type(from) != std::filesystem::file_type::regular) { + throw std::filesystem::filesystem_error("SaveData: not a plain file", from, std::make_error_code(std::errc::operation_not_permitted)); + } + file_step(step, to); + std::filesystem::copy_file(from, to); +} + +static void copy_tree(const char* step, const std::string& from, const std::string& to) { + file_step(step, to); + std::filesystem::create_directory(to); + for (const auto& entry : std::filesystem::directory_iterator(from)) { + const std::string source = entry.path().string(); + const std::string target = to + "/" + entry.path().filename().string(); + if (plain_type(source) == std::filesystem::file_type::directory) { + copy_tree(step, source, target); + } else { + copy_file_step(step, source, target); + } + } +} + +static std::map tree_sizes(const std::string& root) { + std::map sizes; + for (const auto& entry : std::filesystem::recursive_directory_iterator(root)) { + sizes[entry.path().lexically_relative(root).generic_string()] = entry.is_directory() ? std::numeric_limits::max() : entry.file_size(); + } + return sizes; +} + +static void flush_path(const std::string& path, bool directory) { +#ifdef _WIN32 + if (directory) { + return; + } + const HANDLE handle = CreateFileW(std::filesystem::path(path).c_str(), GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + if (handle == INVALID_HANDLE_VALUE) { + throw std::filesystem::filesystem_error("SaveData: cannot open to flush", path, std::error_code(static_cast(GetLastError()), std::system_category())); + } + const bool flushed = FlushFileBuffers(handle) != 0; + const DWORD error = GetLastError(); + CloseHandle(handle); + if (!flushed) { + throw std::filesystem::filesystem_error("SaveData: cannot flush", path, std::error_code(static_cast(error), std::system_category())); + } +#else + const int descriptor = ::open(path.c_str(), O_RDONLY | O_CLOEXEC | (directory ? O_DIRECTORY : 0)); + if (descriptor < 0) { + throw std::filesystem::filesystem_error("SaveData: cannot open to flush", path, std::error_code(errno, std::generic_category())); + } + const int result = ::fsync(descriptor); + const int error = errno; + ::close(descriptor); + if (result != 0 && error != EINVAL) { + throw std::filesystem::filesystem_error("SaveData: cannot flush", path, std::error_code(error, std::generic_category())); + } +#endif +} + +static void flush_tree(const std::string& root) { + for (const auto& entry : std::filesystem::recursive_directory_iterator(root)) { + flush_path(entry.path().string(), entry.is_directory()); + } + flush_path(root, true); +} + +struct FileSwap { + std::vector> moved; + + void move(const char* step, const std::string& from, const std::string& to) { + file_step(step, to); + std::filesystem::rename(from, to); + moved.emplace_back(from, to); + flush_path(std::filesystem::path(from).parent_path().string(), true); + flush_path(std::filesystem::path(to).parent_path().string(), true); + } + + void undo(const char* step) { + while (!moved.empty()) { + const auto [from, to] = moved.back(); + file_step(step, from); + std::filesystem::rename(to, from); + moved.pop_back(); + flush_path(std::filesystem::path(from).parent_path().string(), true); + flush_path(std::filesystem::path(to).parent_path().string(), true); + } + } +}; + +static std::string backup_entry(const char* root, const std::string& dirName) { + return std::string(root) + "/" + dirName; +} + +static std::string entry_save(const std::string& entry) { + return entry + "/save"; +} + +static void require_plain_paths(const std::string& dirName, std::initializer_list roots) { + for (const char* root : roots) { + if (plain_type(root) == std::filesystem::file_type::regular) { + throw std::filesystem::filesystem_error("SaveData: a save root is not a directory", root, std::make_error_code(std::errc::not_a_directory)); + } + const std::string entry = backup_entry(root, dirName); + for (const std::string& path : {entry, param_path(entry), blocks_path(entry)}) { + require_inside(root, path); + require_plain_tree(path); + } + } +} + +static bool parse_dir_name(const SceSaveDataDirName* name, std::string* dirName) { + const auto* nameEnd = static_cast(std::memchr(name->data, '\0', sizeof(name->data))); + if (nameEnd == nullptr) { + return false; + } + dirName->assign(name->data, static_cast(nameEnd - name->data)); + return !dirName->empty() && *dirName != "." && *dirName != ".." && dirName->find_first_of("/\\:") == std::string::npos; +} + +static bool running_title(const SceSaveDataTitleId* titleId) { + return titleId == nullptr || std::strncmp(titleId->data, GetAppTitleId_nid_postfix().value, sizeof(titleId->data)) == 0; +} + +static int read_backup(const std::string& dirName, SaveDataParam* param) { + const std::string entry = backup_entry(BACKUP_DIR, dirName); + std::error_code error; + if (!std::filesystem::exists(entry, error)) { + return error ? SAVE_DATA_ERROR_INTERNAL : SAVE_DATA_ERROR_NOT_FOUND; + } + const std::string backup = entry_save(entry); + std::vector bytes; + if (!std::filesystem::is_directory(backup, error) || !std::filesystem::is_regular_file(param_path(backup), error) || !read_file_all(param_path(backup), bytes) || bytes.size() != sizeof(SaveDataParam)) { + return SAVE_DATA_ERROR_INTERNAL; + } + const auto blocks = std::filesystem::status(blocks_path(backup), error).type(); + if (blocks != std::filesystem::file_type::not_found && blocks != std::filesystem::file_type::regular) { + return SAVE_DATA_ERROR_INTERNAL; + } + if (param != nullptr) { + std::memcpy(param, bytes.data(), sizeof(SaveDataParam)); + } + return SAVE_DATA_OK; +} + +static void backupSave(const std::string& dirName) { + const std::string source = save_root() + "/" + dirName; + const std::string entry = backup_entry(BACKUP_DIR, dirName); + const std::string staged = backup_entry(BACKUP_STAGING_DIR, dirName); + const std::string old = backup_entry(BACKUP_OLD_DIR, dirName); + require_plain_paths(dirName, {SAVE_DIR, BACKUP_DIR, BACKUP_STAGING_DIR, BACKUP_OLD_DIR}); + RemoveTree(staged); + RemoveTree(old); + std::filesystem::create_directories(staged); + FileSwap swap; + try { + copy_tree("backup.copy", source, entry_save(staged)); + file_step("backup.copy", param_path(entry_save(staged))); + if (!store_param(entry_save(staged), load_param(source))) { + throw std::filesystem::filesystem_error("SaveData: cannot write the backup's param file", param_path(entry_save(staged)), std::make_error_code(std::errc::io_error)); + } + if (std::filesystem::exists(blocks_path(source))) { + copy_file_step("backup.copy", blocks_path(source), blocks_path(entry_save(staged))); + } + flush_tree(staged); + std::filesystem::create_directories(BACKUP_DIR); + if (std::filesystem::exists(entry)) { + std::filesystem::create_directories(BACKUP_OLD_DIR); + swap.move("backup.aside", entry, old); + } + swap.move("backup.place", staged, entry); + } catch (...) { + swap.undo("backup.rollback"); + std::error_code ignored; + RemoveTree(staged, ignored); + throw; + } + std::error_code ignored; + RemoveTree(old, ignored); +} + +static void restoreSave(const std::string& dirName) { + const std::string live = save_root() + "/" + dirName; + const std::string backupEntry = backup_entry(BACKUP_DIR, dirName); + const std::string backup = entry_save(backupEntry); + const std::string stagedEntry = backup_entry(RESTORE_STAGING_DIR, dirName); + const std::string oldEntry = backup_entry(RESTORE_OLD_DIR, dirName); + const std::string staged = entry_save(stagedEntry); + const std::string old = entry_save(oldEntry); + require_plain_paths(dirName, {SAVE_DIR, BACKUP_DIR, RESTORE_STAGING_DIR, RESTORE_OLD_DIR}); + RemoveTree(stagedEntry); + RemoveTree(oldEntry); + std::filesystem::create_directories(stagedEntry); + std::filesystem::create_directories(oldEntry); + FileSwap swap; + try { + copy_tree("restore.copy", backup, staged); + copy_file_step("restore.copy", param_path(backup), param_path(staged)); + if (std::filesystem::exists(blocks_path(backup))) { + copy_file_step("restore.copy", blocks_path(backup), blocks_path(staged)); + } + if (tree_sizes(stagedEntry) != tree_sizes(backupEntry)) { + throw std::filesystem::filesystem_error("SaveData: the staged copy of the backup is incomplete", stagedEntry, std::make_error_code(std::errc::io_error)); + } + flush_tree(stagedEntry); + swap.move("restore.aside", live, old); + for (const auto& [from, to] : {std::pair{param_path(live), param_path(old)}, std::pair{blocks_path(live), blocks_path(old)}}) { + if (std::filesystem::exists(from)) { + swap.move("restore.aside", from, to); + } + } + swap.move("restore.place", staged, live); + swap.move("restore.place", param_path(staged), param_path(live)); + if (std::filesystem::exists(blocks_path(staged))) { + swap.move("restore.place", blocks_path(staged), blocks_path(live)); + } + } catch (...) { + swap.undo("restore.rollback"); + std::error_code ignored; + RemoveTree(stagedEntry, ignored); + RemoveTree(oldEntry, ignored); + throw; + } + std::error_code ignored; + RemoveTree(oldEntry, ignored); + RemoveTree(stagedEntry, ignored); +} + static int deleteSave(const SaveDataDelete* del) { if (del == nullptr || del->dir_name == nullptr) { return SAVE_DATA_ERROR_PARAMETER; } - const auto* nameEnd = static_cast(std::memchr(del->dir_name->data, '\0', sizeof(del->dir_name->data))); - if (nameEnd == nullptr) { - return SAVE_DATA_ERROR_PARAMETER; - } - const std::string dirName(del->dir_name->data, static_cast(nameEnd - del->dir_name->data)); - if (dirName.empty() || dirName == "." || dirName == ".." || dirName.find_first_of("/\\:") != std::string::npos) { + std::string dirName; + if (!parse_dir_name(del->dir_name, &dirName)) { return SAVE_DATA_ERROR_PARAMETER; } const std::string path = save_root() + "/" + dirName; @@ -375,6 +778,7 @@ static int deleteSave(const SaveDataDelete* del) { std::error_code ec; std::filesystem::remove(param_path(path), ec); std::filesystem::remove(blocks_path(path), ec); + RemoveTree(backup_entry(BACKUP_DIR, dirName)); return SAVE_DATA_OK; } @@ -555,7 +959,7 @@ int APS5_VABI sceSaveDataGetEventResult(const void* event_param, SaveDataEvent* } static int getMountInfo(const SaveDataMountPoint* mount_point, SaveDataMountInfo* info) { - if (mount_point == nullptr || info == nullptr) { + if (!isValidMountPoint(mount_point) || info == nullptr) { return SAVE_DATA_ERROR_PARAMETER; } std::lock_guard lock(g_slots_mutex); @@ -572,12 +976,12 @@ static int getMountInfo(const SaveDataMountPoint* mount_point, SaveDataMountInfo int APS5_VABI sceSaveDataGetMountInfo(const SaveDataMountPoint* mount_point, SaveDataMountInfo* info) { const int rc = getMountInfo(mount_point, info); - SAVEDATA_TRACE("getMountInfo point=%s -> 0x%08x", mount_point != nullptr ? mount_point->data : "(null)", static_cast(rc)); + SAVEDATA_TRACE("getMountInfo point=%.*s -> 0x%08x", static_cast(sizeof(SaveDataMountPoint::data)), mount_point != nullptr ? mount_point->data : "(null)", static_cast(rc)); return rc; } static int getParam(const SaveDataMountPoint* mount_point, uint32_t param_type, void* param_buf, size_t param_buf_size, size_t* got_size) { - if (mount_point == nullptr || param_buf == nullptr) { + if (!isValidMountPoint(mount_point) || param_buf == nullptr) { return SAVE_DATA_ERROR_PARAMETER; } std::lock_guard lock(g_slots_mutex); @@ -599,7 +1003,7 @@ static int getParam(const SaveDataMountPoint* mount_point, uint32_t param_type, int APS5_VABI sceSaveDataGetParam(const SaveDataMountPoint* mount_point, uint32_t param_type, void* param_buf, size_t param_buf_size, size_t* got_size) { const int rc = getParam(mount_point, param_type, param_buf, param_buf_size, got_size); - SAVEDATA_TRACE("getParam point=%s type=%u size=%zu -> 0x%08x got=%zu", mount_point != nullptr ? mount_point->data : "(null)", param_type, param_buf_size, static_cast(rc), got_size != nullptr ? *got_size : static_cast(0)); + SAVEDATA_TRACE("getParam point=%.*s type=%u size=%zu -> 0x%08x got=%zu", static_cast(sizeof(SaveDataMountPoint::data)), mount_point != nullptr ? mount_point->data : "(null)", param_type, param_buf_size, static_cast(rc), got_size != nullptr ? *got_size : static_cast(0)); return rc; } @@ -654,13 +1058,14 @@ int APS5_VABI sceSaveDataGetSaveDataMemory2(SaveDataMemoryGet2* get_param) { int APS5_VABI sceSaveDataInitialize3(const void* init) { (void)init; + std::lock_guard lock(g_slots_mutex); ++g_initializations; return SAVE_DATA_OK; } int APS5_VABI sceSaveDataLoadIcon(const SaveDataMountPoint* mount_point, SaveDataIcon* icon) { (void)icon; - if (mount_point == nullptr) { + if (!isValidMountPoint(mount_point)) { return SAVE_DATA_ERROR_PARAMETER; } std::lock_guard lock(g_slots_mutex); @@ -686,12 +1091,8 @@ static int mount3(const SaveDataMount3* mount, SaveDataMountResult* mount_result if (!create && !create2 && !open) { throw std::runtime_error("sceSaveDataMount3: unknown mount_mode"); } - const auto* nameEnd = static_cast(std::memchr(mount->dir_name->data, '\0', sizeof(mount->dir_name->data))); - if (nameEnd == nullptr) { - return SAVE_DATA_ERROR_PARAMETER; - } - const std::string dirName(mount->dir_name->data, static_cast(nameEnd - mount->dir_name->data)); - if (dirName.empty() || dirName == "." || dirName == ".." || dirName.find_first_of("/\\:") != std::string::npos) { + std::string dirName; + if (!parse_dir_name(mount->dir_name, &dirName)) { return SAVE_DATA_ERROR_PARAMETER; } const std::string real_path = save_root() + "/" + dirName; @@ -744,6 +1145,7 @@ static int mount3(const SaveDataMount3* mount, SaveDataMountResult* mount_result AddPathAlias_nid_no_patch(mountPoint.c_str(), absolutePath.c_str()); std::memset(mount_result, 0, sizeof(*mount_result)); g_slots[slot].used = true; + g_slots[slot].user_id = mount->user_id; g_slots[slot].mount_point = mountPoint; g_slots[slot].real_path = real_path; std::memcpy(mount_result->mount_point.data, mountPoint.c_str(), mountPoint.size() + 1); @@ -800,7 +1202,7 @@ int APS5_VABI sceSaveDataPrepare(const SaveDataMountPoint* mount_point, const Sa int APS5_VABI sceSaveDataSaveIcon(const SaveDataMountPoint* mount_point, const SaveDataIcon* icon) { (void)icon; - if (mount_point == nullptr) { + if (!isValidMountPoint(mount_point)) { return SAVE_DATA_ERROR_PARAMETER; } std::lock_guard lock(g_slots_mutex); @@ -811,13 +1213,15 @@ int APS5_VABI sceSaveDataSaveIcon(const SaveDataMountPoint* mount_point, const S } int APS5_VABI sceSaveDataSaveIconByPath(const SaveDataMountPoint* mount_point, const char* path) { - (void)mount_point; (void)path; + if (!isValidMountPoint(mount_point)) { + return SAVE_DATA_ERROR_PARAMETER; + } return SAVE_DATA_OK; } static int setParam(const SaveDataMountPoint* mount_point, uint32_t param_type, const void* param_buf, size_t param_buf_size) { - if (mount_point == nullptr || param_buf == nullptr) { + if (!isValidMountPoint(mount_point) || param_buf == nullptr) { return SAVE_DATA_ERROR_PARAMETER; } std::lock_guard lock(g_slots_mutex); @@ -848,7 +1252,7 @@ static int setParam(const SaveDataMountPoint* mount_point, uint32_t param_type, int APS5_VABI sceSaveDataSetParam(const SaveDataMountPoint* mount_point, uint32_t param_type, const void* param_buf, size_t param_buf_size) { const int rc = setParam(mount_point, param_type, param_buf, param_buf_size); - SAVEDATA_TRACE("setParam point=%s type=%u size=%zu -> 0x%08x", mount_point != nullptr ? mount_point->data : "(null)", param_type, param_buf_size, static_cast(rc)); + SAVEDATA_TRACE("setParam point=%.*s type=%u size=%zu -> 0x%08x", static_cast(sizeof(SaveDataMountPoint::data)), mount_point != nullptr ? mount_point->data : "(null)", param_type, param_buf_size, static_cast(rc)); return rc; } @@ -992,17 +1396,25 @@ int APS5_VABI sceSaveDataSyncSaveDataMemory(const void* sync_param) { } int APS5_VABI sceSaveDataTerminate(void) { - std::lock_guard lock(g_slots_mutex); - if (g_initializations == 0) { - return SAVE_DATA_ERROR_NOT_INITIALIZED; - } - if (g_initializations == 1 && any_slot_used()) { + if (InEventCallback()) { return SAVE_DATA_ERROR_BUSY; } - --g_initializations; - if (g_initializations == 0) { - g_convertProgress = 0.0f; + EventWorker* stopped = nullptr; + { + std::lock_guard lock(g_slots_mutex); + if (g_initializations == 0) { + return SAVE_DATA_ERROR_NOT_INITIALIZED; + } + if (g_initializations == 1 && any_slot_used()) { + return SAVE_DATA_ERROR_BUSY; + } + --g_initializations; + if (g_initializations == 0) { + g_convertProgress = 0.0f; + stopped = DetachEventDelivery(); + } } + if (stopped != nullptr) JoinEventDelivery(stopped); return SAVE_DATA_OK; } @@ -1023,7 +1435,7 @@ int APS5_VABI sceSaveDataTransferringMount(const SaveDataTransferringMount* moun static int umount2(uint32_t mode, const SaveDataMountPoint* mount_point) { (void)mode; - if (mount_point == nullptr) { + if (!isValidMountPoint(mount_point)) { return SAVE_DATA_ERROR_PARAMETER; } std::lock_guard lock(g_slots_mutex); @@ -1038,7 +1450,7 @@ static int umount2(uint32_t mode, const SaveDataMountPoint* mount_point) { int APS5_VABI sceSaveDataUmount2(uint32_t mode, const SaveDataMountPoint* mount_point) { const int rc = umount2(mode, mount_point); - SAVEDATA_TRACE("umount2 mode=0x%x point=%s -> 0x%08x", mode, mount_point != nullptr ? mount_point->data : "(null)", static_cast(rc)); + SAVEDATA_TRACE("umount2 mode=0x%x point=%.*s -> 0x%08x", mode, static_cast(sizeof(SaveDataMountPoint::data)), mount_point != nullptr ? mount_point->data : "(null)", static_cast(rc)); return rc; } @@ -1068,12 +1480,8 @@ static int convert(const SaveDataConvert* param) { return SAVE_DATA_ERROR_PARAMETER; } g_convertProgress = 0.0f; - const auto* nameEnd = static_cast(std::memchr(param->dir_name->data, '\0', sizeof(param->dir_name->data))); - if (nameEnd == nullptr) { - return SAVE_DATA_ERROR_PARAMETER; - } - const std::string dirName(param->dir_name->data, static_cast(nameEnd - param->dir_name->data)); - if (dirName.empty() || dirName == "." || dirName == ".." || dirName.find_first_of("/\\:") != std::string::npos) { + std::string dirName; + if (!parse_dir_name(param->dir_name, &dirName)) { return SAVE_DATA_ERROR_PARAMETER; } if (!std::filesystem::is_directory(save_root() + "/" + dirName)) { @@ -1195,9 +1603,38 @@ int APS5_VABI sceSaveDataChangeInternal(void) { return 0; } -int APS5_VABI sceSaveDataCheckBackupData(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +static int checkBackupData(const SaveDataCheckBackupData* check) { + if (g_initializations == 0) { + return SAVE_DATA_ERROR_NOT_INITIALIZED; + } + if (check == nullptr || check->dir_name == nullptr) { + return SAVE_DATA_ERROR_PARAMETER; + } + std::string dirName; + if (!parse_dir_name(check->dir_name, &dirName)) { + return SAVE_DATA_ERROR_PARAMETER; + } + if (!running_title(check->title_id)) { + return SAVE_DATA_ERROR_NOT_FOUND; + } + std::lock_guard lock(g_slots_mutex); + std::error_code error; + if (isMountedDirectory(save_root() + "/" + dirName, error)) { + return SAVE_DATA_ERROR_BUSY; + } + if (error) return SAVE_DATA_ERROR_INTERNAL; + try { + require_plain_paths(dirName, {BACKUP_DIR}); + } catch (const std::filesystem::filesystem_error&) { + return SAVE_DATA_ERROR_INTERNAL; + } + return read_backup(dirName, check->param); +} + +int APS5_VABI sceSaveDataCheckBackupData(const SaveDataCheckBackupData* check) { + const int rc = checkBackupData(check); + SAVEDATA_TRACE("checkBackupData dir=%.*s -> 0x%08x", static_cast(sizeof(SceSaveDataDirName::data)), check != nullptr && check->dir_name != nullptr ? check->dir_name->data : "(null)", static_cast(rc)); + return rc; } int APS5_VABI sceSaveDataCheckBackupDataForCdlg(void) { @@ -1377,9 +1814,8 @@ int APS5_VABI sceSaveDataGetUpdatedDataCount(void) { return 0; } -int APS5_VABI sceSaveDataInitialize2() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSaveDataInitialize2(const void* init) { + return sceSaveDataInitialize3(init); } int APS5_VABI sceSaveDataInitializeForCdlg(void) { @@ -1432,14 +1868,63 @@ int APS5_VABI sceSaveDataRebuildDatabase(void) { return 0; } -int APS5_VABI sceSaveDataRegisterEventCallback() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSaveDataRegisterEventCallback(SaveDataEventCallback callback, void* userdata) { + auto& callbacks = Callbacks(); + std::lock_guard calling(callbacks.calling); + std::lock_guard slots(g_slots_mutex); + if (g_initializations == 0) { + return SAVE_DATA_ERROR_NOT_INITIALIZED; + } + if (callback == nullptr) { + return SAVE_DATA_ERROR_PARAMETER; + } + StartEventDelivery(); + std::lock_guard lock(callbacks.mutex); + callbacks.callback = callback; + callbacks.userdata = userdata; + return SAVE_DATA_OK; } -int APS5_VABI sceSaveDataRestoreBackupData(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +static int restoreBackupData(const SaveDataRestoreBackupData* restore) { + if (g_initializations == 0) { + return SAVE_DATA_ERROR_NOT_INITIALIZED; + } + if (restore == nullptr || restore->dir_name == nullptr) { + return SAVE_DATA_ERROR_PARAMETER; + } + std::string dirName; + if (!parse_dir_name(restore->dir_name, &dirName)) { + return SAVE_DATA_ERROR_PARAMETER; + } + if (!running_title(restore->title_id)) { + return SAVE_DATA_ERROR_NOT_FOUND; + } + const std::string target = save_root() + "/" + dirName; + std::lock_guard lock(g_slots_mutex); + std::error_code error; + if (isMountedDirectory(target, error)) { + return SAVE_DATA_ERROR_BUSY; + } + if (error) return SAVE_DATA_ERROR_INTERNAL; + const int backup = read_backup(dirName, nullptr); + if (backup != SAVE_DATA_OK) { + return backup; + } + if (!std::filesystem::is_directory(target)) { + return SAVE_DATA_ERROR_INTERNAL; + } + try { + restoreSave(dirName); + } catch (const std::filesystem::filesystem_error&) { + return SAVE_DATA_ERROR_INTERNAL; + } + return SAVE_DATA_OK; +} + +int APS5_VABI sceSaveDataRestoreBackupData(const SaveDataRestoreBackupData* restore) { + const int rc = restoreBackupData(restore); + SAVEDATA_TRACE("restoreBackupData dir=%.*s -> 0x%08x", static_cast(sizeof(SceSaveDataDirName::data)), restore != nullptr && restore->dir_name != nullptr ? restore->dir_name->data : "(null)", static_cast(rc)); + return rc; } int APS5_VABI sceSaveDataRestoreBackupDataForCdlg(void) { @@ -1487,14 +1972,58 @@ int APS5_VABI sceSaveDataUmountSys(void) { return 0; } -int APS5_VABI sceSaveDataUmountWithBackup(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +static int umountWithBackup(const SaveDataMountPoint* mount_point) { + if (g_initializations == 0) { + return SAVE_DATA_ERROR_NOT_INITIALIZED; + } + if (mount_point == nullptr) { + return SAVE_DATA_ERROR_PARAMETER; + } + SaveDataEvent event{}; + event.type = SAVE_DATA_EVENT_TYPE_UMOUNT_BACKUP; + event.error_code = SAVE_DATA_OK; + { + std::lock_guard lock(g_slots_mutex); + const int slot = find_slot_by_mount_point(mount_point->data); + if (slot == -1) { + return SAVE_DATA_ERROR_NOT_MOUNTED; + } + const AppTitleId title = GetAppTitleId_nid_postfix(); + const MountSlot mounted = g_slots[slot]; + RemovePathAlias_nid_no_patch(mounted.mount_point.c_str()); + g_slots[slot] = MountSlot{}; + const std::string dirName = std::filesystem::path(mounted.real_path).filename().string(); + event.user_id = mounted.user_id; + std::memcpy(event.title_id.data, title.value, strnlen(title.value, sizeof(event.title_id.data) - 1)); + std::memcpy(event.dir_name.data, dirName.data(), std::min(dirName.size(), sizeof(event.dir_name.data) - 1)); + try { + backupSave(dirName); + } catch (const std::exception&) { + event.error_code = SAVE_DATA_ERROR_INTERNAL; + } + } + { + std::lock_guard lock(g_state_mutex); + g_events.push_back(event); + } + DeliverEvent(event); + return SAVE_DATA_OK; } -int APS5_VABI sceSaveDataUnregisterEventCallback() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSaveDataUmountWithBackup(const SaveDataMountPoint* mount_point) { + const int rc = umountWithBackup(mount_point); + SAVEDATA_TRACE("umountWithBackup point=%s -> 0x%08x", mount_point != nullptr ? mount_point->data : "(null)", static_cast(rc)); + return rc; +} + +int APS5_VABI sceSaveDataUnregisterEventCallback(void) { + auto& callbacks = Callbacks(); + std::lock_guard calling(callbacks.calling); + std::lock_guard lock(callbacks.mutex); + callbacks.callback = nullptr; + callbacks.userdata = nullptr; + callbacks.pending.clear(); + return SAVE_DATA_OK; } int APS5_VABI sceSaveDataUpload(void) { diff --git a/core/libs/prx/libSceSaveData.native/SaveData.hpp b/core/libs/prx/libSceSaveData.native/SaveData.hpp index 0dc846f6e5..d2a5c59a31 100644 --- a/core/libs/prx/libSceSaveData.native/SaveData.hpp +++ b/core/libs/prx/libSceSaveData.native/SaveData.hpp @@ -48,6 +48,7 @@ constexpr std::size_t SAVE_DATA_MOUNT_SLOTS = 16; struct MountSlot { bool used = false; + std::int32_t user_id = 0; std::string mount_point; std::string real_path; }; @@ -56,6 +57,9 @@ inline std::array g_slots; inline std::mutex g_slots_mutex; inline int find_slot_by_mount_point(const char* mount_point) { + if (mount_point == nullptr) { + return -1; + } for (int i = 0; i < static_cast(SAVE_DATA_MOUNT_SLOTS); i++) { if (g_slots[i].used && g_slots[i].mount_point == mount_point) { return i; diff --git a/core/libs/prx/libSceSaveData.native/include/Api.hpp b/core/libs/prx/libSceSaveData.native/include/Api.hpp index 78eaadc8c4..aea123fc11 100644 --- a/core/libs/prx/libSceSaveData.native/include/Api.hpp +++ b/core/libs/prx/libSceSaveData.native/include/Api.hpp @@ -52,5 +52,6 @@ int APS5_VABI sceSaveDataUmount(const SaveDataMountPoint* mountPoint); int APS5_VABI sceSaveDataSetupSaveDataMemory(std::int32_t userId, std::size_t memorySize, const SaveDataParam* param); int APS5_VABI sceSaveDataGetSaveDataMemory(std::int32_t userId, void* buf, std::size_t bufSize, std::int64_t offset); int APS5_VABI sceSaveDataSetSaveDataMemory(std::int32_t userId, void* buf, std::size_t bufSize, std::int64_t offset); +void SaveDataSetFileStepHook_nid_no_patch(bool (*hook)(const char* step, const char* path)); } diff --git a/core/libs/prx/libSceSaveData/include/Api.hpp b/core/libs/prx/libSceSaveData/include/Api.hpp index f72f0502ed..2e971f807b 100644 --- a/core/libs/prx/libSceSaveData/include/Api.hpp +++ b/core/libs/prx/libSceSaveData/include/Api.hpp @@ -18,7 +18,7 @@ int APS5_VABI sceSaveDataBindPsnAccount(void); int APS5_VABI sceSaveDataBindPsnAccountForSystemBackup(void); int APS5_VABI sceSaveDataChangeDatabase(void); int APS5_VABI sceSaveDataChangeInternal(void); -int APS5_VABI sceSaveDataCheckBackupData(void); +int APS5_VABI sceSaveDataCheckBackupData(const SaveDataCheckBackupData* check); int APS5_VABI sceSaveDataCheckBackupDataForCdlg(void); int APS5_VABI sceSaveDataCheckBackupDataInternal(void); int APS5_VABI sceSaveDataCheckBackupDataLight(void); @@ -54,7 +54,7 @@ int APS5_VABI sceSaveDataGetSaveDataRootPath(void); int APS5_VABI sceSaveDataGetSaveDataRootUsbPath(void); int APS5_VABI sceSaveDataGetSavePoint(void); int APS5_VABI sceSaveDataGetUpdatedDataCount(void); -int APS5_VABI sceSaveDataInitialize2(); +int APS5_VABI sceSaveDataInitialize2(const void* init); int APS5_VABI sceSaveDataInitializeForCdlg(void); int APS5_VABI sceSaveDataIsDeletingUsbDb(void); int APS5_VABI sceSaveDataIsMounted(); @@ -65,8 +65,8 @@ int APS5_VABI sceSaveDataMount5(); int APS5_VABI sceSaveDataMountInternal(void); int APS5_VABI sceSaveDataMountSys(void); int APS5_VABI sceSaveDataRebuildDatabase(void); -int APS5_VABI sceSaveDataRegisterEventCallback(); -int APS5_VABI sceSaveDataRestoreBackupData(void); +int APS5_VABI sceSaveDataRegisterEventCallback(SaveDataEventCallback callback, void* userdata); +int APS5_VABI sceSaveDataRestoreBackupData(const SaveDataRestoreBackupData* restore); int APS5_VABI sceSaveDataRestoreBackupDataForCdlg(void); int APS5_VABI sceSaveDataRestoreLoadSaveDataMemory(void); int APS5_VABI sceSaveDataSetAutoUploadSetting(void); @@ -76,8 +76,8 @@ int APS5_VABI sceSaveDataShutdownStart(void); int APS5_VABI sceSaveDataSupportedFakeBrokenStatus(void); int APS5_VABI sceSaveDataSyncCloudList(void); int APS5_VABI sceSaveDataUmountSys(void); -int APS5_VABI sceSaveDataUmountWithBackup(void); -int APS5_VABI sceSaveDataUnregisterEventCallback(); +int APS5_VABI sceSaveDataUmountWithBackup(const SaveDataMountPoint* mount_point); +int APS5_VABI sceSaveDataUnregisterEventCallback(void); int APS5_VABI sceSaveDataUpload(void); int APS5_VABI sceSaveDataCheckPremiumMember(); int APS5_VABI sceSaveDataCheckSyncStatus(); diff --git a/core/libs/prx/libSceScreenShot/Export.cpp b/core/libs/prx/libSceScreenShot/Export.cpp index b9335af525..9da588f32e 100644 --- a/core/libs/prx/libSceScreenShot/Export.cpp +++ b/core/libs/prx/libSceScreenShot/Export.cpp @@ -72,7 +72,8 @@ int APS5_VABI sceScreenShotEnable() { int APS5_VABI sceScreenShotIsDisabled(int id, int* isDisabled) { if (id == -1 || !isDisabled) return ErrorArgument; - *isDisabled = disabled ? 1 : 0; + const int value = disabled ? 1 : 0; + if (!GuestCopyOut_nid_no_patch(isDisabled, &value, sizeof(value))) return ErrorArgument; return 0; } diff --git a/core/libs/prx/libSceShare/Export.cpp b/core/libs/prx/libSceShare/Export.cpp index 3076aa4355..310e648421 100644 --- a/core/libs/prx/libSceShare/Export.cpp +++ b/core/libs/prx/libSceShare/Export.cpp @@ -2,6 +2,7 @@ #include "prx/libSceShare/include/InventoryExports.hpp" #include "prx/libSceShare/include/Api.hpp" #include +#include #include #include #include "SceTypes.hpp" @@ -55,7 +56,13 @@ int APS5_VABI sceShareGetCurrentStatus(uint32_t feature_flag, ShareCurrentStatus if (feature_flag == 0 || status == nullptr) { return ERROR_INVALID_PARAM; } - std::memset(status, 0, sizeof(*status)); + const ShareCurrentStatus zeroStatus{}; + const auto savedErrno = errno; + if (!GuestCopyOut_nid_no_patch(status, &zeroStatus, sizeof(zeroStatus))) { + errno = savedErrno; + return ERROR_INVALID_PARAM; + } + errno = savedErrno; return 0; } @@ -122,7 +129,13 @@ int APS5_VABI sceShareGetRunningStatus(uint32_t* status) { if (status == nullptr) { return ERROR_INVALID_PARAM; } - *status = 0; + const std::uint32_t zeroStatus = 0; + const auto savedErrno = errno; + if (!GuestCopyOut_nid_no_patch(status, &zeroStatus, sizeof(zeroStatus))) { + errno = savedErrno; + return ERROR_INVALID_PARAM; + } + errno = savedErrno; return 0; } diff --git a/core/libs/prx/libSceSisrSys/Export.cpp b/core/libs/prx/libSceSisrSys/Export.cpp new file mode 100644 index 0000000000..71d49ab34b --- /dev/null +++ b/core/libs/prx/libSceSisrSys/Export.cpp @@ -0,0 +1,14 @@ +#include "prx/libSceSisrSys/include/Api.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceSisrSysProcessUmdEvent() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceSisrSysSetup() { + NotImplemented_nid_no_patch(__func__); +} + +} diff --git a/core/libs/prx/libSceSisrSys/include/Api.hpp b/core/libs/prx/libSceSisrSys/include/Api.hpp new file mode 100644 index 0000000000..cdab06ee86 --- /dev/null +++ b/core/libs/prx/libSceSisrSys/include/Api.hpp @@ -0,0 +1,10 @@ +#pragma once + +#include "SceTypes.hpp" + +extern "C" { + +int APS5_VABI sceSisrSysProcessUmdEvent(); +int APS5_VABI sceSisrSysSetup(); + +} diff --git a/core/libs/prx/libSceSisrSys/libSceSisrSys.hpp b/core/libs/prx/libSceSisrSys/libSceSisrSys.hpp new file mode 100644 index 0000000000..ea09624d1d --- /dev/null +++ b/core/libs/prx/libSceSisrSys/libSceSisrSys.hpp @@ -0,0 +1,3 @@ +#pragma once + +#include "prx/libSceSisrSys/include/Api.hpp" diff --git a/core/libs/prx/libSceSsl/Export.cpp b/core/libs/prx/libSceSsl/Export.cpp index bae126e949..9db9ac2943 100644 --- a/core/libs/prx/libSceSsl/Export.cpp +++ b/core/libs/prx/libSceSsl/Export.cpp @@ -1311,8 +1311,9 @@ void* APS5_VABI sceSslGetSubjectName(int sslCtxId, void* sslCert) { return ExtractCertName(__func__, sslCtxId, sslCert, true); } -int APS5_VABI CA_MGMT_extractKeyBlobEx() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI CA_MGMT_extractKeyBlobEx(int, const void* keyBlob, std::uint32_t, void* key) { + if (key == nullptr || keyBlob == nullptr) return ErrorInvalidArg; + std::memcpy(static_cast(key) + 8, &keyBlob, sizeof(keyBlob)); return 0; } @@ -1326,8 +1327,9 @@ int APS5_VABI CA_MGMT_freeKeyBlob() { return 0; } -int APS5_VABI CRYPTO_initAsymmetricKey() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI CRYPTO_initAsymmetricKey(void* key) { + const std::uint32_t initialized = 1; + std::memcpy(key, &initialized, sizeof(initialized)); return 0; } @@ -1384,8 +1386,8 @@ int APS5_VABI sceSslEnableOptionInternal() { return 0; } -int APS5_VABI sceSslGetNanoSSLModuleId() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceSslGetNanoSSLModuleId(int sslCtxId, int* moduleId) { + if (moduleId != nullptr) *moduleId = sslCtxId; return 0; } diff --git a/core/libs/prx/libSceSsl/include/Ssl.hpp b/core/libs/prx/libSceSsl/include/Ssl.hpp index 5de6ba4c60..6b5d2fcf69 100644 --- a/core/libs/prx/libSceSsl/include/Ssl.hpp +++ b/core/libs/prx/libSceSsl/include/Ssl.hpp @@ -38,10 +38,10 @@ int APS5_VABI sceSslGetNotAfter(int sslCtxId, void* sslCert, SceSsl::RtcTick* li int APS5_VABI sceSslGetFingerprint(int sslCtxId, void* sslCert, int hashType, unsigned char* fingerprint); int APS5_VABI sceSslGetPem(int sslCtxId, void* sslCert, char* buf, std::size_t* bufLen); void* APS5_VABI sceSslGetSubjectName(int sslCtxId, void* sslCert); -int APS5_VABI CA_MGMT_extractKeyBlobEx(); +int APS5_VABI CA_MGMT_extractKeyBlobEx(int sslCtxId, const void* keyBlob, std::uint32_t keyBlobLength, void* key); int APS5_VABI CA_MGMT_extractPublicKeyInfo(); int APS5_VABI CA_MGMT_freeKeyBlob(); -int APS5_VABI CRYPTO_initAsymmetricKey(); +int APS5_VABI CRYPTO_initAsymmetricKey(void* key); int APS5_VABI CRYPTO_uninitAsymmetricKey(); int APS5_VABI RSA_verifySignature(); int APS5_VABI VLONG_freeVlongQueue(); @@ -53,7 +53,7 @@ int APS5_VABI sceSslDisableOptionInternal(); int APS5_VABI sceSslDisableOptionInternalInsecure(); int APS5_VABI sceSslEnableOption(); int APS5_VABI sceSslEnableOptionInternal(); -int APS5_VABI sceSslGetNanoSSLModuleId(); +int APS5_VABI sceSslGetNanoSSLModuleId(int sslCtxId, int* moduleId); int APS5_VABI sceSslGetPeerCert(); int APS5_VABI sceSslGetSslError(); int APS5_VABI sceSslLoadRootCACert(); diff --git a/core/libs/prx/libSceSysUtil/Export.cpp b/core/libs/prx/libSceSysUtil/Export.cpp index a053ac386f..8e5a63dafc 100644 --- a/core/libs/prx/libSceSysUtil/Export.cpp +++ b/core/libs/prx/libSceSysUtil/Export.cpp @@ -5,6 +5,7 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libkernel/KernelErrors.hpp" #include "prx/libkernel/include/Api.hpp" namespace { @@ -25,8 +26,15 @@ int APS5_VABI sceSysUtilSendSystemNotificationWithText(int32_t type, const char* std::memcpy(request.data() + TypeOffset, &type, sizeof(type)); if (message != nullptr) { std::size_t length = 0; - while (length < TextLimit && message[length] != '\0') ++length; - std::memcpy(request.data() + TextOffset, message, length); + char c = 0; + while (length < TextLimit) { + if (!GuestCopyIn_nid_no_patch(&c, message + length, sizeof(c))) { + return SCE_KERNEL_ERROR_EFAULT; + } + if (c == '\0') break; + request[TextOffset + length] = static_cast(c); + ++length; + } } return sceKernelSendNotificationRequest(0, request.data(), request.size(), 0); } diff --git a/core/libs/prx/libSceSystemService/CMakeLists.txt b/core/libs/prx/libSceSystemService/CMakeLists.txt index 2c9a9034b8..5885fd39d5 100644 --- a/core/libs/prx/libSceSystemService/CMakeLists.txt +++ b/core/libs/prx/libSceSystemService/CMakeLists.txt @@ -1,6 +1,11 @@ +add_library(system_service_host_locale INTERFACE) +if(APPLE) + target_link_libraries(system_service_host_locale INTERFACE "-framework CoreFoundation") +endif() + add_library(libSceSystemService SHARED EXCLUDE_FROM_ALL Export.cpp PendingExports.cpp LoadExec.cpp) target_include_directories(libSceSystemService PRIVATE ${LIBS_INCLUDE_DIR}) -target_link_libraries(libSceSystemService PRIVATE libSceAgcDriver libSceUsbStorage) +target_link_libraries(libSceSystemService PRIVATE libSceAgcDriver libSceUsbStorage system_service_host_locale) configure_windows_unwind(libSceSystemService) if(BUILD_TESTING) @@ -101,6 +106,38 @@ if(BUILD_TESTING) set_tests_properties(guest_system_service_language PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(system_service_language_locale_tests ${LIBS_INCLUDE_DIR}/tests/SystemServiceLanguageLocale.cpp) + target_include_directories(system_service_language_locale_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(system_service_language_locale_tests PRIVATE system_service_host_locale) + configure_host_test_executable(system_service_language_locale_tests) + add_test(NAME system_service_language_locale COMMAND system_service_language_locale_tests) + set_tests_properties(system_service_language_locale PROPERTIES TIMEOUT 20) + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + add_test_executable(guest_system_service_language_auto_tests ${LIBS_INCLUDE_DIR}/tests/GuestSystemServiceLanguageAuto.cpp) + target_include_directories(guest_system_service_language_auto_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_system_service_language_auto_tests PRIVATE libSceSystemService libc system_service_host_locale) + configure_windows_unwind(guest_system_service_language_auto_tests) + set(languageAutoIndex 0) + foreach(languageAutoCase IN ITEMS + "4,-,-,de_DE.UTF-8,-,auto,0" + "17,-,-,pt_BR.UTF-8,-,AUTO,0" + "10,-,-,zh_TW.UTF-8,-,Auto,0" + "11,-,-,zh_CN.UTF-8,-,auto,0" + "20,es_MX.UTF-8,fr_FR.UTF-8,de_DE.UTF-8,-,auto,0" + "2,-,fr_FR.UTF-8,de_DE.UTF-8,-,auto,0" + "3,-,-,en_US.UTF-8,es:fr,auto,0" + "18,-,-,en_GB.UTF-8@euro,-,auto,0" + "0,-,-,ja_JP.UTF-8,-,auto,0" + "1,-,-,C,es,auto,1" + "1,-,-,-,-,auto,1" + "1,-,-,lt_LT.UTF-8,-,auto,1" + "30,-,-,lt_LT.UTF-8,lt:uk:fr,auto,0") + string(REPLACE "," ";" languageAutoArguments "${languageAutoCase}") + add_test(NAME guest_system_service_language_auto_${languageAutoIndex} COMMAND guest_system_service_language_auto_tests ${languageAutoArguments}) + set_tests_properties(guest_system_service_language_auto_${languageAutoIndex} PROPERTIES TIMEOUT 20) + math(EXPR languageAutoIndex "${languageAutoIndex} + 1") + endforeach() + endif() add_test_executable(guest_system_service_time_zone_tests ${LIBS_INCLUDE_DIR}/tests/GuestSystemServiceTimeZone.cpp) target_include_directories(guest_system_service_time_zone_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_system_service_time_zone_tests PRIVATE libSceSystemService libc) @@ -178,3 +215,5 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endif() + +target_sources(libSceSystemService PRIVATE InventoryExports.cpp) diff --git a/core/libs/prx/libSceSystemService/Export.cpp b/core/libs/prx/libSceSystemService/Export.cpp index 9286bfd2ab..dc56656c98 100644 --- a/core/libs/prx/libSceSystemService/Export.cpp +++ b/core/libs/prx/libSceSystemService/Export.cpp @@ -1,5 +1,6 @@ #include "prx/libSceSystemService/include/SystemService.hpp" #include +#include #include #include #include @@ -305,10 +306,70 @@ int ListRunningBigApp(const std::string& caller, std::int32_t* appIds, std::uint return SYSTEM_SERVICE_OK; } +constexpr std::uint32_t GpuLoadEmulationModeOff = 0; +constexpr std::uint32_t GpuLoadEmulationModeNormal = 1; + +std::atomic GpuLoadEmulationMode{GpuLoadEmulationModeNormal}; + +int SetGpuLoadEmulationMode(const char* caller, std::uint32_t mode) { + if (mode != GpuLoadEmulationModeOff && mode != GpuLoadEmulationModeNormal) { + NotImplemented_nid_no_patch((std::string(caller) + ": a mode other than 0 and 1").c_str()); + } + GpuLoadEmulationMode.store(mode, std::memory_order_relaxed); + return SYSTEM_SERVICE_OK; +} + +int GetGpuLoadEmulationModeOfApp(const char* caller, std::int32_t appId) { + if (appId != SYSTEM_SERVICE_RUNNING_APP_ID) NotImplemented_nid_no_patch((std::string(caller) + ": application other than the running title").c_str()); + return static_cast(GpuLoadEmulationMode.load(std::memory_order_relaxed)); +} + +std::atomic SuspendConfirmationDialogEnabled{true}; + } extern "C" { +int APS5_VABI sceSystemServiceEnableSuspendConfirmationDialog() { + SuspendConfirmationDialogEnabled.store(true, std::memory_order_relaxed); + return SYSTEM_SERVICE_OK; +} + +int APS5_VABI sceSystemServiceDisableSuspendConfirmationDialog() { + SuspendConfirmationDialogEnabled.store(false, std::memory_order_relaxed); + return SYSTEM_SERVICE_OK; +} + +bool APS5_VABI sceShellCoreUtilGetSuspendConfirmationDialogFlag(std::int32_t appId) { + if (appId != SYSTEM_SERVICE_RUNNING_APP_ID) NotImplemented_nid_no_patch("sceShellCoreUtilGetSuspendConfirmationDialogFlag: application other than the running title"); + return SuspendConfirmationDialogEnabled.load(std::memory_order_relaxed); +} + +int APS5_VABI sceSystemServiceSetGpuLoadEmulationMode(std::uint32_t mode) { + return SetGpuLoadEmulationMode("sceSystemServiceSetGpuLoadEmulationMode", mode); +} + +int APS5_VABI sceSystemServiceGetGpuLoadEmulationMode() { + return static_cast(GpuLoadEmulationMode.load(std::memory_order_relaxed)); +} + +int APS5_VABI sceShellCoreUtilSetGpuLoadEmulationMode(std::uint32_t mode) { + return SetGpuLoadEmulationMode("sceShellCoreUtilSetGpuLoadEmulationMode", mode); +} + +int APS5_VABI sceShellCoreUtilGetGpuLoadEmulationMode() { + return static_cast(GpuLoadEmulationMode.load(std::memory_order_relaxed)); +} + +int APS5_VABI sceShellCoreUtilSetGpuLoadEmulationModeByAppId(std::int32_t appId, std::uint32_t mode) { + if (appId != SYSTEM_SERVICE_RUNNING_APP_ID) NotImplemented_nid_no_patch("sceShellCoreUtilSetGpuLoadEmulationModeByAppId: application other than the running title"); + return SetGpuLoadEmulationMode("sceShellCoreUtilSetGpuLoadEmulationModeByAppId", mode); +} + +int APS5_VABI sceShellCoreUtilGetGpuLoadEmulationModeByAppId(std::int32_t appId) { + return GetGpuLoadEmulationModeOfApp("sceShellCoreUtilGetGpuLoadEmulationModeByAppId", appId); +} + int APS5_VABI sceSystemServiceLoadExec(const char* path, const char* const* arguments) { if (!path || !*path) return SYSTEM_SERVICE_ERROR_PARAMETER; if (std::strcmp(path, "exit") == 0) LibcExitWithoutHandlers_nid_no_patch(0); @@ -1100,4 +1161,10 @@ int APS5_VABI sceShellCoreUtilIsBgmPlaying(void) { return 0; } +APS5_EXPORT("M+KwxF0amiw", sceSystemServiceUnknown_M_KwxF0amiw); +int APS5_VABI sceSystemServiceUnknown_M_KwxF0amiw(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceSystemService/InventoryExports.cpp b/core/libs/prx/libSceSystemService/InventoryExports.cpp new file mode 100644 index 0000000000..86a1ebb382 --- /dev/null +++ b/core/libs/prx/libSceSystemService/InventoryExports.cpp @@ -0,0 +1,167 @@ +#include "prx/libSceSystemService/include/InventoryExports.hpp" +#include "prx/libc/include/General.hpp" +#include "prx/libc/include/general/ExportMacros.hpp" + +extern "C" { + +int APS5_VABI libSceSystemServiceUnknown_16waHD4tFBo_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("16waHD4tFBo", libSceSystemServiceUnknown_16waHD4tFBo_nid_no_patch); + +int APS5_VABI libSceSystemServiceUnknown_1m_minus_E1Xh8COo_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("1m-E1Xh8COo", libSceSystemServiceUnknown_1m_minus_E1Xh8COo_nid_no_patch); + +int APS5_VABI libSceSystemServiceUnknown_7eu2qjQGigw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("7eu2qjQGigw", libSceSystemServiceUnknown_7eu2qjQGigw_nid_no_patch); + +int APS5_VABI libSceSystemServiceUnknown_90unWbnI0qE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("90unWbnI0qE", libSceSystemServiceUnknown_90unWbnI0qE_nid_no_patch); + +int APS5_VABI libSceSystemServiceUnknown_E_minus_bXVJqwGZw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("E-bXVJqwGZw", libSceSystemServiceUnknown_E_minus_bXVJqwGZw_nid_no_patch); + +int APS5_VABI libSceSystemServiceUnknown_G3oPdw3lkps_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("G3oPdw3lkps", libSceSystemServiceUnknown_G3oPdw3lkps_nid_no_patch); + +int APS5_VABI libSceSystemServiceUnknown_KgiwSrROsIQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("KgiwSrROsIQ", libSceSystemServiceUnknown_KgiwSrROsIQ_nid_no_patch); + +int APS5_VABI libSceSystemServiceUnknown_LxGOxroprpg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("LxGOxroprpg", libSceSystemServiceUnknown_LxGOxroprpg_nid_no_patch); + +int APS5_VABI libSceSystemServiceUnknown_NXFtO8BfVeo_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("NXFtO8BfVeo", libSceSystemServiceUnknown_NXFtO8BfVeo_nid_no_patch); + +int APS5_VABI libSceSystemServiceUnknown_Oxh0CSsTZ78_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("Oxh0CSsTZ78", libSceSystemServiceUnknown_Oxh0CSsTZ78_nid_no_patch); + +int APS5_VABI libSceSystemServiceUnknown_OyTwhCKWXi4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("OyTwhCKWXi4", libSceSystemServiceUnknown_OyTwhCKWXi4_nid_no_patch); + +int APS5_VABI libSceSystemServiceUnknown_U0HutHWnMBM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("U0HutHWnMBM", libSceSystemServiceUnknown_U0HutHWnMBM_nid_no_patch); + +int APS5_VABI libSceSystemServiceUnknown_VDs9VSjSJh0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("VDs9VSjSJh0", libSceSystemServiceUnknown_VDs9VSjSJh0_nid_no_patch); + +int APS5_VABI libSceSystemServiceUnknown_ZWNP_plus__minus_dWr8E_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("ZWNP+-dWr8E", libSceSystemServiceUnknown_ZWNP_plus__minus_dWr8E_nid_no_patch); + +int APS5_VABI libSceSystemServiceUnknown_axzblV8OvWU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("axzblV8OvWU", libSceSystemServiceUnknown_axzblV8OvWU_nid_no_patch); + +int APS5_VABI libSceSystemServiceUnknown_bJyWctnSXyU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("bJyWctnSXyU", libSceSystemServiceUnknown_bJyWctnSXyU_nid_no_patch); + +int APS5_VABI libSceSystemServiceUnknown_bUNkT3XDg0Y_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("bUNkT3XDg0Y", libSceSystemServiceUnknown_bUNkT3XDg0Y_nid_no_patch); + +int APS5_VABI libSceSystemServiceUnknown_c5_plus_4Scso9EU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("c5+4Scso9EU", libSceSystemServiceUnknown_c5_plus_4Scso9EU_nid_no_patch); + +int APS5_VABI libSceSystemServiceUnknown_fBGD_minus_HNimSk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("fBGD-HNimSk", libSceSystemServiceUnknown_fBGD_minus_HNimSk_nid_no_patch); + +int APS5_VABI libSceSystemServiceUnknown_fPAZ5DBsDxo_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("fPAZ5DBsDxo", libSceSystemServiceUnknown_fPAZ5DBsDxo_nid_no_patch); + +int APS5_VABI libSceSystemServiceUnknown_ifJiF5witJ4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("ifJiF5witJ4", libSceSystemServiceUnknown_ifJiF5witJ4_nid_no_patch); + +int APS5_VABI libSceSystemServiceUnknown_kDGjRMtUDxo_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("kDGjRMtUDxo", libSceSystemServiceUnknown_kDGjRMtUDxo_nid_no_patch); + +int APS5_VABI libSceSystemServiceUnknown_lgpIdZ8T0xg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("lgpIdZ8T0xg", libSceSystemServiceUnknown_lgpIdZ8T0xg_nid_no_patch); + +int APS5_VABI libSceSystemServiceUnknown_pj1SzIAxPlk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("pj1SzIAxPlk", libSceSystemServiceUnknown_pj1SzIAxPlk_nid_no_patch); + +int APS5_VABI libSceSystemServiceUnknown_qdTPJWjquDc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("qdTPJWjquDc", libSceSystemServiceUnknown_qdTPJWjquDc_nid_no_patch); + +int APS5_VABI libSceSystemServiceUnknown_rIqPq0oWlrg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("rIqPq0oWlrg", libSceSystemServiceUnknown_rIqPq0oWlrg_nid_no_patch); + +int APS5_VABI libSceSystemServiceUnknown_sgYo_minus_zXHQRE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("sgYo-zXHQRE", libSceSystemServiceUnknown_sgYo_minus_zXHQRE_nid_no_patch); + +int APS5_VABI libSceSystemServiceUnknown_tPMOYbbviYs_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("tPMOYbbviYs", libSceSystemServiceUnknown_tPMOYbbviYs_nid_no_patch); + +int APS5_VABI libSceSystemServiceUnknown_uElcdmhh_minus_c8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("uElcdmhh-c8", libSceSystemServiceUnknown_uElcdmhh_minus_c8_nid_no_patch); + +int APS5_VABI libSceSystemServiceUnknown_xn_minus_HgPW29x4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("xn-HgPW29x4", libSceSystemServiceUnknown_xn_minus_HgPW29x4_nid_no_patch); + +int APS5_VABI libSceSystemServiceUnknown_y16IXiJfafA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("y16IXiJfafA", libSceSystemServiceUnknown_y16IXiJfafA_nid_no_patch); + +int APS5_VABI libSceSystemServiceUnknown_yOiZq_plus_9_minus_ZMQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("yOiZq+9-ZMQ", libSceSystemServiceUnknown_yOiZq_plus_9_minus_ZMQ_nid_no_patch); + +} diff --git a/core/libs/prx/libSceSystemService/LanguageLocale.hpp b/core/libs/prx/libSceSystemService/LanguageLocale.hpp new file mode 100644 index 0000000000..286573b07a --- /dev/null +++ b/core/libs/prx/libSceSystemService/LanguageLocale.hpp @@ -0,0 +1,378 @@ +#ifndef CORE_LIBS_PRX_LIBSCESYSTEMSERVICE_LANGUAGELOCALE_HPP +#define CORE_LIBS_PRX_LIBSCESYSTEMSERVICE_LANGUAGELOCALE_HPP + +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(_WIN32) +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#elif defined(__APPLE__) +#include +#endif + +namespace SystemServiceLocale { + +struct Tag { + std::string language; + std::string script; + std::string region; +}; + +enum ConsoleLanguage : int { + Japanese = 0, + EnglishUs = 1, + French = 2, + Spanish = 3, + German = 4, + Italian = 5, + Dutch = 6, + PortuguesePortugal = 7, + Russian = 8, + Korean = 9, + ChineseTraditional = 10, + ChineseSimplified = 11, + Finnish = 12, + Swedish = 13, + Danish = 14, + Norwegian = 15, + Polish = 16, + PortugueseBrazil = 17, + EnglishUk = 18, + Turkish = 19, + SpanishLatinAmerica = 20, + Arabic = 21, + FrenchCanada = 22, + Czech = 23, + Hungarian = 24, + Greek = 25, + Romanian = 26, + Thai = 27, + Vietnamese = 28, + Indonesian = 29, + Ukrainian = 30, +}; + +constexpr int NoConsoleLanguage = -1; + +struct Row { + std::string_view language; + std::string_view script; + std::string_view region; + int id; +}; + +namespace Detail { + +constexpr bool IsAlpha(char c) { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); } +constexpr bool IsDigit(char c) { return c >= '0' && c <= '9'; } +constexpr char Lower(char c) { return c >= 'A' && c <= 'Z' ? static_cast(c - 'A' + 'a') : c; } +constexpr char Upper(char c) { return c >= 'a' && c <= 'z' ? static_cast(c - 'a' + 'A') : c; } + +constexpr bool AllOf(std::string_view text, bool (*predicate)(char)) { + for (char c : text) { + if (!predicate(c)) return false; + } + return true; +} + +constexpr bool IsAlphanumeric(char c) { return IsAlpha(c) || IsDigit(c); } + +constexpr bool EqualsIgnoringCase(std::string_view left, std::string_view right) { + if (left.size() != right.size()) return false; + for (std::size_t i = 0; i < left.size(); ++i) { + if (Lower(left[i]) != Lower(right[i])) return false; + } + return true; +} + +inline constexpr Row LatinEuropeanRows[] = { + {"en", "", "", EnglishUs}, + {"en", "", "GB", EnglishUk}, + {"en", "", "IE", EnglishUk}, + {"en", "", "AU", EnglishUk}, + {"en", "", "NZ", EnglishUk}, + {"fr", "", "", French}, + {"fr", "", "CA", FrenchCanada}, + {"de", "", "", German}, + {"it", "", "", Italian}, + {"nl", "", "", Dutch}, + {"ro", "", "", Romanian}, + {"mo", "", "", Romanian}, +}; + +inline constexpr Row IberianRows[] = { + {"es", "", "", Spanish}, + {"es", "", "419", SpanishLatinAmerica}, + {"es", "", "MX", SpanishLatinAmerica}, + {"es", "", "AR", SpanishLatinAmerica}, + {"es", "", "BO", SpanishLatinAmerica}, + {"es", "", "CL", SpanishLatinAmerica}, + {"es", "", "CO", SpanishLatinAmerica}, + {"es", "", "CR", SpanishLatinAmerica}, + {"es", "", "CU", SpanishLatinAmerica}, + {"es", "", "DO", SpanishLatinAmerica}, + {"es", "", "EC", SpanishLatinAmerica}, + {"es", "", "GT", SpanishLatinAmerica}, + {"es", "", "HN", SpanishLatinAmerica}, + {"es", "", "NI", SpanishLatinAmerica}, + {"es", "", "PA", SpanishLatinAmerica}, + {"es", "", "PE", SpanishLatinAmerica}, + {"es", "", "PR", SpanishLatinAmerica}, + {"es", "", "PY", SpanishLatinAmerica}, + {"es", "", "SV", SpanishLatinAmerica}, + {"es", "", "US", SpanishLatinAmerica}, + {"es", "", "UY", SpanishLatinAmerica}, + {"es", "", "VE", SpanishLatinAmerica}, + {"pt", "", "", PortugueseBrazil}, + {"pt", "", "PT", PortuguesePortugal}, + {"pt", "", "AO", PortuguesePortugal}, + {"pt", "", "MZ", PortuguesePortugal}, + {"pt", "", "CV", PortuguesePortugal}, + {"pt", "", "GW", PortuguesePortugal}, + {"pt", "", "ST", PortuguesePortugal}, + {"pt", "", "TL", PortuguesePortugal}, + {"pt", "", "MO", PortuguesePortugal}, +}; + +inline constexpr Row ChineseRows[] = { + {"zh", "", "", ChineseSimplified}, + {"zh", "Hans", "", ChineseSimplified}, + {"zh", "Hant", "", ChineseTraditional}, + {"zh", "", "TW", ChineseTraditional}, + {"zh", "", "HK", ChineseTraditional}, + {"zh", "", "MO", ChineseTraditional}, + {"cmn", "", "", ChineseSimplified}, + {"cmn", "Hant", "", ChineseTraditional}, + {"cmn", "", "TW", ChineseTraditional}, + {"yue", "", "", ChineseTraditional}, + {"yue", "Hans", "", ChineseSimplified}, +}; + +inline constexpr Row JapaneseKoreanRows[] = { + {"ja", "", "", Japanese}, + {"ko", "", "", Korean}, +}; + +inline constexpr Row SlavicNordicRows[] = { + {"ru", "", "", Russian}, + {"uk", "", "", Ukrainian}, + {"pl", "", "", Polish}, + {"cs", "", "", Czech}, + {"fi", "", "", Finnish}, + {"sv", "", "", Swedish}, + {"da", "", "", Danish}, + {"no", "", "", Norwegian}, + {"nb", "", "", Norwegian}, + {"nn", "", "", Norwegian}, +}; + +inline constexpr Row OtherRows[] = { + {"ar", "", "", Arabic}, + {"th", "", "", Thai}, + {"vi", "", "", Vietnamese}, + {"id", "", "", Indonesian}, + {"in", "", "", Indonesian}, + {"tr", "", "", Turkish}, + {"el", "", "", Greek}, + {"hu", "", "", Hungarian}, +}; + +inline constexpr std::span RowGroups[] = { + LatinEuropeanRows, + IberianRows, + ChineseRows, + JapaneseKoreanRows, + SlavicNordicRows, + OtherRows, +}; + +} + +inline bool ParseTag(std::string_view text, Tag& tag) { + Tag parsed; + std::size_t index = 0; + bool seenScript = false; + bool seenRegion = false; + bool tail = false; + std::size_t position = 0; + while (true) { + const std::size_t end = text.find('-', position); + const std::string_view subtag = text.substr(position, end == std::string_view::npos ? std::string_view::npos : end - position); + if (subtag.empty() || subtag.size() > 8 || !Detail::AllOf(subtag, Detail::IsAlphanumeric)) return false; + if (index == 0) { + if ((subtag.size() != 2 && subtag.size() != 3) || !Detail::AllOf(subtag, Detail::IsAlpha)) return false; + for (char c : subtag) parsed.language += Detail::Lower(c); + } else if (!tail) { + if (subtag.size() == 1) { + tail = true; + } else if (!seenScript && !seenRegion && subtag.size() == 4 && Detail::AllOf(subtag, Detail::IsAlpha)) { + seenScript = true; + parsed.script += Detail::Upper(subtag[0]); + for (std::size_t i = 1; i < subtag.size(); ++i) parsed.script += Detail::Lower(subtag[i]); + } else if (!seenRegion && ((subtag.size() == 2 && Detail::AllOf(subtag, Detail::IsAlpha)) || (subtag.size() == 3 && Detail::AllOf(subtag, Detail::IsDigit)))) { + seenRegion = true; + for (char c : subtag) parsed.region += Detail::Upper(c); + } else { + tail = true; + } + } + ++index; + if (end == std::string_view::npos) break; + position = end + 1; + } + tag = std::move(parsed); + return true; +} + +inline std::string NormalizePosixLocale(std::string_view text) { + std::string_view modifier; + if (const std::size_t at = text.find('@'); at != std::string_view::npos) { + modifier = text.substr(at + 1); + text = text.substr(0, at); + } + text = text.substr(0, text.find('.')); + std::string name; + for (char c : text) name += c == '_' ? '-' : c; + if (name.empty() || Detail::EqualsIgnoringCase(name, "C") || Detail::EqualsIgnoringCase(name, "POSIX")) return {}; + std::string_view script; + if (Detail::EqualsIgnoringCase(modifier, "latin")) script = "Latn"; + if (Detail::EqualsIgnoringCase(modifier, "cyrillic")) script = "Cyrl"; + if (!script.empty()) { + const std::size_t dash = name.find('-'); + const std::string_view second = dash == std::string::npos ? std::string_view{} : std::string_view(name).substr(dash + 1, 4); + const bool hasScript = second.size() == 4 && Detail::AllOf(second, Detail::IsAlpha) && (dash + 5 == name.size() || name[dash + 5] == '-'); + if (!hasScript) name.insert(dash == std::string::npos ? name.size() : dash, std::string("-") + std::string(script)); + } + return name; +} + +inline bool ParseLocale(std::string_view text, Tag& tag) { + const std::string normalised = NormalizePosixLocale(text); + return !normalised.empty() && ParseTag(normalised, tag); +} + +inline int ConsoleLanguageForTag(const Tag& tag) { + int best = NoConsoleLanguage; + int bestScore = -1; + for (const std::span group : Detail::RowGroups) { + for (const Row& row : group) { + if (row.language != tag.language) continue; + if (!row.script.empty() && row.script != tag.script) continue; + if (!row.region.empty() && row.region != tag.region) continue; + const int score = (row.script.empty() ? 0 : 2) + (row.region.empty() ? 0 : 1); + if (score > bestScore) { + best = row.id; + bestScore = score; + } + } + } + return best; +} + +struct Match { + int id; + bool recognised; +}; + +inline Match MatchLocale(std::string_view locale) { + Tag tag; + const int id = ParseLocale(locale, tag) ? ConsoleLanguageForTag(tag) : NoConsoleLanguage; + if (id == NoConsoleLanguage) return {EnglishUs, false}; + return {id, true}; +} + +using Environment = std::function; + +inline std::vector PosixHostLocales(const Environment& environment) { + const auto read = [&environment](const char* name) { + const char* value = environment(name); + return std::string_view(value != nullptr ? value : ""); + }; + std::string_view effective; + for (const char* name : {"LC_ALL", "LC_MESSAGES", "LANG"}) { + effective = read(name); + if (!effective.empty()) break; + } + std::vector locales; + if (!NormalizePosixLocale(effective).empty()) { + std::string_view preferred = read("LANGUAGE"); + while (!preferred.empty()) { + const std::size_t colon = preferred.find(':'); + const std::string_view entry = preferred.substr(0, colon); + if (!entry.empty()) locales.emplace_back(entry); + preferred = colon == std::string_view::npos ? std::string_view{} : preferred.substr(colon + 1); + } + } + if (!effective.empty()) locales.emplace_back(effective); + return locales; +} + +#if defined(_WIN32) +inline std::vector WindowsHostLocales() { + wchar_t name[LOCALE_NAME_MAX_LENGTH]; + const int length = GetUserDefaultLocaleName(name, LOCALE_NAME_MAX_LENGTH); + if (length <= 1) return {}; + std::string locale; + for (int i = 0; i < length - 1; ++i) { + if (name[i] <= 0 || name[i] > 127) return {}; + locale += static_cast(name[i]); + } + return {locale}; +} +#endif + +#if defined(__APPLE__) +inline std::vector MacHostLocales() { + std::vector locales; + CFArrayRef languages = CFLocaleCopyPreferredLanguages(); + if (languages == nullptr) return locales; + const CFIndex count = CFArrayGetCount(languages); + for (CFIndex i = 0; i < count; ++i) { + const auto name = static_cast(CFArrayGetValueAtIndex(languages, i)); + char buffer[128]; + if (CFStringGetCString(name, buffer, sizeof buffer, kCFStringEncodingUTF8)) locales.emplace_back(buffer); + } + CFRelease(languages); + return locales; +} +#endif + +inline std::vector HostLocales() { +#if defined(_WIN32) + return WindowsHostLocales(); +#elif defined(__APPLE__) + return MacHostLocales(); +#else + return PosixHostLocales([](const char* name) { return std::getenv(name); }); +#endif +} + +using HostLocaleSource = std::function()>; +using HostLocaleNotice = std::function; + +inline int ResolveHostLanguage(const HostLocaleSource& source, const HostLocaleNotice& notice) { + const std::vector locales = source(); + for (const std::string& locale : locales) { + const Match match = MatchLocale(locale); + if (match.recognised) return match.id; + } + const std::string detected = locales.empty() ? "the host reports no locale" : "the host locale \"" + locales.front() + "\" has no console language"; + notice("ANYPS5_LANGUAGE=auto: " + detected + ", using English (United States), language number " + std::to_string(EnglishUs)); + return EnglishUs; +} + +} + +#endif diff --git a/core/libs/prx/libSceSystemService/PendingExports.cpp b/core/libs/prx/libSceSystemService/PendingExports.cpp index 695bf98c67..925e860a40 100644 --- a/core/libs/prx/libSceSystemService/PendingExports.cpp +++ b/core/libs/prx/libSceSystemService/PendingExports.cpp @@ -841,16 +841,6 @@ bool APS5_VABI sceShellCoreUtilGetGnmCompositorOnScreenProfilerFlag() { return 0; } -int APS5_VABI sceShellCoreUtilGetGpuLoadEmulationMode() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceShellCoreUtilGetGpuLoadEmulationModeByAppId(std::int32_t appId) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceShellCoreUtilGetHidConfigFileInfoString(char* value, std::size_t capacity, std::int32_t configIndex) { NotImplemented_nid_no_patch(__func__); return 0; @@ -966,11 +956,6 @@ int APS5_VABI sceShellCoreUtilGetSupportSiteURL(std::uint32_t first, std::uint32 return 0; } -bool APS5_VABI sceShellCoreUtilGetSuspendConfirmationDialogFlag(std::int32_t appId) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceShellCoreUtilGetSystemBGState(std::int32_t appId, std::uint32_t* state) { NotImplemented_nid_no_patch(__func__); return 0; @@ -1321,16 +1306,6 @@ int APS5_VABI sceShellCoreUtilSetGnmCompositorOnScreenProfilerFlag(std::int32_t return 0; } -int APS5_VABI sceShellCoreUtilSetGpuLoadEmulationMode(std::uint32_t mode) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceShellCoreUtilSetGpuLoadEmulationModeByAppId(std::int32_t appId, std::uint32_t mode) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceShellCoreUtilSetIDUMode(std::uint8_t mode) { NotImplemented_nid_no_patch(__func__); return 0; @@ -1700,11 +1675,6 @@ int APS5_VABI sceSystemServiceDisablePersonalEyeToEyeDistanceSetting() { return 0; } -int APS5_VABI sceSystemServiceDisableSuspendConfirmationDialog() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceSystemServiceDisableVoiceRecognition() { return SystemServiceUnavailable; } @@ -1714,11 +1684,6 @@ int APS5_VABI sceSystemServiceEnablePersonalEyeToEyeDistanceSetting() { return 0; } -int APS5_VABI sceSystemServiceEnableSuspendConfirmationDialog() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceSystemServiceGetAppCategoryType(std::int32_t appId, std::uint32_t* categoryType) { NotImplemented_nid_no_patch(__func__); return 0; @@ -1749,11 +1714,6 @@ int APS5_VABI sceSystemServiceGetEventForDaemon(SystemServiceDaemonEvent* event) return 0; } -int APS5_VABI sceSystemServiceGetGpuLoadEmulationMode() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceSystemServiceGetLocalProcessStatusList(SystemServiceLocalProcessStatus* statuses, std::uint32_t capacity, std::uint32_t* count) { NotImplemented_nid_no_patch(__func__); return 0; @@ -1992,11 +1952,6 @@ int APS5_VABI sceSystemServiceSetControllerFocusPermission(std::uint8_t permissi return 0; } -int APS5_VABI sceSystemServiceSetGpuLoadEmulationMode(std::uint32_t mode) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceSystemServiceSetOutOfVrPlayAreaFlag(std::uint8_t flag) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceSystemService/SystemService.hpp b/core/libs/prx/libSceSystemService/SystemService.hpp index dc9d45e4d9..af106ea7dc 100644 --- a/core/libs/prx/libSceSystemService/SystemService.hpp +++ b/core/libs/prx/libSceSystemService/SystemService.hpp @@ -4,12 +4,15 @@ #include #include #include +#include #include #include #include #include #include +#include "prx/libSceSystemService/LanguageLocale.hpp" + constexpr int SYSTEM_SERVICE_OK = 0; constexpr int SYSTEM_SERVICE_ERROR_PARAMETER = -2136932349; constexpr int SYSTEM_SERVICE_ERROR_NO_EVENT = -2136932348; @@ -41,16 +44,23 @@ 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; +inline int SystemServiceHostLanguage() { + static const int language = SystemServiceLocale::ResolveHostLanguage( + SystemServiceLocale::HostLocales, [](const std::string& line) { std::fprintf(stderr, "%s\n", line.c_str()); }); + return language; +} + inline int SystemServiceConsoleLanguage(std::string_view caller) { const char* configured = std::getenv("ANYPS5_LANGUAGE"); if (configured == nullptr || configured[0] == '\0') return SYSTEM_SERVICE_PARAM_LANG_ENGLISH_US; const std::string_view text(configured); + if (SystemServiceLocale::Detail::EqualsIgnoringCase(text, "auto")) return SystemServiceHostLanguage(); int language = -1; const auto [end, error] = std::from_chars(text.data(), text.data() + text.size(), language); if (error != std::errc{} || end != text.data() + text.size() || language < SYSTEM_SERVICE_PARAM_LANG_JAPANESE || language > SYSTEM_SERVICE_PARAM_LANG_UKRAINIAN) { throw std::runtime_error(std::string(caller) + ": ANYPS5_LANGUAGE must be a console language number from " + std::to_string(SYSTEM_SERVICE_PARAM_LANG_JAPANESE) + " to " + std::to_string(SYSTEM_SERVICE_PARAM_LANG_UKRAINIAN) + - ", not \"" + std::string(text) + "\""); + " or auto, not \"" + std::string(text) + "\""); } return language; } diff --git a/core/libs/prx/libSceSystemService/include/InventoryExports.hpp b/core/libs/prx/libSceSystemService/include/InventoryExports.hpp new file mode 100644 index 0000000000..52c56ee1f1 --- /dev/null +++ b/core/libs/prx/libSceSystemService/include/InventoryExports.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include "SceTypes.hpp" + +extern "C" { + +int APS5_VABI libSceSystemServiceUnknown_16waHD4tFBo_nid_no_patch(); +int APS5_VABI libSceSystemServiceUnknown_1m_minus_E1Xh8COo_nid_no_patch(); +int APS5_VABI libSceSystemServiceUnknown_7eu2qjQGigw_nid_no_patch(); +int APS5_VABI libSceSystemServiceUnknown_90unWbnI0qE_nid_no_patch(); +int APS5_VABI libSceSystemServiceUnknown_E_minus_bXVJqwGZw_nid_no_patch(); +int APS5_VABI libSceSystemServiceUnknown_G3oPdw3lkps_nid_no_patch(); +int APS5_VABI libSceSystemServiceUnknown_KgiwSrROsIQ_nid_no_patch(); +int APS5_VABI libSceSystemServiceUnknown_LxGOxroprpg_nid_no_patch(); +int APS5_VABI libSceSystemServiceUnknown_NXFtO8BfVeo_nid_no_patch(); +int APS5_VABI libSceSystemServiceUnknown_Oxh0CSsTZ78_nid_no_patch(); +int APS5_VABI libSceSystemServiceUnknown_OyTwhCKWXi4_nid_no_patch(); +int APS5_VABI libSceSystemServiceUnknown_U0HutHWnMBM_nid_no_patch(); +int APS5_VABI libSceSystemServiceUnknown_VDs9VSjSJh0_nid_no_patch(); +int APS5_VABI libSceSystemServiceUnknown_ZWNP_plus__minus_dWr8E_nid_no_patch(); +int APS5_VABI libSceSystemServiceUnknown_axzblV8OvWU_nid_no_patch(); +int APS5_VABI libSceSystemServiceUnknown_bJyWctnSXyU_nid_no_patch(); +int APS5_VABI libSceSystemServiceUnknown_bUNkT3XDg0Y_nid_no_patch(); +int APS5_VABI libSceSystemServiceUnknown_c5_plus_4Scso9EU_nid_no_patch(); +int APS5_VABI libSceSystemServiceUnknown_fBGD_minus_HNimSk_nid_no_patch(); +int APS5_VABI libSceSystemServiceUnknown_fPAZ5DBsDxo_nid_no_patch(); +int APS5_VABI libSceSystemServiceUnknown_ifJiF5witJ4_nid_no_patch(); +int APS5_VABI libSceSystemServiceUnknown_kDGjRMtUDxo_nid_no_patch(); +int APS5_VABI libSceSystemServiceUnknown_lgpIdZ8T0xg_nid_no_patch(); +int APS5_VABI libSceSystemServiceUnknown_pj1SzIAxPlk_nid_no_patch(); +int APS5_VABI libSceSystemServiceUnknown_qdTPJWjquDc_nid_no_patch(); +int APS5_VABI libSceSystemServiceUnknown_rIqPq0oWlrg_nid_no_patch(); +int APS5_VABI libSceSystemServiceUnknown_sgYo_minus_zXHQRE_nid_no_patch(); +int APS5_VABI libSceSystemServiceUnknown_tPMOYbbviYs_nid_no_patch(); +int APS5_VABI libSceSystemServiceUnknown_uElcdmhh_minus_c8_nid_no_patch(); +int APS5_VABI libSceSystemServiceUnknown_xn_minus_HgPW29x4_nid_no_patch(); +int APS5_VABI libSceSystemServiceUnknown_y16IXiJfafA_nid_no_patch(); +int APS5_VABI libSceSystemServiceUnknown_yOiZq_plus_9_minus_ZMQ_nid_no_patch(); + +} diff --git a/core/libs/prx/libSceSystemService/include/SystemService.hpp b/core/libs/prx/libSceSystemService/include/SystemService.hpp index 47b2c0e0db..a44ed286ce 100644 --- a/core/libs/prx/libSceSystemService/include/SystemService.hpp +++ b/core/libs/prx/libSceSystemService/include/SystemService.hpp @@ -259,6 +259,7 @@ int APS5_VABI sceCoredumpSequenceHandlerGetCoredumpStatusByAppId(void); int APS5_VABI sceCoredumpSequenceHandlerCancelCoredump(void); int APS5_VABI sceSystemServiceIsBgmPlaying(void); int APS5_VABI sceShellCoreUtilIsBgmPlaying(void); +int APS5_VABI sceSystemServiceUnknown_M_KwxF0amiw(void); int APS5_VABI sceAppMessagingClearEventFlag(std::int32_t appId, std::uint64_t flags); int APS5_VABI sceAppMessagingReceiveMsg(AppMessagingMessage* message); int APS5_VABI sceAppMessagingSendMsg(std::uint32_t appId, std::uint32_t messageId, const void* data, std::size_t size, std::uint32_t flags); diff --git a/core/libs/prx/libSceSystemService/libSceSystemService.hpp b/core/libs/prx/libSceSystemService/libSceSystemService.hpp index e3ff176277..1268c62468 100644 --- a/core/libs/prx/libSceSystemService/libSceSystemService.hpp +++ b/core/libs/prx/libSceSystemService/libSceSystemService.hpp @@ -1,3 +1,4 @@ #pragma once #include "prx/libSceSystemService/include/SystemService.hpp" +#include "prx/libSceSystemService/include/InventoryExports.hpp" diff --git a/core/libs/prx/libSceTEEClient/Export.cpp b/core/libs/prx/libSceTEEClient/Export.cpp index f550aa63e3..d3d2404fc4 100644 --- a/core/libs/prx/libSceTEEClient/Export.cpp +++ b/core/libs/prx/libSceTEEClient/Export.cpp @@ -78,10 +78,9 @@ int APS5_VABI libSceTEEClientUnknown_Xn5kfzvPht4_nid_no_patch() { } APS5_EXPORT("Xn5kfzvPht4", libSceTEEClientUnknown_Xn5kfzvPht4_nid_no_patch); -int APS5_VABI libSceTEEClientUnknown_ZAJdq_minus_IONZ4_nid_no_patch() { +int APS5_VABI AMDTEE_DLM_StartTADebug() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("ZAJdq-IONZ4", libSceTEEClientUnknown_ZAJdq_minus_IONZ4_nid_no_patch); int APS5_VABI libSceTEEClientUnknown_d_minus_OLk3nvFIA_nid_no_patch() { NotImplemented_nid_no_patch(__func__); diff --git a/core/libs/prx/libSceTEEClient/include/InventoryExports.hpp b/core/libs/prx/libSceTEEClient/include/InventoryExports.hpp index fa490466d4..7dcaeddb10 100644 --- a/core/libs/prx/libSceTEEClient/include/InventoryExports.hpp +++ b/core/libs/prx/libSceTEEClient/include/InventoryExports.hpp @@ -7,7 +7,7 @@ extern "C" { int APS5_VABI libSceTEEClientUnknown_H9IE7wXy3qY_nid_no_patch(); int APS5_VABI libSceTEEClientUnknown_VaBqpbmaWpc_nid_no_patch(); int APS5_VABI libSceTEEClientUnknown_Xn5kfzvPht4_nid_no_patch(); -int APS5_VABI libSceTEEClientUnknown_ZAJdq_minus_IONZ4_nid_no_patch(); +int APS5_VABI AMDTEE_DLM_StartTADebug(); int APS5_VABI libSceTEEClientUnknown_d_minus_OLk3nvFIA_nid_no_patch(); } diff --git a/core/libs/prx/libSceUserService/CMakeLists.txt b/core/libs/prx/libSceUserService/CMakeLists.txt index ee3f746eb4..53e0c73aac 100644 --- a/core/libs/prx/libSceUserService/CMakeLists.txt +++ b/core/libs/prx/libSceUserService/CMakeLists.txt @@ -27,6 +27,18 @@ if(BUILD_TESTING) endif() endif() +if(BUILD_TESTING) + add_test_executable(guest_user_service_user_kind_tests ${LIBS_INCLUDE_DIR}/tests/GuestUserServiceUserKind.cpp) + target_include_directories(guest_user_service_user_kind_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_user_service_user_kind_tests PRIVATE libSceUserService libSceSystemService) + add_test(NAME guest_user_service_user_kind COMMAND guest_user_service_user_kind_tests) + set_tests_properties(guest_user_service_user_kind PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_user_service_user_kind PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + if(BUILD_TESTING) add_test_executable(guest_user_service_lifecycle_tests ${LIBS_INCLUDE_DIR}/tests/GuestUserServiceLifecycle.cpp) target_include_directories(guest_user_service_lifecycle_tests PRIVATE ${LIBS_INCLUDE_DIR}) diff --git a/core/libs/prx/libSceUserService/Export.cpp b/core/libs/prx/libSceUserService/Export.cpp index b73509e392..7601922a34 100644 --- a/core/libs/prx/libSceUserService/Export.cpp +++ b/core/libs/prx/libSceUserService/Export.cpp @@ -353,12 +353,12 @@ int APS5_VABI sceUserServiceGetTraditionalChineseInputType(int user_id, int32_t* } int APS5_VABI sceUserServiceGetNpAccountId(int user_id, uint64_t* account_id) { - if (!IsUserLoggedIn(user_id)) { - return USER_SERVICE_ERROR_UNKNOWN_USER; - } if (account_id == nullptr) { return USER_SERVICE_ERROR_INVALID_ARGUMENT; } + if (!IsUserLoggedIn(user_id)) { + return USER_SERVICE_ERROR_UNKNOWN_USER; + } *account_id = 0; return USER_SERVICE_OK; } @@ -2033,9 +2033,8 @@ int APS5_VABI sceUserServiceSetPbtcWednesdayDuration(void) { return 0; } -int APS5_VABI sceUserServiceIsGuestUser(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceUserServiceIsGuestUser(std::int32_t userId) { + return (static_cast(userId) & 0xe0000000u) == 0x20000000u ? 1 : 0; } int APS5_VABI sceUserServiceGetPadProfileDataHuntList(void) { @@ -2513,9 +2512,8 @@ int APS5_VABI sceUserServiceSetPbtcAdditionalTime(void) { return 0; } -int APS5_VABI sceUserServiceIsSharePlayClientUser(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceUserServiceIsSharePlayClientUser(std::int32_t userId) { + return (static_cast(userId) & 0xf0000000u) == 0x30000000u ? 1 : 0; } int APS5_VABI sceUserServiceGetMonoOutput(void) { @@ -2933,9 +2931,10 @@ int APS5_VABI sceUserServiceGetPbtcSundayHoursEnd(void) { return 0; } -int APS5_VABI sceUserServiceGetIsSpartacusEnabled(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceUserServiceGetIsSpartacusEnabled(std::int32_t, bool* enabled) { + if (enabled == nullptr) return USER_SERVICE_ERROR_INVALID_ARGUMENT; + *enabled = true; + return USER_SERVICE_OK; } } diff --git a/core/libs/prx/libSceUserService/UserService.hpp b/core/libs/prx/libSceUserService/UserService.hpp index 5a0f74d2cb..4d0bf9de3c 100644 --- a/core/libs/prx/libSceUserService/UserService.hpp +++ b/core/libs/prx/libSceUserService/UserService.hpp @@ -395,7 +395,7 @@ int APS5_VABI sceUserServiceSetGlsLiveQuality4(void); int APS5_VABI sceUserServiceGetHearingAssistPresetType(void); int APS5_VABI sceUserServiceGetHearingAssistWeakenAdjustment(void); int APS5_VABI sceUserServiceSetPbtcWednesdayDuration(void); -int APS5_VABI sceUserServiceIsGuestUser(void); +int APS5_VABI sceUserServiceIsGuestUser(std::int32_t userId); int APS5_VABI sceUserServiceGetPadProfileDataHuntList(void); int APS5_VABI sceUserServiceGetAccessibilityKeyremapData(void); int APS5_VABI sceUserServiceSetCaptureStatus2(void); @@ -491,7 +491,7 @@ int APS5_VABI sceUserServiceGetEventUiFlag(void); int APS5_VABI sceUserServiceSetGlsBroadcastUiLayout(void); int APS5_VABI sceUserServiceSetVibrationEnabled(void); int APS5_VABI sceUserServiceSetPbtcAdditionalTime(void); -int APS5_VABI sceUserServiceIsSharePlayClientUser(void); +int APS5_VABI sceUserServiceIsSharePlayClientUser(std::int32_t userId); int APS5_VABI sceUserServiceGetMonoOutput(void); int APS5_VABI sceUserServiceSetNpLoginId(void); int APS5_VABI sceUserServiceGetShareStatus(void); @@ -575,7 +575,7 @@ int APS5_VABI sceUserServiceGetUserGroupNum(void); int APS5_VABI sceUserServiceSetPartySettingFlags(void); int APS5_VABI sceUserServiceGetGlsCameraEffect(void); int APS5_VABI sceUserServiceGetPbtcSundayHoursEnd(void); -int APS5_VABI sceUserServiceGetIsSpartacusEnabled(void); +int APS5_VABI sceUserServiceGetIsSpartacusEnabled(std::int32_t userId, bool* enabled); } #endif diff --git a/core/libs/prx/libSceVdecCore/Export.cpp b/core/libs/prx/libSceVdecCore/Export.cpp index c1c5f2d155..4dd995dc9a 100644 --- a/core/libs/prx/libSceVdecCore/Export.cpp +++ b/core/libs/prx/libSceVdecCore/Export.cpp @@ -4,12 +4,18 @@ #include "prx/libSceVdecCore/include/Api.hpp" #include "prx/libSceVdecCore/include/Decoder.hpp" #include +#include +#include #include #include +#include +#include +#include #include #include #include #include +#include #include #include #include "SceTypes.hpp" @@ -17,12 +23,17 @@ extern "C" { #include +#include } namespace { constexpr int VdecCoreErrorInvalidArgument = static_cast(0x80C00001); constexpr int VdecCoreErrorInvalidCodecType = static_cast(0x80C00003); +constexpr int VdecCoreErrorNoPicture = static_cast(0x80C00010); +constexpr int VdecCoreErrorFatal = static_cast(0x80C00012); +constexpr int VdecCoreErrorDecodeRunning = static_cast(0x80C00018); +constexpr std::int64_t VdecCorePtsInvalid = -1; constexpr std::uint32_t CodecHevc = 4; constexpr std::uint32_t MaxCodecType = 4; constexpr std::uint32_t MaxDecoderType = 8; @@ -31,12 +42,15 @@ constexpr std::uint32_t DefaultFrameWidth = 1920; constexpr std::uint32_t DefaultFrameHeight = 1080; constexpr std::uint32_t DefaultHorizontalAlign = 256; constexpr std::uint32_t FrameHeightAlign = 16; +constexpr std::uint32_t FrameWidthAlign = 16; +constexpr std::uint32_t OutputPitchAlign = 64; constexpr std::uint32_t FrameBufferPadding = 0x1000; constexpr std::uint32_t FrameBufferGranularity = 0x10000; constexpr std::uint32_t FrameBufferAlignment = 256; constexpr std::uint64_t InstanceSize = 0x40000; constexpr std::uint64_t ComputeSize = 0x10000; constexpr std::uint32_t ComputeAlignment = 0x10000; +constexpr std::uint64_t MaxAccessUnitSize = static_cast(std::numeric_limits::max() - AV_INPUT_BUFFER_PADDING_SIZE - 1); struct Geometry { std::uint32_t width; @@ -46,15 +60,52 @@ struct Geometry { std::uint32_t frameBufferSize; }; +struct Command { + enum class Type { + Packet, + Reset, + Flush, + }; + Type type = Type::Packet; + AVPacket* packet = nullptr; + std::shared_ptr> done; +}; + struct Decoder { std::uint32_t codecType = 0; Geometry geometry{}; AVCodecContext* context = nullptr; + SwsContext* scaler = nullptr; + AVFrame* decodedFrame = nullptr; std::vector mappedBlocks; std::vector frameBufferSlots; + std::uint32_t currentSlot = 0; + std::vector fallbackFrameBuffer; + std::int64_t lastPts = VdecCorePtsInvalid; + std::int64_t lastDts = VdecCorePtsInvalid; + std::uint64_t lastUserData = 0; + std::mutex mutex; + std::condition_variable inputReady; + std::condition_variable outputReady; + std::deque commands; + std::deque frames; + bool busy = false; + std::jthread worker; ~Decoder() { + { + std::lock_guard lock(mutex); + worker.request_stop(); + } + inputReady.notify_all(); + if (worker.joinable()) worker.join(); + for (auto& command : commands) av_packet_free(&command.packet); + commands.clear(); + for (auto* frame : frames) av_frame_free(&frame); + frames.clear(); + av_frame_free(&decodedFrame); avcodec_free_context(&context); + sws_freeContext(scaler); } }; @@ -102,14 +153,142 @@ std::unordered_map>::iterator Find(Decoder return found; } +Decoder& Lookup(const void* decoder, const char* caller) { + auto& state = State(); + std::lock_guard lock(state.mutex); + return *Find(state, decoder, caller)->second; +} + +void DrainFrames(Decoder& decoder) { + while (true) { + AVFrame* frame = av_frame_alloc(); + if (frame == nullptr) break; + if (avcodec_receive_frame(decoder.context, frame) != 0) { + av_frame_free(&frame); + break; + } + std::lock_guard lock(decoder.mutex); + decoder.frames.push_back(frame); + } +} + +void RunWorker(std::stop_token stop, Decoder* decoder) { + while (!stop.stop_requested()) { + Command command; + { + std::unique_lock lock(decoder->mutex); + decoder->inputReady.wait(lock, [&] { return stop.stop_requested() || !decoder->commands.empty(); }); + if (stop.stop_requested()) break; + command = std::move(decoder->commands.front()); + decoder->commands.pop_front(); + decoder->busy = true; + } + switch (command.type) { + case Command::Type::Packet: { + int result = avcodec_send_packet(decoder->context, command.packet); + if (result == AVERROR_EOF) { + avcodec_flush_buffers(decoder->context); + result = avcodec_send_packet(decoder->context, command.packet); + } + if (result == AVERROR(EAGAIN)) { + DrainFrames(*decoder); + avcodec_send_packet(decoder->context, command.packet); + } + av_packet_free(&command.packet); + DrainFrames(*decoder); + break; + } + case Command::Type::Flush: + avcodec_send_packet(decoder->context, nullptr); + DrainFrames(*decoder); + avcodec_flush_buffers(decoder->context); + command.done->store(true, std::memory_order_release); + break; + case Command::Type::Reset: + avcodec_flush_buffers(decoder->context); + { + std::lock_guard lock(decoder->mutex); + for (auto* frame : decoder->frames) av_frame_free(&frame); + decoder->frames.clear(); + decoder->currentSlot = 0; + } + command.done->store(true, std::memory_order_release); + break; + } + std::lock_guard lock(decoder->mutex); + decoder->busy = !decoder->commands.empty(); + decoder->outputReady.notify_all(); + } +} + +void RunCommand(Decoder& decoder, Command::Type type, std::uint32_t* count) { + auto done = std::make_shared>(false); + std::unique_lock lock(decoder.mutex); + decoder.commands.push_back({type, nullptr, done}); + decoder.inputReady.notify_one(); + decoder.outputReady.wait(lock, [&] { return done->load(std::memory_order_acquire); }); + if (count != nullptr) *count = static_cast(decoder.frames.size()); +} + +AVFrame* ConvertNv12(Decoder& decoder, const AVFrame& frame) { + AVFrame* converted = av_frame_alloc(); + if (converted == nullptr) throw std::runtime_error("VdecCore: cannot allocate an NV12 frame"); + converted->format = AV_PIX_FMT_NV12; + converted->width = frame.width; + converted->height = frame.height; + converted->pts = frame.pts; + converted->pkt_dts = frame.pkt_dts; + converted->opaque = frame.opaque; + if (av_frame_get_buffer(converted, 0) < 0) { + av_frame_free(&converted); + throw std::runtime_error("VdecCore: cannot allocate an NV12 frame"); + } + decoder.scaler = sws_getCachedContext(decoder.scaler, frame.width, frame.height, static_cast(frame.format), frame.width, frame.height, AV_PIX_FMT_NV12, SWS_BILINEAR, nullptr, nullptr, nullptr); + if (decoder.scaler == nullptr || sws_scale(decoder.scaler, frame.data, frame.linesize, 0, frame.height, converted->data, converted->linesize) != frame.height) { + av_frame_free(&converted); + throw std::runtime_error("VdecCore: cannot convert a decoded picture to NV12"); + } + return converted; +} + +void CopyNv12(std::uint8_t* destination, const AVFrame& frame) { + const auto width = static_cast(frame.width); + const auto height = static_cast(frame.height); + const std::size_t pitch = AlignUp(width, OutputPitchAlign); + const std::size_t alignedHeight = AlignUp(height, FrameHeightAlign); + std::uint8_t* luma = destination; + std::uint8_t* chroma = destination + pitch * alignedHeight; + const auto lumaRow = [&](std::size_t row) { return frame.data[0] + row * static_cast(frame.linesize[0]); }; + const auto chromaRow = [&](std::size_t row) { return frame.data[1] + row * static_cast(frame.linesize[1]); }; + for (std::size_t row = 0; row < height; ++row) std::memcpy(luma + row * pitch, lumaRow(row), width); + for (std::size_t row = 0; row < height / 2; ++row) std::memcpy(chroma + row * pitch, chromaRow(row), width); + for (std::size_t row = height; row < alignedHeight && height > 0; ++row) std::memcpy(luma + row * pitch, lumaRow(height - 1), width); + for (std::size_t row = height / 2; row < alignedHeight / 2 && height / 2 > 0; ++row) std::memcpy(chroma + row * pitch, chromaRow(height / 2 - 1), width); +} + +std::uint64_t PictureTime(std::int64_t decoded, std::int64_t last) { + return static_cast(decoded != AV_NOPTS_VALUE ? decoded : last); +} + } extern "C" { +std::uint64_t VdecCoreMaxAccessUnitSize_nid_no_patch() { + return MaxAccessUnitSize; +} + +std::int32_t VdecCoreAccessUnitLength_nid_no_patch(std::uint64_t auSize) { + if (auSize == 0 || auSize > MaxAccessUnitSize) return -1; + return static_cast(auSize); +} + std::uint32_t VdecCoreFrameBufferSlots_nid_no_patch(const void* decoder, void** slots, std::uint32_t capacity) { auto& state = State(); std::lock_guard lock(state.mutex); - const auto& found = Find(state, decoder, "VdecCoreFrameBufferSlots")->second->frameBufferSlots; + auto& target = *Find(state, decoder, "VdecCoreFrameBufferSlots")->second; + std::lock_guard decoderLock(target.mutex); + const auto& found = target.frameBufferSlots; const auto copied = std::min(capacity, found.size()); std::copy_n(found.begin(), copied, slots); return static_cast(found.size()); @@ -132,6 +311,8 @@ int APS5_VABI sceVdecCoreCreateDecoder(const VdecCoreConfigInfo* config, const V created->codecType = config->codecType; created->geometry = FrameGeometry(*config); OpenCodec(*created); + created->decodedFrame = av_frame_alloc(); + created->worker = std::jthread(RunWorker, created.get()); void* handle = created.get(); auto& state = State(); std::lock_guard lock(state.mutex); @@ -158,13 +339,126 @@ int APS5_VABI sceVdecCoreFinalizeComputeResource() { return 0; } -int APS5_VABI sceVdecCoreFlushDecodeOutput() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceVdecCoreFlushDecodeOutput(void* decoder, std::uint32_t* count, void*) { + if (decoder == nullptr) return VdecCoreErrorInvalidArgument; + RunCommand(Lookup(decoder, __func__), Command::Type::Flush, count); return 0; } -int APS5_VABI sceVdecCoreGetDecodeOutput() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceVdecCoreGetDecodeOutput(void* decoder, VdecCoreFrameBuffer* frameBuffer, std::uint32_t, VdecCoreOutputInfo* output, void*) { + if (decoder == nullptr || output == nullptr) return VdecCoreErrorInvalidArgument; + auto& target = Lookup(decoder, __func__); + AVFrame* frame = nullptr; + { + std::lock_guard lock(target.mutex); + if (target.frames.empty()) { + output->frameWidth = target.geometry.width; + output->framePitch = AlignUp(target.geometry.width, OutputPitchAlign); + output->frameHeight = target.geometry.height; + output->pictureCount = 0; + output->frameBuffer = nullptr; + output->frameFormat = 0; + output->reserved0 = 0; + output->pictures[0].frameBuffer = nullptr; + output->pictures[1].frameBuffer = nullptr; + output->lumaBuffer = nullptr; + output->chromaBuffer = nullptr; + return VdecCoreErrorNoPicture; + } + frame = target.frames.front(); + target.frames.pop_front(); + } + struct Release { + AVFrame*& frame; + ~Release() { av_frame_free(&frame); } + } release{frame}; + if (frame->format != AV_PIX_FMT_NV12) { + AVFrame* converted = ConvertNv12(target, *frame); + av_frame_free(&frame); + frame = converted; + } + const auto width = static_cast(frame->width); + const auto height = static_cast(frame->height); + const std::uint32_t alignedWidth = AlignUp(width, FrameWidthAlign); + const std::uint32_t pitch = AlignUp(width, OutputPitchAlign); + const std::uint32_t alignedHeight = AlignUp(height, FrameHeightAlign); + const std::uint32_t pictureSize = pitch * alignedHeight * 3 / 2; + const std::uint32_t sarWidth = frame->sample_aspect_ratio.num > 0 ? static_cast(frame->sample_aspect_ratio.num) : 1; + const std::uint32_t sarHeight = frame->sample_aspect_ratio.den > 0 ? static_cast(frame->sample_aspect_ratio.den) : 1; + std::uint8_t* destination = nullptr; + std::uint64_t capacity = 0; + std::int64_t lastPts = 0; + std::int64_t lastDts = 0; + { + std::lock_guard lock(target.mutex); + lastPts = target.lastPts; + lastDts = target.lastDts; + if (frameBuffer != nullptr && frameBuffer->frameBuffer != nullptr) { + destination = static_cast(frameBuffer->frameBuffer); + capacity = frameBuffer->frameBufferSize > 0 ? frameBuffer->frameBufferSize : target.geometry.frameBufferSize; + } else if (!target.frameBufferSlots.empty()) { + destination = static_cast(target.frameBufferSlots[target.currentSlot % target.frameBufferSlots.size()]); + ++target.currentSlot; + capacity = target.geometry.frameBufferSize; + } else { + const std::uint64_t requested = frameBuffer != nullptr && frameBuffer->frameBufferSize > 0 ? frameBuffer->frameBufferSize : target.geometry.frameBufferSize; + capacity = std::max(std::uint64_t{pictureSize} + sizeof(VdecCoreHwPictureInfo), requested); + if (target.fallbackFrameBuffer.size() < capacity) target.fallbackFrameBuffer.resize(static_cast(capacity)); + destination = target.fallbackFrameBuffer.data(); + } + } + if (pictureSize > capacity) throw std::runtime_error("VdecCore: a decoded picture is larger than its frame buffer"); + CopyNv12(destination, *frame); + const std::uint64_t pts = PictureTime(frame->pts, lastPts); + const std::uint64_t dts = PictureTime(frame->pkt_dts, lastDts); + const auto userData = static_cast(reinterpret_cast(frame->opaque)); + if (capacity - pictureSize >= sizeof(VdecCoreHwPictureInfo)) { + auto* info = reinterpret_cast(destination + capacity - sizeof(VdecCoreHwPictureInfo)); + std::memset(info, 0, sizeof(*info)); + info->codecType = target.codecType == CodecHevc ? 1 : 0; + info->userData = userData; + info->pts = pts; + info->dts = dts; + info->valid = 1; + info->width = alignedWidth; + info->height = alignedHeight; + info->cropLeft = 0; + info->cropRight = pitch > width ? (pitch - width) / 2 : 0; + info->cropTop = 0; + info->cropBottom = alignedHeight > height ? (alignedHeight - height) / 2 : 0; + info->sarWidth = sarWidth; + info->sarHeight = sarHeight; + info->flags = 0x01; + if (info->cropRight > 0 || info->cropBottom > 0) info->flags |= 0x02; + } + output->frameWidth = alignedWidth; + output->framePitch = pitch; + output->frameHeight = alignedHeight; + output->pictureCount = 1; + output->frameBuffer = destination; + output->frameFormat = 0; + output->reserved0 = 0; + output->pictures[0].flags = target.codecType; + output->pictures[0].reserved0 = 0; + output->pictures[0].pts = pts; + output->pictures[0].dts = dts; + output->pictures[0].userData = userData; + output->pictures[0].pictureInfo = nullptr; + output->pictures[0].frameBuffer = destination; + output->pictures[0].frameBufferSize = pictureSize; + output->pictures[0].framePitch = pitch; + output->pictures[0].reserved1 = 0; + output->pictures[1].frameBuffer = nullptr; + output->aspectRatio = 1; + output->sarWidth = sarWidth; + output->videoFormat = 5; + output->colourPrimaries = 1; + output->transferCharacteristics = 1; + output->framePitchInBytes = pitch; + output->lumaBuffer = destination; + output->chromaBuffer = destination + static_cast(pitch) * alignedHeight; + output->videoFullRangeFlag = frame->color_range == AVCOL_RANGE_JPEG ? 1 : 0; + output->colourDescriptionPresentFlag = 1; return 0; } @@ -179,6 +473,7 @@ int APS5_VABI sceVdecCoreMapMemoryBlock(void* decoder, const VdecCoreMemoryBlock std::lock_guard lock(state.mutex); auto& target = *Find(state, decoder, __func__)->second; if (blocks == nullptr || count == 0) return 0; + std::lock_guard decoderLock(target.mutex); for (std::uint32_t index = 0; index < count; ++index) { const VdecCoreMemoryBlock block = blocks[index]; target.mappedBlocks.push_back(block); @@ -223,13 +518,42 @@ int APS5_VABI sceVdecCoreQueryInstanceSize(const VdecCoreConfigInfo* config, std return 0; } -int APS5_VABI sceVdecCoreResetDecoder() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceVdecCoreResetDecoder(void* decoder, void*) { + if (decoder == nullptr) return VdecCoreErrorInvalidArgument; + RunCommand(Lookup(decoder, __func__), Command::Type::Reset, nullptr); return 0; } -int APS5_VABI sceVdecCoreSetDecodeInput() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceVdecCoreSetDecodeInput(void* decoder, const VdecCoreInputData* input, void*, std::uint32_t* status) { + if (decoder == nullptr || input == nullptr) return VdecCoreErrorInvalidArgument; + auto& target = Lookup(decoder, __func__); + if (status != nullptr) { + status[0] = 1; + status[1] = 1; + } + const std::int32_t length = VdecCoreAccessUnitLength_nid_no_patch(input->auSize); + if (input->auData == nullptr || length < 0) return VdecCoreErrorInvalidArgument; + const std::int64_t pts = input->pts == VdecCorePtsInvalid ? AV_NOPTS_VALUE : input->pts; + const std::int64_t dts = input->dts == VdecCorePtsInvalid ? AV_NOPTS_VALUE : input->dts; + AVPacket* packet = av_packet_alloc(); + if (packet == nullptr) return VdecCoreErrorFatal; + if (av_new_packet(packet, length) < 0) { + av_packet_free(&packet); + return VdecCoreErrorFatal; + } + std::memcpy(packet->data, input->auData, static_cast(length)); + packet->pts = pts; + packet->dts = dts; + packet->opaque = reinterpret_cast(static_cast(input->userData)); + { + std::lock_guard lock(target.mutex); + target.lastPts = input->pts; + target.lastDts = input->dts; + target.lastUserData = input->userData; + target.commands.push_back({Command::Type::Packet, packet, nullptr}); + target.busy = true; + } + target.inputReady.notify_one(); return 0; } @@ -238,8 +562,12 @@ int APS5_VABI sceVdecCoreSetDecodeOutputSw() { return 0; } -int APS5_VABI sceVdecCoreSyncDecode() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceVdecCoreSyncDecode(void* decoder, std::uint32_t* count) { + if (decoder == nullptr) return VdecCoreErrorInvalidArgument; + auto& target = Lookup(decoder, __func__); + std::unique_lock lock(target.mutex); + target.outputReady.wait(lock, [&] { return !target.frames.empty() || (!target.busy && target.commands.empty()); }); + if (count != nullptr) *count = static_cast(target.frames.size()); return 0; } @@ -248,13 +576,20 @@ int APS5_VABI sceVdecCoreSyncDecodeOutputSw() { return 0; } -int APS5_VABI sceVdecCoreSyncDecodeWptr() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVdecCoreSyncDecodeWptr(void* decoder, std::uint32_t* count, void*, void*) { + return sceVdecCoreSyncDecode(decoder, count); } -int APS5_VABI sceVdecCoreTrySyncDecode() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceVdecCoreTrySyncDecode(void* decoder, std::uint32_t* count) { + if (decoder == nullptr) return VdecCoreErrorInvalidArgument; + auto& target = Lookup(decoder, __func__); + std::lock_guard lock(target.mutex); + if (!target.frames.empty()) { + if (count != nullptr) *count = static_cast(target.frames.size()); + return 0; + } + if (count != nullptr) *count = 0; + if (target.busy || !target.commands.empty()) return VdecCoreErrorDecodeRunning; return 0; } @@ -263,9 +598,8 @@ int APS5_VABI sceVdecCoreTrySyncDecodeOutputSw() { return 0; } -int APS5_VABI sceVdecCoreTrySyncDecodeWptr() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVdecCoreTrySyncDecodeWptr(void* decoder, std::uint32_t* count, void*, void*) { + return sceVdecCoreTrySyncDecode(decoder, count); } } diff --git a/core/libs/prx/libSceVdecCore/include/Api.hpp b/core/libs/prx/libSceVdecCore/include/Api.hpp index 21278f7108..3161d0aabe 100644 --- a/core/libs/prx/libSceVdecCore/include/Api.hpp +++ b/core/libs/prx/libSceVdecCore/include/Api.hpp @@ -74,6 +74,86 @@ struct VdecCoreMemoryBlock { static_assert(sizeof(VdecCoreMemoryBlock) == 0x18); +struct VdecCoreFrameBuffer { + void* frameBuffer; + std::uint64_t frameBufferSize; +}; + +static_assert(sizeof(VdecCoreFrameBuffer) == 0x10); + +struct VdecCoreInputData { + const void* auData; + std::uint64_t auSize; + std::int64_t pts; + std::int64_t dts; + std::uint64_t userData; + std::uint32_t flags; + std::uint32_t streamId; +}; + +static_assert(sizeof(VdecCoreInputData) == 0x30); + +struct VdecCorePicture { + std::uint32_t flags; + std::uint32_t reserved0; + std::uint64_t pts; + std::uint64_t dts; + std::uint64_t userData; + void* pictureInfo; + void* frameBuffer; + std::uint32_t frameBufferSize; + std::uint32_t framePitch; + std::uint64_t reserved1; +}; + +static_assert(sizeof(VdecCorePicture) == 0x40); + +struct VdecCoreHwPictureInfo { + std::uint32_t codecType; + std::uint32_t reserved0; + std::uint64_t userData; + std::uint64_t pts; + std::uint64_t dts; + std::uint32_t valid; + std::uint32_t reserved1; + std::uint32_t width; + std::uint32_t height; + std::uint32_t cropLeft; + std::uint32_t cropRight; + std::uint32_t cropTop; + std::uint32_t cropBottom; + std::uint32_t sarWidth; + std::uint32_t sarHeight; + std::uint32_t reserved2; + std::uint32_t flags; + std::uint8_t reserved3[0x400 - 0x50]; +}; + +static_assert(sizeof(VdecCoreHwPictureInfo) == 0x400); + +struct VdecCoreOutputInfo { + std::uint32_t frameWidth; + std::uint32_t framePitch; + std::uint32_t frameHeight; + std::uint32_t pictureCount; + void* frameBuffer; + std::uint32_t frameFormat; + std::uint32_t reserved0; + VdecCorePicture pictures[2]; + std::uint32_t aspectRatio; + std::uint32_t sarWidth; + std::uint32_t videoFormat; + std::uint32_t colourPrimaries; + std::uint32_t transferCharacteristics; + std::uint32_t framePitchInBytes; + void* lumaBuffer; + void* chromaBuffer; + std::uint32_t videoFullRangeFlag; + std::uint32_t colourDescriptionPresentFlag; +}; + +static_assert(sizeof(VdecCoreOutputInfo) == 0xd0); + extern "C" { int APS5_VABI sceVdecCoreQueryMemorySizeSvp(); @@ -82,21 +162,21 @@ int APS5_VABI sceVdecCoreAnalyzeStream(); int APS5_VABI sceVdecCoreCreateDecoder(const VdecCoreConfigInfo* config, const VdecCoreMemoryInfo* memory, void* computeQueue, void** decoder); int APS5_VABI sceVdecCoreDeleteDecoder(void* decoder); int APS5_VABI sceVdecCoreFinalizeComputeResource(); -int APS5_VABI sceVdecCoreFlushDecodeOutput(); -int APS5_VABI sceVdecCoreGetDecodeOutput(); +int APS5_VABI sceVdecCoreFlushDecodeOutput(void* decoder, std::uint32_t* count, void* reserved); +int APS5_VABI sceVdecCoreGetDecodeOutput(void* decoder, VdecCoreFrameBuffer* frameBuffer, std::uint32_t flags, VdecCoreOutputInfo* output, void* reserved); int APS5_VABI sceVdecCoreInitializeComputeResource(); int APS5_VABI sceVdecCoreMapMemoryBlock(void* decoder, const VdecCoreMemoryBlock* blocks, std::uint32_t count); int APS5_VABI sceVdecCoreQueryComputeResourceInfo(VdecCoreComputeResourceInfo* info); int APS5_VABI sceVdecCoreQueryFrameBufferInfo(const VdecCoreConfigInfo* config, VdecCoreFrameBufferInfo* info); int APS5_VABI sceVdecCoreQueryInstanceSize(const VdecCoreConfigInfo* config, std::uint64_t* instanceSize, std::uint64_t* computeSize, std::uint64_t* reservedSize); -int APS5_VABI sceVdecCoreResetDecoder(); -int APS5_VABI sceVdecCoreSetDecodeInput(); +int APS5_VABI sceVdecCoreResetDecoder(void* decoder, void* resetParam); +int APS5_VABI sceVdecCoreSetDecodeInput(void* decoder, const VdecCoreInputData* input, void* reserved, std::uint32_t* status); int APS5_VABI sceVdecCoreSetDecodeOutputSw(); -int APS5_VABI sceVdecCoreSyncDecode(); +int APS5_VABI sceVdecCoreSyncDecode(void* decoder, std::uint32_t* count); int APS5_VABI sceVdecCoreSyncDecodeOutputSw(); -int APS5_VABI sceVdecCoreSyncDecodeWptr(); -int APS5_VABI sceVdecCoreTrySyncDecode(); +int APS5_VABI sceVdecCoreSyncDecodeWptr(void* decoder, std::uint32_t* count, void* writePointer, void* readPointer); +int APS5_VABI sceVdecCoreTrySyncDecode(void* decoder, std::uint32_t* count); int APS5_VABI sceVdecCoreTrySyncDecodeOutputSw(); -int APS5_VABI sceVdecCoreTrySyncDecodeWptr(); +int APS5_VABI sceVdecCoreTrySyncDecodeWptr(void* decoder, std::uint32_t* count, void* writePointer, void* readPointer); } diff --git a/core/libs/prx/libSceVdecCore/include/Decoder.hpp b/core/libs/prx/libSceVdecCore/include/Decoder.hpp index ea4d2d84db..32917a00d2 100644 --- a/core/libs/prx/libSceVdecCore/include/Decoder.hpp +++ b/core/libs/prx/libSceVdecCore/include/Decoder.hpp @@ -5,5 +5,7 @@ extern "C" { std::uint32_t VdecCoreFrameBufferSlots_nid_no_patch(const void* decoder, void** slots, std::uint32_t capacity); +std::uint64_t VdecCoreMaxAccessUnitSize_nid_no_patch(); +std::int32_t VdecCoreAccessUnitLength_nid_no_patch(std::uint64_t auSize); } diff --git a/core/libs/prx/libSceVdecsw/Export.cpp b/core/libs/prx/libSceVdecsw/Export.cpp index 8eab884339..50bd647f93 100644 --- a/core/libs/prx/libSceVdecsw/Export.cpp +++ b/core/libs/prx/libSceVdecsw/Export.cpp @@ -319,7 +319,7 @@ int APS5_VABI sceVdecswGetPictureInfo(const OutputInfo* output, void* first, voi static_cast(second); if (output == nullptr || (output->thisSize | 8u) != sizeof(OutputInfo)) throw std::runtime_error("Vdecsw: invalid output info"); if (output->pictureCount == 0) return 0; - if (output->pictureCount > 1) NotImplemented_nid_no_patch(__func__); + if (output->pictureCount > 1) throw std::runtime_error("Vdecsw: second field picture info is not implemented"); if (first == nullptr) return 0; std::uint64_t size; std::memcpy(&size, first, sizeof(size)); diff --git a/core/libs/prx/libSceVideoOut/InventoryExports.cpp b/core/libs/prx/libSceVideoOut/InventoryExports.cpp index e298727bb5..77d4bde7ed 100644 --- a/core/libs/prx/libSceVideoOut/InventoryExports.cpp +++ b/core/libs/prx/libSceVideoOut/InventoryExports.cpp @@ -1,3 +1,4 @@ +#include "prx/libSceVideoOut/include/LaterExports.hpp" #include "prx/libSceVideoOut/include/LegacyExports.hpp" #include "prx/libc/include/general/ExportMacros.hpp" #include "prx/libSceVideoOut/include/InventoryExports.hpp" @@ -827,3 +828,59 @@ int APS5_VABI sceVideoOutSetConfigurationMode() { } } + +extern "C" { + +int APS5_VABI sceVideoOutDebugLatencyMeasureGetLatestLatency() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceVideoOutGetHandleByBus() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceVideoOutPostProcessorAddCallback() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceVideoOutPostProcessorRemoveCallback() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceVideoOutSetContentDynamicRange() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceVideoOutSysCursorSetSize() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceVideoOutSysGetCursorInfo() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceVideoOutSysGetMonitorInfo() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceVideoOutSysGetOutputStatusEx() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceVideoOutSysOpenPostProcessor() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceVideoOutSysSetDrrPeg() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceVideoOutSysSetDrrPegRefreshRate() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceVideoOutSysSetVrrSetting() { + NotImplemented_nid_no_patch(__func__); +} + +} diff --git a/core/libs/prx/libSceVideoOut/include/ImeTextInput.hpp b/core/libs/prx/libSceVideoOut/include/ImeTextInput.hpp index 81cfe59fc1..a1293d2143 100644 --- a/core/libs/prx/libSceVideoOut/include/ImeTextInput.hpp +++ b/core/libs/prx/libSceVideoOut/include/ImeTextInput.hpp @@ -1,11 +1,19 @@ #ifndef CORE_LIBS_PRX_LIBSCEVIDEOOUT_IMETEXTINPUT_HPP #define CORE_LIBS_PRX_LIBSCEVIDEOOUT_IMETEXTINPUT_HPP +#include + #include "SDL_events.h" class ImeTextInput { public: + using ClipboardProvider = bool (*)(std::string& text); + + void SetClipboardProvider(ClipboardProvider provider) { clipboard = provider; } void HandleEvent(const SDL_Event& event, unsigned windowId); + +private: + ClipboardProvider clipboard = nullptr; }; #endif diff --git a/core/libs/prx/libSceVideoOut/include/LaterExports.hpp b/core/libs/prx/libSceVideoOut/include/LaterExports.hpp new file mode 100644 index 0000000000..7e0f2b58d9 --- /dev/null +++ b/core/libs/prx/libSceVideoOut/include/LaterExports.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include "SceTypes.hpp" + +extern "C" { + +int APS5_VABI sceVideoOutDebugLatencyMeasureGetLatestLatency(); +int APS5_VABI sceVideoOutGetHandleByBus(); +int APS5_VABI sceVideoOutPostProcessorAddCallback(); +int APS5_VABI sceVideoOutPostProcessorRemoveCallback(); +int APS5_VABI sceVideoOutSetContentDynamicRange(); +int APS5_VABI sceVideoOutSysCursorSetSize(); +int APS5_VABI sceVideoOutSysGetCursorInfo(); +int APS5_VABI sceVideoOutSysGetMonitorInfo(); +int APS5_VABI sceVideoOutSysGetOutputStatusEx(); +int APS5_VABI sceVideoOutSysOpenPostProcessor(); +int APS5_VABI sceVideoOutSysSetDrrPeg(); +int APS5_VABI sceVideoOutSysSetDrrPegRefreshRate(); +int APS5_VABI sceVideoOutSysSetVrrSetting(); + +} diff --git a/core/libs/prx/libSceVideoOut/libSceVideoOut.hpp b/core/libs/prx/libSceVideoOut/libSceVideoOut.hpp index f917f95ea8..5bd2acab54 100644 --- a/core/libs/prx/libSceVideoOut/libSceVideoOut.hpp +++ b/core/libs/prx/libSceVideoOut/libSceVideoOut.hpp @@ -6,3 +6,4 @@ #include "prx/libSceVideoOut/include/VideoOut.hpp" #include "prx/libSceVideoOut/include/InventoryExports.hpp" #include "prx/libSceVideoOut/include/LegacyExports.hpp" +#include "prx/libSceVideoOut/include/LaterExports.hpp" diff --git a/core/libs/prx/libSceVideoOut/src/Event.cpp b/core/libs/prx/libSceVideoOut/src/Event.cpp index 2c23c1f375..2907c455ae 100644 --- a/core/libs/prx/libSceVideoOut/src/Event.cpp +++ b/core/libs/prx/libSceVideoOut/src/Event.cpp @@ -88,9 +88,7 @@ static int registerVideoOutEvent(int handle, KernelEqueue eq, int16_t eventKind, } static int deleteVideoOutEvent(int handle, KernelEqueue eq, int16_t eventKind) { - if (!VideoOutDriver::Get().IsOpen(handle)) { - throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_HANDLE"); - } + if (!VideoOutDriver::Get().HasConfig(handle)) return VIDEO_OUT_ERROR_INVALID_HANDLE; if (eq == 0 || !EqueuePin_nid_postfix(eq)) { throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_EVENT_QUEUE"); } diff --git a/core/libs/prx/libSceVideoOut/src/ImeTextInput.cpp b/core/libs/prx/libSceVideoOut/src/ImeTextInput.cpp index 26ddb4b216..83f7acb0bf 100644 --- a/core/libs/prx/libSceVideoOut/src/ImeTextInput.cpp +++ b/core/libs/prx/libSceVideoOut/src/ImeTextInput.cpp @@ -5,9 +5,33 @@ #include "SDL_keyboard.h" +namespace { + +bool IsPasteShortcut(const SDL_KeyboardEvent& key) { + const unsigned modifiers = key.keysym.mod; + if ((modifiers & KMOD_ALT) != 0) return false; +#ifdef __APPLE__ + const bool command = (modifiers & KMOD_GUI) != 0; +#else + const bool command = (modifiers & KMOD_CTRL) != 0; +#endif + if (command && key.keysym.sym == SDLK_v) return true; + return (modifiers & KMOD_SHIFT) != 0 && !command && key.keysym.scancode == SDL_SCANCODE_INSERT; +} + +} + void ImeTextInput::HandleEvent(const SDL_Event& event, unsigned windowId) { ImeHostInput input{}; switch (event.type) { + case SDL_TEXTEDITING: + if (event.edit.windowID != windowId) return; + static_assert(sizeof(input.text) >= sizeof(event.edit.text)); + std::memcpy(input.text, event.edit.text, sizeof(event.edit.text)); + input.kind = ImeHostInputKind::Preedit; + input.start = event.edit.start; + input.length = event.edit.length; + break; case SDL_TEXTINPUT: if (event.text.windowID != windowId) return; static_assert(sizeof(input.text) >= sizeof(event.text.text)); @@ -16,6 +40,12 @@ void ImeTextInput::HandleEvent(const SDL_Event& event, unsigned windowId) { break; case SDL_KEYDOWN: if (event.key.windowID != windowId) return; + if (IsPasteShortcut(event.key)) { + if (event.key.repeat != 0 || clipboard == nullptr || !ImeCapturesKeyboard_nid_postfix()) return; + std::string text; + if (clipboard(text) && !text.empty()) ImePublishPaste_nid_postfix(text.data(), text.size()); + return; + } switch (event.key.keysym.scancode) { case SDL_SCANCODE_BACKSPACE: input.kind = ImeHostInputKind::Backspace; break; case SDL_SCANCODE_DELETE: input.kind = ImeHostInputKind::Delete; break; diff --git a/core/libs/prx/libSceVideoOut/src/MouseInput.cpp b/core/libs/prx/libSceVideoOut/src/MouseInput.cpp index 95b53ac6c0..ffa1a6d6a6 100644 --- a/core/libs/prx/libSceVideoOut/src/MouseInput.cpp +++ b/core/libs/prx/libSceVideoOut/src/MouseInput.cpp @@ -14,6 +14,8 @@ void MouseInput::HandleEvent(const SDL_Event& event, unsigned windowId) { if (event.motion.windowID != windowId || !focused || ModalDialogCapturesInput_nid_postfix()) return; input.x = scaled(event.motion.xrel, scaleX, remainderX); input.y = scaled(event.motion.yrel, scaleY, remainderY); + input.countsX = event.motion.xrel; + input.countsY = event.motion.yrel; break; case SDL_MOUSEBUTTONDOWN: case SDL_MOUSEBUTTONUP: diff --git a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp index 428ff3c71b..d4f972d1b3 100644 --- a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp +++ b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp @@ -90,6 +90,15 @@ std::optional configuredResolution() { return resolutionIndex(width, height); } +bool readHostClipboard(std::string& text) { + if (SDL_HasClipboardText() != SDL_TRUE) return false; + char* raw = SDL_GetClipboardText(); + if (raw == nullptr) return false; + text.assign(raw); + SDL_free(raw); + return true; +} + std::vector windowExtensions(SDL_Window* window) { return OnMainThread([&] { unsigned extensionCount = 0; @@ -754,6 +763,7 @@ void VideoOutDriver::presentLoop(std::stop_token token, std::promise& star keyboardInput.emplace(); imeTextInput.emplace(); dialogInput.emplace(); + imeTextInput->SetClipboardProvider(&readHostClipboard); }); while (!token.stop_requested()) { { diff --git a/core/libs/prx/libSceVideoOut/tests/Flip.cpp b/core/libs/prx/libSceVideoOut/tests/Flip.cpp index 839e79bd08..0677ec9012 100644 --- a/core/libs/prx/libSceVideoOut/tests/Flip.cpp +++ b/core/libs/prx/libSceVideoOut/tests/Flip.cpp @@ -228,7 +228,7 @@ void testDeferredBuffer() { } void testCpuFlipOrder() { - const bool inOrder = std::getenv("APS5_CPU_FLIP_IN_ORDER") != nullptr; + const bool inOrder = APS5_ENABLE_TIMING_LOG || std::getenv("APS5_CPU_FLIP_IN_ORDER") != nullptr; const int handle = sceVideoOutOpen(255, 0, 0, nullptr); auto cfg = VideoOutDriver::Get().GetConfig(handle); std::vector allocation(65536 + 65535); @@ -255,7 +255,7 @@ void testCpuFlipOrder() { std::thread flipThread([&] { check(sceVideoOutSubmitFlip(handle, 0, 1, 5) == 0, "the flip thread's flip was not accepted"); }); flipThread.join(); const bool ahead = flipsWithin(1, std::chrono::milliseconds(inOrder ? 1000 : 8000)); - if (inOrder) check(!ahead, "with APS5_CPU_FLIP_IN_ORDER a flip completed while a queue-0 submission ahead of it was held"); + if (inOrder) check(!ahead, "with CPU flips in order a flip completed while a queue-0 submission ahead of it was held"); else check(ahead, "a flip from a thread that never submitted queue-0 work waited for a held queue-0 submission"); first->Release(); check(flipsWithin(1, std::chrono::seconds(30)), "the flip thread's flip did not complete"); @@ -486,7 +486,7 @@ void testControls() { } check(sceVideoOutDeleteOutputModeEvent(queue, handle) == 0, "duplicate output mode unsubscription failed"); expectFailure([&] { sceVideoOutDeleteOutputModeEvent(0, handle); }); - expectFailure([&] { sceVideoOutDeleteOutputModeEvent(queue, -1); }); + check(sceVideoOutDeleteOutputModeEvent(queue, -1) == VIDEO_OUT_ERROR_INVALID_HANDLE, "output mode unsubscription on an invalid handle was accepted"); check(sceVideoOutAddVblankEvent(queue, handle, nullptr) == 0 && sceVideoOutAddVblankEvent(queue, handle, &settings) == 0, "vblank subscription failed"); { std::lock_guard lock(cfg->mutex); @@ -545,7 +545,7 @@ void testControls() { } check(sceVideoOutDeleteVrrActiveStatusEvent(queue, handle) == 0, "duplicate VRR status unsubscription failed"); expectFailure([&] { sceVideoOutDeleteVrrActiveStatusEvent(0, handle); }); - expectFailure([&] { sceVideoOutDeleteVrrActiveStatusEvent(queue, -1); }); + check(sceVideoOutDeleteVrrActiveStatusEvent(queue, -1) == VIDEO_OUT_ERROR_INVALID_HANDLE, "VRR status unsubscription on an invalid handle was accepted"); expectFailure([&] { sceVideoOutVrrPegToFixedRate(handle, 1, 0); }); expectFailure([&] { sceVideoOutVrrPegToFixedRate(handle, 0, 1); }); expectFailure([&] { sceVideoOutVrrPegToFixedRate(handle, 0x100000000ull, 0); }); @@ -1027,6 +1027,8 @@ void testUnopenedEventRegistration() { check(sceKernelCreateEqueue(&eq, "unopened") == 0, "the event queue was not created"); check(sceVideoOutAddFlipEvent(eq, 0, nullptr) == VIDEO_OUT_ERROR_INVALID_HANDLE, "flip event on an unopened port was accepted"); check(sceVideoOutAddVblankEvent(eq, 1, nullptr) == VIDEO_OUT_ERROR_INVALID_HANDLE, "vblank event on an unopened port was accepted"); + check(sceVideoOutDeleteFlipEvent(eq, 0) == VIDEO_OUT_ERROR_INVALID_HANDLE, "flip event deletion on an unopened port was accepted"); + check(sceVideoOutDeleteVblankEvent(eq, 1) == VIDEO_OUT_ERROR_INVALID_HANDLE, "vblank event deletion on an unopened port was accepted"); const int handle = sceVideoOutOpen(255, 0, 0, nullptr); check(sceVideoOutAddFlipEvent(eq, handle, nullptr) == 0, "flip event on an open port was refused"); check(sceVideoOutAddVblankEvent(eq, handle, nullptr) == 0, "vblank event on an open port was refused"); @@ -1034,6 +1036,8 @@ void testUnopenedEventRegistration() { check(sceVideoOutDeleteVblankEvent(eq, handle) == 0, "registered vblank event was not deleted"); sceVideoOutClose(handle); check(sceVideoOutAddFlipEvent(eq, handle, nullptr) == VIDEO_OUT_ERROR_INVALID_HANDLE, "flip event on a closed port was accepted"); + check(sceVideoOutDeleteFlipEvent(eq, handle) == VIDEO_OUT_ERROR_INVALID_HANDLE, "flip event deletion on a closed port was accepted"); + check(sceVideoOutDeleteVblankEvent(eq, handle) == VIDEO_OUT_ERROR_INVALID_HANDLE, "vblank event deletion on a closed port was accepted"); check(sceKernelDeleteEqueue(eq) == 0, "the event queue was not deleted"); LibcRunShutdown_nid_postfix(); } diff --git a/core/libs/prx/libSceVideoOut/tests/PadIdentify.cpp b/core/libs/prx/libSceVideoOut/tests/PadIdentify.cpp index cd07bf2cf7..c518e89a07 100644 --- a/core/libs/prx/libSceVideoOut/tests/PadIdentify.cpp +++ b/core/libs/prx/libSceVideoOut/tests/PadIdentify.cpp @@ -97,25 +97,27 @@ int Run(const char* mode) { if (std::strcmp(mode, "rumble") == 0) Pad::SetVibration(0, 200, 200); const auto bound = Clock::now(); DisplayWindow window; - PadInput padInput; - Require(padInput.SlotOccupied(0), "the virtual controller takes slot 0"); + { + PadInput padInput; + Require(padInput.SlotOccupied(0), "the virtual controller takes slot 0"); - if (std::strcmp(mode, "rumble") == 0) { - Pump(padInput, window, 1500); - Require(pulseEdges.empty(), "no identify pulse is sent while the title is rumbling"); - Require(!titleEdges.empty(), "the title's own rumble reaches the controller"); - Pad::SetVibration(0, 0, 0); - Pump(padInput, window, 1500); - Require(pulseEdges.size() == 1, "after the title stops, one pulse for player 1 is sent"); - Require(pulseEdges[0] - bound >= std::chrono::milliseconds(1500), "the pulse waits for the title to stay quiet"); - } else { - Pump(padInput, window, 3000); - if (std::strcmp(mode, "off") == 0) { - Require(pulseEdges.empty() && titleEdges.empty(), "off sends no pulse"); + if (std::strcmp(mode, "rumble") == 0) { + Pump(padInput, window, 1500); + Require(pulseEdges.empty(), "no identify pulse is sent while the title is rumbling"); + Require(!titleEdges.empty(), "the title's own rumble reaches the controller"); + Pad::SetVibration(0, 0, 0); + Pump(padInput, window, 1500); + Require(pulseEdges.size() == 1, "after the title stops, one pulse for player 1 is sent"); + Require(pulseEdges[0] - bound >= std::chrono::milliseconds(1500), "the pulse waits for the title to stay quiet"); } else { - Require(pulseEdges.size() == 1, "connect sends exactly one pulse for player 1"); - Require(pulseEdges[0] - bound >= std::chrono::milliseconds(450) && pulseEdges[0] - bound <= std::chrono::milliseconds(1500), - "the pulse follows the 500 ms quiet window"); + Pump(padInput, window, 3000); + if (std::strcmp(mode, "off") == 0) { + Require(pulseEdges.empty() && titleEdges.empty(), "off sends no pulse"); + } else { + Require(pulseEdges.size() == 1, "connect sends exactly one pulse for player 1"); + Require(pulseEdges[0] - bound >= std::chrono::milliseconds(450) && pulseEdges[0] - bound <= std::chrono::milliseconds(1500), + "the pulse follows the 500 ms quiet window"); + } } } diff --git a/core/libs/prx/libSceVideodec2/CMakeLists.txt b/core/libs/prx/libSceVideodec2/CMakeLists.txt index 0c8eaed6fc..05974f0d3b 100644 --- a/core/libs/prx/libSceVideodec2/CMakeLists.txt +++ b/core/libs/prx/libSceVideodec2/CMakeLists.txt @@ -33,6 +33,17 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_test_executable(guest_videodec2_hevc_aliases_tests ${LIBS_INCLUDE_DIR}/tests/GuestVideodec2HevcAliases.cpp) + target_include_directories(guest_videodec2_hevc_aliases_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_videodec2_hevc_aliases_tests PRIVATE libSceVideodec2 libc) + configure_windows_unwind(guest_videodec2_hevc_aliases_tests) + add_test(NAME guest_videodec2_hevc_aliases COMMAND guest_videodec2_hevc_aliases_tests) + set_tests_properties(guest_videodec2_hevc_aliases PROPERTIES TIMEOUT 10) + if(WIN32) + set_tests_properties(guest_videodec2_hevc_aliases PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_videodec2_unimplemented_tests ${LIBS_INCLUDE_DIR}/tests/GuestVideodec2Unimplemented.cpp) target_include_directories(guest_videodec2_unimplemented_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_videodec2_unimplemented_tests PRIVATE libSceVideodec2 libc) diff --git a/core/libs/prx/libSceVideodec2/Export.cpp b/core/libs/prx/libSceVideodec2/Export.cpp index a0ca268ea6..703b2fdc66 100644 --- a/core/libs/prx/libSceVideodec2/Export.cpp +++ b/core/libs/prx/libSceVideodec2/Export.cpp @@ -525,6 +525,14 @@ int APS5_VABI sceVideodec2CreateDecoder_nid_postfix(const DecoderConfigInfo* con return 0; } +int APS5_VABI sceVideodec2CreateHevcDecoder(const DecoderConfigInfo* config, const DecoderMemoryInfo* memory, std::uint64_t* handle) { + return sceVideodec2CreateDecoder_nid_postfix(config, memory, handle); +} + +int APS5_VABI sceVideodec2QueryHevcDecoderMemoryInfo(const DecoderConfigInfo* config, DecoderMemoryInfo* memory) { + return sceVideodec2QueryDecoderMemoryInfo_nid_postfix(config, memory); +} + int APS5_VABI sceVideodec2DeleteDecoder_nid_postfix(std::uint64_t handle) { std::lock_guard guard(lock); if (decoders.erase(handle) == 0) throw std::runtime_error("Videodec2: invalid decoder handle"); diff --git a/core/libs/prx/libSceVideodec2/Unimplemented.cpp b/core/libs/prx/libSceVideodec2/Unimplemented.cpp index dfe81fd3de..7cc8ced6e4 100644 --- a/core/libs/prx/libSceVideodec2/Unimplemented.cpp +++ b/core/libs/prx/libSceVideodec2/Unimplemented.cpp @@ -44,14 +44,6 @@ int APS5_VABI sceVideodec2CreateDecoderBid() { NotImplemented_nid_no_patch(__func__); } -int APS5_VABI sceVideodec2CreateHevcDecoder() { - NotImplemented_nid_no_patch(__func__); -} - -int APS5_VABI sceVideodec2QueryHevcDecoderMemoryInfo() { - NotImplemented_nid_no_patch(__func__); -} - int APS5_VABI sceVideodec2InitLibrary() { NotImplemented_nid_no_patch(__func__); } diff --git a/core/libs/prx/libSceVideodec2/include/Api.hpp b/core/libs/prx/libSceVideodec2/include/Api.hpp index 0a24fc2b8b..3cb06dc1d3 100644 --- a/core/libs/prx/libSceVideodec2/include/Api.hpp +++ b/core/libs/prx/libSceVideodec2/include/Api.hpp @@ -285,6 +285,8 @@ int APS5_VABI sceVideodec2AllocateComputeQueue_nid_postfix(const SceVideodec2::C int APS5_VABI sceVideodec2ReleaseComputeQueue_nid_postfix(std::uint64_t queue); int APS5_VABI sceVideodec2QueryDecoderMemoryInfo_nid_postfix(const SceVideodec2::DecoderConfigInfo* config, SceVideodec2::DecoderMemoryInfo* memory); int APS5_VABI sceVideodec2CreateDecoder_nid_postfix(const SceVideodec2::DecoderConfigInfo* config, const SceVideodec2::DecoderMemoryInfo* memory, std::uint64_t* handle); +int APS5_VABI sceVideodec2CreateHevcDecoder(const SceVideodec2::DecoderConfigInfo* config, const SceVideodec2::DecoderMemoryInfo* memory, std::uint64_t* handle); +int APS5_VABI sceVideodec2QueryHevcDecoderMemoryInfo(const SceVideodec2::DecoderConfigInfo* config, SceVideodec2::DecoderMemoryInfo* memory); int APS5_VABI sceVideodec2DeleteDecoder_nid_postfix(std::uint64_t handle); int APS5_VABI sceVideodec2Decode_nid_postfix(std::uint64_t handle, const SceVideodec2::InputData* input, SceVideodec2::FrameBuffer* frame, SceVideodec2::OutputInfo* output); int APS5_VABI sceVideodec2Flush_nid_postfix(std::uint64_t handle, SceVideodec2::FrameBuffer* frame, SceVideodec2::OutputInfo* output); @@ -302,8 +304,6 @@ int APS5_VABI sceVideodec2PresetDecoderConfigInfoAvc(); int APS5_VABI sceVideodec2PresetDecoderConfigInfoHevc(); int APS5_VABI sceVideodec2PresetDecoderConfigInfoVp9(); int APS5_VABI sceVideodec2CreateDecoderBid(); -int APS5_VABI sceVideodec2CreateHevcDecoder(); -int APS5_VABI sceVideodec2QueryHevcDecoderMemoryInfo(); int APS5_VABI sceVideodec2GetHevcPictureInfo_nid_postfix(const SceVideodec2::OutputInfo* output, SceVideodec2::HevcPictureInfo* info); int APS5_VABI sceVideodec2InitLibrary(); diff --git a/core/libs/prx/libSceVoiceChat/Export.cpp b/core/libs/prx/libSceVoiceChat/Export.cpp index cb5daf8bbf..3035923d53 100644 --- a/core/libs/prx/libSceVoiceChat/Export.cpp +++ b/core/libs/prx/libSceVoiceChat/Export.cpp @@ -1,3 +1,4 @@ +#include "prx/libSceVoiceChat/include/InventoryExports.hpp" #include "prx/libSceVoiceChat/include/Api.hpp" #include #include @@ -34,6 +35,15 @@ int offlineRequest(const char* func) { extern "C" { +int APS5_VABI sceVoiceChatRegisterMicEventHandler2() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceVoiceChatUnregisterMicEventHandler2() { + NotImplemented_nid_no_patch(__func__); +} + + int APS5_VABI sceVoiceChatInitialize(void* param) { (void)param; return 0; } int APS5_VABI sceVoiceChatTerminate(void) { return 0; } int APS5_VABI sceVoiceChatRegisterHandlers(void* handlers, void* userArg) { (void)handlers; (void)userArg; return 0; } diff --git a/core/libs/prx/libSceVoiceChat/include/InventoryExports.hpp b/core/libs/prx/libSceVoiceChat/include/InventoryExports.hpp new file mode 100644 index 0000000000..46974b4d6e --- /dev/null +++ b/core/libs/prx/libSceVoiceChat/include/InventoryExports.hpp @@ -0,0 +1,10 @@ +#pragma once + +#include "SceTypes.hpp" + +extern "C" { + +int APS5_VABI sceVoiceChatRegisterMicEventHandler2(); +int APS5_VABI sceVoiceChatUnregisterMicEventHandler2(); + +} diff --git a/core/libs/prx/libSceVoiceChat/libSceVoiceChat.hpp b/core/libs/prx/libSceVoiceChat/libSceVoiceChat.hpp index 81d8245cc3..6e2f16d52e 100644 --- a/core/libs/prx/libSceVoiceChat/libSceVoiceChat.hpp +++ b/core/libs/prx/libSceVoiceChat/libSceVoiceChat.hpp @@ -1,3 +1,4 @@ #pragma once #include "prx/libSceVoiceChat/include/Api.hpp" +#include "prx/libSceVoiceChat/include/InventoryExports.hpp" diff --git a/core/libs/prx/libSceVrTracker2/CMakeLists.txt b/core/libs/prx/libSceVrTracker2/CMakeLists.txt index cccd8e8455..b75b53ea64 100644 --- a/core/libs/prx/libSceVrTracker2/CMakeLists.txt +++ b/core/libs/prx/libSceVrTracker2/CMakeLists.txt @@ -13,4 +13,15 @@ if(BUILD_TESTING) set_tests_properties(guest_vr_tracker2 PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + + add_test_executable(guest_vr_tracker2_unnamed_tests tests/UninitializedNamedAnswers.cpp) + target_include_directories(guest_vr_tracker2_unnamed_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_vr_tracker2_unnamed_tests PRIVATE libSceVrTracker2 libkernel libc) + configure_windows_unwind(guest_vr_tracker2_unnamed_tests) + add_test(NAME guest_vr_tracker2_unnamed COMMAND guest_vr_tracker2_unnamed_tests) + set_tests_properties(guest_vr_tracker2_unnamed PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_vr_tracker2_unnamed PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() diff --git a/core/libs/prx/libSceVrTracker2/tests/UninitializedNamedAnswers.cpp b/core/libs/prx/libSceVrTracker2/tests/UninitializedNamedAnswers.cpp new file mode 100644 index 0000000000..552b1c4c02 --- /dev/null +++ b/core/libs/prx/libSceVrTracker2/tests/UninitializedNamedAnswers.cpp @@ -0,0 +1,67 @@ +#include "prx/libSceVrTracker2/include/Api.hpp" + +#include +#include +#include + +namespace { + +constexpr std::int32_t NotInitialized = static_cast(0x8A740001u); + +constexpr std::uint8_t Sentinel = 0x5a; +constexpr std::size_t ProbeSize = 16 * 1024; + +void Require(bool value, const char* what) { + if (value) return; + std::fprintf(stderr, "FAIL: %s\n", what); + std::abort(); +} + +struct Probe { + unsigned char* bytes; + Probe() : bytes(new unsigned char[ProbeSize]) { + for (std::size_t index = 0; index < ProbeSize; ++index) bytes[index] = Sentinel; + } + ~Probe() { delete[] bytes; } + bool Untouched() const { + for (std::size_t index = 0; index < ProbeSize; ++index) { + if (bytes[index] != Sentinel) return false; + } + return true; + } +}; + +} + +int main() { + Probe output; + Probe parameter; + + Require(sceVrTracker2GetCoordinateSystem(reinterpret_cast(output.bytes)) == NotInitialized, + "get coordinate system before initialization"); + Require(output.Untouched(), "get coordinate system wrote nothing"); + + Require(sceVrTracker2GetPlayAreaBoundaryGeometry(reinterpret_cast(output.bytes)) == NotInitialized, + "get play area boundary geometry before initialization"); + Require(output.Untouched(), "get play area boundary geometry wrote nothing"); + + Require(sceVrTracker2GetPlayAreaOrientedBoundingBox(reinterpret_cast(output.bytes)) == NotInitialized, + "get play area oriented bounding box before initialization"); + Require(output.Untouched(), "get play area oriented bounding box wrote nothing"); + + Require(sceVrTracker2GetResult(reinterpret_cast(parameter.bytes), + reinterpret_cast(output.bytes)) == NotInitialized, + "get result before initialization"); + Require(parameter.Untouched(), "get result wrote nothing to the parameter"); + Require(output.Untouched(), "get result wrote nothing to the result"); + + Require(sceVrTracker2LocateCoordinateSystem(1, reinterpret_cast(output.bytes)) == NotInitialized, + "locate coordinate system before initialization"); + Require(output.Untouched(), "locate coordinate system wrote nothing"); + + Require(sceVrTracker2RegisterDevice(1, 2) == NotInitialized, "register device before initialization"); + Require(sceVrTracker2UnregisterDevice(2) == NotInitialized, "unregister device before initialization"); + Require(sceVrTracker2SetCoordinateSystem(1) == NotInitialized, "set coordinate system before initialization"); + Require(sceVrTracker2ResetLocalCoordinate(2) == NotInitialized, "reset local coordinate before initialization"); + return 0; +} diff --git a/core/libs/prx/libSceXml/Export.cpp b/core/libs/prx/libSceXml/Export.cpp index 9515d73682..d83d703932 100644 --- a/core/libs/prx/libSceXml/Export.cpp +++ b/core/libs/prx/libSceXml/Export.cpp @@ -1,3 +1,4 @@ +#include "prx/libSceXml/include/InventoryExports.hpp" #include "prx/libSceXml/include/Api.hpp" #include #include @@ -653,3 +654,139 @@ int APS5_VABI _ZNK3sce3Xml3Dom8Document10getSiblingENS1_6NodeIdE() { } } + +extern "C" { + +int APS5_VABI _ZNK3sce3Xml3Dom8Document11getNodeTypeENS1_6NodeIdE() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZNK3sce3Xml3Dom8Document12getLastChildENS1_6NodeIdE() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZNK3sce3Xml3Dom8Document7getTextENS1_6NodeIdE() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZNK3sce3Xml3Dom8Document13getChildNodesENS1_6NodeIdEPNS1_8NodeListE() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZNK3sce3Xml3Dom8Document11getNodeNameENS1_6NodeIdE() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZNK3sce3Xml3Dom8Document20getElementsByTagNameENS1_6NodeIdEPKNS0_6StringEPNS1_8NodeListE() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZNK3sce3Xml3Dom8Document13getEntityTypeENS1_6NodeIdE() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZNK3sce3Xml4Attr7getNameEv() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZNK3sce3Xml3Dom8NodeList8findItemENS1_6NodeIdE() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZNK3sce3Xml3Dom8Document13getAttributesENS1_6NodeIdEPNS1_8NodeListE() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZNK3sce3Xml3Dom8Document13getFirstChildENS1_6NodeIdE() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZNK3sce3Xml3Dom8NodeList4itemEj() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZNK3sce3Xml4Attr8getValueEv() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZNK3sce3Xml3Dom8Document11getAttrNameENS1_6NodeIdE() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZNK3sce3Xml3Dom8NodeListixEj() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZNK3sce3Xml3Dom8Document9getStatusEv() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZNK3sce3Xml3Dom8Document14getSkippedTextENS1_6NodeIdE() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZNK3sce3Xml3Dom8NodeList8findItemEPKNS0_6StringE() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZNK3sce3Xml3Dom8Document9getEntityENS1_6NodeIdE() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZNK3sce3Xml3Dom8Document11getNextAttrENS1_6NodeIdE() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZNK3sce3Xml3Dom8Document12getAttrValueENS1_6NodeIdE() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZNK3sce3Xml3Dom8Document12getFirstAttrENS1_6NodeIdE() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZNK3sce3Xml3Dom8NodeList11isAvailableEv() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZNK3sce3Xml3Dom8Document7getRootEv() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZNK3sce3Xml3Dom8Document12getAttributeENS1_6NodeIdEPKNS0_6StringE() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZNK3sce3Xml3Dom8Document10getXmlMetaEv() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZNK3sce3Xml3Dom8Document11isAvailableEv() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZNK3sce3Xml3Dom8Document9getParentENS1_6NodeIdE() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZNK3sce3Xml3Dom8NodeList9getLengthEv() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZNK3sce3Xml3Dom8Document10isReadOnlyEv() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZNK3sce3Xml3Dom8Document13hasChildNodesENS1_6NodeIdE() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZNK3sce3Xml3Dom8Document13hasAttributesENS1_6NodeIdE() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZNK3sce3Xml4Attr11isAvailableEv() { + NotImplemented_nid_no_patch(__func__); +} + +} diff --git a/core/libs/prx/libSceXml/include/InventoryExports.hpp b/core/libs/prx/libSceXml/include/InventoryExports.hpp new file mode 100644 index 0000000000..8cf56b51e7 --- /dev/null +++ b/core/libs/prx/libSceXml/include/InventoryExports.hpp @@ -0,0 +1,41 @@ +#pragma once + +#include "SceTypes.hpp" + +extern "C" { + +int APS5_VABI _ZNK3sce3Xml3Dom8Document11getNodeTypeENS1_6NodeIdE(); +int APS5_VABI _ZNK3sce3Xml3Dom8Document12getLastChildENS1_6NodeIdE(); +int APS5_VABI _ZNK3sce3Xml3Dom8Document7getTextENS1_6NodeIdE(); +int APS5_VABI _ZNK3sce3Xml3Dom8Document13getChildNodesENS1_6NodeIdEPNS1_8NodeListE(); +int APS5_VABI _ZNK3sce3Xml3Dom8Document11getNodeNameENS1_6NodeIdE(); +int APS5_VABI _ZNK3sce3Xml3Dom8Document20getElementsByTagNameENS1_6NodeIdEPKNS0_6StringEPNS1_8NodeListE(); +int APS5_VABI _ZNK3sce3Xml3Dom8Document13getEntityTypeENS1_6NodeIdE(); +int APS5_VABI _ZNK3sce3Xml4Attr7getNameEv(); +int APS5_VABI _ZNK3sce3Xml3Dom8NodeList8findItemENS1_6NodeIdE(); +int APS5_VABI _ZNK3sce3Xml3Dom8Document13getAttributesENS1_6NodeIdEPNS1_8NodeListE(); +int APS5_VABI _ZNK3sce3Xml3Dom8Document13getFirstChildENS1_6NodeIdE(); +int APS5_VABI _ZNK3sce3Xml3Dom8NodeList4itemEj(); +int APS5_VABI _ZNK3sce3Xml4Attr8getValueEv(); +int APS5_VABI _ZNK3sce3Xml3Dom8Document11getAttrNameENS1_6NodeIdE(); +int APS5_VABI _ZNK3sce3Xml3Dom8NodeListixEj(); +int APS5_VABI _ZNK3sce3Xml3Dom8Document9getStatusEv(); +int APS5_VABI _ZNK3sce3Xml3Dom8Document14getSkippedTextENS1_6NodeIdE(); +int APS5_VABI _ZNK3sce3Xml3Dom8NodeList8findItemEPKNS0_6StringE(); +int APS5_VABI _ZNK3sce3Xml3Dom8Document9getEntityENS1_6NodeIdE(); +int APS5_VABI _ZNK3sce3Xml3Dom8Document11getNextAttrENS1_6NodeIdE(); +int APS5_VABI _ZNK3sce3Xml3Dom8Document12getAttrValueENS1_6NodeIdE(); +int APS5_VABI _ZNK3sce3Xml3Dom8Document12getFirstAttrENS1_6NodeIdE(); +int APS5_VABI _ZNK3sce3Xml3Dom8NodeList11isAvailableEv(); +int APS5_VABI _ZNK3sce3Xml3Dom8Document7getRootEv(); +int APS5_VABI _ZNK3sce3Xml3Dom8Document12getAttributeENS1_6NodeIdEPKNS0_6StringE(); +int APS5_VABI _ZNK3sce3Xml3Dom8Document10getXmlMetaEv(); +int APS5_VABI _ZNK3sce3Xml3Dom8Document11isAvailableEv(); +int APS5_VABI _ZNK3sce3Xml3Dom8Document9getParentENS1_6NodeIdE(); +int APS5_VABI _ZNK3sce3Xml3Dom8NodeList9getLengthEv(); +int APS5_VABI _ZNK3sce3Xml3Dom8Document10isReadOnlyEv(); +int APS5_VABI _ZNK3sce3Xml3Dom8Document13hasChildNodesENS1_6NodeIdE(); +int APS5_VABI _ZNK3sce3Xml3Dom8Document13hasAttributesENS1_6NodeIdE(); +int APS5_VABI _ZNK3sce3Xml4Attr11isAvailableEv(); + +} diff --git a/core/libs/prx/libSceXml/libSceXml.hpp b/core/libs/prx/libSceXml/libSceXml.hpp index 02d5a6c3d7..832b8173ac 100644 --- a/core/libs/prx/libSceXml/libSceXml.hpp +++ b/core/libs/prx/libSceXml/libSceXml.hpp @@ -1,3 +1,4 @@ #pragma once #include "prx/libSceXml/include/Api.hpp" +#include "prx/libSceXml/include/InventoryExports.hpp" diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index 7c1fe7b6dd..717cffac77 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -33,6 +33,17 @@ if(BUILD_TESTING) set_tests_properties(guest_stream_purge PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_bsearch_packed_tests ${LIBS_INCLUDE_DIR}/tests/GuestBsearchPacked.cpp) + target_include_directories(guest_bsearch_packed_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_bsearch_packed_tests PRIVATE libc) + configure_windows_unwind(guest_bsearch_packed_tests) + add_test(NAME guest_bsearch_packed COMMAND guest_bsearch_packed_tests) + set_tests_properties(guest_bsearch_packed PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_bsearch_packed PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_safe_error_tests ${LIBS_INCLUDE_DIR}/tests/GuestSafeErrors.cpp) target_include_directories(guest_safe_error_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_safe_error_tests PRIVATE libc) @@ -55,6 +66,19 @@ if(BUILD_TESTING) endif() endif() +if(BUILD_TESTING) + add_test_executable(guest_bounded_search_guards_tests ${LIBS_INCLUDE_DIR}/tests/GuestBoundedSearchGuards.cpp) + target_include_directories(guest_bounded_search_guards_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_bounded_search_guards_tests PRIVATE libc) + configure_windows_unwind(guest_bounded_search_guards_tests) + add_test(NAME guest_bounded_search_guards COMMAND guest_bounded_search_guards_tests) + set_tests_properties(guest_bounded_search_guards PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_bounded_search_guards PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/General.cpp @@ -116,10 +140,12 @@ target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/LocaleSupport.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Process.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Hostname.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/DeviceName.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Environment.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Directory.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Filesystem.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/VersionedFilesystem.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/VersionedFilesystemCalls.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Rename.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Errors.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Syslog.cpp @@ -169,6 +195,16 @@ if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") endif() if(BUILD_TESTING) + add_test_executable(guest_memcmp_protected_tests ${LIBS_INCLUDE_DIR}/tests/GuestMemcmpProtected.cpp) + target_include_directories(guest_memcmp_protected_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_memcmp_protected_tests PRIVATE libc) + configure_windows_unwind(guest_memcmp_protected_tests) + add_test(NAME guest_memcmp_protected COMMAND guest_memcmp_protected_tests) + set_tests_properties(guest_memcmp_protected PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_memcmp_protected PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_update_env_tests ${LIBS_INCLUDE_DIR}/tests/GuestUpdateEnv.cpp) target_include_directories(guest_update_env_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_update_env_tests PRIVATE libc ${CMAKE_DL_LIBS}) @@ -193,6 +229,16 @@ endif() target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/FloatingPoint.cpp) if(BUILD_TESTING) + add_test_executable(guest_memset_extent_tests ${LIBS_INCLUDE_DIR}/tests/GuestMemsetExtent.cpp) + target_include_directories(guest_memset_extent_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_memset_extent_tests PRIVATE libc) + configure_windows_unwind(guest_memset_extent_tests) + add_test(NAME guest_memset_extent COMMAND guest_memset_extent_tests) + set_tests_properties(guest_memset_extent PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_memset_extent PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_raise_except_tests ${LIBS_INCLUDE_DIR}/tests/GuestRaiseExcept.cpp) target_include_directories(guest_raise_except_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_raise_except_tests PRIVATE libc ${CMAKE_DL_LIBS}) @@ -256,6 +302,17 @@ if(BUILD_TESTING) set_tests_properties(guest_sce_v2_filesystem PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + + add_test_executable(guest_sce_v2_filesystem_calls_tests ${LIBS_INCLUDE_DIR}/tests/GuestSceV2FilesystemCalls.cpp) + target_include_directories(guest_sce_v2_filesystem_calls_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_sce_v2_filesystem_calls_tests PRIVATE libc libkernel zarchive) + configure_windows_unwind(guest_sce_v2_filesystem_calls_tests) + add_test(NAME guest_sce_v2_filesystem_calls COMMAND guest_sce_v2_filesystem_calls_tests) + set_tests_properties(guest_sce_v2_filesystem_calls PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_sce_v2_filesystem_calls PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() if(WIN32) @@ -320,6 +377,19 @@ if(BUILD_TESTING) set_tests_properties(guest_memccpy_bounds PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_explicit_memset_tests ${LIBS_INCLUDE_DIR}/tests/GuestExplicitMemset.cpp) + target_include_directories(guest_explicit_memset_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_explicit_memset_tests PRIVATE libc) + configure_windows_unwind(guest_explicit_memset_tests) + foreach(mode empty values ranges guard_pages) + add_test(NAME guest_explicit_memset_${mode} COMMAND guest_explicit_memset_tests ${mode}) + set_tests_properties(guest_explicit_memset_${mode} PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_explicit_memset_${mode} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() + add_test_executable(guest_allocating_strings_tests ${LIBS_INCLUDE_DIR}/tests/GuestAllocatingStrings.cpp) target_include_directories(guest_allocating_strings_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_allocating_strings_tests PRIVATE libc libkernel) @@ -353,6 +423,16 @@ if(BUILD_TESTING) set_tests_properties(guest_array_callbacks PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_strncmp_boundary_tests ${LIBS_INCLUDE_DIR}/tests/GuestStrncmpBoundary.cpp) + target_include_directories(guest_strncmp_boundary_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_strncmp_boundary_tests PRIVATE libc) + configure_windows_unwind(guest_strncmp_boundary_tests) + add_test(NAME guest_strncmp_boundary COMMAND guest_strncmp_boundary_tests) + set_tests_properties(guest_strncmp_boundary PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_strncmp_boundary PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_dynamic_cast_tests ${LIBS_INCLUDE_DIR}/tests/GuestDynamicCast.cpp) target_include_directories(guest_dynamic_cast_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_dynamic_cast_tests PRIVATE libc) @@ -382,6 +462,16 @@ if(BUILD_TESTING) set_tests_properties(guest_mktime PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_strcpy_extent_tests ${LIBS_INCLUDE_DIR}/tests/GuestStrcpyExtent.cpp) + target_include_directories(guest_strcpy_extent_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_strcpy_extent_tests PRIVATE libc) + configure_windows_unwind(guest_strcpy_extent_tests) + add_test(NAME guest_strcpy_extent COMMAND guest_strcpy_extent_tests) + set_tests_properties(guest_strcpy_extent PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_strcpy_extent PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_time_return_tests ${LIBS_INCLUDE_DIR}/tests/GuestTimeReturn.cpp) target_include_directories(guest_time_return_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_time_return_tests PRIVATE libc) @@ -421,6 +511,16 @@ if(BUILD_TESTING) set_tests_properties(guest_scan_count PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_wcsncat_extent_tests ${LIBS_INCLUDE_DIR}/tests/GuestWcsncatExtent.cpp) + target_include_directories(guest_wcsncat_extent_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_wcsncat_extent_tests PRIVATE libc) + configure_windows_unwind(guest_wcsncat_extent_tests) + add_test(NAME guest_wcsncat_extent COMMAND guest_wcsncat_extent_tests) + set_tests_properties(guest_wcsncat_extent PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_wcsncat_extent PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_access_tests ${LIBS_INCLUDE_DIR}/tests/GuestAccess.cpp) target_include_directories(guest_access_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_access_tests PRIVATE libc) @@ -486,6 +586,16 @@ if(BUILD_TESTING) set_tests_properties(guest_multibyte_locale PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_span_protected_tests ${LIBS_INCLUDE_DIR}/tests/GuestSpanProtected.cpp) + target_include_directories(guest_span_protected_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_span_protected_tests PRIVATE libc) + configure_windows_unwind(guest_span_protected_tests) + add_test(NAME guest_span_protected COMMAND guest_span_protected_tests) + set_tests_properties(guest_span_protected PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_span_protected PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_wide_conversion_tests ${LIBS_INCLUDE_DIR}/tests/GuestWideConversion.cpp) target_include_directories(guest_wide_conversion_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_wide_conversion_tests PRIVATE libc) @@ -519,6 +629,16 @@ if(BUILD_TESTING) set_tests_properties(guest_bounds_checking PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_timingsafe_boundary_tests ${LIBS_INCLUDE_DIR}/tests/GuestTimingsafeBoundary.cpp) + target_include_directories(guest_timingsafe_boundary_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_timingsafe_boundary_tests PRIVATE libc) + configure_windows_unwind(guest_timingsafe_boundary_tests) + add_test(NAME guest_timingsafe_boundary COMMAND guest_timingsafe_boundary_tests) + set_tests_properties(guest_timingsafe_boundary PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_timingsafe_boundary PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_zero_runs_tests ${LIBS_INCLUDE_DIR}/tests/GuestZeroRuns.cpp) target_include_directories(guest_zero_runs_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_zero_runs_tests PRIVATE libc) @@ -605,6 +725,16 @@ if(BUILD_TESTING) set_tests_properties(guest_strtod_subnormal PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_strnstr_boundary_tests ${LIBS_INCLUDE_DIR}/tests/GuestStrnstrBoundary.cpp) + target_include_directories(guest_strnstr_boundary_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_strnstr_boundary_tests PRIVATE libc) + configure_windows_unwind(guest_strnstr_boundary_tests) + add_test(NAME guest_strnstr_boundary COMMAND guest_strnstr_boundary_tests) + set_tests_properties(guest_strnstr_boundary PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_strnstr_boundary PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_random_state_tests ${LIBS_INCLUDE_DIR}/tests/GuestRandomState.cpp) target_include_directories(guest_random_state_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_random_state_tests PRIVATE libc) @@ -632,6 +762,16 @@ if(BUILD_TESTING) set_tests_properties(guest_random_device_seed PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_memcpy_protected_tests ${LIBS_INCLUDE_DIR}/tests/GuestMemcpyProtected.cpp) + target_include_directories(guest_memcpy_protected_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_memcpy_protected_tests PRIVATE libc) + configure_windows_unwind(guest_memcpy_protected_tests) + add_test(NAME guest_memcpy_protected COMMAND guest_memcpy_protected_tests) + set_tests_properties(guest_memcpy_protected PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_memcpy_protected PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_count_ones_tests ${LIBS_INCLUDE_DIR}/tests/GuestCountOnes.cpp) target_include_directories(guest_count_ones_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_count_ones_tests PRIVATE libc) @@ -644,6 +784,20 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endforeach() + add_test_executable(guest_c23_bits_tests ${LIBS_INCLUDE_DIR}/tests/GuestC23Bits.cpp) + target_include_directories(guest_c23_bits_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_c23_bits_tests PRIVATE libc) + configure_windows_unwind(guest_c23_bits_tests) + foreach(operation first_leading_zero first_leading_one first_trailing_zero first_trailing_one has_single_bit bit_width bit_floor bit_ceil) + foreach(width uc us ui ul ull) + add_test(NAME guest_c23_bits_${operation}_${width} COMMAND guest_c23_bits_tests ${operation}_${width}) + set_tests_properties(guest_c23_bits_${operation}_${width} PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_c23_bits_${operation}_${width} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() + endforeach() add_test_executable(guest_integer_builtins_tests ${LIBS_INCLUDE_DIR}/tests/GuestIntegerBuiltins.cpp) target_include_directories(guest_integer_builtins_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_integer_builtins_tests PRIVATE libc) @@ -665,6 +819,16 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endforeach() + add_test_executable(guest_strcoll_boundary_tests ${LIBS_INCLUDE_DIR}/tests/GuestStrcollBoundary.cpp) + target_include_directories(guest_strcoll_boundary_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_strcoll_boundary_tests PRIVATE libc) + configure_windows_unwind(guest_strcoll_boundary_tests) + add_test(NAME guest_strcoll_boundary COMMAND guest_strcoll_boundary_tests) + set_tests_properties(guest_strcoll_boundary PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_strcoll_boundary PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_wide_stream_scan_tests ${LIBS_INCLUDE_DIR}/tests/GuestWideStreamScan.cpp) target_include_directories(guest_wide_stream_scan_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_wide_stream_scan_tests PRIVATE libc) @@ -693,6 +857,16 @@ if(BUILD_TESTING) set_tests_properties(guest_execute_once PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_wide_token_boundary_tests ${LIBS_INCLUDE_DIR}/tests/GuestWideTokenBoundary.cpp) + target_include_directories(guest_wide_token_boundary_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_wide_token_boundary_tests PRIVATE libc) + configure_windows_unwind(guest_wide_token_boundary_tests) + add_test(NAME guest_wide_token_boundary COMMAND guest_wide_token_boundary_tests) + set_tests_properties(guest_wide_token_boundary PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_wide_token_boundary PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_std_execute_once_tests ${LIBS_INCLUDE_DIR}/tests/GuestStdExecuteOnce.cpp) target_include_directories(guest_std_execute_once_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_std_execute_once_tests PRIVATE libc) @@ -733,6 +907,15 @@ if(BUILD_TESTING) set_tests_properties(guest_dinkumware_float_bits PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_sce_v2_float_helpers_tests ${LIBS_INCLUDE_DIR}/tests/GuestSceV2FloatHelpers.cpp) + target_include_directories(guest_sce_v2_float_helpers_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_sce_v2_float_helpers_tests PRIVATE libc) + configure_windows_unwind(guest_sce_v2_float_helpers_tests) + add_test(NAME guest_sce_v2_float_helpers COMMAND guest_sce_v2_float_helpers_tests) + if(WIN32) + set_tests_properties(guest_sce_v2_float_helpers PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_bessel_tests ${LIBS_INCLUDE_DIR}/tests/GuestBessel.cpp) target_include_directories(guest_bessel_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_bessel_tests PRIVATE libc) @@ -742,6 +925,16 @@ if(BUILD_TESTING) set_tests_properties(guest_bessel PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_wcslen_boundary_tests ${LIBS_INCLUDE_DIR}/tests/GuestWcslenBoundary.cpp) + target_include_directories(guest_wcslen_boundary_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_wcslen_boundary_tests PRIVATE libc) + configure_windows_unwind(guest_wcslen_boundary_tests) + add_test(NAME guest_wcslen_boundary COMMAND guest_wcslen_boundary_tests) + set_tests_properties(guest_wcslen_boundary PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_wcslen_boundary PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_libc_threads_tests ${LIBS_INCLUDE_DIR}/tests/GuestLibcThreads.cpp) target_include_directories(guest_libc_threads_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_libc_threads_tests PRIVATE libc libkernel) @@ -782,6 +975,16 @@ if(BUILD_TESTING) set_tests_properties(guest_libc_static_imports PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_wcscmp_boundary_tests ${LIBS_INCLUDE_DIR}/tests/GuestWcscmpBoundary.cpp) + target_include_directories(guest_wcscmp_boundary_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_wcscmp_boundary_tests PRIVATE libc) + configure_windows_unwind(guest_wcscmp_boundary_tests) + add_test(NAME guest_wcscmp_boundary COMMAND guest_wcscmp_boundary_tests) + set_tests_properties(guest_wcscmp_boundary PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_wcscmp_boundary PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_fiopen_tests ${LIBS_INCLUDE_DIR}/tests/GuestFiopen.cpp) target_include_directories(guest_fiopen_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_fiopen_tests PRIVATE libc) @@ -811,6 +1014,16 @@ if(BUILD_TESTING) set_tests_properties(guest_realpath PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_strxfrm_boundary_tests ${LIBS_INCLUDE_DIR}/tests/GuestStrxfrmBoundary.cpp) + target_include_directories(guest_strxfrm_boundary_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_strxfrm_boundary_tests PRIVATE libc) + configure_windows_unwind(guest_strxfrm_boundary_tests) + add_test(NAME guest_strxfrm_boundary COMMAND guest_strxfrm_boundary_tests) + set_tests_properties(guest_strxfrm_boundary PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_strxfrm_boundary PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_path_case_tests ${LIBS_INCLUDE_DIR}/tests/GuestPathCase.cpp) target_include_directories(guest_path_case_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_path_case_tests PRIVATE libc libkernel) @@ -839,6 +1052,16 @@ if(BUILD_TESTING) set_tests_properties(guest_directory PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_decimal_boundary_tests ${LIBS_INCLUDE_DIR}/tests/GuestDecimalBoundary.cpp) + target_include_directories(guest_decimal_boundary_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_decimal_boundary_tests PRIVATE libc) + configure_windows_unwind(guest_decimal_boundary_tests) + add_test(NAME guest_decimal_boundary COMMAND guest_decimal_boundary_tests) + set_tests_properties(guest_decimal_boundary PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_decimal_boundary PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_environment_tests ${LIBS_INCLUDE_DIR}/tests/GuestEnvironment.cpp) target_include_directories(guest_environment_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_environment_tests PRIVATE libc) @@ -857,6 +1080,16 @@ if(BUILD_TESTING) set_tests_properties(guest_dinkumware_atomics PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_dinkumware_division_tests ${LIBS_INCLUDE_DIR}/tests/GuestDinkumwareDivision.cpp) + target_include_directories(guest_dinkumware_division_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_dinkumware_division_tests PRIVATE libc) + configure_windows_unwind(guest_dinkumware_division_tests) + add_test(NAME guest_dinkumware_division COMMAND guest_dinkumware_division_tests) + if(MINGW) + set_tests_properties(guest_dinkumware_division 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) @@ -866,6 +1099,16 @@ if(BUILD_TESTING) set_tests_properties(guest_dinkumware_internals PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_device_name_tests ${LIBS_INCLUDE_DIR}/tests/GuestDeviceName.cpp) + target_include_directories(guest_device_name_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_device_name_tests PRIVATE libc) + configure_windows_unwind(guest_device_name_tests) + add_test(NAME guest_device_name COMMAND guest_device_name_tests) + set_tests_properties(guest_device_name PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_device_name 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) @@ -885,6 +1128,20 @@ if(BUILD_TESTING) set_tests_properties(guest_vector_algorithms PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_find_last_trivial_tests ${LIBS_INCLUDE_DIR}/tests/GuestFindLastTrivial.cpp) + target_include_directories(guest_find_last_trivial_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_find_last_trivial_tests PRIVATE libc) + configure_windows_unwind(guest_find_last_trivial_tests) + foreach(width 1 2 4 8) + foreach(mode empty positions values guard_pages) + add_test(NAME guest_find_last_trivial_${width}_${mode} COMMAND guest_find_last_trivial_tests ${width} ${mode}) + set_tests_properties(guest_find_last_trivial_${width}_${mode} PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_find_last_trivial_${width}_${mode} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() + endforeach() add_test_executable(guest_memrchr_tests ${LIBS_INCLUDE_DIR}/tests/GuestMemrchr.cpp) target_include_directories(guest_memrchr_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_memrchr_tests PRIVATE libc) @@ -941,6 +1198,15 @@ if(BUILD_TESTING) set_tests_properties(guest_interfaces PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_sce_v2_binary_scaling_tests ${LIBS_INCLUDE_DIR}/tests/GuestSceV2BinaryScaling.cpp) + target_include_directories(guest_sce_v2_binary_scaling_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_sce_v2_binary_scaling_tests PRIVATE libc) + configure_windows_unwind(guest_sce_v2_binary_scaling_tests) + add_test(NAME guest_sce_v2_binary_scaling COMMAND guest_sce_v2_binary_scaling_tests) + if(WIN32) + set_tests_properties(guest_sce_v2_binary_scaling PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_qsort_tests ${LIBS_INCLUDE_DIR}/tests/GuestQsort.cpp) target_include_directories(guest_qsort_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_qsort_tests PRIVATE libc) @@ -1155,6 +1421,16 @@ if(BUILD_TESTING) set_tests_properties(guest_new_handler_default_heap PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_bzero_extent_tests ${LIBS_INCLUDE_DIR}/tests/GuestBzeroExtent.cpp) + target_include_directories(guest_bzero_extent_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_bzero_extent_tests PRIVATE libc) + configure_windows_unwind(guest_bzero_extent_tests) + add_test(NAME guest_bzero_extent COMMAND guest_bzero_extent_tests) + set_tests_properties(guest_bzero_extent PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_bzero_extent PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_search_tests ${LIBS_INCLUDE_DIR}/tests/GuestSearch.cpp) target_include_directories(guest_search_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_search_tests PRIVATE libc) @@ -1164,6 +1440,16 @@ if(BUILD_TESTING) set_tests_properties(guest_search PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_wmemchr_boundary_tests ${LIBS_INCLUDE_DIR}/tests/GuestWmemchrBoundary.cpp) + target_include_directories(guest_wmemchr_boundary_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_wmemchr_boundary_tests PRIVATE libc) + configure_windows_unwind(guest_wmemchr_boundary_tests) + add_test(NAME guest_wmemchr_boundary COMMAND guest_wmemchr_boundary_tests) + set_tests_properties(guest_wmemchr_boundary PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_wmemchr_boundary PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_demangle_tests ${LIBS_INCLUDE_DIR}/tests/GuestDemangle.cpp) target_include_directories(guest_demangle_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_demangle_tests PRIVATE libc) @@ -1408,6 +1694,16 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_test_executable(guest_strcmp_boundary_tests ${LIBS_INCLUDE_DIR}/tests/GuestStrcmpBoundary.cpp) + target_include_directories(guest_strcmp_boundary_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_strcmp_boundary_tests PRIVATE libc) + configure_windows_unwind(guest_strcmp_boundary_tests) + add_test(NAME guest_strcmp_boundary COMMAND guest_strcmp_boundary_tests) + set_tests_properties(guest_strcmp_boundary PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_strcmp_boundary PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_heap_report_for_game_tests ${LIBS_INCLUDE_DIR}/tests/GuestHeapReportForGame.cpp) target_include_directories(guest_heap_report_for_game_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_heap_report_for_game_tests PRIVATE libc) @@ -1470,6 +1766,18 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(guest_sce_v2_locale_tests EXCLUDE_FROM_ALL ${LIBS_INCLUDE_DIR}/tests/GuestSceV2Locale.cpp) +target_include_directories(guest_sce_v2_locale_tests PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(guest_sce_v2_locale_tests PRIVATE libc) +configure_windows_unwind(guest_sce_v2_locale_tests) +if(BUILD_TESTING) + set_target_properties(guest_sce_v2_locale_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) + add_test(NAME guest_sce_v2_locale COMMAND guest_sce_v2_locale_tests) + if(WIN32) + set_tests_properties(guest_sce_v2_locale PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(guest_libc_mutex_init_with_name_tests EXCLUDE_FROM_ALL ${LIBS_INCLUDE_DIR}/tests/GuestLibcMutexInitWithName.cpp) target_include_directories(guest_libc_mutex_init_with_name_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_libc_mutex_init_with_name_tests PRIVATE libc) diff --git a/core/libs/prx/libc/include/Api.hpp b/core/libs/prx/libc/include/Api.hpp index 72bfca6ee8..6f8d573656 100644 --- a/core/libs/prx/libc/include/Api.hpp +++ b/core/libs/prx/libc/include/Api.hpp @@ -237,6 +237,22 @@ int APS5_VABI _ZNSt8__sce_v210filesystem10_Close_dirEPv_nid_postfix(void* handle bool APS5_VABI _ZNSt8__sce_v210filesystem12_Current_getERA260_c_nid_postfix(char* target); void APS5_VABI _ZNSt8__sce_v210filesystem18_Xfilesystem_errorEPKcNS_10error_codeE_nid_postfix(); void APS5_VABI _ZNSt8__sce_v210filesystem18_Xfilesystem_errorEPKcRKNS0_4pathENS_10error_codeE_nid_postfix(); +int APS5_VABI _ZNSt8__sce_v210filesystem6_LstatEPKcPNS0_5permsEPi_nid_postfix(const char* path, int* perms, int* error); +int APS5_VABI _ZNSt8__sce_v210filesystem6_LstatEPKcPNS0_5permsE_nid_postfix(const char* path, int* perms); +std::uint64_t APS5_VABI _ZNSt8__sce_v210filesystem10_File_sizeEPKcPi_nid_postfix(const char* path, int* error); +std::uint64_t APS5_VABI _ZNSt8__sce_v210filesystem10_File_sizeEPKc_nid_postfix(const char* path); +std::int64_t APS5_VABI _ZNSt8__sce_v210filesystem16_Last_write_timeEPKc_nid_postfix(const char* path); +int APS5_VABI _ZNSt8__sce_v210filesystem16_Last_write_timeEPKcPlS3__nid_postfix(const char* path, std::int64_t* seconds, std::int64_t* nanoseconds); +bool APS5_VABI _ZNSt8__sce_v210filesystem20_Set_last_write_timeEPKcl_nid_postfix(const char* path, std::int64_t microseconds); +int APS5_VABI _ZNSt8__sce_v210filesystem9_Make_dirEPKcS2__nid_postfix(const char* path, const char* existing); +bool APS5_VABI _ZNSt8__sce_v210filesystem11_Remove_dirEPKc_nid_postfix(const char* path); +int APS5_VABI _ZNSt8__sce_v210filesystem7_UnlinkEPKc_nid_postfix(const char* path); +int APS5_VABI _ZNSt8__sce_v210filesystem7_RenameEPKcS2__nid_postfix(const char* from, const char* to); +int APS5_VABI _ZNSt8__sce_v210filesystem7_ResizeEPKcm_nid_postfix(const char* path, std::uint64_t size); +int APS5_VABI _ZNSt8__sce_v210filesystem6_ChmodEPKcNS0_5permsE_nid_postfix(const char* path, int perms); +int APS5_VABI _ZNSt8__sce_v210filesystem10_Copy_fileEPKcS2__nid_postfix(const char* from, const char* to); +int APS5_VABI _ZNSt8__sce_v210filesystem8_StatvfsEPKcRNS0_10space_infoE_nid_postfix(const char* path, std::uint64_t* space); +int APS5_VABI _ZNSt8__sce_v210filesystem13_Current_set2EPKc_nid_postfix(const char* path); char* APS5_VABI getenv_nid_postfix(const char* name); int APS5_VABI setenv_nid_postfix(const char* name, const char* value, int overwrite); int APS5_VABI unsetenv_nid_postfix(const char* name); @@ -421,6 +437,8 @@ char* APS5_VABI getcwd_nid_postfix(char* buffer, std::size_t size); void APS5_VABI sceLibcInternalBacktraceForGame_nid_postfix(const char* heapName); void APS5_VABI sceLibcInternalHeapErrorReportForGame_nid_postfix(void* heap, void* block, std::uint32_t error); int APS5_VABI gethostname_nid_postfix(char* name, std::size_t length); +char* APS5_VABI devname_nid_postfix(std::uint32_t device, std::uint16_t type); +char* APS5_VABI devname_r_nid_postfix(std::uint32_t device, std::uint16_t type, char* buffer, int length); std::int64_t APS5_VABI __ashldi3_nid_postfix(std::int64_t value, int shift); std::int64_t APS5_VABI __ashrdi3_nid_postfix(std::int64_t value, int shift); std::int64_t APS5_VABI __lshrdi3_nid_postfix(std::int64_t value, int shift); @@ -471,6 +489,54 @@ std::uint32_t APS5_VABI stdc_count_ones_us_nid_postfix(std::uint16_t value); std::uint32_t APS5_VABI stdc_count_ones_ui_nid_postfix(std::uint32_t value); std::uint32_t APS5_VABI stdc_count_ones_ul_nid_postfix(std::uint64_t value); std::uint32_t APS5_VABI stdc_count_ones_ull_nid_postfix(std::uint64_t value); + +std::uint32_t APS5_VABI stdc_first_leading_zero_uc_nid_postfix(std::uint8_t value); +std::uint32_t APS5_VABI stdc_first_leading_zero_us_nid_postfix(std::uint16_t value); +std::uint32_t APS5_VABI stdc_first_leading_zero_ui_nid_postfix(std::uint32_t value); +std::uint32_t APS5_VABI stdc_first_leading_zero_ul_nid_postfix(std::uint64_t value); +std::uint32_t APS5_VABI stdc_first_leading_zero_ull_nid_postfix(std::uint64_t value); + +std::uint32_t APS5_VABI stdc_first_leading_one_uc_nid_postfix(std::uint8_t value); +std::uint32_t APS5_VABI stdc_first_leading_one_us_nid_postfix(std::uint16_t value); +std::uint32_t APS5_VABI stdc_first_leading_one_ui_nid_postfix(std::uint32_t value); +std::uint32_t APS5_VABI stdc_first_leading_one_ul_nid_postfix(std::uint64_t value); +std::uint32_t APS5_VABI stdc_first_leading_one_ull_nid_postfix(std::uint64_t value); + +std::uint32_t APS5_VABI stdc_first_trailing_zero_uc_nid_postfix(std::uint8_t value); +std::uint32_t APS5_VABI stdc_first_trailing_zero_us_nid_postfix(std::uint16_t value); +std::uint32_t APS5_VABI stdc_first_trailing_zero_ui_nid_postfix(std::uint32_t value); +std::uint32_t APS5_VABI stdc_first_trailing_zero_ul_nid_postfix(std::uint64_t value); +std::uint32_t APS5_VABI stdc_first_trailing_zero_ull_nid_postfix(std::uint64_t value); + +std::uint32_t APS5_VABI stdc_first_trailing_one_uc_nid_postfix(std::uint8_t value); +std::uint32_t APS5_VABI stdc_first_trailing_one_us_nid_postfix(std::uint16_t value); +std::uint32_t APS5_VABI stdc_first_trailing_one_ui_nid_postfix(std::uint32_t value); +std::uint32_t APS5_VABI stdc_first_trailing_one_ul_nid_postfix(std::uint64_t value); +std::uint32_t APS5_VABI stdc_first_trailing_one_ull_nid_postfix(std::uint64_t value); + +bool APS5_VABI stdc_has_single_bit_uc_nid_postfix(std::uint8_t value); +bool APS5_VABI stdc_has_single_bit_us_nid_postfix(std::uint16_t value); +bool APS5_VABI stdc_has_single_bit_ui_nid_postfix(std::uint32_t value); +bool APS5_VABI stdc_has_single_bit_ul_nid_postfix(std::uint64_t value); +bool APS5_VABI stdc_has_single_bit_ull_nid_postfix(std::uint64_t value); + +std::uint32_t APS5_VABI stdc_bit_width_uc_nid_postfix(std::uint8_t value); +std::uint32_t APS5_VABI stdc_bit_width_us_nid_postfix(std::uint16_t value); +std::uint32_t APS5_VABI stdc_bit_width_ui_nid_postfix(std::uint32_t value); +std::uint32_t APS5_VABI stdc_bit_width_ul_nid_postfix(std::uint64_t value); +std::uint32_t APS5_VABI stdc_bit_width_ull_nid_postfix(std::uint64_t value); + +std::uint8_t APS5_VABI stdc_bit_floor_uc_nid_postfix(std::uint8_t value); +std::uint16_t APS5_VABI stdc_bit_floor_us_nid_postfix(std::uint16_t value); +std::uint32_t APS5_VABI stdc_bit_floor_ui_nid_postfix(std::uint32_t value); +std::uint64_t APS5_VABI stdc_bit_floor_ul_nid_postfix(std::uint64_t value); +std::uint64_t APS5_VABI stdc_bit_floor_ull_nid_postfix(std::uint64_t value); + +std::uint8_t APS5_VABI stdc_bit_ceil_uc_nid_postfix(std::uint8_t value); +std::uint16_t APS5_VABI stdc_bit_ceil_us_nid_postfix(std::uint16_t value); +std::uint32_t APS5_VABI stdc_bit_ceil_ui_nid_postfix(std::uint32_t value); +std::uint64_t APS5_VABI stdc_bit_ceil_ul_nid_postfix(std::uint64_t value); +std::uint64_t APS5_VABI stdc_bit_ceil_ull_nid_postfix(std::uint64_t value); int APS5_VABI __cmpdi2_nid_postfix(std::int64_t a, std::int64_t b); int APS5_VABI __cmpti2_nid_postfix(Int128 a, Int128 b); int APS5_VABI __ucmpdi2_nid_postfix(std::uint64_t a, std::uint64_t b); @@ -552,9 +618,17 @@ const short* APS5_VABI _Getptolower_nid_postfix(); const short* APS5_VABI _Getptoupper_nid_postfix(); mbstate_t* APS5_VABI _Getpmbstate_nid_postfix(); mbstate_t* APS5_VABI _Getpwcstate_nid_postfix(); +GuestLocale::TimeInfo* APS5_VABI _Getptimes_nid_postfix(); std::uint32_t APS5_VABI _Towctrans_nid_postfix(std::uint32_t c, std::uint64_t desc); void APS5_VABI _ZNSt6locale7_LocimpC1ERKS0__nid_postfix(GuestLocale::Implementation* self, const GuestLocale::Implementation* source); void APS5_VABI _ZNSt6locale7_Locimp7_AddfacEPNS_5facetEm_nid_postfix(GuestLocale::Implementation* self, GuestLocale::Facet* facet, std::size_t id); +GuestLocale::Implementation* APS5_VABI _ZNSt8__sce_v26locale5_InitEb_nid_postfix(bool doIncref); +GuestLocale::Implementation* APS5_VABI _ZNSt8__sce_v26locale16_GetgloballocaleEv_nid_postfix(); +void APS5_VABI _ZNSt8__sce_v28ios_base7_AddstdEPS0__nid_postfix(GuestLocale::SceV2IosBase* self); +void APS5_VABI _ZNSt8__sce_v28ios_base14_Ios_base_dtorEPS0__nid_postfix(GuestLocale::SceV2IosBase* self); +void APS5_VABI _ZNSt8__sce_v215_Facet_RegisterEPNS_11_Facet_baseE_nid_postfix(GuestLocale::Facet* facet); +GuestLocale::Implementation* APS5_VABI _ZNSt8__sce_v26locale7_Locimp11_New_LocimpERKS1__nid_postfix(const GuestLocale::Implementation* source); +void APS5_VABI _ZNSt8__sce_v26locale7_Locimp14_Locimp_AddfacEPS1_PNS0_5facetEm_nid_postfix(GuestLocale::Implementation* self, GuestLocale::Facet* facet, std::size_t id); void APS5_VABI _init_env_nid_postfix(); void APS5_VABI init_env_nid_postfix(const InitEnvParams* params); SonyLconv* APS5_VABI localeconv_nid_postfix(void); @@ -804,9 +878,23 @@ short APS5_VABI _LDscale_nid_postfix(long double*, std::int64_t); short APS5_VABI _Dunscale_nid_postfix(short*, double*); short APS5_VABI _FDunscale_nid_postfix(short*, float*); short APS5_VABI _LDunscale_nid_postfix(short*, long double*); +short APS5_VABI _ZNSt8__sce_v27_DscaleEPdl_nid_postfix(double* value, std::int64_t exponent); +short APS5_VABI _ZNSt8__sce_v28_FDscaleEPfl_nid_postfix(float* value, std::int64_t exponent); +short APS5_VABI _ZNSt8__sce_v28_LDscaleEPel_nid_postfix(double* value, std::int64_t exponent); +short APS5_VABI _ZNSt8__sce_v29_DunscaleEPsPd_nid_postfix(short* exponent, double* value); +short APS5_VABI _ZNSt8__sce_v210_FDunscaleEPsPf_nid_postfix(short* exponent, float* value); +short APS5_VABI _ZNSt8__sce_v210_LDunscaleEPsPe_nid_postfix(short* exponent, double* value); short APS5_VABI _Dnorm_nid_postfix(std::uint16_t*); short APS5_VABI _FDnorm_nid_postfix(std::uint16_t*); short APS5_VABI _LDnorm_nid_postfix(std::uint16_t*); +short APS5_VABI _ZNSt8__sce_v26_DtestEPd_nid_postfix(double* value); +short APS5_VABI _ZNSt8__sce_v27_FDtestEPf_nid_postfix(float* value); +short APS5_VABI _ZNSt8__sce_v27_LDtestEPe_nid_postfix(double* value); +short APS5_VABI _ZNSt8__sce_v25_DintEPds_nid_postfix(double* value, short scale); +short APS5_VABI _ZNSt8__sce_v26_FDintEPfs_nid_postfix(float* value, short scale); +short APS5_VABI _ZNSt8__sce_v26_LDintEPes_nid_postfix(double* value, short scale); +short APS5_VABI _ZNSt8__sce_v26_DnormEPNS_5_DvalE_nid_postfix(std::uint16_t* words); +short APS5_VABI _ZNSt8__sce_v27_FDnormEPNS_5_FvalE_nid_postfix(std::uint16_t* words); int APS5_VABI _Fpcomp_nid_postfix(double, double); int APS5_VABI _FFpcomp_nid_postfix(float, float); int APS5_VABI _LFpcomp_nid_postfix(long double, long double); @@ -1014,11 +1102,13 @@ char* APS5_VABI strpbrk_nid_postfix(const char* text, const char* accept); std::size_t APS5_VABI strcspn_nid_postfix(const char* text, const char* reject); std::size_t APS5_VABI strlcat_nid_postfix(char* destination, const char* source, std::size_t capacity); char* APS5_VABI stpcpy_nid_postfix(char* destination, const char* source); +char* APS5_VABI stpncpy_nid_postfix(char* destination, const char* source, std::size_t count); char* APS5_VABI strtok_r_nid_postfix(char* text, const char* delimiters, char** state); char* APS5_VABI strtok_nid_postfix(char* text, const char* delimiters); int APS5_VABI __inet_aton_nid_postfix(const char* text, void* address); std::uint32_t APS5_VABI __inet_addr_nid_postfix(const char* text); void* APS5_VABI memset_nid_postfix(void* s, int c, size_t n); +void* APS5_VABI memset_explicit_nid_postfix(void* destination, int value, size_t count); void APS5_VABI bzero_nid_postfix(void* destination, size_t count); void APS5_VABI bcopy_nid_postfix(const void* source, void* destination, size_t count); void* APS5_VABI memcpy_nid_postfix(void* dest, const void* src, size_t n); @@ -1192,6 +1282,7 @@ GuestTm* APS5_VABI gmtime_s_nid_postfix(const int64_t* timer, GuestTm* result); GuestTm* APS5_VABI gmtime_nid_postfix(const int64_t* timer); int64_t APS5_VABI libc_mktime_nid_postfix(GuestTm* timeptr); int64_t APS5_VABI mktime_nid_postfix(GuestTm* timeptr); +int64_t APS5_VABI timelocal_nid_postfix(GuestTm* timeptr); size_t APS5_VABI libc_strftime_nid_postfix(char* str, size_t count, const char* format, const GuestTm* timeptr); char* APS5_VABI asctime_nid_postfix(const GuestTm* timeptr); char* APS5_VABI ctime_nid_postfix(const int64_t* timer); @@ -1210,6 +1301,10 @@ const void* APS5_VABI __std_find_trivial_1_nid_postfix(const void* first, const const void* APS5_VABI __std_find_trivial_2_nid_postfix(const void* first, const void* last, std::uint16_t value); const void* APS5_VABI __std_find_trivial_4_nid_postfix(const void* first, const void* last, std::uint32_t value); const void* APS5_VABI __std_find_trivial_8_nid_postfix(const void* first, const void* last, std::uint64_t value); +const void* APS5_VABI __std_find_last_trivial_1_nid_postfix(const void* first, const void* last, std::uint8_t value); +const void* APS5_VABI __std_find_last_trivial_2_nid_postfix(const void* first, const void* last, std::uint16_t value); +const void* APS5_VABI __std_find_last_trivial_4_nid_postfix(const void* first, const void* last, std::uint32_t value); +const void* APS5_VABI __std_find_last_trivial_8_nid_postfix(const void* first, const void* last, std::uint64_t value); const void* APS5_VABI __std_find_trivial_unsized_1_nid_postfix(const void* first, std::uint8_t value); const void* APS5_VABI __std_find_trivial_unsized_2_nid_postfix(const void* first, std::uint16_t value); const void* APS5_VABI __std_find_trivial_unsized_4_nid_postfix(const void* first, std::uint32_t value); @@ -1564,6 +1659,8 @@ std::uint64_t APS5_VABI _Stoulx_nid_postfix(const char* text, char** end, int ba std::int64_t APS5_VABI _Stolx_nid_postfix(const char* text, char** end, int base, int* error); std::size_t APS5_VABI _ZNSt7codecvtIcc9_MbstatetE7_GetcatEPPKNSt6locale5facetEPKS2__nid_postfix(GuestLocale::Facet** facet, const GuestLocale::Implementation* const*); std::size_t APS5_VABI _ZNSt8numpunctIcE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(GuestLocale::Facet** facet, const GuestLocale::Implementation* const*); +extern std::uint64_t _ZNSt8numpunctIwE2idE_nid_postfix; +std::size_t APS5_VABI _ZNSt8numpunctIwE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(GuestLocale::Facet** facet, const GuestLocale::Implementation* const*); std::size_t APS5_VABI _ZNSt7num_putIcSt19ostreambuf_iteratorIcSt11char_traitsIcEEE7_GetcatEPPKNSt6locale5facetEPKS5__nid_postfix(GuestLocale::Facet** facet, const GuestLocale::Implementation* const*); std::size_t APS5_VABI _ZNSt8time_putIcSt19ostreambuf_iteratorIcSt11char_traitsIcEEE7_GetcatEPPKNSt6locale5facetEPKS5__nid_postfix(GuestLocale::Facet** facet, const GuestLocale::Implementation* const*); int APS5_VABI siginterrupt_nid_postfix(int guest, int flag); diff --git a/core/libs/prx/libc/include/FileStream.hpp b/core/libs/prx/libc/include/FileStream.hpp index d21b0816d5..a2f9bedde0 100644 --- a/core/libs/prx/libc/include/FileStream.hpp +++ b/core/libs/prx/libc/include/FileStream.hpp @@ -325,6 +325,17 @@ public: #endif } + bool OpenForReading() { +#ifdef _WIN32 + constexpr long UcrtReading = 0x0001; + constexpr long UcrtUpdate = 0x0004; + std::atomic_ref state(reinterpret_cast(GetHandle())->flags); + return (state.load() & (UcrtReading | UcrtUpdate)) != 0; +#else + return true; +#endif + } + bool SwitchToReadingUnlocked() { #ifdef _WIN32 constexpr long UcrtReading = 0x0001; @@ -332,6 +343,12 @@ public: constexpr long UcrtUpdate = 0x0004; auto* native = GetHandle(); std::atomic_ref state(reinterpret_cast(native)->flags); + if ((state.load() & (UcrtReading | UcrtUpdate)) == 0) { + errno = 9; + guestError = true; + SyncUnlockedStatus(); + return false; + } if ((state.load() & (UcrtReading | UcrtUpdate)) != UcrtUpdate) return true; const bool switched = (state.load() & UcrtWriting) == 0 || ::_fflush_nolock(native) == 0; if (switched) state.fetch_or(UcrtReading); diff --git a/core/libs/prx/libc/include/FilesystemForwards.hpp b/core/libs/prx/libc/include/FilesystemForwards.hpp new file mode 100644 index 0000000000..97e5c2e761 --- /dev/null +++ b/core/libs/prx/libc/include/FilesystemForwards.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include "SceTypes.hpp" +#include "general/VabiMacros.hpp" + +#include +#include + +struct FilesystemForwards_nid_no_patch { + int (APS5_VABI* stat)(const char*, FileStat*); + int (APS5_VABI* fstat)(int, FileStat*); + int (APS5_VABI* open)(const char*, int, std::uint16_t); + std::int64_t (APS5_VABI* read)(int, void*, std::size_t); + std::int64_t (APS5_VABI* write)(int, const void*, std::size_t); + int (APS5_VABI* close)(int); + int (APS5_VABI* mkdir)(const char*, std::uint16_t); + int (APS5_VABI* unlink)(const char*); + int (APS5_VABI* rename)(const char*, const char*); + int (APS5_VABI* truncate)(const char*, std::int64_t); + int (APS5_VABI* chmod)(const char*, std::uint16_t); + int (APS5_VABI* fchmod)(int, std::uint16_t); + int (APS5_VABI* utimes)(const char*, const KernelTimeval*); + int (APS5_VABI* posixStat)(const char*, FileStat*); + int (APS5_VABI* posixRmdir)(const char*); +}; + +extern "C" void RegisterFilesystemForwards_nid_no_patch(const FilesystemForwards_nid_no_patch* forwards); diff --git a/core/libs/prx/libc/include/GuestAllocations.hpp b/core/libs/prx/libc/include/GuestAllocations.hpp index 3b5c8a3a79..4ea19487df 100644 --- a/core/libs/prx/libc/include/GuestAllocations.hpp +++ b/core/libs/prx/libc/include/GuestAllocations.hpp @@ -73,6 +73,7 @@ void GuestAllocationsUnmap_nid_postfix(void* mutation, const void* pointer, std: Lease GuestAllocationsAcquire_nid_postfix(); Lease GuestAllocationsAcquireMapped_nid_no_patch(const Mapped& mapped); Lease GuestAllocationsAcquireAll_nid_postfix(); +Lease GuestAllocationsAcquireRegistered_nid_postfix(); std::shared_ptr GuestAllocationsLookup_nid_postfix(std::uintptr_t address, bool next); // Changes whenever a mutation ends, so callers can cache facts about guest mappings between changes. std::uint64_t GuestAllocationsGeneration_nid_postfix(); diff --git a/core/libs/prx/libc/include/GuestArena.hpp b/core/libs/prx/libc/include/GuestArena.hpp index 2e3180e69a..74880ddac0 100644 --- a/core/libs/prx/libc/include/GuestArena.hpp +++ b/core/libs/prx/libc/include/GuestArena.hpp @@ -56,6 +56,7 @@ std::uint64_t GuestArenaCommitGeneration_nid_postfix(); void GuestArenaSetPrivateMappingObserver_nid_postfix(void (*callback)(std::uintptr_t address, std::size_t bytes, std::uint64_t generation)); void GuestArenaMap_nid_postfix(void* pointer, std::size_t bytes, void* section, std::uint64_t offset, std::uint32_t protection); void GuestArenaMapDirect_nid_postfix(void* pointer, std::size_t bytes, void* section, std::uint64_t offset, std::uint32_t protection); +void GuestArenaMapZero_nid_postfix(void* pointer, std::size_t bytes); void GuestArenaMapFile_nid_postfix(void* pointer, std::size_t bytes, void* section, std::uint64_t offset, std::uint32_t protection, std::uint32_t viewProtection, std::size_t sectionBytes); void* GuestArenaMapAlias_nid_postfix(std::uintptr_t address, std::size_t bytes); void GuestArenaUnmapAlias_nid_postfix(void* alias); diff --git a/core/libs/prx/libc/include/GuestLocale.hpp b/core/libs/prx/libc/include/GuestLocale.hpp index 958adc5cac..4991822517 100644 --- a/core/libs/prx/libc/include/GuestLocale.hpp +++ b/core/libs/prx/libc/include/GuestLocale.hpp @@ -34,6 +34,30 @@ struct Facet { std::uint32_t reserved; }; +struct TimeInfo { + const char* amPm; + const char* days; + const char* abbreviatedDays; + const char* fullDays; + const char* months; + const char* abbreviatedMonths; + const char* fullMonths; + const char* formats; + const char* dateTimeFormat; + const char* dateFormat; + const char* timeFormat; + const char* timeFormatAmPm; + const char* eraFormats; + const char* eraDateTimeFormat; + const char* eraDateFormat; + const char* eraTimeFormat; + const char* eraTimeFormatAmPm; + const char* era; + const char* alternativeDigits; + const char* daylightSaving; + const char* timeZone; +}; + struct Implementation { Facet base; Facet** facets; @@ -98,6 +122,24 @@ struct NumpunctVtable { String* (APS5_VABI *truename)(String* result, const NumpunctFacet* self); }; +struct WideNumpunctFacet { + Facet base; + const char* grouping; + std::uint16_t decimalPoint; + std::uint16_t thousandsSep; + const std::uint16_t* falsename; + const std::uint16_t* truename; +}; + +struct WideNumpunctVtable { + FacetVtable facet; + std::uint16_t (APS5_VABI *decimalPoint)(const WideNumpunctFacet* self); + std::uint16_t (APS5_VABI *thousandsSep)(const WideNumpunctFacet* self); + String* (APS5_VABI *grouping)(String* result, const WideNumpunctFacet* self); + String* (APS5_VABI *falsename)(String* result, const WideNumpunctFacet* self); + String* (APS5_VABI *truename)(String* result, const WideNumpunctFacet* self); +}; + struct IosBase { const void* vtable; std::int64_t standardStream; @@ -112,6 +154,20 @@ struct IosBase { Implementation** locale; }; +struct SceV2IosBase { + const void* vtable; + std::int64_t standardStream; + unsigned char reserved[0x20]; + void* streamNodes; + void* callbackNodes; + Implementation** locale; +}; + +static_assert(offsetof(SceV2IosBase, streamNodes) == 0x30); +static_assert(offsetof(SceV2IosBase, callbackNodes) == 0x38); +static_assert(offsetof(SceV2IosBase, locale) == 0x40); +static_assert(sizeof(SceV2IosBase) == 0x48); + struct Streambuf; struct StreambufVtable { @@ -193,6 +249,14 @@ static_assert(offsetof(NumpunctFacet, falsename) == 0x20); static_assert(offsetof(NumpunctFacet, truename) == 0x28); static_assert(offsetof(NumpunctVtable, decimalPoint) == 0x20); static_assert(offsetof(NumpunctVtable, truename) == 0x40); +static_assert(sizeof(WideNumpunctFacet) == 0x30); +static_assert(offsetof(WideNumpunctFacet, grouping) == 0x10); +static_assert(offsetof(WideNumpunctFacet, decimalPoint) == 0x18); +static_assert(offsetof(WideNumpunctFacet, thousandsSep) == 0x1a); +static_assert(offsetof(WideNumpunctFacet, falsename) == 0x20); +static_assert(offsetof(WideNumpunctFacet, truename) == 0x28); +static_assert(offsetof(WideNumpunctVtable, decimalPoint) == 0x20); +static_assert(offsetof(WideNumpunctVtable, truename) == 0x40); static_assert(alignof(LocinfoStorage) == 8); static_assert(sizeof(SceV2Locinfo) == 24); static_assert(offsetof(SceV2Locinfo, name) == 8); diff --git a/core/libs/prx/libc/include/PrimedSharedMutex.hpp b/core/libs/prx/libc/include/PrimedSharedMutex.hpp new file mode 100644 index 0000000000..7f6a9d8e7f --- /dev/null +++ b/core/libs/prx/libc/include/PrimedSharedMutex.hpp @@ -0,0 +1,14 @@ +#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_PRIMEDSHAREDMUTEX_HPP +#define CORE_LIBS_PRX_LIBC_INCLUDE_PRIMEDSHAREDMUTEX_HPP + +#include + +class PrimedSharedMutex : public std::shared_mutex { +public: + PrimedSharedMutex() { + lock(); + unlock(); + } +}; + +#endif diff --git a/core/libs/prx/libc/include/ScanWideField.hpp b/core/libs/prx/libc/include/ScanWideField.hpp index f210ec5d5e..c34330d411 100644 --- a/core/libs/prx/libc/include/ScanWideField.hpp +++ b/core/libs/prx/libc/include/ScanWideField.hpp @@ -1,13 +1,11 @@ #ifndef CORE_LIBS_PRX_LIBC_INCLUDE_SCANWIDEFIELD_HPP #define CORE_LIBS_PRX_LIBC_INCLUDE_SCANWIDEFIELD_HPP -#include #include namespace LibcDetail { -std::size_t ReadWideScanCharacter(const char* text, char32_t* point); -bool InWideScanset(const std::string& specifier, char32_t point); +bool InWideScanset(const std::string& specifier, unsigned char byte); void RequireNoPositionalWideScan(const char* format); } diff --git a/core/libs/prx/libc/include/WindowsMappings.hpp b/core/libs/prx/libc/include/WindowsMappings.hpp index 98d3b80a64..ee8f9b279c 100644 --- a/core/libs/prx/libc/include/WindowsMappings.hpp +++ b/core/libs/prx/libc/include/WindowsMappings.hpp @@ -108,6 +108,17 @@ public: reset(reinterpret_cast(address), bytes); } + void MapZero(void* address, std::size_t bytes) { + std::lock_guard lock(mutex); + if (zeroSection == nullptr && (zeroSection = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_READONLY, 0, static_cast(pageBytes), nullptr)) == nullptr) fail("create the read-only zero guest page"); + const auto start = reinterpret_cast(address); + reset(start, bytes); + for (auto cursor = start; cursor < start + bytes; cursor += pageBytes) { + split(cursor, pageBytes); + if (!map(zeroSection, GetCurrentProcess(), reinterpret_cast(cursor), 0, pageBytes, MEM_REPLACE_PLACEHOLDER, PAGE_READONLY, nullptr, 0)) fail("map the read-only zero guest page"); + } + } + void Map(void* address, std::size_t bytes, HANDLE section, std::uint64_t offset, DWORD protection, bool watched = false, bool resident = false, DWORD viewProtection = PAGE_EXECUTE_READWRITE, std::size_t sectionBytes = SIZE_MAX, std::vector>* createdPrivate = nullptr) { if (sectionBytes < bytes && (resident || sectionBytes == 0 || bytes - sectionBytes >= pageBytes)) throw std::runtime_error("shared guest mapping extends a whole page past its section"); std::lock_guard lock(mutex); @@ -1308,6 +1319,7 @@ private: static inline std::atomic queries{0}; #endif std::mutex mutex; + HANDLE zeroSection = nullptr; AllocateFunction allocate = nullptr; MapFunction map = nullptr; UnmapFunction unmap = nullptr; diff --git a/core/libs/prx/libc/include/WindowsScanning.hpp b/core/libs/prx/libc/include/WindowsScanning.hpp index ab3f6ef625..98909e04e8 100644 --- a/core/libs/prx/libc/include/WindowsScanning.hpp +++ b/core/libs/prx/libc/include/WindowsScanning.hpp @@ -9,6 +9,36 @@ namespace LibcDetail { +inline std::string TitleScanset(const char* begin, const char* end, bool negated) { + bool members[256]{}; + for (const char* member = begin; member != end;) { + const auto first = static_cast(member[0]); + if (end - member > 2 && member[1] == '-') { + const auto last = static_cast(member[2]); + for (unsigned value = first; value <= last; ++value) members[value] = true; + member += 3; + } else { + members[first] = true; + ++member; + } + } + bool empty = true; + for (unsigned value = 1; value < 256; ++value) { + if (negated) members[value] = !members[value]; + if (members[value] && value != '^' && value != '-') empty = false; + } + const bool complement = empty; + std::string text = complement ? "[^" : "["; + const auto listed = [&](unsigned value) { return members[value] != complement; }; + if (listed(']')) text += ']'; + for (unsigned value = 1; value < 256; ++value) { + if (listed(value) && value != ']' && value != '-' && value != '^') text += static_cast(value); + } + if (listed('^')) text += '^'; + if (listed('-')) text += '-'; + return text + ']'; +} + template inline int ScanWindowsArguments_nid_no_patch(const char* format, const void* source, TScanner scan) { if (!format || !source) { errno = 22; return EOF; } @@ -46,8 +76,11 @@ inline int ScanWindowsArguments_nid_no_patch(const char* format, const void* sou if ((length == "h" && *format == 'h') || (length == "l" && *format == 'l')) length += *format++; } const char conversion = *format; - if (!conversion) { errno = 22; return EOF; } - if (!std::strchr("diouxXaAeEfFgGcspn[", conversion)) { + if (!conversion || !std::strchr("diouxXaAeEfFgGcspn[SC", conversion)) { + translated.resize(directive); + break; + } + if (conversion == 'S' || conversion == 'C') { NotImplemented_nid_no_patch("vsscanf format conversion"); return EOF; } @@ -55,15 +88,19 @@ inline int ScanWindowsArguments_nid_no_patch(const char* format, const void* sou const bool narrow = !suppressed && std::strchr("diouxX", conversion) && (length.empty() || length == "h" || length == "hh"); if (narrow || (std::strchr("diouxXn", conversion) && (length == "l" || length == "j" || length == "z" || length == "t"))) translated += "ll"; - else translated += length; - translated += conversion; + else if (!std::strchr("sc[", conversion) || length == "l") translated += length; if (conversion == '[') { - if (*format == '^') translated += *format++; - if (*format == ']') translated += *format++; - while (*format && *format != ']') translated += *format++; - if (*format != ']') { errno = 22; return EOF; } - translated += *format++; - } + const bool negated = *format == '^'; + if (negated) ++format; + const char* begin = format; + if (*format == ']') ++format; + while (*format && *format != ']') ++format; + if (*format != ']') { + translated.resize(directive); + break; + } + translated += TitleScanset(begin, format++, negated); + } else translated += conversion; if (narrow) { const bool isUnsigned = std::strchr("ouxX", conversion) != nullptr; narrowIntegers.push_back({args.Next(), length.empty() ? sizeof(int) : length == "h" ? sizeof(short) : sizeof(char), isUnsigned, 0, 0, -1}); diff --git a/core/libs/prx/libc/include/WindowsVerbatimPath.hpp b/core/libs/prx/libc/include/WindowsVerbatimPath.hpp new file mode 100644 index 0000000000..b9e6e03048 --- /dev/null +++ b/core/libs/prx/libc/include/WindowsVerbatimPath.hpp @@ -0,0 +1,265 @@ +#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_WINDOWSVERBATIMPATH_HPP +#define CORE_LIBS_PRX_LIBC_INCLUDE_WINDOWSVERBATIMPATH_HPP + +#include +#include +#include +#include +#include +#include + +constexpr std::size_t WindowsMaxPathLength = 260; +constexpr std::size_t WindowsMaxDirectoryPathLength = 248; + +template +constexpr CharT ToAsciiLower_nid_no_patch(CharT c) { + return (c >= static_cast('A') && c <= static_cast('Z')) + ? static_cast(c + (static_cast('a') - static_cast('A'))) + : c; +} + +template +constexpr bool IsDosDeviceStem_nid_no_patch(std::basic_string_view stem) { + while (!stem.empty() && (stem.back() == static_cast(' ') || stem.back() == static_cast('.'))) { + stem.remove_suffix(1); + } + if (stem.size() == 3) { + const auto c0 = ToAsciiLower_nid_no_patch(stem[0]); + const auto c1 = ToAsciiLower_nid_no_patch(stem[1]); + const auto c2 = ToAsciiLower_nid_no_patch(stem[2]); + return (c0 == static_cast('c') && c1 == static_cast('o') && c2 == static_cast('n')) || + (c0 == static_cast('p') && c1 == static_cast('r') && c2 == static_cast('n')) || + (c0 == static_cast('a') && c1 == static_cast('u') && c2 == static_cast('x')) || + (c0 == static_cast('n') && c1 == static_cast('u') && c2 == static_cast('l')); + } + if (stem.size() == 4) { + const auto c0 = ToAsciiLower_nid_no_patch(stem[0]); + const auto c1 = ToAsciiLower_nid_no_patch(stem[1]); + const auto c2 = ToAsciiLower_nid_no_patch(stem[2]); + if ((c0 == static_cast('c') && c1 == static_cast('o') && c2 == static_cast('m')) || + (c0 == static_cast('l') && c1 == static_cast('p') && c2 == static_cast('t'))) { + const auto c3 = stem[3]; + if (c3 >= static_cast('1') && c3 <= static_cast('9')) { + return true; + } + using UCharT = std::make_unsigned_t; + const auto u3 = static_cast(c3); + if (u3 == static_cast(0x00B9) || u3 == static_cast(0x00B2) || u3 == static_cast(0x00B3)) { + return true; + } + } + } + if constexpr (sizeof(CharT) == 1) { + if (stem.size() == 5) { + const auto c0 = ToAsciiLower_nid_no_patch(stem[0]); + const auto c1 = ToAsciiLower_nid_no_patch(stem[1]); + const auto c2 = ToAsciiLower_nid_no_patch(stem[2]); + if ((c0 == static_cast('c') && c1 == static_cast('o') && c2 == static_cast('m')) || + (c0 == static_cast('l') && c1 == static_cast('p') && c2 == static_cast('t'))) { + using UCharT = std::make_unsigned_t; + const auto u3 = static_cast(stem[3]); + const auto u4 = static_cast(stem[4]); + if (u3 == static_cast(0xC2) && + (u4 == static_cast(0xB9) || u4 == static_cast(0xB2) || u4 == static_cast(0xB3))) { + return true; + } + } + } + } + if (stem.size() == 6) { + const auto c0 = ToAsciiLower_nid_no_patch(stem[0]); + const auto c1 = ToAsciiLower_nid_no_patch(stem[1]); + const auto c2 = ToAsciiLower_nid_no_patch(stem[2]); + const auto c3 = ToAsciiLower_nid_no_patch(stem[3]); + const auto c4 = ToAsciiLower_nid_no_patch(stem[4]); + const auto c5 = stem[5]; + return c0 == static_cast('c') && c1 == static_cast('o') && c2 == static_cast('n') && + c3 == static_cast('i') && c4 == static_cast('n') && c5 == static_cast('$'); + } + if (stem.size() == 7) { + const auto c0 = ToAsciiLower_nid_no_patch(stem[0]); + const auto c1 = ToAsciiLower_nid_no_patch(stem[1]); + const auto c2 = ToAsciiLower_nid_no_patch(stem[2]); + const auto c3 = ToAsciiLower_nid_no_patch(stem[3]); + const auto c4 = ToAsciiLower_nid_no_patch(stem[4]); + const auto c5 = ToAsciiLower_nid_no_patch(stem[5]); + const auto c6 = stem[6]; + return c0 == static_cast('c') && c1 == static_cast('o') && c2 == static_cast('n') && + c3 == static_cast('o') && c4 == static_cast('u') && c5 == static_cast('t') && + c6 == static_cast('$'); + } + return false; +} + +template +inline bool IsDosDeviceStem_nid_no_patch(const std::basic_string& stem) { + return IsDosDeviceStem_nid_no_patch(std::basic_string_view(stem)); +} + +template +inline bool IsDosDeviceStem_nid_no_patch(const CharT* stem) { + return IsDosDeviceStem_nid_no_patch(std::basic_string_view(stem)); +} + +template +constexpr bool ComponentNeedsVerbatimPrefix_nid_no_patch(std::basic_string_view component) { + if (component.empty()) return false; + if (component.size() == 1 && component[0] == static_cast('.')) return false; + if (component.size() == 2 && component[0] == static_cast('.') && component[1] == static_cast('.')) return false; + if (component.back() == static_cast('.') || component.back() == static_cast(' ')) return true; + const auto dotPos = component.find(static_cast('.')); + const auto stem = (dotPos == std::basic_string_view::npos) ? component : component.substr(0, dotPos); + return IsDosDeviceStem_nid_no_patch(stem); +} + +template +inline bool ComponentNeedsVerbatimPrefix_nid_no_patch(const std::basic_string& component) { + return ComponentNeedsVerbatimPrefix_nid_no_patch(std::basic_string_view(component)); +} + +template +inline bool ComponentNeedsVerbatimPrefix_nid_no_patch(const CharT* component) { + return ComponentNeedsVerbatimPrefix_nid_no_patch(std::basic_string_view(component)); +} + +template +bool PathNeedsVerbatimPrefix_nid_no_patch(std::basic_string_view path) { + const CharT backslash = static_cast('\\'); + const CharT slash = static_cast('/'); + const CharT qmark = static_cast('?'); + const CharT dot = static_cast('.'); + if (path.size() >= 4 && + (path[0] == backslash || path[0] == slash) && + (path[1] == backslash || path[1] == slash) && + (path[2] == qmark || path[2] == dot) && + (path[3] == backslash || path[3] == slash)) { + return false; + } + std::size_t start = 0; + while (start < path.size()) { + while (start < path.size() && (path[start] == slash || path[start] == backslash)) { + ++start; + } + if (start >= path.size()) break; + std::size_t end = start; + while (end < path.size() && path[end] != slash && path[end] != backslash) { + ++end; + } + auto component = path.substr(start, end - start); + if (component.size() >= 2 && component[1] == static_cast(':') && + ((component[0] >= static_cast('a') && component[0] <= static_cast('z')) || + (component[0] >= static_cast('A') && component[0] <= static_cast('Z')))) { + component = component.substr(2); + } + if (ComponentNeedsVerbatimPrefix_nid_no_patch(component)) return true; + start = end; + } + return false; +} + +template +inline bool PathNeedsVerbatimPrefix_nid_no_patch(const std::basic_string& path) { + return PathNeedsVerbatimPrefix_nid_no_patch(std::basic_string_view(path)); +} + +template +inline bool PathNeedsVerbatimPrefix_nid_no_patch(const CharT* path) { + return PathNeedsVerbatimPrefix_nid_no_patch(std::basic_string_view(path)); +} + +inline bool NeedsVerbatimPrefix_nid_no_patch(const std::filesystem::path& path) { + using CharT = std::filesystem::path::value_type; + const auto& native = path.native(); + return PathNeedsVerbatimPrefix_nid_no_patch(std::basic_string_view(native)); +} + +template +inline StringT FormatVerbatimWindowsPath_nid_no_patch(const StringT& normalizedPath) { + using CharT = typename StringT::value_type; + const CharT backslash = static_cast('\\'); + const CharT slash = static_cast('/'); + const CharT qmark = static_cast('?'); + const CharT dot = static_cast('.'); + + if (normalizedPath.size() >= 4 && + (normalizedPath[0] == backslash || normalizedPath[0] == slash) && + (normalizedPath[1] == backslash || normalizedPath[1] == slash) && + (normalizedPath[2] == qmark || normalizedPath[2] == dot) && + (normalizedPath[3] == backslash || normalizedPath[3] == slash)) { + StringT result = normalizedPath; + for (auto& c : result) { + if (c == slash) c = backslash; + } + return result; + } + + StringT clean = normalizedPath; + for (auto& c : clean) { + if (c == slash) c = backslash; + } + + if (clean.size() >= 2 && clean[0] == backslash && clean[1] == backslash) { + if constexpr (std::is_same_v) { + return std::wstring(L"\\\\?\\UNC\\") + clean.substr(2); + } else { + return std::string(R"(\\?\UNC\)") + clean.substr(2); + } + } + + if constexpr (std::is_same_v) { + return std::wstring(L"\\\\?\\") + clean; + } else { + return std::string(R"(\\?\)") + clean; + } +} + +template +inline std::basic_string MakeVerbatimWindowsPathForTest_nid_no_patch( + std::basic_string_view normalizedPath, + std::size_t maxPathLength = WindowsMaxDirectoryPathLength) { + if (normalizedPath.size() < maxPathLength && !PathNeedsVerbatimPrefix_nid_no_patch(normalizedPath)) { + return std::basic_string(normalizedPath); + } + return FormatVerbatimWindowsPath_nid_no_patch(std::basic_string(normalizedPath)); +} + +template +inline std::basic_string MakeVerbatimWindowsPathForTest_nid_no_patch( + const std::basic_string& normalizedPath, + std::size_t maxPathLength = WindowsMaxDirectoryPathLength) { + return MakeVerbatimWindowsPathForTest_nid_no_patch(std::basic_string_view(normalizedPath), maxPathLength); +} + +template +inline std::basic_string MakeVerbatimWindowsPathForTest_nid_no_patch( + const CharT* normalizedPath, + std::size_t maxPathLength = WindowsMaxDirectoryPathLength) { + return MakeVerbatimWindowsPathForTest_nid_no_patch(std::basic_string_view(normalizedPath), maxPathLength); +} + +#ifdef _WIN32 +inline std::filesystem::path NativeVerbatimPath_nid_no_patch( + const std::filesystem::path& path, + std::error_code& error, + std::size_t maxPathLength = WindowsMaxDirectoryPathLength) { + const auto& input = path.native(); + if (input.empty()) return path; + if (input.starts_with(L"\\\\?\\") || input.starts_with(L"\\\\.\\")) return path; + auto absolute = path.is_absolute() ? path : (std::filesystem::current_path(error) / path); + if (error) return {}; + absolute = absolute.lexically_normal().make_preferred(); + const auto& native = absolute.native(); + if (native.size() < maxPathLength && !NeedsVerbatimPrefix_nid_no_patch(absolute)) return absolute; + if (native.starts_with(L"\\\\?\\") || native.starts_with(L"\\\\.\\")) return absolute; + return std::filesystem::path(FormatVerbatimWindowsPath_nid_no_patch(native)); +} +#else +inline std::filesystem::path NativeVerbatimPath_nid_no_patch( + const std::filesystem::path& path, + std::error_code&, + std::size_t = WindowsMaxDirectoryPathLength) { + return path; +} +#endif + +#endif diff --git a/core/libs/prx/libc/src/DeviceName.cpp b/core/libs/prx/libc/src/DeviceName.cpp new file mode 100644 index 0000000000..b9b6fcf032 --- /dev/null +++ b/core/libs/prx/libc/src/DeviceName.cpp @@ -0,0 +1,55 @@ +#include "prx/libc/include/Api.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include + +namespace { +constexpr std::uint32_t NoDevice = 0xFFFFFFFFu; +constexpr std::uint16_t TypeMask = 0170000; +constexpr std::uint16_t CharacterType = 0020000; +constexpr std::uint16_t BlockType = 0060000; +constexpr int ErrorOverflow = 84; +constexpr std::size_t SpecialNameLength = 63; + +void CopyString(char* destination, std::size_t size, const char* text, std::size_t length) { + if (size == 0) return; + const std::size_t count = std::min(length, size - 1); + std::copy_n(text, count, destination); + destination[count] = '\0'; +} +} + +extern "C" char* APS5_VABI devname_r_nid_postfix(std::uint32_t device, std::uint16_t type, char* buffer, int length) { + const auto size = static_cast(length); + const bool character = (type & TypeMask) == CharacterType; + if (device == NoDevice || !(character || (device & TypeMask) == BlockType)) { + CopyString(buffer, size, "#NODEV", 6); + return buffer; + } + char text[4 + 2 + sizeof(device) * 2 + 1]; + char* end = text; + *end++ = '#'; + *end++ = character ? 'C' : 'B'; + *end++ = ':'; + if (device != 0) { + *end++ = '0'; + *end++ = 'x'; + } + int digits = 1; + for (std::uint32_t rest = device >> 4; rest != 0; rest >>= 4) ++digits; + for (int index = digits - 1; index >= 0; --index) *end++ = "0123456789abcdef"[(device >> (index * 4)) & 0xF]; + if (size > static_cast(INT_MAX) + 1) { + *__error_nid_postfix() = ErrorOverflow; + *buffer = '\0'; + return buffer; + } + CopyString(buffer, size, text, static_cast(end - text)); + return buffer; +} + +extern "C" char* APS5_VABI devname_nid_postfix(std::uint32_t device, std::uint16_t type) { + static char buffer[SpecialNameLength + 1]; + return devname_r_nid_postfix(device, type, buffer, sizeof(buffer)); +} diff --git a/core/libs/prx/libc/src/Directory.cpp b/core/libs/prx/libc/src/Directory.cpp index 63848ea167..374de0f86b 100644 --- a/core/libs/prx/libc/src/Directory.cpp +++ b/core/libs/prx/libc/src/Directory.cpp @@ -17,6 +17,7 @@ #include #ifdef _WIN32 #include "prx/libc/include/WindowsDescriptor.hpp" +#include "prx/libc/include/WindowsVerbatimPath.hpp" #include #include #endif @@ -63,12 +64,15 @@ int NativeOpenError(DWORD error) { return WindowsOpenError_nid_no_patch(error); } NativeDirectory* OpenNative(const std::filesystem::path& path) { - const DWORD attributes = GetFileAttributesW(path.c_str()); + std::error_code error; + const auto verbatim = NativeVerbatimPath_nid_no_patch(path, error, WindowsMaxDirectoryPathLength); + const auto& openPath = error ? path : verbatim; + const DWORD attributes = GetFileAttributesW(openPath.c_str()); if (attributes == INVALID_FILE_ATTRIBUTES) { errno = NativeOpenError(GetLastError()); return nullptr; } if ((attributes & FILE_ATTRIBUTE_DIRECTORY) == 0) { errno = ENOTDIR; if ((attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0) { - const HANDLE followed = CreateFileW(path.c_str(), FILE_READ_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + const HANDLE followed = CreateFileW(openPath.c_str(), FILE_READ_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); if (followed != INVALID_HANDLE_VALUE) CloseHandle(followed); else if (GetLastError() == ERROR_CANT_RESOLVE_FILENAME) errno = ELOOP; @@ -76,7 +80,7 @@ NativeDirectory* OpenNative(const std::filesystem::path& path) { return nullptr; } auto native = std::make_unique_for_overwrite(); - native->handle = CreateFileW(path.c_str(), FILE_LIST_DIRECTORY | SYNCHRONIZE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + native->handle = CreateFileW(openPath.c_str(), FILE_LIST_DIRECTORY | SYNCHRONIZE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); if (native->handle == INVALID_HANDLE_VALUE) { errno = NativeOpenError(GetLastError()); return nullptr; } BY_HANDLE_FILE_INFORMATION identity{}; diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index 07c8e9dba8..7a97bedd10 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -23,6 +23,7 @@ #include #include #include "prx/libc/include/WindowsDescriptor.hpp" +#include "prx/libc/include/WindowsVerbatimPath.hpp" #endif #include "prx/libc/include/SonyCrt.hpp" @@ -69,16 +70,10 @@ static std::optional ParseFileMode(const char* mode) { 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; + std::error_code pathError; + const auto resolved = NativeVerbatimPath_nid_no_patch(path, pathError); + if (pathError) { errno = pathError.default_error_condition().value(); return std::nullopt; } + return resolved.native(); } #endif diff --git a/core/libs/prx/libc/src/Filesystem.cpp b/core/libs/prx/libc/src/Filesystem.cpp index b6c54e3d2d..4664d57ff0 100644 --- a/core/libs/prx/libc/src/Filesystem.cpp +++ b/core/libs/prx/libc/src/Filesystem.cpp @@ -8,15 +8,19 @@ #define NOMINMAX #endif #include +#include "prx/libc/include/WindowsVerbatimPath.hpp" #else #include #endif #ifdef _WIN32 static std::filesystem::path FollowedPath(const std::filesystem::path& path) { - const DWORD attributes = GetFileAttributesW(path.c_str()); - if (attributes == INVALID_FILE_ATTRIBUTES || (attributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0) return path; - const HANDLE handle = CreateFileW(path.c_str(), 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, + std::error_code pathError; + const auto verbatim = NativeVerbatimPath_nid_no_patch(path, pathError); + const auto& targetPath = pathError ? path : verbatim; + const DWORD attributes = GetFileAttributesW(targetPath.c_str()); + if (attributes == INVALID_FILE_ATTRIBUTES || (attributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0) return targetPath; + const HANDLE handle = CreateFileW(targetPath.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) return {}; std::wstring target(MAX_PATH, L'\0'); @@ -147,17 +151,20 @@ extern "C" int APS5_VABI remove_nid_postfix(const char* path) { } std::error_code error; #ifdef _WIN32 - const DWORD attributes = GetFileAttributesW(resolved.c_str()); + std::error_code pathError; + const auto verbatim = NativeVerbatimPath_nid_no_patch(resolved, pathError); + const auto& targetPath = pathError ? resolved : verbatim; + const DWORD attributes = GetFileAttributesW(targetPath.c_str()); bool removed = false; if (attributes != INVALID_FILE_ATTRIBUTES) { const DWORD writable = attributes & ~FILE_ATTRIBUTE_READONLY; const bool unlocked = writable != attributes && - SetFileAttributesW(resolved.c_str(), writable ? writable : FILE_ATTRIBUTE_NORMAL); + SetFileAttributesW(targetPath.c_str(), writable ? writable : FILE_ATTRIBUTE_NORMAL); removed = (attributes & FILE_ATTRIBUTE_DIRECTORY) ? - RemoveDirectoryW(resolved.c_str()) != 0 : DeleteFileW(resolved.c_str()) != 0; + RemoveDirectoryW(targetPath.c_str()) != 0 : DeleteFileW(targetPath.c_str()) != 0; if (!removed && unlocked) { const DWORD nativeError = GetLastError(); - SetFileAttributesW(resolved.c_str(), attributes); + SetFileAttributesW(targetPath.c_str(), attributes); SetLastError(nativeError); } } diff --git a/core/libs/prx/libc/src/Formatting.cpp b/core/libs/prx/libc/src/Formatting.cpp index 04b1717642..6d20c4f2fc 100644 --- a/core/libs/prx/libc/src/Formatting.cpp +++ b/core/libs/prx/libc/src/Formatting.cpp @@ -58,6 +58,11 @@ bool RequiresGuestFormatting(const char* format) { std::size_t HostFormatSize(std::size_t size) { return std::min(size, static_cast(INT_MAX) + 1); } +#endif + +bool IsWideScan(const std::string& length, char conversion) { + return conversion == 'S' || conversion == 'C' || (length == "l" && (conversion == 's' || conversion == 'c' || conversion == '[')); +} bool HasGuestWideScan(const char* format) { if (format == nullptr) return false; @@ -65,8 +70,9 @@ bool HasGuestWideScan(const char* format) { ++format; if (*format == '%') { ++format; continue; } while (*format && std::strchr("-+ #0'0123456789.*$", *format)) ++format; - if (*format == 'S' || *format == 'C' || (*format == 'l' && format[1] != '\0' && std::strchr("sc[", format[1]))) - return true; + const char* length = format; + while (*format && std::strchr("hljztL", *format)) ++format; + if (*format && IsWideScan(std::string(length, format), *format)) return true; if (*format == '[') { ++format; if (*format == '^') ++format; @@ -84,14 +90,6 @@ bool ScansWideInProcess(const char* format) { return true; } -void RejectGuestWideScan(const char* format) { - if (HasGuestWideScan(format)) NotImplemented_nid_no_patch("fscanf 16-bit wide conversion on Linux"); -} - -bool IsWideScan(const std::string& length, char conversion) { - return conversion == 'S' || conversion == 'C' || (length == "l" && (conversion == 's' || conversion == 'c' || conversion == '[')); -} - const char* CutAtQuadScanDirective(const char* format, std::string& head) { const char* const quad = LibcDetail::FindQuadScanDirective(format); if (quad == nullptr) return format; @@ -99,12 +97,11 @@ const char* CutAtQuadScanDirective(const char* format, std::string& head) { return head.c_str(); } -#endif - std::string HostLength(const std::string& length, char conversion) { if (std::strchr("sc[", conversion) && length != "l") return {}; #ifdef _WIN32 if (length == "l" && std::strchr("diouxXn", conversion) != nullptr) return "ll"; + if (length != "l" && std::strchr("sc[", conversion) != nullptr) return std::string(); #else (void)conversion; #endif @@ -207,6 +204,7 @@ public: int Peek() const { return *position == '\0' ? EOF : static_cast(*position); } void Skip() { ++position; } + void PutBack(int byte) { if (byte != EOF) --position; } void Advance(std::ptrdiff_t count) { position += count; } std::ptrdiff_t Consumed() const { return position - start; } const char* Remaining() const { return position; } @@ -236,6 +234,12 @@ public: ++consumed; } + void PutBack(int byte) { + if (byte == EOF) return; + if (std::ungetc(byte, file) == EOF) throw std::runtime_error("wide scan character pushback failed"); + --consumed; + } + void Advance(std::ptrdiff_t count) { consumed += count; } std::ptrdiff_t Consumed() const { return consumed; } @@ -271,7 +275,10 @@ int ScanSource(TSource source, const char* format, bool secure, NextPointer next unsigned long width = 0; while (std::isdigit(static_cast(*cursor))) width = width * 10 + static_cast(*cursor++ - '0'); const char* lengthStart = cursor; - while (std::strchr("hljztL", *cursor) != nullptr && *cursor != '\0') ++cursor; + if (*cursor != '\0' && std::strchr("hljztL", *cursor)) { + const char first = *cursor++; + if ((first == 'h' || first == 'l') && *cursor == first) ++cursor; + } const std::string length(lengthStart, cursor); const char conversion = *cursor; if (conversion == '\0') return finish(assigned); @@ -282,7 +289,12 @@ int ScanSource(TSource source, const char* format, bool secure, NextPointer next if (*cursor == ']') ++cursor; while (*cursor != '\0' && *cursor != ']') ++cursor; if (*cursor != ']') return finish(assigned); +#ifdef _WIN32 + const bool negated = setStart[1] == '^'; + specifier = LibcDetail::TitleScanset(setStart + 1 + negated, cursor, negated); +#else specifier.assign(setStart, cursor + 1); +#endif } ++cursor; if (conversion == 'n') { @@ -300,15 +312,13 @@ int ScanSource(TSource source, const char* format, bool secure, NextPointer next } continue; } -#ifndef _WIN32 - if constexpr (TSource::Contiguous) if (IsWideScan(length, conversion)) { - const char* const input = source.Remaining(); - const char kind = conversion == 'S' ? 's' : conversion == 'C' ? 'c' : conversion; - const char* field = input; + if (IsWideScan(length, conversion)) { + if (conversion == 'C') return finish(assigned); + const char kind = conversion == 'S' ? 's' : conversion; if (kind == 's') { - while (std::isspace(static_cast(*field))) ++field; + while (std::isspace(source.Peek())) source.Skip(); } - if (*field == '\0') return finish(converted ? assigned : EOF); + if (source.Peek() == EOF) return finish(converted ? assigned : EOF); auto* target = suppress ? nullptr : static_cast(nextPointer()); unsigned long limit = ULONG_MAX; if (secure && !suppress) { @@ -321,38 +331,54 @@ int ScanSource(TSource source, const char* format, bool secure, NextPointer next } const unsigned long wanted = width != 0 ? width : (kind == 'c' ? 1 : ULONG_MAX); const unsigned long taken = std::min(wanted, limit); - std::u16string text; - const auto accepts = [&](char32_t point) { - if (kind == 's') return point >= 0x80 || !std::isspace(static_cast(point)); - if (kind == '[') return LibcDetail::InWideScanset(specifier, point); + const auto accepts = [&](int byte) { + if (kind == 's') return !std::isspace(byte); + if (kind == '[') return LibcDetail::InWideScanset(specifier, static_cast(byte)); return true; }; - while (text.size() < taken && *field != '\0') { - char32_t point = 0; - const std::size_t size = LibcDetail::ReadWideScanCharacter(field, &point); - if (!accepts(point)) break; - text.push_back(static_cast(point)); - field += size; + std::u16string text; + unsigned char state[16]{}; + char pending[2]{}; + std::size_t held = 0; + for (unsigned long read = 0; read < wanted; ++read) { + const int next = source.Peek(); + if (next == EOF || !accepts(next) || (held == 0 && text.size() == taken)) break; + pending[held] = static_cast(next); + std::uint16_t unit = 0; + const std::size_t result = mbrtowc_nid_postfix(&unit, pending, held + 1, state); + if (result == static_cast(-1)) { + source.Skip(); + source.PutBack(static_cast(pending[held])); + if (held != 0) source.PutBack(static_cast(pending[0])); + if (target != nullptr) std::memcpy(target, text.data(), text.size() * sizeof(char16_t)); + return finish(read == 0 && !converted ? EOF : assigned); + } + source.Skip(); + if (result == static_cast(-2)) { + ++held; + continue; + } + held = 0; + text.push_back(static_cast(unit)); } + if (target != nullptr) std::memcpy(target, text.data(), text.size() * sizeof(char16_t)); + if (held != 0) return finish(converted ? assigned : EOF); if (text.empty()) return finish(assigned); - if (target != nullptr && kind != 'c' && wanted > limit && text.size() == limit && *field != '\0') { - char32_t point = 0; - LibcDetail::ReadWideScanCharacter(field, &point); - if (accepts(point)) { + if (target != nullptr && kind != 'c' && wanted > limit && text.size() == limit) { + const int next = source.Peek(); + if (next != EOF && accepts(next)) { target[0] = u'\0'; return finish(assigned); } } if (target != nullptr) { - std::memcpy(target, text.data(), text.size() * sizeof(char16_t)); if (kind != 'c') target[text.size()] = u'\0'; ++assigned; } - source.Advance(field - input); converted = true; continue; } -#endif + if (!std::strchr("diouxXaAeEfFgGcspn[SC", conversion)) return finish(assigned); if (conversion != 'c' && conversion != '[') { while (std::isspace(source.Peek())) source.Skip(); if (source.Peek() == EOF) return finish(converted ? assigned : EOF); @@ -446,9 +472,7 @@ int ScanGuestStream(FileStream* stream, const char* format, NextPointer nextPoin auto* native = GetNativeStream(stream); stream->Orientation(-1); if (!stream->SwitchToReading()) return EOF; -#ifndef _WIN32 - RejectGuestWideScan(format); -#endif + if (HasGuestWideScan(format)) LibcDetail::RequireNoPositionalWideScan(format); const int result = ScanSource(StreamSource(native), format, true, nextPointer, nextCapacity); stream->SyncStatus(); return result; @@ -545,13 +569,14 @@ int APS5_VABI fscanf_nid_postfix(FileStream* stream, const char* format, ...) { #ifdef _WIN32 __builtin_sysv_va_list args; __builtin_sysv_va_start(args, format); - const int result = LibcDetail::ScanFileWindows_nid_no_patch(native, format, args); + const int result = ScansWideInProcess(format) ? ScanSource(StreamSource(native), format, false, [&] { return __builtin_va_arg(args, void*); }, [] { return 0u; }) + : LibcDetail::ScanFileWindows_nid_no_patch(native, format, args); __builtin_sysv_va_end(args); #else - RejectGuestWideScan(format); std::va_list args; va_start(args, format); - const int result = HostVfscanf(native, format, args); + const int result = ScansWideInProcess(format) ? ScanSource(StreamSource(native), format, false, [&] { return va_arg(args, void*); }, [] { return 0u; }) + : HostVfscanf(native, format, args); va_end(args); #endif stream->SyncStatus(); @@ -638,15 +663,15 @@ int APS5_VABI sprintf_nid_postfix(char* buffer, const char* format, ...) { #endif int APS5_VABI vsscanf_nid_postfix(const char* input, const char* format, VaList* args) { -#ifdef _WIN32 - return LibcDetail::ScanWindows(input, format, args); -#else std::string head; format = CutAtQuadScanDirective(format, head); if (ScansWideInProcess(format)) { LibcDetail::FormatArguments arguments(args); return ScanGuest(input, format, false, [&] { return arguments.Next(); }, [] { return 0u; }); } +#ifdef _WIN32 + return LibcDetail::ScanWindows(input, format, args); +#else std::va_list copy; va_copy(copy, *reinterpret_cast(args)); const int result = HostVsscanf(input, format, copy); @@ -885,15 +910,20 @@ int APS5_VABI vfscanf_nid_postfix(FileStream* stream, const char* format, VaList auto* native = GetNativeStream(stream); stream->Orientation(-1); if (!stream->SwitchToReading()) return EOF; + int result = 0; + if (ScansWideInProcess(format)) { + LibcDetail::FormatArguments arguments(args); + result = ScanSource(StreamSource(native), format, false, [&] { return arguments.Next(); }, [] { return 0u; }); + } else { #ifdef _WIN32 - const int result = LibcDetail::ScanFileWindows_nid_no_patch(native, format, args); + result = LibcDetail::ScanFileWindows_nid_no_patch(native, format, args); #else - RejectGuestWideScan(format); - std::va_list copy; - va_copy(copy, *reinterpret_cast(args)); - const int result = HostVfscanf(native, format, copy); - va_end(copy); + std::va_list copy; + va_copy(copy, *reinterpret_cast(args)); + result = HostVfscanf(native, format, copy); + va_end(copy); #endif + } stream->SyncStatus(); stream->KeepDirectoryReadError(result == -1); return result; diff --git a/core/libs/prx/libc/src/GuestAllocations.cpp b/core/libs/prx/libc/src/GuestAllocations.cpp index d74092d1bf..cb518ad307 100644 --- a/core/libs/prx/libc/src/GuestAllocations.cpp +++ b/core/libs/prx/libc/src/GuestAllocations.cpp @@ -47,6 +47,7 @@ struct Registry { std::shared_ptr readableLease; std::shared_ptr allLease; std::uint64_t backingSerials = 0; + std::vector> retiring; }; Registry& registry() { @@ -70,6 +71,13 @@ void renewBacking(std::uintptr_t address, std::size_t bytes) { } } +bool isRetiring(const Range& range) { + for (const auto& [begin, end] : registry().retiring) { + if (range.address < end && begin < range.address + range.bytes) return true; + } + return false; +} + std::atomic generation{1}; std::atomic invalidator{nullptr}; std::atomic pinWaiter{nullptr}; @@ -433,6 +441,20 @@ void GuestAllocationsRequireUnpinned_nid_postfix(void* mutation, const void* poi auto* state = static_cast(mutation); const auto deadline = std::chrono::steady_clock::now() + pinWait(); int syncedRounds = 0; + bool marked = false; + const auto unmark = [&] { + if (!marked) return; + if (!state->lock.owns_lock()) state->lock.lock(); + auto& retiring = registry().retiring; + retiring.erase(std::find(retiring.begin(), retiring.end(), std::pair{address, end})); + registry().readableLease.reset(); + registry().allLease.reset(); + marked = false; + }; + struct Unmark { + const decltype(unmark)& release; + ~Unmark() { release(); } + } unmarkOnExit{unmark}; for (;;) { registry().readableLease.reset(); registry().allLease.reset(); @@ -448,9 +470,14 @@ void GuestAllocationsRequireUnpinned_nid_postfix(void* mutation, const void* poi } } if (!pinned) { + unmark(); if (state != nullptr && bytes != 0) state->unpinned.emplace_back(address, bytes); return; } + if (!marked && state != nullptr && state->lock.owns_lock()) { + registry().retiring.emplace_back(address, end); + marked = true; + } if (std::chrono::steady_clock::now() >= deadline) PinnedFailure(address, bytes, ("waited " + std::to_string(pinWait().count()) + " ms for the lease (APS5_PIN_WAIT_MS)").c_str()); const auto waiter = pinWaiter.load(std::memory_order_acquire); bool progressed = false; @@ -755,9 +782,9 @@ Lease GuestAllocationsAcquire_nid_postfix() { auto ranges = std::make_shared(); ranges->reserve(registry().ranges.size()); for (const auto& [address, range] : registry().ranges) { - if (range->readable && range->bytes != 0) ranges->push_back(range); + if (range->readable && range->bytes != 0 && !isRetiring(*range)) ranges->push_back(range); } - registry().readableLease = ranges; + if (registry().retiring.empty()) registry().readableLease = ranges; return Lease(std::move(ranges)); } @@ -797,6 +824,19 @@ Lease GuestAllocationsAcquireMapped_nid_no_patch(const Mapped& mapped) { return Lease(std::move(ranges)); } +Lease GuestAllocationsAcquireRegistered_nid_postfix() { + std::lock_guard lock(registry().mutex); + if (registry().retiring.empty()) { + if (auto shared = registry().readableLease) return Lease(std::move(shared)); + } + auto ranges = std::make_shared(); + for (const auto& [address, range] : registry().ranges) { + if (range->readable && range->bytes != 0) ranges->push_back(range); + } + if (registry().retiring.empty()) registry().readableLease = ranges; + return Lease(std::move(ranges)); +} + std::shared_ptr GuestAllocationsLookup_nid_postfix(std::uintptr_t address, bool next) { std::lock_guard lock(registry().mutex); const auto& ranges = registry().ranges; @@ -819,9 +859,9 @@ Lease GuestAllocationsAcquireAll_nid_postfix() { auto ranges = std::make_shared(); ranges->reserve(registry().ranges.size()); for (const auto& [address, range] : registry().ranges) { - if (range->bytes != 0) ranges->push_back(range); + if (range->bytes != 0 && !isRetiring(*range)) ranges->push_back(range); } - registry().allLease = ranges; + if (registry().retiring.empty()) registry().allLease = ranges; return Lease(std::move(ranges)); } diff --git a/core/libs/prx/libc/src/GuestArena.cpp b/core/libs/prx/libc/src/GuestArena.cpp index 8e6fb59547..a1bb54c48e 100644 --- a/core/libs/prx/libc/src/GuestArena.cpp +++ b/core/libs/prx/libc/src/GuestArena.cpp @@ -565,6 +565,14 @@ void GuestArenaMap_nid_postfix(void* pointer, std::size_t bytes, void* section, noteAccess(pointer, bytes); } +void GuestArenaMapZero_nid_postfix(void* pointer, std::size_t bytes) { + if (!Arena::Get().Contains(pointer, bytes)) throw OutsideArena("zero page mapping", pointer, bytes); + const auto generation = commitGeneration.fetch_add(1, std::memory_order_acq_rel) + 1; + WindowsMappings::Get().MapZero(pointer, bytes); + if (const auto callback = privateMappingObserver.load(std::memory_order_acquire)) callback(reinterpret_cast(pointer), bytes, generation); + noteAccess(pointer, bytes); +} + void GuestArenaMapDirect_nid_postfix(void* pointer, std::size_t bytes, void* section, std::uint64_t offset, std::uint32_t protection) { if (!Arena::Get().Contains(pointer, bytes)) throw OutsideArena("direct memory mapping", pointer, bytes); const auto generation = commitGeneration.fetch_add(1, std::memory_order_acq_rel) + 1; diff --git a/core/libs/prx/libc/src/GuestHeap.cpp b/core/libs/prx/libc/src/GuestHeap.cpp index 2c6d1ab563..a584f85f5e 100644 --- a/core/libs/prx/libc/src/GuestHeap.cpp +++ b/core/libs/prx/libc/src/GuestHeap.cpp @@ -384,8 +384,7 @@ void* GuestHeapRealign_nid_postfix(void* pointer, std::size_t bytes, std::size_t const auto block = validatedBlock(pointer, range.bytes); if (bytes == 0) { if (alignment == 0 || (alignment & (alignment - 1)) != 0) throw std::invalid_argument("invalid guest heap alignment"); - if (reinterpret_cast(pointer) % alignment != 0) - throw std::invalid_argument("reallocalign to 0 bytes with an alignment the block does not have is not measured on the console"); + if (reinterpret_cast(pointer) % alignment != 0) return allocate(mutation, alignment, 0, range.gpu); return emptyInPlace(mutation, pointer, range.gpu); } if (alignment != 0 && (alignment & (alignment - 1)) == 0 && reinterpret_cast(pointer) % alignment == 0 && keepsBlockSize(block, pointer, alignment, bytes)) diff --git a/core/libs/prx/libc/src/IntegerBuiltins.cpp b/core/libs/prx/libc/src/IntegerBuiltins.cpp index ae0bdca936..1510b72245 100644 --- a/core/libs/prx/libc/src/IntegerBuiltins.cpp +++ b/core/libs/prx/libc/src/IntegerBuiltins.cpp @@ -1,6 +1,7 @@ #include "prx/libc/include/Api.hpp" #include #include +#include #include #include @@ -167,6 +168,12 @@ int CountTrailingZeros128(Uint128 value) { return low != 0 ? std::countr_zero(low) : 64 + std::countr_zero(High(value)); } +template +TValue CeilBit(TValue value) { + constexpr TValue highest = TValue{1} << (std::numeric_limits::digits - 1); + return value > highest ? TValue{0} : std::bit_ceil(value); +} + template int Compare(TValue a, TValue b) { if (a < b) return 0; @@ -260,6 +267,54 @@ std::uint32_t APS5_VABI stdc_count_ones_ui_nid_postfix(std::uint32_t value) { re std::uint32_t APS5_VABI stdc_count_ones_ul_nid_postfix(std::uint64_t value) { return std::popcount(value); } std::uint32_t APS5_VABI stdc_count_ones_ull_nid_postfix(std::uint64_t value) { return std::popcount(value); } +std::uint32_t APS5_VABI stdc_first_leading_zero_uc_nid_postfix(std::uint8_t value) { return value == std::numeric_limits::max() ? 0 : std::countl_one(value) + 1; } +std::uint32_t APS5_VABI stdc_first_leading_zero_us_nid_postfix(std::uint16_t value) { return value == std::numeric_limits::max() ? 0 : std::countl_one(value) + 1; } +std::uint32_t APS5_VABI stdc_first_leading_zero_ui_nid_postfix(std::uint32_t value) { return value == std::numeric_limits::max() ? 0 : std::countl_one(value) + 1; } +std::uint32_t APS5_VABI stdc_first_leading_zero_ul_nid_postfix(std::uint64_t value) { return value == std::numeric_limits::max() ? 0 : std::countl_one(value) + 1; } +std::uint32_t APS5_VABI stdc_first_leading_zero_ull_nid_postfix(std::uint64_t value) { return value == std::numeric_limits::max() ? 0 : std::countl_one(value) + 1; } + +std::uint32_t APS5_VABI stdc_first_leading_one_uc_nid_postfix(std::uint8_t value) { return value == 0 ? 0 : std::countl_zero(value) + 1; } +std::uint32_t APS5_VABI stdc_first_leading_one_us_nid_postfix(std::uint16_t value) { return value == 0 ? 0 : std::countl_zero(value) + 1; } +std::uint32_t APS5_VABI stdc_first_leading_one_ui_nid_postfix(std::uint32_t value) { return value == 0 ? 0 : std::countl_zero(value) + 1; } +std::uint32_t APS5_VABI stdc_first_leading_one_ul_nid_postfix(std::uint64_t value) { return value == 0 ? 0 : std::countl_zero(value) + 1; } +std::uint32_t APS5_VABI stdc_first_leading_one_ull_nid_postfix(std::uint64_t value) { return value == 0 ? 0 : std::countl_zero(value) + 1; } + +std::uint32_t APS5_VABI stdc_first_trailing_zero_uc_nid_postfix(std::uint8_t value) { return value == std::numeric_limits::max() ? 0 : std::countr_one(value) + 1; } +std::uint32_t APS5_VABI stdc_first_trailing_zero_us_nid_postfix(std::uint16_t value) { return value == std::numeric_limits::max() ? 0 : std::countr_one(value) + 1; } +std::uint32_t APS5_VABI stdc_first_trailing_zero_ui_nid_postfix(std::uint32_t value) { return value == std::numeric_limits::max() ? 0 : std::countr_one(value) + 1; } +std::uint32_t APS5_VABI stdc_first_trailing_zero_ul_nid_postfix(std::uint64_t value) { return value == std::numeric_limits::max() ? 0 : std::countr_one(value) + 1; } +std::uint32_t APS5_VABI stdc_first_trailing_zero_ull_nid_postfix(std::uint64_t value) { return value == std::numeric_limits::max() ? 0 : std::countr_one(value) + 1; } + +std::uint32_t APS5_VABI stdc_first_trailing_one_uc_nid_postfix(std::uint8_t value) { return value == 0 ? 0 : std::countr_zero(value) + 1; } +std::uint32_t APS5_VABI stdc_first_trailing_one_us_nid_postfix(std::uint16_t value) { return value == 0 ? 0 : std::countr_zero(value) + 1; } +std::uint32_t APS5_VABI stdc_first_trailing_one_ui_nid_postfix(std::uint32_t value) { return value == 0 ? 0 : std::countr_zero(value) + 1; } +std::uint32_t APS5_VABI stdc_first_trailing_one_ul_nid_postfix(std::uint64_t value) { return value == 0 ? 0 : std::countr_zero(value) + 1; } +std::uint32_t APS5_VABI stdc_first_trailing_one_ull_nid_postfix(std::uint64_t value) { return value == 0 ? 0 : std::countr_zero(value) + 1; } + +bool APS5_VABI stdc_has_single_bit_uc_nid_postfix(std::uint8_t value) { return std::has_single_bit(value); } +bool APS5_VABI stdc_has_single_bit_us_nid_postfix(std::uint16_t value) { return std::has_single_bit(value); } +bool APS5_VABI stdc_has_single_bit_ui_nid_postfix(std::uint32_t value) { return std::has_single_bit(value); } +bool APS5_VABI stdc_has_single_bit_ul_nid_postfix(std::uint64_t value) { return std::has_single_bit(value); } +bool APS5_VABI stdc_has_single_bit_ull_nid_postfix(std::uint64_t value) { return std::has_single_bit(value); } + +std::uint32_t APS5_VABI stdc_bit_width_uc_nid_postfix(std::uint8_t value) { return std::bit_width(value); } +std::uint32_t APS5_VABI stdc_bit_width_us_nid_postfix(std::uint16_t value) { return std::bit_width(value); } +std::uint32_t APS5_VABI stdc_bit_width_ui_nid_postfix(std::uint32_t value) { return std::bit_width(value); } +std::uint32_t APS5_VABI stdc_bit_width_ul_nid_postfix(std::uint64_t value) { return std::bit_width(value); } +std::uint32_t APS5_VABI stdc_bit_width_ull_nid_postfix(std::uint64_t value) { return std::bit_width(value); } + +std::uint8_t APS5_VABI stdc_bit_floor_uc_nid_postfix(std::uint8_t value) { return std::bit_floor(value); } +std::uint16_t APS5_VABI stdc_bit_floor_us_nid_postfix(std::uint16_t value) { return std::bit_floor(value); } +std::uint32_t APS5_VABI stdc_bit_floor_ui_nid_postfix(std::uint32_t value) { return std::bit_floor(value); } +std::uint64_t APS5_VABI stdc_bit_floor_ul_nid_postfix(std::uint64_t value) { return std::bit_floor(value); } +std::uint64_t APS5_VABI stdc_bit_floor_ull_nid_postfix(std::uint64_t value) { return std::bit_floor(value); } + +std::uint8_t APS5_VABI stdc_bit_ceil_uc_nid_postfix(std::uint8_t value) { return CeilBit(value); } +std::uint16_t APS5_VABI stdc_bit_ceil_us_nid_postfix(std::uint16_t value) { return CeilBit(value); } +std::uint32_t APS5_VABI stdc_bit_ceil_ui_nid_postfix(std::uint32_t value) { return CeilBit(value); } +std::uint64_t APS5_VABI stdc_bit_ceil_ul_nid_postfix(std::uint64_t value) { return CeilBit(value); } +std::uint64_t APS5_VABI stdc_bit_ceil_ull_nid_postfix(std::uint64_t value) { return CeilBit(value); } + int APS5_VABI __cmpdi2_nid_postfix(std::int64_t a, std::int64_t b) { return Compare(a, b); } int APS5_VABI __cmpti2_nid_postfix(Int128 a, Int128 b) { return Compare(a, b); } int APS5_VABI __ucmpdi2_nid_postfix(std::uint64_t a, std::uint64_t b) { return Compare(a, b); } diff --git a/core/libs/prx/libc/src/Interfaces.cpp b/core/libs/prx/libc/src/Interfaces.cpp index 5dd301556e..27dd2cba78 100644 --- a/core/libs/prx/libc/src/Interfaces.cpp +++ b/core/libs/prx/libc/src/Interfaces.cpp @@ -20,12 +20,12 @@ #include #include #include -#include #include #include #endif #include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include #include @@ -118,7 +118,63 @@ std::vector BroadcastAddress(const std::vector& addr return broadcast; } +#if defined(_WIN32) || defined(__linux__) || defined(__APPLE__) +constexpr std::uint8_t guestLinkFamily = 18; +constexpr std::size_t guestLinkNameArea = 8 + 16; +constexpr std::size_t guestLinkMinimum = 54; +constexpr std::uint8_t guestTypeOther = 0x1; +constexpr std::uint8_t guestTypeEthernet = 0x6; +constexpr std::uint8_t guestTypePpp = 0x17; +constexpr std::uint8_t guestTypeLoopback = 0x18; +constexpr std::uint8_t guestLinkStateDown = 1; +constexpr std::uint8_t guestLinkStateUp = 2; +constexpr std::size_t guestInterfaceDataSize = 152; + +template +void Store(std::vector& bytes, std::size_t offset, TValue value) { + std::memcpy(bytes.data() + offset, &value, sizeof(value)); +} + +std::vector GuestLinkAddress(const std::string& name, std::uint16_t index, std::uint8_t type, const std::uint8_t* hardware, std::size_t hardwareLength) { + std::size_t size = std::max(guestLinkMinimum, guestLinkNameArea + hardwareLength); + size = Aligned(size); + if (size > 0xFF) throw std::runtime_error("getifaddrs: link-level address of " + name + " does not fit sockaddr_dl"); + std::vector guest(size, 0); + guest[0] = static_cast(size); + guest[1] = guestLinkFamily; + Store(guest, 2, index); + guest[4] = type; + guest[5] = static_cast(name.size()); + guest[6] = static_cast(hardwareLength); + std::memcpy(guest.data() + 8, name.data(), name.size()); + if (hardwareLength != 0) std::memcpy(guest.data() + 8 + name.size(), hardware, hardwareLength); + return guest; +} +#endif + #ifdef _WIN32 +std::vector GuestInterfaceData(const IP_ADAPTER_ADDRESSES* adapter, std::uint8_t type, std::uint8_t hardwareLength) { + MIB_IF_ROW2 row{}; + row.InterfaceLuid = adapter->Luid; + const auto result = GetIfEntry2(&row); + if (result != NO_ERROR) throw std::runtime_error("getifaddrs: GetIfEntry2 failed with " + std::to_string(result)); + std::vector data(guestInterfaceDataSize, 0); + data[0] = type; + data[2] = hardwareLength; + data[3] = type == guestTypeEthernet ? 14 : 0; + data[4] = adapter->OperStatus == IfOperStatusUp ? guestLinkStateUp : guestLinkStateDown; + Store(data, 6, static_cast(guestInterfaceDataSize)); + Store(data, 8, static_cast(adapter->Mtu)); + Store(data, 16, static_cast(row.TransmitLinkSpeed)); + const std::pair counters[] = { + {row.InUcastPkts + row.InNUcastPkts, 24}, {row.InErrors, 32}, {row.OutUcastPkts + row.OutNUcastPkts, 40}, {row.OutErrors, 48}, + {row.InOctets, 64}, {row.OutOctets, 72}, {row.InNUcastPkts, 80}, {row.OutNUcastPkts, 88}, + {row.InDiscards, 96}, {row.OutDiscards, 104}, {row.InUnknownProtos, 112}, + }; + for (const auto& [value, offset] : counters) Store(data, offset, value); + return data; +} + std::vector PrefixMask(int family, unsigned prefix) { const unsigned bits = family == AF_INET ? 32 : 128; if (prefix > bits) prefix = bits; @@ -158,11 +214,19 @@ int HostEntries(std::vector& entries) { if (result == ERROR_NOT_ENOUGH_MEMORY) { *__error_nid_postfix() = 12; return -1; } if (result != NO_ERROR) throw std::runtime_error("getifaddrs: GetAdaptersAddresses failed with " + std::to_string(result)); for (auto* adapter = reinterpret_cast(storage.data()); adapter; adapter = adapter->Next) { - std::string name; + const std::string name = InterfaceName(adapter); + const auto index = adapter->IfIndex ? adapter->IfIndex : adapter->Ipv6IfIndex; + if (index == 0 || index > 0xFFFF) throw std::runtime_error("getifaddrs: interface index of " + name + " does not fit sockaddr_dl"); + const std::uint8_t type = adapter->IfType == IF_TYPE_ETHERNET_CSMACD ? guestTypeEthernet + : adapter->IfType == IF_TYPE_SOFTWARE_LOOPBACK ? guestTypeLoopback : adapter->IfType == IF_TYPE_PPP ? guestTypePpp : guestTypeOther; + if (adapter->PhysicalAddressLength > sizeof(adapter->PhysicalAddress)) throw std::runtime_error("getifaddrs: invalid hardware address length on " + name); + const std::uint8_t hardwareLength = type == guestTypeLoopback ? 0 : static_cast(adapter->PhysicalAddressLength); + entries.push_back(Entry{name, InterfaceFlags(adapter), + GuestLinkAddress(name, static_cast(index), type, adapter->PhysicalAddress, hardwareLength), {}, {}, + GuestInterfaceData(adapter, type, hardwareLength)}); for (auto* unicast = adapter->FirstUnicastAddress; unicast; unicast = unicast->Next) { const sockaddr* address = unicast->Address.lpSockaddr; if (!address || (address->sa_family != AF_INET && address->sa_family != AF_INET6)) continue; - if (name.empty()) name = InterfaceName(adapter); Entry entry{name, InterfaceFlags(adapter), GuestAddress(address), PrefixMask(address->sa_family, unicast->OnLinkPrefixLength), {}}; if (address->sa_family == AF_INET && (entry.flags & guestBroadcast)) entry.destination = BroadcastAddress(entry.address, entry.netmask); @@ -172,18 +236,6 @@ int HostEntries(std::vector& entries) { return 0; } #else -#if defined(__linux__) || defined(__APPLE__) -constexpr std::uint8_t guestLinkFamily = 18; -constexpr std::size_t guestLinkNameArea = 8 + 16; -constexpr std::size_t guestLinkMinimum = 54; -constexpr std::uint8_t guestTypeOther = 0x1; -constexpr std::uint8_t guestTypeEthernet = 0x6; -constexpr std::uint8_t guestTypePpp = 0x17; -constexpr std::uint8_t guestTypeLoopback = 0x18; -constexpr std::uint8_t guestLinkStateDown = 1; -constexpr std::uint8_t guestLinkStateUp = 2; -constexpr std::size_t guestInterfaceDataSize = 152; -#endif #ifdef __linux__ constexpr unsigned int hostLowerUp = 0x10000; #endif @@ -195,29 +247,6 @@ struct HostInterface { std::vector addresses; }; -#if defined(__linux__) || defined(__APPLE__) -template -void Store(std::vector& bytes, std::size_t offset, TValue value) { - std::memcpy(bytes.data() + offset, &value, sizeof(value)); -} - -std::vector GuestLinkAddress(const std::string& name, std::uint16_t index, std::uint8_t type, const std::uint8_t* hardware, std::size_t hardwareLength) { - std::size_t size = std::max(guestLinkMinimum, guestLinkNameArea + hardwareLength); - size = Aligned(size); - if (size > 0xFF) throw std::runtime_error("getifaddrs: link-level address of " + name + " does not fit sockaddr_dl"); - std::vector guest(size, 0); - guest[0] = static_cast(size); - guest[1] = guestLinkFamily; - Store(guest, 2, index); - guest[4] = type; - guest[5] = static_cast(name.size()); - guest[6] = static_cast(hardwareLength); - std::memcpy(guest.data() + 8, name.data(), name.size()); - if (hardwareLength != 0) std::memcpy(guest.data() + 8 + name.size(), hardware, hardwareLength); - return guest; -} -#endif - #ifdef __linux__ std::optional ReadSysfsCounter(const std::string& name, const char* file) { std::ifstream input("/sys/class/net/" + name + "/" + file); diff --git a/core/libs/prx/libc/src/LocaleSupport.cpp b/core/libs/prx/libc/src/LocaleSupport.cpp index fb8343411e..b852745dc4 100644 --- a/core/libs/prx/libc/src/LocaleSupport.cpp +++ b/core/libs/prx/libc/src/LocaleSupport.cpp @@ -216,6 +216,69 @@ std::int64_t APS5_VABI WideCollateHash(const GuestLocale::CollateFacet*, const c const GuestLocale::CollateVtable g_wideCollateVtable{{DestroyCollate, DeleteCollate, RetainFacet, ReleaseFacet}, WideCollateCompare, WideCollateTransform, WideCollateHash}; +GuestLocale::String* APS5_VABI AssignCString(GuestLocale::String* result, const char* source) { + if (result == nullptr || source == nullptr) throw std::invalid_argument("numpunct string: null argument"); + const std::size_t length = std::strlen(source); + char* data = result->buffer; + std::size_t capacity = GuestStringInlineCapacity; + if (length > GuestStringInlineCapacity) { + capacity = length | GuestStringCapacityMask; + data = static_cast(GuestLocale::AllocateStringStorage(capacity + 1)); + } + if (length != 0) std::memcpy(data, source, length); + data[length] = 0; + if (data != result->buffer) result->pointer = data; + result->size = length; + result->capacity = capacity; + return result; +} + +const char g_wideNumpunctGrouping[] = ""; +const std::uint16_t g_wideNumpunctFalseName[] = {'f', 'a', 'l', 's', 'e', 0}; +const std::uint16_t g_wideNumpunctTrueName[] = {'t', 'r', 'u', 'e', 0}; + +GuestLocale::String* APS5_VABI AssignWideCString(GuestLocale::String* result, const std::uint16_t* source) { + if (result == nullptr || source == nullptr) throw std::invalid_argument("numpunct string: null argument"); + std::size_t length = 0; + while (source[length] != 0) ++length; + const auto bytes = length * GuestWideCharacterSize; + char* data = result->buffer; + std::size_t capacity = GuestWideInlineCapacity; + if (length > GuestWideInlineCapacity) { + capacity = length | GuestWideInlineCapacity; + data = static_cast(GuestLocale::AllocateStringStorage((capacity + 1) * GuestWideCharacterSize)); + } + if (bytes != 0) std::memcpy(data, source, bytes); + data[bytes] = 0; + data[bytes + 1] = 0; + if (data != result->buffer) result->pointer = data; + result->size = length; + result->capacity = capacity; + return result; +} + +std::uint16_t APS5_VABI WideNumpunctDecimalPoint(const GuestLocale::WideNumpunctFacet* self) { + return self->decimalPoint; +} + +std::uint16_t APS5_VABI WideNumpunctThousandsSep(const GuestLocale::WideNumpunctFacet* self) { + return self->thousandsSep; +} + +GuestLocale::String* APS5_VABI WideNumpunctGrouping(GuestLocale::String* result, const GuestLocale::WideNumpunctFacet* self) { + return AssignCString(result, self->grouping); +} + +GuestLocale::String* APS5_VABI WideNumpunctFalsename(GuestLocale::String* result, const GuestLocale::WideNumpunctFacet* self) { + return AssignWideCString(result, self->falsename); +} + +GuestLocale::String* APS5_VABI WideNumpunctTruename(GuestLocale::String* result, const GuestLocale::WideNumpunctFacet* self) { + return AssignWideCString(result, self->truename); +} + +const GuestLocale::WideNumpunctVtable g_wideNumpunctVtable{{DestroyCollate, DeleteCollate, RetainFacet, ReleaseFacet}, WideNumpunctDecimalPoint, WideNumpunctThousandsSep, WideNumpunctGrouping, WideNumpunctFalsename, WideNumpunctTruename}; + bool APS5_VABI CodecvtAlwaysNoconv(const GuestLocale::Facet*) { return true; } @@ -241,23 +304,6 @@ std::size_t APS5_VABI CodecvtLength(GuestLocale::Facet*, std::uint64_t*, const c const GuestLocale::CodecvtVtable g_codecvtVtable{{DestroyCollate, DeleteCollate, RetainFacet, ReleaseFacet}, CodecvtAlwaysNoconv, CodecvtConstantOne, CodecvtConstantOne, CodecvtNoConversion, CodecvtNoConversion, CodecvtUnshift, CodecvtLength}; -GuestLocale::String* APS5_VABI AssignCString(GuestLocale::String* result, const char* source) { - if (result == nullptr || source == nullptr) throw std::invalid_argument("numpunct string: null argument"); - const std::size_t length = std::strlen(source); - char* data = result->buffer; - std::size_t capacity = GuestStringInlineCapacity; - if (length > GuestStringInlineCapacity) { - capacity = length | GuestStringCapacityMask; - data = static_cast(GuestLocale::AllocateStringStorage(capacity + 1)); - } - if (length != 0) std::memcpy(data, source, length); - data[length] = 0; - if (data != result->buffer) result->pointer = data; - result->size = length; - result->capacity = capacity; - return result; -} - char APS5_VABI NumpunctDecimalPoint(const GuestLocale::NumpunctFacet* self) { return self->decimalPoint; } @@ -634,6 +680,7 @@ std::uint64_t _ZNSt8time_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_ std::uint64_t _ZNSt7num_putIwSt19ostreambuf_iteratorIwSt11char_traitsIwEEE2idE_nid_postfix = 0; std::uintptr_t _ZTVSt7num_putIwSt19ostreambuf_iteratorIwSt11char_traitsIwEEE_nid_postfix[14] {}; std::uint64_t _ZNSt8numpunctIcE2idE_nid_postfix = 0; +std::uint64_t _ZNSt8numpunctIwE2idE_nid_postfix = 0; std::uint64_t _ZNSt8time_putIcSt19ostreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix = 0; std::uintptr_t _ZTVSt8time_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE_nid_postfix[13] {}; std::uintptr_t _ZTVSt8time_getIwSt19istreambuf_iteratorIwSt11char_traitsIwEEE_nid_postfix[13] {}; @@ -713,6 +760,118 @@ GuestLocale::Implementation* APS5_VABI _ZNSt6locale16_GetgloballocaleEv_nid_post namespace { +constexpr std::int64_t SceV2StandardStreamSlots = 8; +std::mutex g_sceV2StandardStreamMutex; +GuestLocale::SceV2IosBase* g_sceV2StandardStreams[SceV2StandardStreamSlots + 1] = {}; +unsigned char g_sceV2StandardStreamOpens[SceV2StandardStreamSlots + 1] = {}; + +struct SceV2StreamNode { + SceV2StreamNode* next; + unsigned char reserved[24]; +}; + +struct SceV2CallbackNode { + SceV2CallbackNode* next; + std::int32_t id; + std::uint32_t reserved; + void (APS5_VABI *callback)(int, void*, std::int32_t); +}; + +static_assert(sizeof(SceV2StreamNode) == 0x20); +static_assert(sizeof(SceV2CallbackNode) == 0x18); + +std::vector g_sceV2RegisteredFacets; + +} + +std::uint64_t _ZNSt8__sce_v25ctypeIcE2idE_nid_postfix = 0; +std::uint64_t _ZNSt8__sce_v25ctypeIwE2idE_nid_postfix = 0; +std::uint64_t _ZNSt8__sce_v28numpunctIcE2idE_nid_postfix = 0; +std::uint64_t _ZNSt8__sce_v27num_putIcNS_19ostreambuf_iteratorIcNS_11char_traitsIcEEEEE2idE_nid_postfix = 0; +std::uint64_t _ZNSt8__sce_v27codecvtIwc9_MbstatetE2idE_nid_postfix = 0; +std::int32_t _ZNSt8__sce_v26locale2id7_Id_cntE_nid_postfix = 0; +GuestLocale::Implementation* _ZNSt8__sce_v221_sceLibcClassicLocaleE_nid_postfix = &g_classicLocale; +alignas(16) unsigned char _ZNSt8__sce_v24coutE_nid_postfix[0x400] {}; +alignas(16) unsigned char _ZNSt8__sce_v24cerrE_nid_postfix[0x400] {}; +alignas(16) unsigned char _ZNSt8__sce_v25wcoutE_nid_postfix[0x400] {}; +alignas(16) unsigned char _ZNSt8__sce_v25wcerrE_nid_postfix[0x400] {}; +alignas(16) unsigned char _ZNSt8__sce_v23cinE_nid_postfix[0x400] {}; +alignas(16) unsigned char _ZNSt8__sce_v24wcinE_nid_postfix[0x400] {}; + +GuestLocale::Implementation* APS5_VABI _ZNSt8__sce_v26locale5_InitEb_nid_postfix(bool doIncref) { + std::lock_guard lock(g_localeInitMutex); + if (doIncref) RetainLocale(&g_classicLocale.base); + return &g_classicLocale; +} + +GuestLocale::Implementation* APS5_VABI _ZNSt8__sce_v26locale16_GetgloballocaleEv_nid_postfix() { + return _ZNSt8__sce_v221_sceLibcClassicLocaleE_nid_postfix; +} + +void APS5_VABI _ZNSt8__sce_v28ios_base7_AddstdEPS0__nid_postfix(GuestLocale::SceV2IosBase* self) { + if (self == nullptr) throw std::invalid_argument("ios_base _Addstd: null stream"); + std::lock_guard lock(g_sceV2StandardStreamMutex); + for (self->standardStream = 1; self->standardStream < SceV2StandardStreamSlots; ++self->standardStream) { + if (g_sceV2StandardStreams[self->standardStream] == nullptr || g_sceV2StandardStreams[self->standardStream] == self) break; + } + g_sceV2StandardStreams[self->standardStream] = self; + ++g_sceV2StandardStreamOpens[self->standardStream]; +} + +void APS5_VABI _ZNSt8__sce_v28ios_base14_Ios_base_dtorEPS0__nid_postfix(GuestLocale::SceV2IosBase* self) { + if (self == nullptr) throw std::invalid_argument("ios_base destructor: null object"); + { + std::lock_guard lock(g_sceV2StandardStreamMutex); + if (self->standardStream > 0) { + if (self->standardStream > SceV2StandardStreamSlots) throw std::runtime_error("ios_base destructor: unknown standard stream slot"); + if (0 < --g_sceV2StandardStreamOpens[self->standardStream]) return; + } + } + for (auto* node = static_cast(self->callbackNodes); node != nullptr; node = node->next) { + node->callback(0, self, node->id); + } + for (auto* node = static_cast(self->streamNodes); node != nullptr;) { + auto* next = node->next; + _ZdlPv_nid_postfix(node); + node = next; + } + self->streamNodes = nullptr; + for (auto* node = static_cast(self->callbackNodes); node != nullptr;) { + auto* next = node->next; + _ZdlPv_nid_postfix(node); + node = next; + } + self->callbackNodes = nullptr; + auto** locale = self->locale; + if (locale != nullptr && locale != &_ZNSt8__sce_v221_sceLibcClassicLocaleE_nid_postfix) { + GuestLocale::Implementation* implementation = *locale; + if (implementation != nullptr) { + const GuestLocale::FacetVtable* vtable = implementation->base.vtable; + GuestLocale::Facet* released = vtable != nullptr ? vtable->release(&implementation->base) : nullptr; + if (released != nullptr && released->vtable != nullptr) released->vtable->deleteObject(released); + } + } + self->locale = nullptr; +} + +void APS5_VABI _ZNSt8__sce_v215_Facet_RegisterEPNS_11_Facet_baseE_nid_postfix(GuestLocale::Facet* facet) { + std::lock_guard lock(g_localeInitMutex); + g_sceV2RegisteredFacets.push_back(facet); +} + +GuestLocale::Implementation* APS5_VABI _ZNSt8__sce_v26locale7_Locimp11_New_LocimpERKS1__nid_postfix(const GuestLocale::Implementation* source) { + if (source == nullptr) throw std::invalid_argument("_Locimp constructor: null source"); + auto* self = static_cast(_Znwm_nid_postfix(sizeof(GuestLocale::Implementation))); + _ZNSt6locale7_LocimpC1ERKS0__nid_postfix(self, source); + return self; +} + +void APS5_VABI _ZNSt8__sce_v26locale7_Locimp14_Locimp_AddfacEPS1_PNS0_5facetEm_nid_postfix(GuestLocale::Implementation* self, GuestLocale::Facet* facet, std::size_t id) { + _ZNSt6locale7_Locimp7_AddfacEPNS_5facetEm_nid_postfix(self, facet, id); +} + +namespace { + GuestLocaleFacade g_classicLocaleFacade{&g_classicLocale}; } @@ -758,6 +917,15 @@ std::size_t APS5_VABI _ZNSt8numpunctIcE7_GetcatEPPKNSt6locale5facetEPKS1__nid_po return 4; } +std::size_t APS5_VABI _ZNSt8numpunctIwE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(GuestLocale::Facet** facet, const GuestLocale::Implementation* const*) { + if (facet != nullptr && *facet == nullptr) { + auto* punct = static_cast(_Znwm_nid_postfix(sizeof(GuestLocale::WideNumpunctFacet))); + *punct = {{&g_wideNumpunctVtable.facet, 0, 0}, g_wideNumpunctGrouping, '.', ',', g_wideNumpunctFalseName, g_wideNumpunctTrueName}; + *facet = &punct->base; + } + return 4; +} + std::size_t APS5_VABI _ZNSt7num_putIcSt19ostreambuf_iteratorIcSt11char_traitsIcEEE7_GetcatEPPKNSt6locale5facetEPKS5__nid_postfix(GuestLocale::Facet** facet, const GuestLocale::Implementation* const*) { if (facet != nullptr && *facet == nullptr) { auto* object = static_cast(_Znwm_nid_postfix(sizeof(GuestLocale::Facet))); @@ -840,6 +1008,21 @@ mbstate_t* APS5_VABI _Getpwcstate_nid_postfix() { return &state; } +GuestLocale::TimeInfo* APS5_VABI _Getptimes_nid_postfix() { + static_assert(sizeof(GuestLocale::TimeInfo) == 0xa8); + static constexpr char days[] = ":Sun:Sunday:Mon:Monday:Tue:Tuesday:Wed:Wednesday:Thu:Thursday:Fri:Friday:Sat:Saturday"; + static constexpr char months[] = ":Jan:January:Feb:February:Mar:March:Apr:April:May:May:Jun:June:Jul:July:Aug:August" + ":Sep:September:Oct:October:Nov:November:Dec:December"; + static constexpr char formats[] = "|%a %b %e %T %Y|%m/%d/%y|%H:%M:%S|%I:%M:%S %p"; + static constexpr char era[] = ""; + static constexpr char alternativeDigits[] = ""; + static constexpr char daylightSaving[] = ""; + static constexpr char timeZone[] = ""; + static GuestLocale::TimeInfo times {":AM:PM", days, days, days, months, months, months, formats, formats, formats, formats, + formats, formats, formats, formats, formats, formats, era, alternativeDigits, daylightSaving, timeZone}; + return × +} + std::uint32_t APS5_VABI _Towctrans_nid_postfix(std::uint32_t c, std::uint64_t desc) { constexpr std::uint64_t ToLower = 1; constexpr std::uint64_t ToUpper = 2; diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index 5d6d432cc7..4118c24566 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -58,7 +58,7 @@ * * Developed at SunSoft, a Sun Microsystems, Inc. business. * Permission to use, copy, modify, and distribute this - * software is freely granted, provided that this notice + * software is freely granted, provided that this notice * is preserved. * ==================================================== */ @@ -681,10 +681,8 @@ float APS5_VABI copysignf_nid_postfix(float x, float y) { int APS5_VABI abs_nid_postfix(int x) { return x < 0 ? static_cast(0u - static_cast(x)) : x; } double APS5_VABI trunc_nid_postfix(double x) { return std::isnan(x) ? x + x : std::trunc(x); } double APS5_VABI ceil_nid_postfix(double x) { return std::isnan(x) ? x + x : std::ceil(x); } -double APS5_VABI floor_nid_postfix(double x) { return std::isnan(x) ? x + x : std::floor(x); } float APS5_VABI truncf_nid_postfix(float x) { return std::isnan(x) ? x + x : std::trunc(x); } float APS5_VABI ceilf_nid_postfix(float x) { return std::isnan(x) ? x + x : std::ceil(x); } -float APS5_VABI floorf_nid_postfix(float x) { return std::isnan(x) ? x + x : std::floor(x); } double APS5_VABI fma_nid_postfix(double x, double y, double z) { return std::fma(x, y, z); } float APS5_VABI fmaf_nid_postfix(float x, float y, float z) { return std::fma(x, y, z); } double APS5_VABI fmin_nid_postfix(double x, double y) { return GuestFmin(x, y); } @@ -757,7 +755,84 @@ long double APS5_VABI frexpl_nid_postfix(long double x, int* exponent) { if (!std::isfinite(x)) return x; return std::frexp(x, exponent); } -double APS5_VABI exp_nid_postfix(double x) { return std::exp(x); } +/* + * ==================================================== + * Copyright (C) 2004 by Sun Microsystems, Inc. All rights reserved. + * + * Permission to use, copy, modify, and distribute this + * software is freely granted, provided that this notice + * is preserved. + * ==================================================== + */ +/* Adapted from FreeBSD release/11.4.0 lib/msun/src/e_exp.c: https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/e_exp.c */ +double APS5_VABI exp_nid_postfix(double x) { + static constexpr double halF[2] = {0.5, -0.5}; + static constexpr double ln2HI[2] = {6.93147180369123816490e-01, -6.93147180369123816490e-01}; + static constexpr double ln2LO[2] = {1.90821492927058770002e-10, -1.90821492927058770002e-10}; + static volatile double huge = 1.0e+300; + static volatile double twom1000 = 9.33263618503218878990e-302; + const double invln2 = 1.44269504088896338700e+00; + const double P1 = 1.66666666666666019037e-01; + const double P2 = -2.77777777770155933842e-03; + const double P3 = 6.61375632143793436117e-05; + const double P4 = -1.65339022054652515390e-06; + const double P5 = 4.13813679705723846039e-08; + double hi = 0.0; + double lo = 0.0; + std::int32_t k = 0; + const std::uint64_t bits = std::bit_cast(x); + std::uint32_t hx = static_cast(bits >> 32); + const std::int32_t xsb = (hx >> 31) & 1; + hx &= 0x7fffffff; + if (hx >= 0x40862e42) { + if (hx >= 0x7ff00000) { + if (((hx & 0xfffff) | static_cast(bits)) != 0) { + return x + x; + } + return xsb == 0 ? x : 0.0; + } + if (x > 7.09782712893383973096e+02) { + errno = ERANGE; + return huge * huge; + } + if (x < -7.45133219101941108420e+02) { + errno = ERANGE; + return twom1000 * twom1000; + } + } + if (x == 1.0) { + return std::bit_cast(std::uint64_t{0x4005bf0a8b145769}); + } + if (hx > 0x3fd62e42) { + if (hx < 0x3ff0a2b2) { + hi = x - ln2HI[xsb]; + lo = ln2LO[xsb]; + k = 1 - xsb - xsb; + } else { + k = static_cast(invln2 * x + halF[xsb]); + const double t = k; + hi = x - t * ln2HI[0]; + lo = t * ln2LO[0]; + } + x = hi - lo; + } else if (hx < 0x3e300000) { + return 1.0 + x; + } + const double t = x * x; + const double twopk = std::bit_cast(static_cast(0x3ff + (k >= -1021 ? k : k + 1000)) << 52); + const double c = x - t * (P1 + t * (P2 + t * (P3 + t * (P4 + t * P5)))); + if (k == 0) { + return 1.0 - ((x * c) / (c - 2.0) - x); + } + const double y = 1.0 - ((lo - (x * c) / (2.0 - c)) - hi); + if (k >= -1021) { + if (k == 1024) { + return y * 2.0 * 0x1p1023; + } + return y * twopk; + } + return y * twopk * twom1000; +} double APS5_VABI atan_nid_postfix(double x) { static constexpr double atanhi[] = {4.63647609000806093515e-01, 7.85398163397448278999e-01, 9.82793723247329054082e-01, 1.57079632679489655800e+00}; static constexpr double atanlo[] = {2.26987774529616870924e-17, 3.06161699786838301793e-17, 1.39033110312309984516e-17, 6.12323399573676603587e-17}; @@ -1417,7 +1492,7 @@ static double SinhLdexpExpHalf(double x) { * * Developed at SunSoft, a Sun Microsystems, Inc. business. * Permission to use, copy, modify, and distribute this - * software is freely granted, provided that this notice + * software is freely granted, provided that this notice * is preserved. * ==================================================== */ diff --git a/core/libs/prx/libc/src/MathExact.cpp b/core/libs/prx/libc/src/MathExact.cpp index d54a25def7..7423ded136 100644 --- a/core/libs/prx/libc/src/MathExact.cpp +++ b/core/libs/prx/libc/src/MathExact.cpp @@ -170,6 +170,79 @@ short NormalizeFraction(void* value) { return exponent; } +template +short ScaleFieldBits(void* value, std::int64_t exponent) { + constexpr TBits Sign = TBits{1} << (FractionBits + ExponentBits); + constexpr TBits Hidden = TBits{1} << FractionBits; + constexpr TBits FractionMask = Hidden - 1; + constexpr std::int64_t Maximum = (std::int64_t{1} << ExponentBits) - 1; + TBits bits; + std::memcpy(&bits, value, sizeof(bits)); + const TBits sign = bits & Sign; + TBits fraction = bits & FractionMask; + std::int64_t field = static_cast((bits >> FractionBits) & Maximum); + if (field == Maximum) return fraction != 0 ? NotANumber : Infinite; + if (field == 0) { + if (fraction == 0) return Zero; + const int shift = FractionBits + 1 - static_cast(std::bit_width(fraction)); + fraction = (fraction << shift) & FractionMask; + field = 1 - shift; + } + if (exponent > 0 && Maximum - field <= exponent) { + bits = sign | (static_cast(Maximum) << FractionBits); + std::memcpy(value, &bits, sizeof(bits)); + return Infinite; + } + if (-field < exponent) { + bits = sign | (static_cast(exponent + field) << FractionBits) | fraction; + std::memcpy(value, &bits, sizeof(bits)); + return Finite; + } + const auto shifted = static_cast(static_cast(exponent) + static_cast(field - 1)); + if (shifted < -(FractionBits + 1) || shifted >= 0) { + std::memcpy(value, &sign, sizeof(sign)); + return Zero; + } + const int drop = static_cast(-shifted); + const TBits significand = Hidden | fraction; + TBits result = significand >> drop; + const TBits lost = significand & ((TBits{1} << drop) - 1); + const TBits half = TBits{1} << (drop - 1); + if (lost > half || (lost == half && (result & 1) != 0)) ++result; + bits = sign | result; + std::memcpy(value, &bits, sizeof(bits)); + return result != 0 ? Finite : Zero; +} + +template +short UnscaleFieldBits(short* exponent, void* value) { + constexpr TBits Sign = TBits{1} << (FractionBits + ExponentBits); + constexpr TBits FractionMask = (TBits{1} << FractionBits) - 1; + constexpr int Maximum = (1 << ExponentBits) - 1; + constexpr int Half = (1 << (ExponentBits - 1)) - 2; + TBits bits; + std::memcpy(&bits, value, sizeof(bits)); + TBits fraction = bits & FractionMask; + int field = static_cast((bits >> FractionBits) & Maximum); + if (field == Maximum) { + *exponent = 0; + return fraction != 0 ? NotANumber : Infinite; + } + if (field == 0) { + if (fraction == 0) { + *exponent = 0; + return Zero; + } + const int shift = FractionBits + 1 - static_cast(std::bit_width(fraction)); + fraction = (fraction << shift) & FractionMask; + field = 1 - shift; + } + bits = (bits & Sign) | (static_cast(Half) << FractionBits) | fraction; + std::memcpy(value, &bits, sizeof(bits)); + *exponent = static_cast(field - Half); + return Finite; +} + template int Compare(TFloat x, TFloat y) { if (x < y) return 1; @@ -282,6 +355,8 @@ long double APS5_VABI fmal_nid_postfix(long double x, long double y, long double return std::fma(x, y, z); } +double APS5_VABI floor_nid_postfix(double x) { return std::isnan(x) ? x + x : std::floor(x); } +float APS5_VABI floorf_nid_postfix(float x) { return std::isnan(x) ? x + x : std::floor(x); } double APS5_VABI rint_nid_postfix(double x) { return std::isnan(x) ? x + x : std::rint(x); } float APS5_VABI rintf_nid_postfix(float x) { return std::isnan(x) ? x + x : std::rint(x); } float APS5_VABI nearbyintf_nid_postfix(float x) { return std::isnan(x) ? x + x : std::rintf(x); } @@ -410,6 +485,13 @@ short APS5_VABI _LDunscale_nid_postfix(short* exponent, long double* value) { return UnscaleBinary(exponent, value); } +short APS5_VABI _ZNSt8__sce_v27_DscaleEPdl_nid_postfix(double* value, std::int64_t exponent) { return ScaleFieldBits(value, exponent); } +short APS5_VABI _ZNSt8__sce_v28_FDscaleEPfl_nid_postfix(float* value, std::int64_t exponent) { return ScaleFieldBits(value, exponent); } +short APS5_VABI _ZNSt8__sce_v28_LDscaleEPel_nid_postfix(double* value, std::int64_t exponent) { return ScaleFieldBits(value, exponent); } +short APS5_VABI _ZNSt8__sce_v29_DunscaleEPsPd_nid_postfix(short* exponent, double* value) { return UnscaleFieldBits(exponent, value); } +short APS5_VABI _ZNSt8__sce_v210_FDunscaleEPsPf_nid_postfix(short* exponent, float* value) { return UnscaleFieldBits(exponent, value); } +short APS5_VABI _ZNSt8__sce_v210_LDunscaleEPsPe_nid_postfix(short* exponent, double* value) { return UnscaleFieldBits(exponent, value); } + short APS5_VABI _Dnorm_nid_postfix(std::uint16_t* words) { return NormalizeFraction(words); } short APS5_VABI _FDnorm_nid_postfix(std::uint16_t* words) { return NormalizeFraction(words); } @@ -423,6 +505,15 @@ short APS5_VABI _LDnorm_nid_postfix(std::uint16_t* words) { return static_cast(-shift); } +short APS5_VABI _ZNSt8__sce_v26_DtestEPd_nid_postfix(double* value) { return _Dtest_nid_postfix(value); } +short APS5_VABI _ZNSt8__sce_v27_FDtestEPf_nid_postfix(float* value) { return _FDtest_nid_postfix(value); } +short APS5_VABI _ZNSt8__sce_v27_LDtestEPe_nid_postfix(double* value) { return _Dtest_nid_postfix(value); } +short APS5_VABI _ZNSt8__sce_v25_DintEPds_nid_postfix(double* value, short scale) { return _Dint_nid_postfix(value, scale); } +short APS5_VABI _ZNSt8__sce_v26_FDintEPfs_nid_postfix(float* value, short scale) { return _FDint_nid_postfix(value, scale); } +short APS5_VABI _ZNSt8__sce_v26_LDintEPes_nid_postfix(double* value, short scale) { return _Dint_nid_postfix(value, scale); } +short APS5_VABI _ZNSt8__sce_v26_DnormEPNS_5_DvalE_nid_postfix(std::uint16_t* words) { return _Dnorm_nid_postfix(words); } +short APS5_VABI _ZNSt8__sce_v27_FDnormEPNS_5_FvalE_nid_postfix(std::uint16_t* words) { return _FDnorm_nid_postfix(words); } + int APS5_VABI _Fpcomp_nid_postfix(double x, double y) { return std::isnan(x) || std::isnan(y) ? 0 : Compare(x, y); } int APS5_VABI _FFpcomp_nid_postfix(float x, float y) { return std::isnan(x) || std::isnan(y) ? 0 : Compare(x, y); } int APS5_VABI _LFpcomp_nid_postfix(long double x, long double y) { return ExtendedNan(Bits(x)) || ExtendedNan(Bits(y)) ? 0 : Compare(x, y); } diff --git a/core/libs/prx/libc/src/Rename.cpp b/core/libs/prx/libc/src/Rename.cpp index 3f6e049813..4d93860a6e 100644 --- a/core/libs/prx/libc/src/Rename.cpp +++ b/core/libs/prx/libc/src/Rename.cpp @@ -6,6 +6,7 @@ #include #include #ifdef _WIN32 +#include "prx/libc/include/WindowsVerbatimPath.hpp" #include #include #include @@ -32,7 +33,7 @@ std::error_code RenameNative_nid_no_patch(const std::filesystem::path& source, c std::error_code error; #ifdef _WIN32 if (replacesExisting) { - const auto target = fs::absolute(destination, error); + const auto target = NativeVerbatimPath_nid_no_patch(destination, error); if (error) return error; struct RenameInfo { DWORD flags; HANDLE root; DWORD nameBytes; WCHAR name[1]; }; static_assert(sizeof(RenameInfo) == sizeof(FILE_RENAME_INFO) && offsetof(RenameInfo, name) == offsetof(FILE_RENAME_INFO, FileName)); @@ -41,25 +42,37 @@ std::error_code RenameNative_nid_no_patch(const std::filesystem::path& source, c std::vector buffer(sizeof(RenameInfo) + nameBytes); auto* info = reinterpret_cast(buffer.data()); constexpr DWORD replaceExisting = 0x00000001, posixSemantics = 0x00000002, ignoreReadOnly = 0x00000040; - const auto attributes = GetFileAttributesW(destination.c_str()); + const auto attributes = GetFileAttributesW(target.c_str()); info->flags = replaceExisting | posixSemantics | (attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_READONLY) ? ignoreReadOnly : 0); info->nameBytes = static_cast(nameBytes); std::memcpy(info->name, target.c_str(), nameBytes); - const auto handle = CreateFileW(source.c_str(), DELETE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr); + const auto sourcePath = NativeVerbatimPath_nid_no_patch(source, error); + const auto& openSource = error ? source : sourcePath; + const auto handle = CreateFileW(openSource.c_str(), DELETE, 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 std::error_code(static_cast(GetLastError()), std::system_category()); constexpr auto renameInfoEx = static_cast(22); if (!SetFileInformationByHandle(handle, renameInfoEx, info, static_cast(buffer.size()))) error = std::error_code(static_cast(GetLastError()), std::system_category()); CloseHandle(handle); return error; } -#endif + const auto verbatimSource = NativeVerbatimPath_nid_no_patch(source, error); + if (error) return error; + const auto verbatimDest = NativeVerbatimPath_nid_no_patch(destination, error); + if (error) return error; + fs::rename(verbatimSource, verbatimDest, error); + return error; +#else fs::rename(source, destination, error); return error; +#endif } #ifdef _WIN32 static bool BelowSymbolicLinkLoop(const std::filesystem::path& directory) { - const auto handle = CreateFileW(directory.c_str(), FILE_READ_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + std::error_code error; + const auto verbatim = NativeVerbatimPath_nid_no_patch(directory, error); + const auto& checkDir = error ? directory : verbatim; + const auto handle = CreateFileW(checkDir.c_str(), FILE_READ_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); if (handle == INVALID_HANDLE_VALUE) return GetLastError() == ERROR_CANT_RESOLVE_FILENAME; CloseHandle(handle); @@ -93,11 +106,21 @@ static int RenamePath_nid_no_patch(const std::filesystem::path& source, const st constexpr int GuestEnotempty = 66; namespace fs = std::filesystem; std::error_code error; - const auto sourceStatus = LinkStatus(source, error); +#ifdef _WIN32 + std::error_code verbatimError; + const auto verbatimSource = NativeVerbatimPath_nid_no_patch(source, verbatimError); + const auto& actualSource = verbatimError ? source : verbatimSource; + const auto verbatimDest = NativeVerbatimPath_nid_no_patch(destination, verbatimError); + const auto& actualDest = verbatimError ? destination : verbatimDest; +#else + const auto& actualSource = source; + const auto& actualDest = destination; +#endif + const auto sourceStatus = LinkStatus(actualSource, error); if (error && error != std::errc::no_such_file_or_directory) return FilesystemError(error); if (sourceStatus.type() == fs::file_type::not_found) return GuestEnoent; - const auto parent = destination.parent_path().empty() ? fs::path(".") : destination.parent_path(); + const auto parent = actualDest.parent_path().empty() ? fs::path(".") : actualDest.parent_path(); error.clear(); const auto parentStatus = fs::status(parent, error); if (error) return FilesystemError(error); @@ -105,7 +128,7 @@ static int RenamePath_nid_no_patch(const std::filesystem::path& source, const st if (!fs::is_directory(parentStatus)) return GuestEnotdir; error.clear(); - const auto destinationStatus = LinkStatus(destination, error); + const auto destinationStatus = LinkStatus(actualDest, error); if (error && error != std::errc::no_such_file_or_directory) return FilesystemError(error); const bool destinationExists = destinationStatus.type() != fs::file_type::not_found; const bool sourceIsDirectory = fs::is_directory(sourceStatus) && !IsWindowsLink(source); @@ -113,7 +136,7 @@ static int RenamePath_nid_no_patch(const std::filesystem::path& source, const st bool sameFile = false; if (destinationExists) { error.clear(); - sameFile = fs::equivalent(source, destination, error); + sameFile = fs::equivalent(actualSource, actualDest, error); if (error == std::errc::no_such_file_or_directory) error.clear(); #ifdef __APPLE__ if (error == std::errc::not_supported) error.clear(); @@ -124,26 +147,33 @@ static int RenamePath_nid_no_patch(const std::filesystem::path& source, const st } if (sourceIsDirectory && !sameFile) { error.clear(); - const auto canonicalSource = fs::weakly_canonical(source, error); + const auto canonicalSource = fs::weakly_canonical(actualSource, error); if (error) return FilesystemError(error); - const auto canonicalDestination = fs::weakly_canonical(destination, error); + const auto canonicalDestination = fs::weakly_canonical(actualDest, error); if (error) return FilesystemError(error); const auto relative = canonicalDestination.lexically_relative(canonicalSource); if (!relative.empty() && *relative.begin() != "..") return GuestEinval; } if (destinationExists && !sameFile && destinationIsDirectory) { error.clear(); - if (!fs::is_empty(destination, error)) { + if (!fs::is_empty(actualDest, error)) { return error ? FilesystemError(error) : GuestEnotempty; } } - error = RenameNative_nid_no_patch(source, destination, destinationExists && (!sourceIsDirectory || !sameFile)); + error = RenameNative_nid_no_patch(actualSource, actualDest, destinationExists && (!sourceIsDirectory || !sameFile)); return error ? FilesystemError(error) : 0; } static int SourceLookupError_nid_no_patch(const std::filesystem::path& source) { std::error_code status; - const auto sourceStatus = LinkStatus(source, status); +#ifdef _WIN32 + std::error_code verbatimError; + const auto verbatim = NativeVerbatimPath_nid_no_patch(source, verbatimError); + const auto& actualSource = verbatimError ? source : verbatim; +#else + const auto& actualSource = source; +#endif + const auto sourceStatus = LinkStatus(actualSource, status); if (HasNonDirectoryPrefix_nid_no_patch(source)) return 20; if (status && status != std::errc::no_such_file_or_directory) return FilesystemError(status); return sourceStatus.type() == std::filesystem::file_type::not_found ? 2 : 0; @@ -172,7 +202,14 @@ extern "C" int RenameGuestPath_nid_no_patch(const char* from, const char* to) { int error = RenamePath_nid_no_patch(source, destination); if (error == 2) { std::error_code status; - if (HasNonDirectoryPrefix_nid_no_patch(source) || (std::filesystem::exists(source, status) && HasNonDirectoryPrefix_nid_no_patch(destination))) error = 20; +#ifdef _WIN32 + std::error_code verbatimError; + const auto verbatim = NativeVerbatimPath_nid_no_patch(source, verbatimError); + const auto& actualSource = verbatimError ? source : verbatim; +#else + const auto& actualSource = source; +#endif + if (HasNonDirectoryPrefix_nid_no_patch(source) || (std::filesystem::exists(actualSource, status) && HasNonDirectoryPrefix_nid_no_patch(destination))) error = 20; } #ifdef _WIN32 constexpr int GuestEloop = 62; diff --git a/core/libs/prx/libc/src/ScanWideField.cpp b/core/libs/prx/libc/src/ScanWideField.cpp index 5d5a35559d..cdc07afa5c 100644 --- a/core/libs/prx/libc/src/ScanWideField.cpp +++ b/core/libs/prx/libc/src/ScanWideField.cpp @@ -3,66 +3,29 @@ #include -namespace { - -std::size_t DecodeUtf8(const char* text, char32_t* point) { - const auto lead = static_cast(text[0]); - if (lead < 0x80) { - *point = lead; - return 1; - } - std::size_t size = 0; - char32_t value = 0; - if ((lead & 0xE0) == 0xC0) { size = 2; value = lead & 0x1F; } - else if ((lead & 0xF0) == 0xE0) { size = 3; value = lead & 0x0F; } - else if ((lead & 0xF8) == 0xF0) { size = 4; value = lead & 0x07; } - else return 0; - for (std::size_t index = 1; index < size; ++index) { - const auto next = static_cast(text[index]); - if ((next & 0xC0) != 0x80) return 0; - value = (value << 6) | (next & 0x3F); - } - constexpr char32_t Smallest[] = {0, 0, 0x80, 0x800, 0x10000}; - if (value < Smallest[size] || value > 0x10FFFF || (value >= 0xD800 && value <= 0xDFFF)) return 0; - *point = value; - return size; -} - -} - namespace LibcDetail { -std::size_t ReadWideScanCharacter(const char* text, char32_t* point) { - const std::size_t size = DecodeUtf8(text, point); - if (size == 0) NotImplemented_nid_no_patch("scanf 16-bit wide conversion of input that is not UTF-8"); - if (*point > 0xFFFF) NotImplemented_nid_no_patch("scanf 16-bit wide conversion of a character outside the BMP"); - return size; -} - -bool InWideScanset(const std::string& specifier, char32_t point) { +bool InWideScanset(const std::string& specifier, unsigned char byte) { std::size_t index = 1; const bool negated = specifier[index] == '^'; if (negated) ++index; const std::size_t end = specifier.size() - 1; bool member = false; - for (std::size_t first = index; index < end; ++index) { - const auto value = static_cast(specifier[index]); - if (value >= 0x80) NotImplemented_nid_no_patch("scanf 16-bit wide scanset with a character outside ASCII"); - if (specifier[index] == '-' && index != first && index + 1 < end) { - const auto low = static_cast(specifier[index - 1]); - const auto high = static_cast(specifier[index + 1]); - if (high >= 0x80) NotImplemented_nid_no_patch("scanf 16-bit wide scanset with a character outside ASCII"); - if (point >= low && point <= high) member = true; + while (index < end && !member) { + const auto first = static_cast(specifier[index]); + if (end - index >= 3 && specifier[index + 1] == '-') { + member = byte >= first && byte <= static_cast(specifier[index + 2]); + index += 3; + } else { + member = byte == first; ++index; - continue; } - if (point == value) member = true; } return member != negated; } void RequireNoPositionalWideScan(const char* format) { - if (std::strchr(format, '$') != nullptr) NotImplemented_nid_no_patch("sscanf positional arguments with a 16-bit wide conversion on Linux"); + if (std::strchr(format, '$') != nullptr) NotImplemented_nid_no_patch("sscanf positional arguments with a 16-bit wide conversion"); } } diff --git a/core/libs/prx/libc/src/StandardStreams.cpp b/core/libs/prx/libc/src/StandardStreams.cpp index 91256f0eac..6dae398fb2 100644 --- a/core/libs/prx/libc/src/StandardStreams.cpp +++ b/core/libs/prx/libc/src/StandardStreams.cpp @@ -144,6 +144,7 @@ std::int32_t APS5_VABI ungetwc_nid_postfix(std::int32_t value, FileStream* strea return -1; } auto* native = GetNativeStream(stream); + if (!stream->OpenForReading()) return -1; if (!stream->SwitchToReading()) return -1; const int result = std::ungetc(value, native); stream->SyncStatus(); @@ -223,6 +224,7 @@ int APS5_VABI putchar_nid_postfix(int value) { return fputc_nid_postfix(value, _ int APS5_VABI putchar_unlocked_nid_postfix(int value) { return putc_unlocked_nid_postfix(value, __stdoutp_nid_postfix); } int APS5_VABI ungetc_nid_postfix(int value, FileStream* stream) { if (stream && !stream->IsOpen()) { errno = 9; return -1; } + if (value != EOF && stream && !stream->OpenForReading()) return -1; auto* native = GetNativeStream(stream); stream->Orientation(-1); if (value != EOF && !stream->SwitchToReading()) return -1; diff --git a/core/libs/prx/libc/src/StringExtensions.cpp b/core/libs/prx/libc/src/StringExtensions.cpp index a1b2c2af2b..44aa0b08ed 100644 --- a/core/libs/prx/libc/src/StringExtensions.cpp +++ b/core/libs/prx/libc/src/StringExtensions.cpp @@ -150,6 +150,17 @@ char* APS5_VABI stpcpy_nid_postfix(char* destination, const char* source) { return destination + length; } +char* APS5_VABI stpncpy_nid_postfix(char* destination, const char* source, std::size_t count) { + for (; count != 0; --count, ++destination, ++source) { + if ((*destination = *source) == '\0') { + char* result = destination; + while (--count != 0) *++destination = '\0'; + return result; + } + } + return destination; +} + char* APS5_VABI strtok_r_nid_postfix(char* text, const char* delimiters, char** state) { static const char exhausted[] = ""; if (text == nullptr) text = *state; @@ -171,6 +182,7 @@ char* APS5_VABI strtok_nid_postfix(char* text, const char* delimiters) { } int APS5_VABI __inet_aton_nid_postfix(const char* text, void* address) { + if (text == nullptr) throw std::invalid_argument("__inet_aton: null text"); std::uint8_t parts[3]; std::size_t count = 0; std::uint64_t value = 0; diff --git a/core/libs/prx/libc/src/Strings.cpp b/core/libs/prx/libc/src/Strings.cpp index fa755d0431..b744273d3a 100644 --- a/core/libs/prx/libc/src/Strings.cpp +++ b/core/libs/prx/libc/src/Strings.cpp @@ -307,6 +307,12 @@ void* APS5_VABI memset_nid_postfix(void* s, int c, size_t n) { return std::memset(s, c, n); } +void* APS5_VABI memset_explicit_nid_postfix(void* destination, int value, size_t count) { + auto* bytes = static_cast(destination); + for (size_t index = 0; index < count; ++index) bytes[index] = static_cast(value); + return destination; +} + void APS5_VABI bzero_nid_postfix(void* destination, size_t count) { std::memset(destination, 0, count); } @@ -576,7 +582,7 @@ int APS5_VABI strncasecmp_nid_postfix(const char* s1, const char* s2, size_t n) } int APS5_VABI bcmp_nid_postfix(const void* s1, const void* s2, size_t n) { - return std::memcmp(s1, s2, n); + return n != 0 && std::memcmp(s1, s2, n) != 0 ? 1 : 0; } size_t APS5_VABI strspn_nid_postfix(const char* s, const char* accept) { @@ -829,7 +835,7 @@ char16_t* APS5_VABI wmemset_nid_postfix(char16_t* s, char16_t c, size_t n) { } double APS5_VABI wcstod_nid_postfix(const char16_t* str, char16_t** endptr) { - return ParseAsciiPrefix(str, endptr, [](const char* text, char** end) { return ParseFloat(text, end, std::strtod); }); + return ParseAsciiPrefix(str, endptr, [](const char* text, char** end) { return strtod_nid_postfix(text, end); }); } float APS5_VABI wcstof_nid_postfix(const char16_t* str, char16_t** endptr) { diff --git a/core/libs/prx/libc/src/Time.cpp b/core/libs/prx/libc/src/Time.cpp index f17a2f10c7..c8bebdd422 100644 --- a/core/libs/prx/libc/src/Time.cpp +++ b/core/libs/prx/libc/src/Time.cpp @@ -649,6 +649,11 @@ int64_t APS5_VABI mktime_nid_postfix(GuestTm* timeptr) { return libc_mktime_nid_postfix(timeptr); } +int64_t APS5_VABI timelocal_nid_postfix(GuestTm* timeptr) { + if (timeptr != nullptr) timeptr->tm_isdst = -1; + return libc_mktime_nid_postfix(timeptr); +} + size_t APS5_VABI libc_strftime_nid_postfix(char* str, size_t count, const char* format, const GuestTm* timeptr) { const ZoneScope zone; tzset_nid_postfix(); diff --git a/core/libs/prx/libc/src/VectorAlgorithms.cpp b/core/libs/prx/libc/src/VectorAlgorithms.cpp index e56da3b63b..a536953460 100644 --- a/core/libs/prx/libc/src/VectorAlgorithms.cpp +++ b/core/libs/prx/libc/src/VectorAlgorithms.cpp @@ -27,6 +27,21 @@ const void* Find(const void* first, const void* last, TValue value) { constexpr std::uintptr_t UnsizedBlock = 32; +template +const void* FindLast(const void* first, const void* last, TValue value) { + if (first == last) return last; + const auto beginAddress = reinterpret_cast(first); + const auto endAddress = reinterpret_cast(last); + if (!first || !last || endAddress < beginAddress || (endAddress - beginAddress) % sizeof(TValue) != 0) NotImplemented_nid_no_patch(__func__); + const auto* begin = static_cast(first); + auto* current = static_cast(last); + while (current != begin) { + current -= sizeof(TValue); + if (Read(current) == value) return current; + } + return last; +} + template const void* FindUnsized(const void* first, TValue value) { auto* start = static_cast(first); @@ -112,6 +127,10 @@ const void* APS5_VABI __std_find_trivial_1_nid_postfix(const void* first, const const void* APS5_VABI __std_find_trivial_2_nid_postfix(const void* first, const void* last, std::uint16_t value) { return Find(first, last, value); } const void* APS5_VABI __std_find_trivial_4_nid_postfix(const void* first, const void* last, std::uint32_t value) { return Find(first, last, value); } const void* APS5_VABI __std_find_trivial_8_nid_postfix(const void* first, const void* last, std::uint64_t value) { return Find(first, last, value); } +const void* APS5_VABI __std_find_last_trivial_1_nid_postfix(const void* first, const void* last, std::uint8_t value) { return FindLast(first, last, value); } +const void* APS5_VABI __std_find_last_trivial_2_nid_postfix(const void* first, const void* last, std::uint16_t value) { return FindLast(first, last, value); } +const void* APS5_VABI __std_find_last_trivial_4_nid_postfix(const void* first, const void* last, std::uint32_t value) { return FindLast(first, last, value); } +const void* APS5_VABI __std_find_last_trivial_8_nid_postfix(const void* first, const void* last, std::uint64_t value) { return FindLast(first, last, value); } const void* APS5_VABI __std_find_trivial_unsized_1_nid_postfix(const void* first, std::uint8_t value) { return FindUnsized(first, value); } const void* APS5_VABI __std_find_trivial_unsized_2_nid_postfix(const void* first, std::uint16_t value) { return FindUnsized(first, value); } const void* APS5_VABI __std_find_trivial_unsized_4_nid_postfix(const void* first, std::uint32_t value) { return FindUnsized(first, value); } diff --git a/core/libs/prx/libc/src/VersionedFilesystemCalls.cpp b/core/libs/prx/libc/src/VersionedFilesystemCalls.cpp new file mode 100644 index 0000000000..ea3eb73e13 --- /dev/null +++ b/core/libs/prx/libc/src/VersionedFilesystemCalls.cpp @@ -0,0 +1,212 @@ +#include "prx/libc/include/Api.hpp" +#include "prx/libc/include/FilesystemForwards.hpp" +#include "prx/libc/include/General.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr std::uint32_t KernelErrnoBase = 0x80020000u; +constexpr std::uint32_t KernelExists = 0x80020011u; +constexpr std::uint32_t KernelNoSuchFile = 0x80020002u; +constexpr std::uint64_t NotFoundCodes = 0x2000000000140001ull; +constexpr int OutOfMemory = 12; +constexpr int NotSupported = 45; +constexpr int NoSystemCall = 78; +constexpr int TypeNotFound = -1; +constexpr int TypeNone = 0; +constexpr int TypeUnknown = 8; +constexpr int TypesByMode[] = {6, 5, 8, 2, 8, 4, 8, 1, 8, 3, 8, 7}; +constexpr std::uint16_t DefaultDirectoryMode = 0777; +constexpr std::uint16_t CopiedPermissions = 06777; +constexpr int CopyTargetFlags = 0x601; +constexpr std::uint16_t CopyTargetMode = 0777; +constexpr std::size_t CopyBlock = 0x10000; +constexpr std::int64_t MicrosecondsPerSecond = 1000000; +constexpr std::int64_t NanosecondsPerMicrosecond = 1000; + +std::atomic registeredForwards{nullptr}; + +const FilesystemForwards_nid_no_patch& Kernel(const char* function) { + const auto* forwards = registeredForwards.load(std::memory_order_acquire); + if (!forwards) throw std::runtime_error(std::string(function) + ": libkernel filesystem calls are not registered"); + return *forwards; +} + +int ErrorOf(int result) { + const auto code = static_cast(result); + return (code & 0xffff0000u) == KernelErrnoBase ? static_cast(code & 0xffffu) : 1; +} + +int StatusOf(int result) { + return result == 0 ? 0 : ErrorOf(result); +} + +bool NotFound(int result) { + const auto index = static_cast(result) - KernelNoSuchFile; + return index <= 0x3du && ((NotFoundCodes >> index) & 1u) != 0; +} + +int TypeOf(std::uint16_t mode) { + const auto index = static_cast(mode) - 0x1000u; + return index >= 0xc000u ? TypeUnknown : TypesByMode[index >> 12]; +} + +int StatusType(const char* function, const char* path, int* perms, int* error) { + FileStat status{}; + const int result = Kernel(function).stat(path, &status); + int type = TypeNone; + if (NotFound(result)) { + type = TypeNotFound; + } else if (result == 0) { + if (perms) *perms = status.st_mode & 0xfff; + type = TypeOf(status.st_mode); + } + if (error) *error = StatusOf(result); + return type; +} + +int CopyContents(const FilesystemForwards_nid_no_patch& kernel, int source, int target, std::byte* buffer) { + for (;;) { + const auto count = kernel.read(source, buffer, CopyBlock); + if (count == 0) { + FileStat status{}; + int result = kernel.fstat(source, &status); + if (result == 0) result = kernel.fchmod(target, static_cast(status.st_mode & CopiedPermissions)); + return StatusOf(result); + } + if (count < 0) return StatusOf(static_cast(count)); + for (std::int64_t done = 0; done < count;) { + const auto written = kernel.write(target, buffer + done, static_cast(count - done)); + if (written < 0) return StatusOf(static_cast(written)); + done += written; + } + } +} + +} + +extern "C" { + +void RegisterFilesystemForwards_nid_no_patch(const FilesystemForwards_nid_no_patch* forwards) { + registeredForwards.store(forwards, std::memory_order_release); +} + +int APS5_VABI _ZNSt8__sce_v210filesystem6_LstatEPKcPNS0_5permsEPi_nid_postfix(const char* path, int* perms, int* error) { + return StatusType(__func__, path, perms, error); +} + +int APS5_VABI _ZNSt8__sce_v210filesystem6_LstatEPKcPNS0_5permsE_nid_postfix(const char* path, int* perms) { + return StatusType(__func__, path, perms, nullptr); +} + +std::uint64_t APS5_VABI _ZNSt8__sce_v210filesystem10_File_sizeEPKcPi_nid_postfix(const char* path, int* error) { + FileStat status{}; + const int result = Kernel(__func__).stat(path, &status); + if (result != 0 && error) *error = result; + return result != 0 ? UINT64_MAX : static_cast(status.st_size); +} + +std::uint64_t APS5_VABI _ZNSt8__sce_v210filesystem10_File_sizeEPKc_nid_postfix(const char* path) { + FileStat status{}; + return Kernel(__func__).stat(path, &status) != 0 ? UINT64_MAX : static_cast(status.st_size); +} + +std::int64_t APS5_VABI _ZNSt8__sce_v210filesystem16_Last_write_timeEPKc_nid_postfix(const char* path) { + FileStat status{}; + return Kernel(__func__).posixStat(path, &status) != 0 ? -1 : status.st_mtim.tv_sec; +} + +int APS5_VABI _ZNSt8__sce_v210filesystem16_Last_write_timeEPKcPlS3__nid_postfix(const char* path, std::int64_t* seconds, std::int64_t* nanoseconds) { + FileStat status{}; + const int result = Kernel(__func__).stat(path, &status); + if (result != 0) return ErrorOf(result); + *seconds = status.st_mtim.tv_sec; + *nanoseconds = status.st_mtim.tv_nsec; + return 0; +} + +bool APS5_VABI _ZNSt8__sce_v210filesystem20_Set_last_write_timeEPKcl_nid_postfix(const char* path, std::int64_t microseconds) { + const auto& kernel = Kernel(__func__); + FileStat status{}; + if (kernel.stat(path, &status) != 0) return false; + std::int64_t seconds = microseconds / MicrosecondsPerSecond; + std::int64_t remainder = microseconds - seconds * MicrosecondsPerSecond; + if (remainder < 0) { + --seconds; + remainder += MicrosecondsPerSecond; + } + const KernelTimeval times[2] = {{status.st_atim.tv_sec, status.st_atim.tv_nsec / NanosecondsPerMicrosecond}, {seconds, remainder}}; + return kernel.utimes(path, times) == 0; +} + +int APS5_VABI _ZNSt8__sce_v210filesystem9_Make_dirEPKcS2__nid_postfix(const char* path, const char* existing) { + const auto& kernel = Kernel(__func__); + std::uint16_t mode = DefaultDirectoryMode; + if (existing) { + FileStat status{}; + const int result = kernel.stat(existing, &status); + if (result != 0) return -ErrorOf(result); + mode = status.st_mode; + } + const int result = kernel.mkdir(path, mode); + if (result == 0) return 1; + if (static_cast(result) == KernelExists) return 0; + return -ErrorOf(result); +} + +bool APS5_VABI _ZNSt8__sce_v210filesystem11_Remove_dirEPKc_nid_postfix(const char* path) { + return Kernel(__func__).posixRmdir(path) == 0; +} + +int APS5_VABI _ZNSt8__sce_v210filesystem7_UnlinkEPKc_nid_postfix(const char* path) { + return StatusOf(Kernel(__func__).unlink(path)); +} + +int APS5_VABI _ZNSt8__sce_v210filesystem7_RenameEPKcS2__nid_postfix(const char* from, const char* to) { + return StatusOf(Kernel(__func__).rename(from, to)); +} + +int APS5_VABI _ZNSt8__sce_v210filesystem7_ResizeEPKcm_nid_postfix(const char* path, std::uint64_t size) { + return StatusOf(Kernel(__func__).truncate(path, static_cast(size))); +} + +int APS5_VABI _ZNSt8__sce_v210filesystem6_ChmodEPKcNS0_5permsE_nid_postfix(const char* path, int perms) { + return StatusOf(Kernel(__func__).chmod(path, static_cast(perms))); +} + +int APS5_VABI _ZNSt8__sce_v210filesystem10_Copy_fileEPKcS2__nid_postfix(const char* from, const char* to) { + const auto& kernel = Kernel(__func__); + const int source = kernel.open(from, 0, 0); + if (source < 0) return ErrorOf(source); + const int target = kernel.open(to, CopyTargetFlags, CopyTargetMode); + if (target < 0) { + kernel.close(source); + return ErrorOf(target); + } + const std::unique_ptr buffer(new (std::nothrow) std::byte[CopyBlock]); + const int result = buffer ? CopyContents(kernel, source, target, buffer.get()) : OutOfMemory; + kernel.close(target); + kernel.close(source); + return result; +} + +int APS5_VABI _ZNSt8__sce_v210filesystem8_StatvfsEPKcRNS0_10space_infoE_nid_postfix(const char*, std::uint64_t* space) { + space[0] = UINT64_MAX; + space[1] = UINT64_MAX; + space[2] = UINT64_MAX; + return NoSystemCall; +} + +int APS5_VABI _ZNSt8__sce_v210filesystem13_Current_set2EPKc_nid_postfix(const char* path) { + return std::strncmp(path, "/", 2) == 0 ? 0 : NotSupported; +} + +} diff --git a/core/libs/prx/libc/src/exception/TypeInfo.cpp b/core/libs/prx/libc/src/exception/TypeInfo.cpp index 082f3a48fa..60f343163c 100644 --- a/core/libs/prx/libc/src/exception/TypeInfo.cpp +++ b/core/libs/prx/libc/src/exception/TypeInfo.cpp @@ -105,22 +105,52 @@ struct TypeInfoVtable { static_assert(offsetof(TypeInfoVtable, destroy) == 2 * sizeof(void*)); static_assert(sizeof(TypeInfoVtable) == 11 * sizeof(void*)); +struct SiTypeRecord { const void* const* vtable; const char* name; const std::type_info* base; }; +struct PointerTypeRecord { const void* const* vtable; const char* name; unsigned flags; const std::type_info* pointee; }; + +} + +namespace { +const std::type_info* AsTypeInfo(const LibcException::SiTypeRecord& record) { + return reinterpret_cast(&record); +} } extern "C" { -LibcException::TypeInfoVtable _ZTVN10__cxxabiv117__class_type_infoE_nid_postfix {0, &typeid(__cxxabiv1::__class_type_info)}; -LibcException::TypeInfoVtable _ZTVN10__cxxabiv120__si_class_type_infoE_nid_postfix {0, &typeid(__cxxabiv1::__si_class_type_info)}; -LibcException::TypeInfoVtable _ZTVN10__cxxabiv121__vmi_class_type_infoE_nid_postfix {0, &typeid(__cxxabiv1::__vmi_class_type_info)}; +extern LibcException::SiTypeRecord _ZTISt9type_info_nid_postfix; +extern LibcException::SiTypeRecord _ZTIN10__cxxabiv116__shim_type_infoE_nid_postfix; +extern LibcException::SiTypeRecord _ZTIN10__cxxabiv116__enum_type_infoE_nid_postfix; +extern LibcException::SiTypeRecord _ZTIN10__cxxabiv117__array_type_infoE_nid_postfix; +extern LibcException::SiTypeRecord _ZTIN10__cxxabiv117__class_type_infoE_nid_postfix; +extern LibcException::SiTypeRecord _ZTIN10__cxxabiv117__pbase_type_infoE_nid_postfix; +extern LibcException::SiTypeRecord _ZTIN10__cxxabiv119__pointer_type_infoE_nid_postfix; +extern LibcException::SiTypeRecord _ZTIN10__cxxabiv120__function_type_infoE_nid_postfix; +extern LibcException::SiTypeRecord _ZTIN10__cxxabiv120__si_class_type_infoE_nid_postfix; +extern LibcException::SiTypeRecord _ZTIN10__cxxabiv121__vmi_class_type_infoE_nid_postfix; +extern LibcException::SiTypeRecord _ZTIN10__cxxabiv123__fundamental_type_infoE_nid_postfix; +extern LibcException::SiTypeRecord _ZTIN10__cxxabiv129__pointer_to_member_type_infoE_nid_postfix; +LibcException::TypeInfoVtable _ZTVN10__cxxabiv117__class_type_infoE_nid_postfix { + 0, + AsTypeInfo(_ZTIN10__cxxabiv117__class_type_infoE_nid_postfix) +}; +LibcException::TypeInfoVtable _ZTVN10__cxxabiv120__si_class_type_infoE_nid_postfix { + 0, + AsTypeInfo(_ZTIN10__cxxabiv120__si_class_type_infoE_nid_postfix) +}; +LibcException::TypeInfoVtable _ZTVN10__cxxabiv121__vmi_class_type_infoE_nid_postfix { + 0, + AsTypeInfo(_ZTIN10__cxxabiv121__vmi_class_type_infoE_nid_postfix) +}; LibcException::TypeInfoVtable _ZTVN10__cxxabiv119__pointer_type_infoE_nid_postfix { 0, - &typeid(__cxxabiv1::__pointer_type_info), + AsTypeInfo(_ZTIN10__cxxabiv119__pointer_type_infoE_nid_postfix), LibcException::DestroyTypeInfo, LibcException::DeleteTypeInfo, LibcException::IsPointerTypeInfoTrue }; LibcException::TypeInfoVtable _ZTVN10__cxxabiv120__function_type_infoE_nid_postfix { 0, - &typeid(__cxxabiv1::__function_type_info), + AsTypeInfo(_ZTIN10__cxxabiv120__function_type_infoE_nid_postfix), LibcException::DestroyTypeInfo, LibcException::DeleteTypeInfo, LibcException::IsPointerTypeInfo, @@ -128,7 +158,27 @@ LibcException::TypeInfoVtable _ZTVN10__cxxabiv120__function_type_infoE_nid_postf }; LibcException::TypeInfoVtable _ZTVN10__cxxabiv123__fundamental_type_infoE_nid_postfix { 0, - &typeid(__cxxabiv1::__fundamental_type_info) + AsTypeInfo(_ZTIN10__cxxabiv123__fundamental_type_infoE_nid_postfix) +}; +LibcException::TypeInfoVtable _ZTVN10__cxxabiv116__shim_type_infoE_nid_postfix { + 0, + AsTypeInfo(_ZTIN10__cxxabiv116__shim_type_infoE_nid_postfix) +}; +LibcException::TypeInfoVtable _ZTVN10__cxxabiv116__enum_type_infoE_nid_postfix { + 0, + AsTypeInfo(_ZTIN10__cxxabiv116__enum_type_infoE_nid_postfix) +}; +LibcException::TypeInfoVtable _ZTVN10__cxxabiv117__array_type_infoE_nid_postfix { + 0, + AsTypeInfo(_ZTIN10__cxxabiv117__array_type_infoE_nid_postfix) +}; +LibcException::TypeInfoVtable _ZTVN10__cxxabiv117__pbase_type_infoE_nid_postfix { + 0, + AsTypeInfo(_ZTIN10__cxxabiv117__pbase_type_infoE_nid_postfix) +}; +LibcException::TypeInfoVtable _ZTVN10__cxxabiv129__pointer_to_member_type_infoE_nid_postfix { + 0, + AsTypeInfo(_ZTIN10__cxxabiv129__pointer_to_member_type_infoE_nid_postfix) }; } @@ -163,3 +213,84 @@ FundamentalTypeRecord _ZTIDi_nid_postfix {reinterpret_cast(&_ZTVN10 FundamentalTypeRecord _ZTIDu_nid_postfix {reinterpret_cast(&_ZTVN10__cxxabiv123__fundamental_type_infoE_nid_postfix.destroy), "Du"}; FundamentalTypeRecord _ZTIDh_nid_postfix {reinterpret_cast(&_ZTVN10__cxxabiv123__fundamental_type_infoE_nid_postfix.destroy), "Dh"}; } + +namespace { +const void* const* ClassVtable() { + return reinterpret_cast(&_ZTVN10__cxxabiv117__class_type_infoE_nid_postfix) + 2; +} +const void* const* SingleVtable() { + return reinterpret_cast(&_ZTVN10__cxxabiv120__si_class_type_infoE_nid_postfix) + 2; +} +const void* const* PointerVtable() { + return reinterpret_cast(&_ZTVN10__cxxabiv119__pointer_type_infoE_nid_postfix) + 2; +} +const std::type_info* AsTypeInfo(const FundamentalTypeRecord& record) { + return reinterpret_cast(&record); +} +} + +extern "C" { +LibcException::SiTypeRecord _ZTISt9type_info_nid_postfix {ClassVtable(), "St9type_info", nullptr}; +LibcException::SiTypeRecord _ZTIN10__cxxabiv116__shim_type_infoE_nid_postfix {SingleVtable(), "N10__cxxabiv116__shim_type_infoE", AsTypeInfo(_ZTISt9type_info_nid_postfix)}; +LibcException::SiTypeRecord _ZTIN10__cxxabiv116__enum_type_infoE_nid_postfix {SingleVtable(), "N10__cxxabiv116__enum_type_infoE", AsTypeInfo(_ZTIN10__cxxabiv116__shim_type_infoE_nid_postfix)}; +LibcException::SiTypeRecord _ZTIN10__cxxabiv117__array_type_infoE_nid_postfix {SingleVtable(), "N10__cxxabiv117__array_type_infoE", AsTypeInfo(_ZTIN10__cxxabiv116__shim_type_infoE_nid_postfix)}; +LibcException::SiTypeRecord _ZTIN10__cxxabiv117__class_type_infoE_nid_postfix {SingleVtable(), "N10__cxxabiv117__class_type_infoE", AsTypeInfo(_ZTIN10__cxxabiv116__shim_type_infoE_nid_postfix)}; +LibcException::SiTypeRecord _ZTIN10__cxxabiv117__pbase_type_infoE_nid_postfix {SingleVtable(), "N10__cxxabiv117__pbase_type_infoE", AsTypeInfo(_ZTIN10__cxxabiv116__shim_type_infoE_nid_postfix)}; +LibcException::SiTypeRecord _ZTIN10__cxxabiv119__pointer_type_infoE_nid_postfix {SingleVtable(), "N10__cxxabiv119__pointer_type_infoE", AsTypeInfo(_ZTIN10__cxxabiv117__pbase_type_infoE_nid_postfix)}; +LibcException::SiTypeRecord _ZTIN10__cxxabiv120__function_type_infoE_nid_postfix {SingleVtable(), "N10__cxxabiv120__function_type_infoE", AsTypeInfo(_ZTIN10__cxxabiv116__shim_type_infoE_nid_postfix)}; +LibcException::SiTypeRecord _ZTIN10__cxxabiv120__si_class_type_infoE_nid_postfix {SingleVtable(), "N10__cxxabiv120__si_class_type_infoE", AsTypeInfo(_ZTIN10__cxxabiv117__class_type_infoE_nid_postfix)}; +LibcException::SiTypeRecord _ZTIN10__cxxabiv121__vmi_class_type_infoE_nid_postfix {SingleVtable(), "N10__cxxabiv121__vmi_class_type_infoE", AsTypeInfo(_ZTIN10__cxxabiv117__class_type_infoE_nid_postfix)}; +LibcException::SiTypeRecord _ZTIN10__cxxabiv123__fundamental_type_infoE_nid_postfix {SingleVtable(), "N10__cxxabiv123__fundamental_type_infoE", AsTypeInfo(_ZTIN10__cxxabiv116__shim_type_infoE_nid_postfix)}; +LibcException::SiTypeRecord _ZTIN10__cxxabiv129__pointer_to_member_type_infoE_nid_postfix {SingleVtable(), "N10__cxxabiv129__pointer_to_member_type_infoE", AsTypeInfo(_ZTIN10__cxxabiv117__pbase_type_infoE_nid_postfix)}; + +LibcException::PointerTypeRecord _ZTIPv_nid_postfix {PointerVtable(), "Pv", 0, AsTypeInfo(_ZTIv_nid_postfix)}; +LibcException::PointerTypeRecord _ZTIPw_nid_postfix {PointerVtable(), "Pw", 0, AsTypeInfo(_ZTIw_nid_postfix)}; +LibcException::PointerTypeRecord _ZTIPb_nid_postfix {PointerVtable(), "Pb", 0, AsTypeInfo(_ZTIb_nid_postfix)}; +LibcException::PointerTypeRecord _ZTIPc_nid_postfix {PointerVtable(), "Pc", 0, AsTypeInfo(_ZTIc_nid_postfix)}; +LibcException::PointerTypeRecord _ZTIPa_nid_postfix {PointerVtable(), "Pa", 0, AsTypeInfo(_ZTIa_nid_postfix)}; +LibcException::PointerTypeRecord _ZTIPh_nid_postfix {PointerVtable(), "Ph", 0, AsTypeInfo(_ZTIh_nid_postfix)}; +LibcException::PointerTypeRecord _ZTIPs_nid_postfix {PointerVtable(), "Ps", 0, AsTypeInfo(_ZTIs_nid_postfix)}; +LibcException::PointerTypeRecord _ZTIPt_nid_postfix {PointerVtable(), "Pt", 0, AsTypeInfo(_ZTIt_nid_postfix)}; +LibcException::PointerTypeRecord _ZTIPi_nid_postfix {PointerVtable(), "Pi", 0, AsTypeInfo(_ZTIi_nid_postfix)}; +LibcException::PointerTypeRecord _ZTIPj_nid_postfix {PointerVtable(), "Pj", 0, AsTypeInfo(_ZTIj_nid_postfix)}; +LibcException::PointerTypeRecord _ZTIPl_nid_postfix {PointerVtable(), "Pl", 0, AsTypeInfo(_ZTIl_nid_postfix)}; +LibcException::PointerTypeRecord _ZTIPm_nid_postfix {PointerVtable(), "Pm", 0, AsTypeInfo(_ZTIm_nid_postfix)}; +LibcException::PointerTypeRecord _ZTIPx_nid_postfix {PointerVtable(), "Px", 0, AsTypeInfo(_ZTIx_nid_postfix)}; +LibcException::PointerTypeRecord _ZTIPy_nid_postfix {PointerVtable(), "Py", 0, AsTypeInfo(_ZTIy_nid_postfix)}; +LibcException::PointerTypeRecord _ZTIPn_nid_postfix {PointerVtable(), "Pn", 0, AsTypeInfo(_ZTIn_nid_postfix)}; +LibcException::PointerTypeRecord _ZTIPo_nid_postfix {PointerVtable(), "Po", 0, AsTypeInfo(_ZTIo_nid_postfix)}; +LibcException::PointerTypeRecord _ZTIPf_nid_postfix {PointerVtable(), "Pf", 0, AsTypeInfo(_ZTIf_nid_postfix)}; +LibcException::PointerTypeRecord _ZTIPd_nid_postfix {PointerVtable(), "Pd", 0, AsTypeInfo(_ZTId_nid_postfix)}; +LibcException::PointerTypeRecord _ZTIPe_nid_postfix {PointerVtable(), "Pe", 0, AsTypeInfo(_ZTIe_nid_postfix)}; +LibcException::PointerTypeRecord _ZTIPg_nid_postfix {PointerVtable(), "Pg", 0, AsTypeInfo(_ZTIg_nid_postfix)}; +LibcException::PointerTypeRecord _ZTIPDn_nid_postfix {PointerVtable(), "PDn", 0, AsTypeInfo(_ZTIDn_nid_postfix)}; +LibcException::PointerTypeRecord _ZTIPDs_nid_postfix {PointerVtable(), "PDs", 0, AsTypeInfo(_ZTIDs_nid_postfix)}; +LibcException::PointerTypeRecord _ZTIPDi_nid_postfix {PointerVtable(), "PDi", 0, AsTypeInfo(_ZTIDi_nid_postfix)}; +LibcException::PointerTypeRecord _ZTIPDu_nid_postfix {PointerVtable(), "PDu", 0, AsTypeInfo(_ZTIDu_nid_postfix)}; +LibcException::PointerTypeRecord _ZTIPDh_nid_postfix {PointerVtable(), "PDh", 0, AsTypeInfo(_ZTIDh_nid_postfix)}; +LibcException::PointerTypeRecord _ZTIPKv_nid_postfix {PointerVtable(), "PKv", 1, AsTypeInfo(_ZTIv_nid_postfix)}; +LibcException::PointerTypeRecord _ZTIPKw_nid_postfix {PointerVtable(), "PKw", 1, AsTypeInfo(_ZTIw_nid_postfix)}; +LibcException::PointerTypeRecord _ZTIPKb_nid_postfix {PointerVtable(), "PKb", 1, AsTypeInfo(_ZTIb_nid_postfix)}; +LibcException::PointerTypeRecord _ZTIPKc_nid_postfix {PointerVtable(), "PKc", 1, AsTypeInfo(_ZTIc_nid_postfix)}; +LibcException::PointerTypeRecord _ZTIPKa_nid_postfix {PointerVtable(), "PKa", 1, AsTypeInfo(_ZTIa_nid_postfix)}; +LibcException::PointerTypeRecord _ZTIPKh_nid_postfix {PointerVtable(), "PKh", 1, AsTypeInfo(_ZTIh_nid_postfix)}; +LibcException::PointerTypeRecord _ZTIPKs_nid_postfix {PointerVtable(), "PKs", 1, AsTypeInfo(_ZTIs_nid_postfix)}; +LibcException::PointerTypeRecord _ZTIPKt_nid_postfix {PointerVtable(), "PKt", 1, AsTypeInfo(_ZTIt_nid_postfix)}; +LibcException::PointerTypeRecord _ZTIPKi_nid_postfix {PointerVtable(), "PKi", 1, AsTypeInfo(_ZTIi_nid_postfix)}; +LibcException::PointerTypeRecord _ZTIPKj_nid_postfix {PointerVtable(), "PKj", 1, AsTypeInfo(_ZTIj_nid_postfix)}; +LibcException::PointerTypeRecord _ZTIPKl_nid_postfix {PointerVtable(), "PKl", 1, AsTypeInfo(_ZTIl_nid_postfix)}; +LibcException::PointerTypeRecord _ZTIPKm_nid_postfix {PointerVtable(), "PKm", 1, AsTypeInfo(_ZTIm_nid_postfix)}; +LibcException::PointerTypeRecord _ZTIPKx_nid_postfix {PointerVtable(), "PKx", 1, AsTypeInfo(_ZTIx_nid_postfix)}; +LibcException::PointerTypeRecord _ZTIPKy_nid_postfix {PointerVtable(), "PKy", 1, AsTypeInfo(_ZTIy_nid_postfix)}; +LibcException::PointerTypeRecord _ZTIPKn_nid_postfix {PointerVtable(), "PKn", 1, AsTypeInfo(_ZTIn_nid_postfix)}; +LibcException::PointerTypeRecord _ZTIPKo_nid_postfix {PointerVtable(), "PKo", 1, AsTypeInfo(_ZTIo_nid_postfix)}; +LibcException::PointerTypeRecord _ZTIPKf_nid_postfix {PointerVtable(), "PKf", 1, AsTypeInfo(_ZTIf_nid_postfix)}; +LibcException::PointerTypeRecord _ZTIPKd_nid_postfix {PointerVtable(), "PKd", 1, AsTypeInfo(_ZTId_nid_postfix)}; +LibcException::PointerTypeRecord _ZTIPKe_nid_postfix {PointerVtable(), "PKe", 1, AsTypeInfo(_ZTIe_nid_postfix)}; +LibcException::PointerTypeRecord _ZTIPKg_nid_postfix {PointerVtable(), "PKg", 1, AsTypeInfo(_ZTIg_nid_postfix)}; +LibcException::PointerTypeRecord _ZTIPKDn_nid_postfix {PointerVtable(), "PKDn", 1, AsTypeInfo(_ZTIDn_nid_postfix)}; +LibcException::PointerTypeRecord _ZTIPKDs_nid_postfix {PointerVtable(), "PKDs", 1, AsTypeInfo(_ZTIDs_nid_postfix)}; +LibcException::PointerTypeRecord _ZTIPKDi_nid_postfix {PointerVtable(), "PKDi", 1, AsTypeInfo(_ZTIDi_nid_postfix)}; +LibcException::PointerTypeRecord _ZTIPKDu_nid_postfix {PointerVtable(), "PKDu", 1, AsTypeInfo(_ZTIDu_nid_postfix)}; +LibcException::PointerTypeRecord _ZTIPKDh_nid_postfix {PointerVtable(), "PKDh", 1, AsTypeInfo(_ZTIDh_nid_postfix)}; +} diff --git a/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp b/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp index ca9e252ab4..0f342e0c79 100644 --- a/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp +++ b/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp @@ -76,6 +76,7 @@ bool IsFatal(DWORD code) { case EXCEPTION_PRIV_INSTRUCTION: case EXCEPTION_STACK_OVERFLOW: case EXCEPTION_INT_DIVIDE_BY_ZERO: + case EXCEPTION_INT_OVERFLOW: case EXCEPTION_ARRAY_BOUNDS_EXCEEDED: case EXCEPTION_IN_PAGE_ERROR: return true; @@ -372,7 +373,7 @@ LONG WINAPI ReportCrash(EXCEPTION_POINTERS* info) { char line[MAX_PATH + 64]; char threadName[128] = ""; if (PWSTR description = ThreadDescription(GetCurrentThread())) { - WideCharToMultiByte(CP_UTF8, 0, description, -1, threadName, sizeof(threadName), nullptr, nullptr); + WideCharToMultiByte(CP_UTF8, 0, description, -1, threadName, sizeof(threadName) - 1, nullptr, nullptr); LocalFree(description); } Report("\nFATAL: unhandled exception 0x%08lx on thread %lu '%s'\n", record->ExceptionCode, GetCurrentThreadId(), threadName); @@ -590,7 +591,7 @@ void ReportAllThreads() { ResumeThread(thread); char name[128] = ""; if (PWSTR description = ThreadDescription(thread)) { - WideCharToMultiByte(CP_UTF8, 0, description, -1, name, sizeof(name), nullptr, nullptr); + WideCharToMultiByte(CP_UTF8, 0, description, -1, name, sizeof(name) - 1, nullptr, nullptr); LocalFree(description); } CloseHandle(thread); 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 8a8658366e..c6013bb4db 100644 --- a/core/libs/prx/libc/src/specifics/x86_64/CallTrace.cpp +++ b/core/libs/prx/libc/src/specifics/x86_64/CallTrace.cpp @@ -3,6 +3,7 @@ #include #elif defined(__APPLE__) #include +#include #include #include #include @@ -14,6 +15,7 @@ #include #else #include +#include #include #include #include @@ -89,6 +91,23 @@ bool Selected(std::string_view library) { return false; } +std::string DefiningLibrary(const void* target) { +#ifdef _WIN32 + MEMORY_BASIC_INFORMATION memory{}; + if (VirtualQuery(target, &memory, sizeof(memory)) != sizeof(memory)) return {}; + std::wstring path(32768, L'\0'); + const auto length = GetModuleFileNameW(static_cast(memory.AllocationBase), path.data(), static_cast(path.size())); + if (length == 0 || length >= path.size()) return {}; + path.resize(length); + const auto filename = std::filesystem::path(path).filename().u8string(); + return {reinterpret_cast(filename.data()), filename.size()}; +#else + Dl_info info{}; + if (::dladdr(target, &info) == 0 || info.dli_fname == nullptr) return {}; + return std::filesystem::path(info.dli_fname).filename().string(); +#endif +} + bool WriteCallCounts(const std::filesystem::path& path) { const std::size_t recorded = g_recordCount.load(std::memory_order_acquire); const ImportRecord* records = g_records.load(std::memory_order_acquire); @@ -429,7 +448,15 @@ void APS5_VABI CallTraceRecord_nid_no_patch(const void* record, std::uint64_t re } void* TraceImport_nid_no_patch(void* target, const char* name, const char* library) { - if (target == nullptr || name == nullptr || library == nullptr || !Selected(library)) return target; + if (target == nullptr || name == nullptr || library == nullptr) return target; + std::string definingLibrary; + if (*library == '\0') { + const auto* selected = std::getenv("APS5_TRACE_CALLS"); + if (selected == nullptr || *selected == '\0') return target; + definingLibrary = DefiningLibrary(target); + library = definingLibrary.c_str(); + } + if (!Selected(library)) return target; std::lock_guard lock(g_mutex); if (g_ring.load(std::memory_order_relaxed) == nullptr) { MapThunkArea(); diff --git a/core/libs/prx/libcairo/CMakeLists.txt b/core/libs/prx/libcairo/CMakeLists.txt new file mode 100644 index 0000000000..f24b385f75 --- /dev/null +++ b/core/libs/prx/libcairo/CMakeLists.txt @@ -0,0 +1,16 @@ +add_library(libcairo SHARED EXCLUDE_FROM_ALL Export.cpp Matrix.cpp) +target_include_directories(libcairo PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libcairo) + +if(BUILD_TESTING) + add_test_executable(guest_cairo_matrix_tests ${LIBS_INCLUDE_DIR}/tests/GuestCairoMatrix.cpp) + target_include_directories(guest_cairo_matrix_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_cairo_matrix_tests PRIVATE libcairo libc) + configure_windows_unwind(guest_cairo_matrix_tests) + add_test(NAME guest_cairo_matrix COMMAND guest_cairo_matrix_tests) + set_tests_properties(guest_cairo_matrix PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_cairo_matrix PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libcairo/Export.cpp b/core/libs/prx/libcairo/Export.cpp index db5ce4bbcd..15829b95e8 100644 --- a/core/libs/prx/libcairo/Export.cpp +++ b/core/libs/prx/libcairo/Export.cpp @@ -56,10 +56,6 @@ int APS5_VABI cairo_set_antialias() { NotImplemented_nid_no_patch(__func__); } -int APS5_VABI cairo_matrix_translate() { - NotImplemented_nid_no_patch(__func__); -} - int APS5_VABI cairo_rotate() { NotImplemented_nid_no_patch(__func__); } @@ -104,22 +100,10 @@ int APS5_VABI cairo_pattern_reference() { NotImplemented_nid_no_patch(__func__); } -int APS5_VABI cairo_matrix_scale() { - NotImplemented_nid_no_patch(__func__); -} - -int APS5_VABI cairo_matrix_invert() { - NotImplemented_nid_no_patch(__func__); -} - int APS5_VABI cairo_scaled_font_get_font_options() { NotImplemented_nid_no_patch(__func__); } -int APS5_VABI cairo_matrix_init_translate() { - NotImplemented_nid_no_patch(__func__); -} - int APS5_VABI cairo_surface_get_type() { NotImplemented_nid_no_patch(__func__); } @@ -184,10 +168,6 @@ int APS5_VABI cairo_rectangle() { NotImplemented_nid_no_patch(__func__); } -int APS5_VABI cairo_matrix_multiply() { - NotImplemented_nid_no_patch(__func__); -} - int APS5_VABI cairo_get_matrix() { NotImplemented_nid_no_patch(__func__); } @@ -264,10 +244,6 @@ int APS5_VABI cairo_destroy() { NotImplemented_nid_no_patch(__func__); } -int APS5_VABI cairo_matrix_init() { - NotImplemented_nid_no_patch(__func__); -} - int APS5_VABI cairo_clip_extents() { NotImplemented_nid_no_patch(__func__); } @@ -276,10 +252,6 @@ int APS5_VABI cairo_set_scaled_font() { NotImplemented_nid_no_patch(__func__); } -int APS5_VABI cairo_matrix_transform_point() { - NotImplemented_nid_no_patch(__func__); -} - int APS5_VABI cairo_image_surface_get_format() { NotImplemented_nid_no_patch(__func__); } @@ -292,10 +264,6 @@ int APS5_VABI cairo_fill() { NotImplemented_nid_no_patch(__func__); } -int APS5_VABI cairo_matrix_transform_distance() { - NotImplemented_nid_no_patch(__func__); -} - int APS5_VABI cairo_transform() { NotImplemented_nid_no_patch(__func__); } @@ -408,10 +376,6 @@ int APS5_VABI cairo_region_reference() { NotImplemented_nid_no_patch(__func__); } -int APS5_VABI cairo_matrix_init_identity() { - NotImplemented_nid_no_patch(__func__); -} - int APS5_VABI cairo_scaled_font_status() { NotImplemented_nid_no_patch(__func__); } @@ -596,10 +560,6 @@ int APS5_VABI cairo_get_operator() { NotImplemented_nid_no_patch(__func__); } -int APS5_VABI cairo_matrix_rotate() { - NotImplemented_nid_no_patch(__func__); -} - int APS5_VABI cairo_scaled_font_create() { NotImplemented_nid_no_patch(__func__); } diff --git a/core/libs/prx/libcairo/Matrix.cpp b/core/libs/prx/libcairo/Matrix.cpp new file mode 100644 index 0000000000..fcb0865d67 --- /dev/null +++ b/core/libs/prx/libcairo/Matrix.cpp @@ -0,0 +1,100 @@ +#include "prx/libcairo/include/Matrix.hpp" +#include + +namespace { + +CairoMatrix Product(const CairoMatrix& a, const CairoMatrix& b) { + return { + a.xx * b.xx + a.yx * b.xy, + a.xx * b.yx + a.yx * b.yy, + a.xy * b.xx + a.yy * b.xy, + a.xy * b.yx + a.yy * b.yy, + a.x0 * b.xx + a.y0 * b.xy + b.x0, + a.x0 * b.yx + a.y0 * b.yy + b.y0 + }; +} + +} + +extern "C" { + +void APS5_VABI cairo_matrix_init(CairoMatrix* matrix, double xx, double yx, double xy, double yy, double x0, double y0) { + *matrix = {xx, yx, xy, yy, x0, y0}; +} + +void APS5_VABI cairo_matrix_init_identity(CairoMatrix* matrix) { + *matrix = {1, 0, 0, 1, 0, 0}; +} + +void APS5_VABI cairo_matrix_init_translate(CairoMatrix* matrix, double tx, double ty) { + *matrix = {1, 0, 0, 1, tx, ty}; +} + +void APS5_VABI cairo_matrix_multiply(CairoMatrix* result, const CairoMatrix* a, const CairoMatrix* b) { + *result = Product(*a, *b); +} + +void APS5_VABI cairo_matrix_translate(CairoMatrix* matrix, double tx, double ty) { + const CairoMatrix translation{1, 0, 0, 1, tx, ty}; + *matrix = Product(translation, *matrix); +} + +void APS5_VABI cairo_matrix_scale(CairoMatrix* matrix, double sx, double sy) { + const CairoMatrix scaling{sx, 0, 0, sy, 0, 0}; + *matrix = Product(scaling, *matrix); +} + +void APS5_VABI cairo_matrix_rotate(CairoMatrix* matrix, double radians) { + const double c = std::cos(radians); + const double s = std::sin(radians); + const CairoMatrix rotation{c, s, -s, c, 0, 0}; + *matrix = Product(rotation, *matrix); +} + +int APS5_VABI cairo_matrix_invert(CairoMatrix* matrix) { + if (matrix->xy == 0 && matrix->yx == 0) { + matrix->x0 = -matrix->x0; + matrix->y0 = -matrix->y0; + if (matrix->xx != 1) { + if (matrix->xx == 0) return 5; + matrix->xx = 1 / matrix->xx; + matrix->x0 *= matrix->xx; + } + if (matrix->yy != 1) { + if (matrix->yy == 0) return 5; + matrix->yy = 1 / matrix->yy; + matrix->y0 *= matrix->yy; + } + return 0; + } + + const CairoMatrix original = *matrix; + const double determinant = original.xx * original.yy - original.yx * original.xy; + if (!std::isfinite(determinant) || determinant == 0) return 5; + const double inverse = 1 / determinant; + *matrix = { + original.yy * inverse, + -original.yx * inverse, + -original.xy * inverse, + original.xx * inverse, + (original.xy * original.y0 - original.yy * original.x0) * inverse, + (original.yx * original.x0 - original.xx * original.y0) * inverse + }; + return 0; +} + +void APS5_VABI cairo_matrix_transform_distance(const CairoMatrix* matrix, double* dx, double* dy) { + const double x = *dx; + const double y = *dy; + *dx = matrix->xx * x + matrix->xy * y; + *dy = matrix->yx * x + matrix->yy * y; +} + +void APS5_VABI cairo_matrix_transform_point(const CairoMatrix* matrix, double* x, double* y) { + const double oldX = *x; + const double oldY = *y; + *x = matrix->xx * oldX + matrix->xy * oldY + matrix->x0; + *y = matrix->yx * oldX + matrix->yy * oldY + matrix->y0; +} + +} diff --git a/core/libs/prx/libcairo/include/InventoryExports.hpp b/core/libs/prx/libcairo/include/InventoryExports.hpp index c5e5ee360e..4bbd829fad 100644 --- a/core/libs/prx/libcairo/include/InventoryExports.hpp +++ b/core/libs/prx/libcairo/include/InventoryExports.hpp @@ -17,7 +17,6 @@ int APS5_VABI cairo_image_surface_get_stride(); int APS5_VABI cairo_user_to_device(); int APS5_VABI cairo_reference(); int APS5_VABI cairo_set_antialias(); -int APS5_VABI cairo_matrix_translate(); int APS5_VABI cairo_rotate(); int APS5_VABI cairo_image_surface_create_from_png_stream(); int APS5_VABI cairo_get_fill_rule(); @@ -29,10 +28,7 @@ int APS5_VABI cairo_get_dash(); int APS5_VABI cairo_set_source_rgb(); int APS5_VABI cairo_scaled_font_destroy(); int APS5_VABI cairo_pattern_reference(); -int APS5_VABI cairo_matrix_scale(); -int APS5_VABI cairo_matrix_invert(); int APS5_VABI cairo_scaled_font_get_font_options(); -int APS5_VABI cairo_matrix_init_translate(); int APS5_VABI cairo_surface_get_type(); int APS5_VABI cairo_in_stroke(); int APS5_VABI cairo_scaled_font_get_ctm(); @@ -49,7 +45,6 @@ int APS5_VABI cairo_curve_to(); int APS5_VABI cairo_surface_reference(); int APS5_VABI cairo_copy_path(); int APS5_VABI cairo_rectangle(); -int APS5_VABI cairo_matrix_multiply(); int APS5_VABI cairo_get_matrix(); int APS5_VABI cairo_user_to_device_distance(); int APS5_VABI cairo_scaled_font_extents(); @@ -69,14 +64,11 @@ int APS5_VABI cairo_scaled_font_text_extents(); int APS5_VABI cairo_has_current_point(); int APS5_VABI cairo_pattern_create_linear(); int APS5_VABI cairo_destroy(); -int APS5_VABI cairo_matrix_init(); int APS5_VABI cairo_clip_extents(); int APS5_VABI cairo_set_scaled_font(); -int APS5_VABI cairo_matrix_transform_point(); int APS5_VABI cairo_image_surface_get_format(); int APS5_VABI cairo_region_create(); int APS5_VABI cairo_fill(); -int APS5_VABI cairo_matrix_transform_distance(); int APS5_VABI cairo_transform(); int APS5_VABI cairo_set_line_join(); int APS5_VABI cairo_pop_group_to_source(); @@ -105,7 +97,6 @@ int APS5_VABI cairo_surface_mark_dirty_rectangle(); int APS5_VABI cairo_append_path(); int APS5_VABI cairo_region_union_rectangle(); int APS5_VABI cairo_region_reference(); -int APS5_VABI cairo_matrix_init_identity(); int APS5_VABI cairo_scaled_font_status(); int APS5_VABI cairo_in_fill(); int APS5_VABI cairo_get_line_join(); @@ -152,7 +143,6 @@ int APS5_VABI cairo_get_current_point(); int APS5_VABI cairo_move_to(); int APS5_VABI cairo_scale(); int APS5_VABI cairo_get_operator(); -int APS5_VABI cairo_matrix_rotate(); int APS5_VABI cairo_scaled_font_create(); int APS5_VABI cairo_pattern_create_radial(); int APS5_VABI cairo_surface_get_content(); diff --git a/core/libs/prx/libcairo/include/Matrix.hpp b/core/libs/prx/libcairo/include/Matrix.hpp new file mode 100644 index 0000000000..7c49006ac1 --- /dev/null +++ b/core/libs/prx/libcairo/include/Matrix.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include "SceTypes.hpp" +#include + +struct CairoMatrix { + double xx; + double yx; + double xy; + double yy; + double x0; + double y0; +}; + +static_assert(sizeof(CairoMatrix) == 6 * sizeof(double)); +static_assert(offsetof(CairoMatrix, yx) == sizeof(double)); +static_assert(offsetof(CairoMatrix, x0) == 4 * sizeof(double)); + +extern "C" { +void APS5_VABI cairo_matrix_init(CairoMatrix* matrix, double xx, double yx, double xy, double yy, double x0, double y0); +void APS5_VABI cairo_matrix_init_identity(CairoMatrix* matrix); +void APS5_VABI cairo_matrix_init_translate(CairoMatrix* matrix, double tx, double ty); +void APS5_VABI cairo_matrix_translate(CairoMatrix* matrix, double tx, double ty); +void APS5_VABI cairo_matrix_scale(CairoMatrix* matrix, double sx, double sy); +void APS5_VABI cairo_matrix_rotate(CairoMatrix* matrix, double radians); +int APS5_VABI cairo_matrix_invert(CairoMatrix* matrix); +void APS5_VABI cairo_matrix_multiply(CairoMatrix* result, const CairoMatrix* a, const CairoMatrix* b); +void APS5_VABI cairo_matrix_transform_point(const CairoMatrix* matrix, double* x, double* y); +void APS5_VABI cairo_matrix_transform_distance(const CairoMatrix* matrix, double* dx, double* dy); +} diff --git a/core/libs/prx/libcairo/libcairo.hpp b/core/libs/prx/libcairo/libcairo.hpp index d3d5da7295..1370182733 100644 --- a/core/libs/prx/libcairo/libcairo.hpp +++ b/core/libs/prx/libcairo/libcairo.hpp @@ -1,3 +1,4 @@ #pragma once #include "prx/libcairo/include/InventoryExports.hpp" +#include "prx/libcairo/include/Matrix.hpp" diff --git a/core/libs/prx/libfreetype/Export.cpp b/core/libs/prx/libfreetype/Export.cpp index b8064f6b90..705f5e9e4d 100644 --- a/core/libs/prx/libfreetype/Export.cpp +++ b/core/libs/prx/libfreetype/Export.cpp @@ -5,128 +5,128 @@ extern "C" { -int APS5_VABI libfreetypeUnknown__plus_AF9KH7aZbc_nid_no_patch() { +int APS5_VABI libfreetypePaletteDataGet_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("+AF9KH7aZbc", libfreetypeUnknown__plus_AF9KH7aZbc_nid_no_patch); +APS5_EXPORT("+AF9KH7aZbc", libfreetypePaletteDataGet_nid_no_patch); -int APS5_VABI libfreetypeUnknown__plus_sDVkl3aAR8_nid_no_patch() { +int APS5_VABI libfreetypeGetVarAxisFlags_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("+sDVkl3aAR8", libfreetypeUnknown__plus_sDVkl3aAR8_nid_no_patch); +APS5_EXPORT("+sDVkl3aAR8", libfreetypeGetVarAxisFlags_nid_no_patch); -int APS5_VABI libfreetypeUnknown_iuebdW9Mi0U_nid_no_patch() { +int APS5_VABI libfreetypeSetDefaultProperties_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("iuebdW9Mi0U", libfreetypeUnknown_iuebdW9Mi0U_nid_no_patch); +APS5_EXPORT("iuebdW9Mi0U", libfreetypeSetDefaultProperties_nid_no_patch); -int APS5_VABI libfreetypeUnknown_qCyybpSyZzI_nid_no_patch() { +int APS5_VABI libfreetypePaletteSelect_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("qCyybpSyZzI", libfreetypeUnknown_qCyybpSyZzI_nid_no_patch); +APS5_EXPORT("qCyybpSyZzI", libfreetypePaletteSelect_nid_no_patch); -int APS5_VABI libfreetypeUnknown_ypZKJwPSVgQ_nid_no_patch() { +int APS5_VABI libfreetypeGetColorGlyphLayer_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("ypZKJwPSVgQ", libfreetypeUnknown_ypZKJwPSVgQ_nid_no_patch); +APS5_EXPORT("ypZKJwPSVgQ", libfreetypeGetColorGlyphLayer_nid_no_patch); } extern "C" { -int APS5_VABI freetypeLegacyUnknown_4kVFLShKGkQ_nid_no_patch() { +int APS5_VABI freetypeLegacyGetBDFProperty_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("4kVFLShKGkQ", freetypeLegacyUnknown_4kVFLShKGkQ_nid_no_patch); +APS5_EXPORT("4kVFLShKGkQ", freetypeLegacyGetBDFProperty_nid_no_patch); -int APS5_VABI freetypeLegacyUnknown_C3_minus_qsnriefE_nid_no_patch() { +int APS5_VABI freetypeLegacyBitmapBlend_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("C3-qsnriefE", freetypeLegacyUnknown_C3_minus_qsnriefE_nid_no_patch); +APS5_EXPORT("C3-qsnriefE", freetypeLegacyBitmapBlend_nid_no_patch); -int APS5_VABI freetypeLegacyUnknown_DYbLD_minus_s1oiQ_nid_no_patch() { +int APS5_VABI freetypeLegacyFtcManagerNew_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("DYbLD-s1oiQ", freetypeLegacyUnknown_DYbLD_minus_s1oiQ_nid_no_patch); +APS5_EXPORT("DYbLD-s1oiQ", freetypeLegacyFtcManagerNew_nid_no_patch); -int APS5_VABI freetypeLegacyUnknown_EPNrOPQ5B14_nid_no_patch() { +int APS5_VABI freetypeLegacyStreamOpenGzip_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("EPNrOPQ5B14", freetypeLegacyUnknown_EPNrOPQ5B14_nid_no_patch); +APS5_EXPORT("EPNrOPQ5B14", freetypeLegacyStreamOpenGzip_nid_no_patch); -int APS5_VABI freetypeLegacyUnknown_JuHk_minus_xDmRmI_nid_no_patch() { +int APS5_VABI freetypeLegacyStreamOpenBzip2_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("JuHk-xDmRmI", freetypeLegacyUnknown_JuHk_minus_xDmRmI_nid_no_patch); +APS5_EXPORT("JuHk-xDmRmI", freetypeLegacyStreamOpenBzip2_nid_no_patch); -int APS5_VABI freetypeLegacyUnknown_K9ZOPLPOqbY_nid_no_patch() { +int APS5_VABI freetypeLegacyGetMMWeightVector_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("K9ZOPLPOqbY", freetypeLegacyUnknown_K9ZOPLPOqbY_nid_no_patch); +APS5_EXPORT("K9ZOPLPOqbY", freetypeLegacyGetMMWeightVector_nid_no_patch); -int APS5_VABI freetypeLegacyUnknown_KVRW5HPnbgU_nid_no_patch() { +int APS5_VABI freetypeLegacyGetBDFCharsetID_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("KVRW5HPnbgU", freetypeLegacyUnknown_KVRW5HPnbgU_nid_no_patch); +APS5_EXPORT("KVRW5HPnbgU", freetypeLegacyGetBDFCharsetID_nid_no_patch); -int APS5_VABI freetypeLegacyUnknown_MDsMjUGulyY_nid_no_patch() { +int APS5_VABI freetypeLegacyGetSfntLangTag_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("MDsMjUGulyY", freetypeLegacyUnknown_MDsMjUGulyY_nid_no_patch); +APS5_EXPORT("MDsMjUGulyY", freetypeLegacyGetSfntLangTag_nid_no_patch); -int APS5_VABI freetypeLegacyUnknown_NbFQEYieSS0_nid_no_patch() { +int APS5_VABI freetypeLegacyPaletteSetForegroundColor_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("NbFQEYieSS0", freetypeLegacyUnknown_NbFQEYieSS0_nid_no_patch); +APS5_EXPORT("NbFQEYieSS0", freetypeLegacyPaletteSetForegroundColor_nid_no_patch); -int APS5_VABI freetypeLegacyUnknown_OGVR5us9otU_nid_no_patch() { +int APS5_VABI freetypeLegacySetMMWeightVector_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("OGVR5us9otU", freetypeLegacyUnknown_OGVR5us9otU_nid_no_patch); +APS5_EXPORT("OGVR5us9otU", freetypeLegacySetMMWeightVector_nid_no_patch); -int APS5_VABI freetypeLegacyUnknown_PS_minus_iwpPa2tc_nid_no_patch() { +int APS5_VABI freetypeLegacyGetMMBlendCoordinates_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("PS-iwpPa2tc", freetypeLegacyUnknown_PS_minus_iwpPa2tc_nid_no_patch); +APS5_EXPORT("PS-iwpPa2tc", freetypeLegacyGetMMBlendCoordinates_nid_no_patch); -int APS5_VABI freetypeLegacyUnknown_V476bsxEylk_nid_no_patch() { +int APS5_VABI freetypeLegacySetNamedInstance_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("V476bsxEylk", freetypeLegacyUnknown_V476bsxEylk_nid_no_patch); +APS5_EXPORT("V476bsxEylk", freetypeLegacySetNamedInstance_nid_no_patch); -int APS5_VABI freetypeLegacyUnknown_VKuZXhyjtoo_nid_no_patch() { +int APS5_VABI freetypeLegacyFaceProperties_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("VKuZXhyjtoo", freetypeLegacyUnknown_VKuZXhyjtoo_nid_no_patch); +APS5_EXPORT("VKuZXhyjtoo", freetypeLegacyFaceProperties_nid_no_patch); -int APS5_VABI freetypeLegacyUnknown_Xxtq_plus_Qr5lzI_nid_no_patch() { +int APS5_VABI freetypeLegacyLibrarySetLcdGeometry_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("Xxtq+Qr5lzI", freetypeLegacyUnknown_Xxtq_plus_Qr5lzI_nid_no_patch); +APS5_EXPORT("Xxtq+Qr5lzI", freetypeLegacyLibrarySetLcdGeometry_nid_no_patch); -int APS5_VABI freetypeLegacyUnknown_boXwTAWsas0_nid_no_patch() { +int APS5_VABI freetypeLegacyErrorString_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("boXwTAWsas0", freetypeLegacyUnknown_boXwTAWsas0_nid_no_patch); +APS5_EXPORT("boXwTAWsas0", freetypeLegacyErrorString_nid_no_patch); -int APS5_VABI freetypeLegacyUnknown_ctqkAfkma_minus_k_nid_no_patch() { +int APS5_VABI freetypeLegacyStreamOpenLZW_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("ctqkAfkma-k", freetypeLegacyUnknown_ctqkAfkma_minus_k_nid_no_patch); +APS5_EXPORT("ctqkAfkma-k", freetypeLegacyStreamOpenLZW_nid_no_patch); -int APS5_VABI freetypeLegacyUnknown_hKvvpNeUdKo_nid_no_patch() { +int APS5_VABI freetypeLegacyGzipUncompress_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("hKvvpNeUdKo", freetypeLegacyUnknown_hKvvpNeUdKo_nid_no_patch); +APS5_EXPORT("hKvvpNeUdKo", freetypeLegacyGzipUncompress_nid_no_patch); -int APS5_VABI freetypeLegacyUnknown_sZq9oSwMGSs_nid_no_patch() { +int APS5_VABI freetypeLegacyNewGlyph_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("sZq9oSwMGSs", freetypeLegacyUnknown_sZq9oSwMGSs_nid_no_patch); +APS5_EXPORT("sZq9oSwMGSs", freetypeLegacyNewGlyph_nid_no_patch); -int APS5_VABI freetypeLegacyUnknown_suWwpZzQl00_nid_no_patch() { +int APS5_VABI freetypeLegacyGetWinFNTHeader_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("suWwpZzQl00", freetypeLegacyUnknown_suWwpZzQl00_nid_no_patch); +APS5_EXPORT("suWwpZzQl00", freetypeLegacyGetWinFNTHeader_nid_no_patch); } diff --git a/core/libs/prx/libfreetype/include/InventoryExports.hpp b/core/libs/prx/libfreetype/include/InventoryExports.hpp index 032458af25..26bea5f451 100644 --- a/core/libs/prx/libfreetype/include/InventoryExports.hpp +++ b/core/libs/prx/libfreetype/include/InventoryExports.hpp @@ -4,10 +4,10 @@ extern "C" { -int APS5_VABI libfreetypeUnknown__plus_AF9KH7aZbc_nid_no_patch(); -int APS5_VABI libfreetypeUnknown__plus_sDVkl3aAR8_nid_no_patch(); -int APS5_VABI libfreetypeUnknown_iuebdW9Mi0U_nid_no_patch(); -int APS5_VABI libfreetypeUnknown_qCyybpSyZzI_nid_no_patch(); -int APS5_VABI libfreetypeUnknown_ypZKJwPSVgQ_nid_no_patch(); +int APS5_VABI libfreetypePaletteDataGet_nid_no_patch(); +int APS5_VABI libfreetypeGetVarAxisFlags_nid_no_patch(); +int APS5_VABI libfreetypeSetDefaultProperties_nid_no_patch(); +int APS5_VABI libfreetypePaletteSelect_nid_no_patch(); +int APS5_VABI libfreetypeGetColorGlyphLayer_nid_no_patch(); } diff --git a/core/libs/prx/libfreetype/include/LegacyExports.hpp b/core/libs/prx/libfreetype/include/LegacyExports.hpp index a3aaf7c301..7829df1fa4 100644 --- a/core/libs/prx/libfreetype/include/LegacyExports.hpp +++ b/core/libs/prx/libfreetype/include/LegacyExports.hpp @@ -4,24 +4,24 @@ extern "C" { -int APS5_VABI freetypeLegacyUnknown_4kVFLShKGkQ_nid_no_patch(); -int APS5_VABI freetypeLegacyUnknown_C3_minus_qsnriefE_nid_no_patch(); -int APS5_VABI freetypeLegacyUnknown_DYbLD_minus_s1oiQ_nid_no_patch(); -int APS5_VABI freetypeLegacyUnknown_EPNrOPQ5B14_nid_no_patch(); -int APS5_VABI freetypeLegacyUnknown_JuHk_minus_xDmRmI_nid_no_patch(); -int APS5_VABI freetypeLegacyUnknown_K9ZOPLPOqbY_nid_no_patch(); -int APS5_VABI freetypeLegacyUnknown_KVRW5HPnbgU_nid_no_patch(); -int APS5_VABI freetypeLegacyUnknown_MDsMjUGulyY_nid_no_patch(); -int APS5_VABI freetypeLegacyUnknown_NbFQEYieSS0_nid_no_patch(); -int APS5_VABI freetypeLegacyUnknown_OGVR5us9otU_nid_no_patch(); -int APS5_VABI freetypeLegacyUnknown_PS_minus_iwpPa2tc_nid_no_patch(); -int APS5_VABI freetypeLegacyUnknown_V476bsxEylk_nid_no_patch(); -int APS5_VABI freetypeLegacyUnknown_VKuZXhyjtoo_nid_no_patch(); -int APS5_VABI freetypeLegacyUnknown_Xxtq_plus_Qr5lzI_nid_no_patch(); -int APS5_VABI freetypeLegacyUnknown_boXwTAWsas0_nid_no_patch(); -int APS5_VABI freetypeLegacyUnknown_ctqkAfkma_minus_k_nid_no_patch(); -int APS5_VABI freetypeLegacyUnknown_hKvvpNeUdKo_nid_no_patch(); -int APS5_VABI freetypeLegacyUnknown_sZq9oSwMGSs_nid_no_patch(); -int APS5_VABI freetypeLegacyUnknown_suWwpZzQl00_nid_no_patch(); +int APS5_VABI freetypeLegacyGetBDFProperty_nid_no_patch(); +int APS5_VABI freetypeLegacyBitmapBlend_nid_no_patch(); +int APS5_VABI freetypeLegacyFtcManagerNew_nid_no_patch(); +int APS5_VABI freetypeLegacyStreamOpenGzip_nid_no_patch(); +int APS5_VABI freetypeLegacyStreamOpenBzip2_nid_no_patch(); +int APS5_VABI freetypeLegacyGetMMWeightVector_nid_no_patch(); +int APS5_VABI freetypeLegacyGetBDFCharsetID_nid_no_patch(); +int APS5_VABI freetypeLegacyGetSfntLangTag_nid_no_patch(); +int APS5_VABI freetypeLegacyPaletteSetForegroundColor_nid_no_patch(); +int APS5_VABI freetypeLegacySetMMWeightVector_nid_no_patch(); +int APS5_VABI freetypeLegacyGetMMBlendCoordinates_nid_no_patch(); +int APS5_VABI freetypeLegacySetNamedInstance_nid_no_patch(); +int APS5_VABI freetypeLegacyFaceProperties_nid_no_patch(); +int APS5_VABI freetypeLegacyLibrarySetLcdGeometry_nid_no_patch(); +int APS5_VABI freetypeLegacyErrorString_nid_no_patch(); +int APS5_VABI freetypeLegacyStreamOpenLZW_nid_no_patch(); +int APS5_VABI freetypeLegacyGzipUncompress_nid_no_patch(); +int APS5_VABI freetypeLegacyNewGlyph_nid_no_patch(); +int APS5_VABI freetypeLegacyGetWinFNTHeader_nid_no_patch(); } diff --git a/core/libs/prx/libkernel/Aio/src/Aio.cpp b/core/libs/prx/libkernel/Aio/src/Aio.cpp index e228a480e5..90b425bcc1 100644 --- a/core/libs/prx/libkernel/Aio/src/Aio.cpp +++ b/core/libs/prx/libkernel/Aio/src/Aio.cpp @@ -252,6 +252,28 @@ int ValidateIds(const std::int32_t* id, std::int32_t num, const void* out) { return 0; } +int CopyStateOut(std::int32_t* out, std::int32_t state) { + return GuestCopyOut_nid_no_patch(out, &state, sizeof(state)) ? 0 : SCE_KERNEL_ERROR_EFAULT; +} + +int ApplyToIds(const std::int32_t* guestIds, std::int32_t num, std::int32_t* out, const char* function, bool requireFinished, + std::int32_t (*operation)(std::int32_t)) { + const int error = ValidateIds(guestIds, num, out); + if (error != 0) return error; + const std::size_t bytes = static_cast(num) * sizeof(std::int32_t); + std::array ids{}; + if (!GuestCopyIn_nid_no_patch(ids.data(), guestIds, bytes)) return SCE_KERNEL_ERROR_EFAULT; + std::array states{}; + { + std::lock_guard lock(g_mutex); + if (requireFinished) { + for (std::int32_t i = 0; i < num; ++i) RequireFinishedLocked(ids[i], function); + } + for (std::int32_t i = 0; i < num; ++i) states[i] = operation(ids[i]); + } + return GuestCopyOut_nid_no_patch(out, states.data(), bytes) ? 0 : SCE_KERNEL_ERROR_EFAULT; +} + } extern "C" { @@ -271,36 +293,32 @@ void AioAdvanceIdentityForTest_nid_no_patch(std::int64_t next) { int APS5_VABI sceKernelAioDeleteRequest(int32_t id, int32_t* ret) { if (ret == nullptr) return SCE_KERNEL_ERROR_EFAULT; - std::lock_guard lock(g_mutex); - RequireFinishedLocked(id, __func__); - *ret = DeleteLocked(id); - return 0; + std::int32_t result; + { + std::lock_guard lock(g_mutex); + RequireFinishedLocked(id, __func__); + result = DeleteLocked(id); + } + return CopyStateOut(ret, result); } int APS5_VABI sceKernelAioDeleteRequests(int32_t* id, int32_t num, int32_t* ret) { - const int error = ValidateIds(id, num, ret); - if (error != 0) return error; - std::lock_guard lock(g_mutex); - for (int32_t i = 0; i < num; ++i) RequireFinishedLocked(id[i], __func__); - for (int32_t i = 0; i < num; ++i) ret[i] = DeleteLocked(id[i]); - return 0; + return ApplyToIds(id, num, ret, __func__, true, DeleteLocked); } int APS5_VABI sceKernelAioCancelRequest(int32_t id, int32_t* state) { if (state == nullptr) return SCE_KERNEL_ERROR_EFAULT; - std::lock_guard lock(g_mutex); - RequireFinishedLocked(id, __func__); - *state = CancelLocked(id); - return 0; + std::int32_t result; + { + std::lock_guard lock(g_mutex); + RequireFinishedLocked(id, __func__); + result = CancelLocked(id); + } + return CopyStateOut(state, result); } int APS5_VABI sceKernelAioCancelRequests(int32_t* id, int32_t num, int32_t* state) { - const int error = ValidateIds(id, num, state); - if (error != 0) return error; - std::lock_guard lock(g_mutex); - for (int32_t i = 0; i < num; ++i) RequireFinishedLocked(id[i], __func__); - for (int32_t i = 0; i < num; ++i) state[i] = CancelLocked(id[i]); - return 0; + return ApplyToIds(id, num, state, __func__, true, CancelLocked); } int APS5_VABI sceKernelAioInitializeImpl(void* param, int32_t size) { @@ -349,9 +367,12 @@ int APS5_VABI sceKernelAioSubmitWriteCommandsMultiple(KernelAioRwRequest* req, i int APS5_VABI sceKernelAioPollRequest(int32_t id, int32_t* state) { if (state == nullptr) return SCE_KERNEL_ERROR_EFAULT; - std::lock_guard lock(g_mutex); - *state = ReportLocked(id); - return 0; + std::int32_t result; + { + std::lock_guard lock(g_mutex); + result = ReportLocked(id); + } + return CopyStateOut(state, result); } int APS5_VABI sceKernelAioWaitRequests(int32_t* id, int32_t num, int32_t* state, uint32_t mode, uint32_t* usec) { @@ -423,11 +444,7 @@ int APS5_VABI sceKernelAioWaitRequest(int32_t id, int32_t* state, uint32_t* usec } int APS5_VABI sceKernelAioPollRequests(int32_t* id, int32_t num, int32_t* state) { - const int error = ValidateIds(id, num, state); - if (error != 0) return error; - std::lock_guard lock(g_mutex); - for (int32_t i = 0; i < num; ++i) state[i] = ReportLocked(id[i]); - return 0; + return ApplyToIds(id, num, state, __func__, false, ReportLocked); } } diff --git a/core/libs/prx/libkernel/AppMetadata/src/AppMetadata.cpp b/core/libs/prx/libkernel/AppMetadata/src/AppMetadata.cpp index bae222a7c4..270a4d24c2 100644 --- a/core/libs/prx/libkernel/AppMetadata/src/AppMetadata.cpp +++ b/core/libs/prx/libkernel/AppMetadata/src/AppMetadata.cpp @@ -56,8 +56,26 @@ void ensureTitleLoaded() { g_paramSfoAttribute = parsed.paramSfoAttribute; std::memcpy(g_userDefinedParams, parsed.userDefinedParams, sizeof(g_userDefinedParams)); g_titleLoaded = true; + if (g_titleId[0] != '\0') { + const auto app0 = ResolvePath_nid_no_patch("/app0"); + AddPathAlias_nid_no_patch((std::string("/mnt/sandbox/") + g_titleId + "_000/app0").c_str(), app0.string().c_str()); + } } +const bool g_sandboxApp0Mounted = [] { + try { + const auto run = std::filesystem::current_path(); + const auto metadata = run / "app0" / "sce_sys"; + if (!std::filesystem::exists(metadata / "param.json") && !std::filesystem::exists(metadata / "param.sfo")) return false; + const auto parsed = parseParams(metadata / "param.json", metadata / "param.sfo"); + if (parsed.titleId.empty()) return false; + AddPathAlias_nid_no_patch((std::string("/mnt/sandbox/") + parsed.titleId + "_000/app0").c_str(), (run / "app0").string().c_str()); + return true; + } catch (const std::exception&) { + return false; + } +}(); + const bool g_downloadDataMounted = [] { try { const auto run = std::filesystem::current_path(); diff --git a/core/libs/prx/libkernel/Apr/src/Apr.cpp b/core/libs/prx/libkernel/Apr/src/Apr.cpp index 0ba7bbafeb..e79d6c5e09 100644 --- a/core/libs/prx/libkernel/Apr/src/Apr.cpp +++ b/core/libs/prx/libkernel/Apr/src/Apr.cpp @@ -41,6 +41,7 @@ namespace { constexpr int GUEST_ENOENT = 2; +constexpr int GUEST_EFAULT = 14; constexpr int GUEST_EINVAL = 22; struct AprFile { @@ -416,7 +417,7 @@ void _requirePrt(AmmState& state, std::uint64_t address, std::uint64_t size) { void _zeroPrt(std::uint64_t address, std::uint64_t size) { constexpr int GpuRead = 0x10; void* target = reinterpret_cast(address); - if (DoMapAnon(&target, size, GpuRead, GuestMapFixed) != 0) throw std::runtime_error("AMM: cannot map the unbacked PRT pages"); + if (DoMapZero(&target, size, GpuRead, GuestMapFixed) != 0) throw std::runtime_error("AMM: cannot map the unbacked PRT pages"); } void _ammUnmap(const Apr::AmmUnmapCommand& command) { @@ -861,13 +862,16 @@ int APS5_VABI sceKernelAprResolveFilepathsWithPrefixToIds(const char* prefix, co int APS5_VABI sceKernelAprGetFileSize(uint32_t id, uint64_t* size) { if (!size) return _fail(GUEST_EINVAL); - *size = _file(id).size; + const std::uint64_t bytes = _file(id).size; + if (!GuestCopyOut_nid_no_patch(size, &bytes, sizeof(bytes))) return _fail(GUEST_EFAULT); return 0; } int APS5_VABI sceKernelAprGetFileStat(uint32_t id, FileStat* stat) { if (!stat) return _fail(GUEST_EINVAL); - File::FillFileStat(_file(id).path, stat); + FileStat result{}; + File::FillFileStat(_file(id).path, &result); + if (!GuestCopyOut_nid_no_patch(stat, &result, sizeof(result))) return _fail(GUEST_EFAULT); return 0; } diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 02907b0392..1f193b5ec3 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -10,6 +10,7 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Equeue/src/Kqueue.cpp ${CMAKE_CURRENT_SOURCE_DIR}/File/src/DirectoryDescriptor.cpp ${CMAKE_CURRENT_SOURCE_DIR}/File/src/FileLock.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/File/src/FilesystemForwards.cpp ${CMAKE_CURRENT_SOURCE_DIR}/File/src/NativeStat.cpp ${CMAKE_CURRENT_SOURCE_DIR}/File/src/Terminal.cpp ${CMAKE_CURRENT_SOURCE_DIR}/File/src/Open.cpp @@ -122,6 +123,15 @@ if(BUILD_TESTING) set_tests_properties(kernel_error PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_direct_memory_for_app_tests ${LIBS_INCLUDE_DIR}/tests/GuestDirectMemoryForApp.cpp) + target_include_directories(guest_direct_memory_for_app_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_direct_memory_for_app_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_direct_memory_for_app_tests) + add_test(NAME guest_direct_memory_for_app COMMAND guest_direct_memory_for_app_tests) + if(WIN32) + set_tests_properties(guest_direct_memory_for_app PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_apr_read_tests ${LIBS_INCLUDE_DIR}/tests/GuestAprRead.cpp) target_include_directories(guest_apr_read_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_apr_read_tests PRIVATE libkernel libc) @@ -219,6 +229,16 @@ if(BUILD_TESTING) set_tests_properties(guest_posix_sem_init guest_posix_sem_post PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_sem_getvalue_uninit_tests ${LIBS_INCLUDE_DIR}/tests/GuestSemGetvalueUninit.cpp) + target_include_directories(guest_sem_getvalue_uninit_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_sem_getvalue_uninit_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_sem_getvalue_uninit_tests) + add_test(NAME guest_sem_getvalue_uninit COMMAND guest_sem_getvalue_uninit_tests) + set_tests_properties(guest_sem_getvalue_uninit PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_sem_getvalue_uninit PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_posix_named_sem_tests ${LIBS_INCLUDE_DIR}/tests/GuestPosixNamedSem.cpp) target_include_directories(guest_posix_named_sem_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_posix_named_sem_tests PRIVATE libkernel libc) @@ -601,6 +621,13 @@ if(BUILD_TESTING) set_tests_properties(guest_kernel_is_stack PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + add_test_executable(guest_main_stack_tests ${LIBS_INCLUDE_DIR}/tests/GuestMainStack.cpp) + target_include_directories(guest_main_stack_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_main_stack_tests PRIVATE libkernel libc) + add_test(NAME guest_main_stack COMMAND guest_main_stack_tests) + set_tests_properties(guest_main_stack PROPERTIES TIMEOUT 20) + endif() if(APPLE) add_test_executable(guest_main_stack_move_tests ${LIBS_INCLUDE_DIR}/tests/GuestMainStackMove.cpp) target_include_directories(guest_main_stack_move_tests PRIVATE ${LIBS_INCLUDE_DIR}) @@ -1226,6 +1253,12 @@ if(BUILD_TESTING) if(WIN32) set_tests_properties(guest_signals PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + else() + add_test_executable(guest_broken_pipe_tests ${LIBS_INCLUDE_DIR}/tests/GuestBrokenPipe.cpp) + target_include_directories(guest_broken_pipe_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_broken_pipe_tests PRIVATE libkernel libc) + add_test(NAME guest_broken_pipe COMMAND guest_broken_pipe_tests) + set_tests_properties(guest_broken_pipe PROPERTIES TIMEOUT 20) endif() add_test_executable(guest_signal_handler_mask_tests ${LIBS_INCLUDE_DIR}/tests/GuestSignalHandlerMask.cpp) target_include_directories(guest_signal_handler_mask_tests PRIVATE ${LIBS_INCLUDE_DIR}) @@ -1254,8 +1287,9 @@ if(BUILD_TESTING) add_test(NAME guest_itimer_pending_default COMMAND guest_itimer_tests pending-default) set_tests_properties(guest_itimer_pending_default PROPERTIES TIMEOUT 20 PASS_REGULAR_EXPRESSION "SIGALRM: a pending SIGALRM was unblocked with the default disposition; the process terminates") + add_test(NAME guest_itimer_short_expiry COMMAND guest_itimer_tests short-expiry) if(WIN32) - set_tests_properties(guest_itimer guest_itimer_pending_default PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_itimer guest_itimer_pending_default guest_itimer_short_expiry PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test_executable(guest_parent_process_tests ${LIBS_INCLUDE_DIR}/tests/GuestParentProcess.cpp) @@ -1692,6 +1726,15 @@ if(BUILD_TESTING) set_tests_properties(guest_thread_sanitizer PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$;PATH=path_list_prepend:$") endif() + add_test_executable(guest_sanitizer_replace_tables_tests ${LIBS_INCLUDE_DIR}/tests/GuestSanitizerReplaceTables.cpp) + target_include_directories(guest_sanitizer_replace_tables_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_sanitizer_replace_tables_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_sanitizer_replace_tables_tests) + add_test(NAME guest_sanitizer_replace_tables COMMAND guest_sanitizer_replace_tables_tests) + if(WIN32) + set_tests_properties(guest_sanitizer_replace_tables PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_system_command_tests ${LIBS_INCLUDE_DIR}/tests/GuestSystemCommand.cpp) target_include_directories(guest_system_command_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_system_command_tests PRIVATE libkernel libc) @@ -2264,6 +2307,16 @@ if(BUILD_TESTING) set_tests_properties(guest_truncate_extend PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_posix_exit_tests ${LIBS_INCLUDE_DIR}/tests/GuestPosixExit.cpp) + target_include_directories(guest_posix_exit_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_posix_exit_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_posix_exit_tests) + add_test(NAME guest_posix_exit COMMAND guest_posix_exit_tests) + set_tests_properties(guest_posix_exit PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_posix_exit PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_fstatfs_tests ${LIBS_INCLUDE_DIR}/tests/GuestFstatfs.cpp) target_include_directories(guest_fstatfs_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_fstatfs_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp index 673d7f9a3d..c562b3872d 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp @@ -36,6 +36,13 @@ #include #include #include +#elif defined(__APPLE__) +#include "prx/libc/include/specifics/linux/ElfTypes.hpp" +#include +#include +#include +#include +#include #else #include #include "prx/libc/include/WindowsDescriptor.hpp" @@ -241,6 +248,31 @@ void* MapAtOrAbove(std::uintptr_t start, size_t len, int prot, size_t alignment) } throw std::runtime_error("Hinted mmap kept racing with other mappings"); } +#elif defined(__APPLE__) +void* MapAtOrAbove(std::uintptr_t start, size_t len, int prot, size_t alignment) { + auto address = static_cast(start); + const auto result = mach_vm_map(mach_task_self(), &address, len, alignment - 1, VM_FLAGS_ANYWHERE, MEMORY_OBJECT_NULL, 0, FALSE, prot, VM_PROT_ALL, VM_INHERIT_DEFAULT); + if (result != KERN_SUCCESS) throw std::runtime_error(std::string("Hinted mach_vm_map failed: ") + mach_error_string(result)); + if (address < start) { + mach_vm_deallocate(mach_task_self(), address, len); + throw std::runtime_error("No free range above the mapping address hint"); + } + return reinterpret_cast(address); +} + +void* MapFixedNoReplace(void* addr, size_t len, int prot) { + auto address = reinterpret_cast(addr); + const auto result = mach_vm_map(mach_task_self(), &address, len, 0, VM_FLAGS_FIXED, MEMORY_OBJECT_NULL, 0, FALSE, prot, VM_PROT_ALL, VM_INHERIT_DEFAULT); + if (result == KERN_NO_SPACE) { + errno = EEXIST; + return MAP_FAILED; + } + if (result != KERN_SUCCESS) { + errno = ENOMEM; + return MAP_FAILED; + } + return reinterpret_cast(address); +} #endif void ValidateLength(size_t len) { @@ -398,6 +430,11 @@ public: std::snprintf(message, sizeof(message), "create direct memory backing of 0x%llx bytes (%llu MiB)", static_cast(size), static_cast((size + 0xFFFFF) >> 20)); throw std::system_error(error, std::system_category(), message); } +#elif defined(__APPLE__) + static_cast(start); + memory_object_size_t size = bytes; + const auto result = mach_make_memory_entry_64(mach_task_self(), &size, 0, MAP_MEM_NAMED_CREATE | VM_PROT_ALL, &entry, MACH_PORT_NULL); + if (result != KERN_SUCCESS) throw std::runtime_error(std::string("create direct memory backing: ") + mach_error_string(result)); #else file = PhysicalFile(); fileOffset = start; @@ -430,6 +467,8 @@ public: } else if (!GuestArena::GuestArenaForgetReusableSection_nid_no_patch(section) || !SectionPool::Get().Keep(section, bytes)) { CloseHandle(section); } +#elif defined(__APPLE__) + mach_port_deallocate(mach_task_self(), entry); #else if (hostWriteView != nullptr) ::munmap(hostWriteView, bytes); #endif @@ -445,6 +484,10 @@ public: GuestArena::GuestArenaCommit_nid_postfix(reinterpret_cast(address), bytes, 0x04, PS5_PAGE_SIZE); #elif defined(_WIN32) GuestArena::GuestArenaMapDirect_nid_postfix(reinterpret_cast(address), bytes, section, offset, WinProtFromPosix(protection)); +#elif defined(__APPLE__) + auto target = static_cast(address); + const auto result = mach_vm_map(mach_task_self(), &target, bytes, 0, VM_FLAGS_FIXED | VM_FLAGS_OVERWRITE, entry, offset, FALSE, protection, VM_PROT_ALL, VM_INHERIT_SHARE); + if (result != KERN_SUCCESS) throw std::runtime_error(std::string("map direct memory backing: ") + mach_error_string(result)); #else if (::mmap(reinterpret_cast(address), bytes, protection, MAP_SHARED | MAP_FIXED, file, static_cast(fileOffset + offset)) == MAP_FAILED) throw std::system_error(errno, std::generic_category(), "map direct memory backing"); #endif @@ -456,6 +499,8 @@ private: #if defined(__EMSCRIPTEN__) #elif defined(_WIN32) HANDLE section = nullptr; +#elif defined(__APPLE__) + mach_port_t entry = MACH_PORT_NULL; #else int file = -1; void* hostWriteView = nullptr; @@ -644,7 +689,7 @@ void ValidatePhysicalRange(std::uint64_t phys, std::size_t len) { throw std::invalid_argument(message); } -#if defined(__linux__) +#if defined(__linux__) || defined(__APPLE__) void WatchMapping(std::uintptr_t address, std::size_t len) { GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(reinterpret_cast(address), len); const auto end = address + len; @@ -771,7 +816,7 @@ void AddMapping(std::uintptr_t address, std::size_t len, std::uint64_t phys, int if (records) InsertMapping(address + (cursor - phys), DirectMapping{address + (cursor - phys) + bytes, cursor, it->second.backing->MemoryType(), nativeProt, it->second.backing}); cursor = runEnd; } -#if defined(__linux__) +#if defined(__linux__) || defined(__APPLE__) WatchMapping(address, len); #endif } @@ -808,7 +853,7 @@ void RequireAliasesUnpinned(GuestAllocations::Mutation& mutation, std::uint64_t #endif } -bool RemapFixedIntoRegistered(GuestAllocations::Mutation& mutation, void* addr, size_t len, int prot, int flags, int64_t physStart = -1) { +bool RemapFixedIntoRegistered(GuestAllocations::Mutation& mutation, void* addr, size_t len, int prot, int flags, int64_t physStart = -1, [[maybe_unused]] bool zero = false) { constexpr int GuestMapFixed = 0x10; constexpr int GuestMapNoOverwrite = 0x80; if (addr == nullptr || (flags & GuestMapFixed) == 0 || (flags & GuestMapNoOverwrite) != 0 || !mutation.Covers(addr, len)) return false; @@ -831,8 +876,11 @@ bool RemapFixedIntoRegistered(GuestAllocations::Mutation& mutation, void* addr, } else { EraseMappings(address, address + len); #ifdef _WIN32 - GuestArena::GuestArenaReset_nid_postfix(addr, len); - if (nativeProtection != PROT_NONE) CommitArenaRange(addr, len, WinProtFromPosix(nativeProtection)); + if (zero) GuestArena::GuestArenaMapZero_nid_postfix(addr, len); + else { + GuestArena::GuestArenaReset_nid_postfix(addr, len); + if (nativeProtection != PROT_NONE) CommitArenaRange(addr, len, WinProtFromPosix(nativeProtection)); + } #elif defined(__EMSCRIPTEN__) GuestArena::GuestArenaReset_nid_postfix(addr, len); GuestArena::GuestArenaCommit_nid_postfix(addr, len, 0x04, PS5_PAGE_SIZE); @@ -846,7 +894,7 @@ bool RemapFixedIntoRegistered(GuestAllocations::Mutation& mutation, void* addr, return true; } -#if defined(__linux__) || defined(__EMSCRIPTEN__) +#if defined(__linux__) || defined(__EMSCRIPTEN__) || defined(__APPLE__) bool ArenaAvailable() { return GuestArena::GuestArenaAvailable_nid_postfix(); } void* ArenaCommit(void* addr, size_t len, int prot) { @@ -865,7 +913,7 @@ void ArenaForget(void* addr, size_t len) { #endif void Unmap(void* addr, size_t len) { -#if defined(__linux__) || defined(__EMSCRIPTEN__) +#if defined(__linux__) || defined(__EMSCRIPTEN__) || defined(__APPLE__) if (ArenaAvailable() && GuestArena::GuestArenaContains_nid_postfix(addr, len)) GuestArena::GuestArenaReset_nid_postfix(addr, len); else if (munmap(addr, len) != 0) throw std::system_error(errno, std::generic_category(), "munmap failed"); @@ -917,7 +965,7 @@ void* MapPlaced(void* addr, size_t len, int prot, int flags, size_t alignment) { } } #endif -#if defined(__linux__) || defined(__EMSCRIPTEN__) +#if defined(__linux__) || defined(__EMSCRIPTEN__) || defined(__APPLE__) if (ArenaAvailable() && !GuestArena::GuestArenaContains_nid_postfix(addr, len)) throw std::invalid_argument("fixed mapping is outside the guest address space arena"); if (ArenaAvailable()) { @@ -926,11 +974,15 @@ void* MapPlaced(void* addr, size_t len, int prot, int flags, size_t alignment) { GuestArena::GuestArenaRelease_nid_postfix(addr, len); throw std::system_error(errno, std::generic_category(), "Arena mapping failed"); } - const int placement = (flags & GuestMapNoOverwrite) != 0 ? MAP_FIXED_NOREPLACE : MAP_FIXED; -#else - const int placement = MAP_FIXED; #endif +#if defined(__linux__) || defined(__EMSCRIPTEN__) + const int placement = (flags & GuestMapNoOverwrite) != 0 ? MAP_FIXED_NOREPLACE : MAP_FIXED; void* result = mmap(addr, len, prot, MAP_PRIVATE | MAP_ANONYMOUS | placement, -1, 0); +#elif defined(__APPLE__) + void* result = (flags & GuestMapNoOverwrite) != 0 ? MapFixedNoReplace(addr, len, prot) : mmap(addr, len, prot, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0); +#else + void* result = mmap(addr, len, prot, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0); +#endif if (result == MAP_FAILED) { // return SCE_KERNEL_ERROR_ENOMEM; throw std::system_error(errno, std::generic_category(), "Fixed mmap failed"); @@ -938,7 +990,7 @@ void* MapPlaced(void* addr, size_t len, int prot, int flags, size_t alignment) { return result; } if (addr) { -#if defined(__linux__) || defined(__EMSCRIPTEN__) +#if defined(__linux__) || defined(__EMSCRIPTEN__) || defined(__APPLE__) if (ArenaAvailable()) { void* placed = GuestArena::GuestArenaAllocateFromHint_nid_postfix( reinterpret_cast(addr), len, alignment); @@ -958,7 +1010,7 @@ void* MapPlaced(void* addr, size_t len, int prot, int flags, size_t alignment) { #ifdef _WIN32 return mmap_aligned(len, prot, alignment); #endif -#if defined(__linux__) || defined(__EMSCRIPTEN__) +#if defined(__linux__) || defined(__EMSCRIPTEN__) || defined(__APPLE__) if (ArenaAvailable()) { void* placed = GuestArena::GuestArenaAllocate_nid_postfix(len, alignment); if (void* committed = ArenaCommit(placed, len, prot); committed != MAP_FAILED) return committed; @@ -1042,7 +1094,7 @@ void RecordProtection(const void* addr, size_t len, int prot) { void* MapAligned(void* addr, size_t len, int prot, int flags, size_t alignment) { void* mapped = MapPlaced(addr, len, prot, flags, alignment); -#if defined(__linux__) +#if defined(__linux__) || defined(__APPLE__) GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(mapped, len); #endif return mapped; @@ -1077,7 +1129,7 @@ void UnmapRegistered(GuestAllocations::Mutation& mutation, void* addr, size_t le auto* pieceAddress = const_cast(piece); std::lock_guard lock(g_directLock); EraseMappings(reinterpret_cast(piece), reinterpret_cast(piece) + pieceBytes); -#if defined(__linux__) || defined(__EMSCRIPTEN__) +#if defined(__linux__) || defined(__EMSCRIPTEN__) || defined(__APPLE__) Unmap(pieceAddress, pieceBytes); #else if (KernelArena::Get().Contains(pieceAddress, pieceBytes)) munmap(pieceAddress, pieceBytes); @@ -1147,6 +1199,35 @@ int DoMapDirect(void** addr, size_t len, int prot, int flags, int64_t physStart, return 0; } +static int MapAnonymous(void** addr, size_t len, int prot, int flags, size_t alignment, bool exclusive, bool zero) { + GuestAllocations::Mutation mutation; + if (exclusive && mutation.Overlaps(*addr, len)) return SCE_KERNEL_ERROR_ENOMEM; + flags = PlaceHintInReservation(*addr, len, flags); + if (FixedNoOverwriteConflict(mutation, *addr, len, flags)) return SCE_KERNEL_ERROR_ENOMEM; + if (RemapFixedIntoRegistered(mutation, *addr, len, prot, flags, -1, zero)) { + EraseReservations(*addr, len); + RecordProtection(*addr, len, prot); + return 0; + } + ReplaceFixedOverlap(mutation, *addr, len, flags); + void* mapped = MapAligned(*addr, len, zero ? PROT_NONE : LinuxProtFromSce(prot), flags, alignment); + try { +#ifdef _WIN32 + if (zero) GuestArena::GuestArenaMapZero_nid_postfix(mapped, len); +#endif + mutation.Add(mapped, len, (prot & 3) != 0, (prot & 2) != 0, (prot & GuestProtGpuReadWrite) != 0); + if ((prot & GuestProtGpuReadWrite) != 0) mutation.NoteGpuMapping(mapped, len); + } catch (...) { + Unmap(mapped, len); + throw; + } + *addr = mapped; + EraseReservations(mapped, len); + RecordProtection(mapped, len, prot); + Trace("map anon %p+0x%zx prot=0x%x flags=0x%x", mapped, len, prot, flags); + return 0; +} + int DoMapAnon(void** addr, size_t len, int prot, int flags, size_t alignment, bool exclusive, [[maybe_unused]] bool allowStackReplacement) { ValidateOutput(addr); if (len == 0 || (len & (PS5_PAGE_SIZE - 1)) != 0) return SCE_KERNEL_ERROR_EINVAL; @@ -1197,29 +1278,18 @@ int DoMapAnon(void** addr, size_t len, int prot, int flags, size_t alignment, bo return result; } #endif - GuestAllocations::Mutation mutation; - if (exclusive && mutation.Overlaps(*addr, len)) return SCE_KERNEL_ERROR_ENOMEM; - flags = PlaceHintInReservation(*addr, len, flags); - if (FixedNoOverwriteConflict(mutation, *addr, len, flags)) return SCE_KERNEL_ERROR_ENOMEM; - if (RemapFixedIntoRegistered(mutation, *addr, len, prot, flags)) { - EraseReservations(*addr, len); - RecordProtection(*addr, len, prot); - return 0; - } - ReplaceFixedOverlap(mutation, *addr, len, flags); - void* mapped = MapAligned(*addr, len, LinuxProtFromSce(prot), flags, alignment); - try { - mutation.Add(mapped, len, (prot & 3) != 0, (prot & 2) != 0, (prot & GuestProtGpuReadWrite) != 0); - if ((prot & GuestProtGpuReadWrite) != 0) mutation.NoteGpuMapping(mapped, len); - } catch (...) { - Unmap(mapped, len); - throw; - } - *addr = mapped; - EraseReservations(mapped, len); - RecordProtection(mapped, len, prot); - Trace("map anon %p+0x%zx prot=0x%x flags=0x%x", mapped, len, prot, flags); - return 0; + return MapAnonymous(addr, len, prot, flags, alignment, exclusive, false); +} + +int DoMapZero(void** addr, size_t len, int prot, int flags) { +#ifdef _WIN32 + ValidateOutput(addr); + if (len == 0 || (len & (PS5_PAGE_SIZE - 1)) != 0) return SCE_KERNEL_ERROR_EINVAL; + if (LinuxProtFromSce(prot) != PROT_READ) throw std::invalid_argument("Zero pages are only readable: " + std::to_string(prot)); + return MapAnonymous(addr, len, prot, flags, PS5_PAGE_SIZE, false, true); +#else + return DoMapAnon(addr, len, prot, flags); +#endif } int DoMapFileBacked_nid_no_patch(void** addr, size_t len, int prot, int flags, int descriptor, std::uint64_t offset, size_t alignment) { diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp index ca61ec1bda..2787be97b1 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp @@ -40,6 +40,7 @@ int DirectMemoryLargestFree(int64_t searchStart, int64_t searchEnd, size_t align size_t DirectMemoryFreeRun(uint64_t offset, uint64_t limit); int DoMapDirect(void** addr, size_t len, int prot, int flags, int64_t physStart, size_t alignment); int DoMapAnon(void** addr, size_t len, int prot, int flags, size_t alignment = PS5_PAGE_SIZE, bool exclusive = false, bool allowStackReplacement = false); +int DoMapZero(void** addr, size_t len, int prot, int flags); int DoMapFileBacked_nid_no_patch(void** addr, size_t len, int prot, int flags, int descriptor, std::uint64_t offset, size_t alignment); void ReleaseFlexibleMappingRange(std::uintptr_t address, size_t len); int DoMapShared(void** addr, size_t len, int prot, HostSharedMemory object); diff --git a/core/libs/prx/libkernel/DirectMemory/Export.cpp b/core/libs/prx/libkernel/DirectMemory/Export.cpp index 51b226b0d8..60798b7c19 100644 --- a/core/libs/prx/libkernel/DirectMemory/Export.cpp +++ b/core/libs/prx/libkernel/DirectMemory/Export.cpp @@ -236,6 +236,10 @@ int APS5_VABI sceKernelAllocateMainDirectMemory(size_t len, size_t alignment, in return sceKernelAllocateDirectMemory(0, static_cast(DIRECT_MEMORY_SIZE), len, alignment, memory_type, phys_addr_out); } +int APS5_VABI sceKernelAllocateDirectMemoryForApp(int64_t search_start, int64_t search_end, size_t len, size_t alignment, int memory_type, int64_t* phys_addr_out) { + return sceKernelAllocateDirectMemory(search_start, search_end, len, alignment, memory_type, phys_addr_out); +} + int APS5_VABI sceKernelAvailableDirectMemorySize(int64_t search_start, int64_t search_end, size_t alignment, int64_t* phys_addr_out, size_t* size_out) { int64_t start = 0; size_t size = 0; diff --git a/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp b/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp index aa4451a535..2f3cd26c9e 100644 --- a/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp @@ -331,6 +331,7 @@ int APS5_VABI msync_nid_postfix(void* address, std::size_t length, int flags) { constexpr std::uintptr_t mask = PS5_PAGE_SIZE - 1; const auto first = reinterpret_cast(address); const auto start = first & ~mask; + if (length > std::numeric_limits::max() - (first - start) - mask) return failed(GuestInvalid); const std::uintptr_t size = (length + (first - start) + mask) & ~mask; const auto end = start + size; if (end < start) return failed(GuestInvalid); diff --git a/core/libs/prx/libkernel/Equeue/src/EdgeWatch.cpp b/core/libs/prx/libkernel/Equeue/src/EdgeWatch.cpp index 60e001c6b3..f8f0499e02 100644 --- a/core/libs/prx/libkernel/Equeue/src/EdgeWatch.cpp +++ b/core/libs/prx/libkernel/Equeue/src/EdgeWatch.cpp @@ -8,6 +8,10 @@ #ifdef __linux__ #include #include +#elif defined(__APPLE__) +#include +#include +#include #endif namespace EdgeWatch { @@ -37,6 +41,43 @@ bool Watch::TakeEdge() { return ::epoll_wait(m_descriptor, &ready, 1, 0) > 0; } +#elif defined(__APPLE__) + +std::shared_ptr Watch::Open(std::intptr_t hostDescriptor, bool write) { + auto watch = std::make_shared(-1); + const int descriptor = ::kqueue(); + if (descriptor < 0) { + throw std::runtime_error("kevent: EV_CLEAR: kqueue failed, host errno " + std::to_string(errno)); + } + watch->m_descriptor = descriptor; + if (::fcntl(descriptor, F_SETFD, FD_CLOEXEC) != 0) { + throw std::runtime_error("kevent: EV_CLEAR: setting close-on-exec failed, host errno " + std::to_string(errno)); + } + struct kevent interest{}; + EV_SET(&interest, static_cast(hostDescriptor), write ? EVFILT_WRITE : EVFILT_READ, EV_ADD | EV_CLEAR, 0, 0, nullptr); + if (::kevent(descriptor, &interest, 1, nullptr, 0, nullptr) != 0) { + throw std::runtime_error("kevent: EV_CLEAR: watching host descriptor " + std::to_string(hostDescriptor) + " failed, host errno " + std::to_string(errno)); + } + return watch; +} + +Watch::~Watch() { + if (m_descriptor >= 0) ::close(m_descriptor); +} + +bool Watch::TakeEdge() { + struct kevent ready{}; + const timespec timeout{}; + int result; + do { + result = ::kevent(m_descriptor, nullptr, 0, &ready, 1, &timeout); + } while (result < 0 && errno == EINTR); + if (result < 0) { + throw std::runtime_error("kevent: EV_CLEAR: retrieving host event failed, host errno " + std::to_string(errno)); + } + return result > 0; +} + #else std::shared_ptr Watch::Open(std::intptr_t, bool) { diff --git a/core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp b/core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp index 65ae972430..4351a30052 100644 --- a/core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp +++ b/core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp @@ -4,6 +4,7 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/PrimedSharedMutex.hpp" #include "prx/libkernel/Time/include/Time.hpp" #include "prx/libkernel/Time/include/TimedWait.hpp" #include @@ -97,7 +98,7 @@ struct EventFlagHandle { bool opened = false; }; -std::shared_mutex eventFlagRegistryMutex; +PrimedSharedMutex eventFlagRegistryMutex; std::unordered_map> eventFlagRegistry; std::shared_ptr ResolveEventFlag(KernelEventFlag handle) { diff --git a/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp b/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp index 84b32480ad..28d75e6f20 100644 --- a/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp +++ b/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp @@ -24,6 +24,7 @@ #ifdef _WIN32 #include "prx/libc/include/FileStream.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/WindowsVerbatimPath.hpp" #include #include #include @@ -227,9 +228,11 @@ void Load(DirectoryState& state, int fd) { #ifdef _WIN32 int OpenPlaceholder(const std::filesystem::path& path) { std::error_code error; - if (std::filesystem::is_directory(path, error)) { + const auto verbatimPath = NativeVerbatimPath_nid_no_patch(path, error, WindowsMaxDirectoryPathLength); + const auto& openPath = error ? path : verbatimPath; + if (std::filesystem::is_directory(openPath, error)) { SECURITY_ATTRIBUTES security{sizeof(SECURITY_ATTRIBUTES), nullptr, TRUE}; - const auto handle = CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + const auto handle = CreateFileW(openPath.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, &security, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); if (handle == INVALID_HANDLE_VALUE) { errno = WindowsOpenError_nid_no_patch(GetLastError()); diff --git a/core/libs/prx/libkernel/File/src/FilesystemForwards.cpp b/core/libs/prx/libkernel/File/src/FilesystemForwards.cpp new file mode 100644 index 0000000000..83eb7605a6 --- /dev/null +++ b/core/libs/prx/libkernel/File/src/FilesystemForwards.cpp @@ -0,0 +1,35 @@ +#include "prx/libkernel/include/Api.hpp" +#include "prx/libkernel/File/include/File.hpp" +#include "prx/libc/include/FilesystemForwards.hpp" + +namespace { + +struct FilesystemForwardsRegistration { + FilesystemForwardsRegistration() { + static const FilesystemForwards_nid_no_patch forwards{ + sceKernelStat, + sceKernelFstat, + sceKernelOpen, + sceKernelRead, + sceKernelWrite, + sceKernelClose, + sceKernelMkdir, + sceKernelUnlink, + sceKernelRename, + sceKernelTruncate_nid_postfix, + sceKernelChmod_nid_postfix, + sceKernelFchmod, + sceKernelUtimes_nid_postfix, + stat_nid_postfix, + rmdir_nid_postfix, + }; + RegisterFilesystemForwards_nid_no_patch(&forwards); + } + ~FilesystemForwardsRegistration() { RegisterFilesystemForwards_nid_no_patch(nullptr); } + FilesystemForwardsRegistration(const FilesystemForwardsRegistration&) = delete; + FilesystemForwardsRegistration& operator=(const FilesystemForwardsRegistration&) = delete; +}; + +const FilesystemForwardsRegistration registration; + +} diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index b66e8538ab..068ef2304d 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -37,6 +37,7 @@ #include #include #include "prx/libc/include/WindowsDescriptor.hpp" +#include "prx/libc/include/WindowsVerbatimPath.hpp" #include "prx/libkernel/File/include/WindowsSymbolicLink.hpp" static int NativeOpen(const std::filesystem::path& p, int nativeFlags, std::uint16_t mode) { const int access = nativeFlags & (_O_RDONLY | _O_WRONLY | _O_RDWR); @@ -116,15 +117,7 @@ static int NativeUnlink(const std::filesystem::path& p) { return -1; } static std::filesystem::path NativeMetadataPath(const std::filesystem::path& path, std::error_code& error) { - const auto& input = path.native(); - if (input.starts_with(L"\\\\?\\") || input.starts_with(L"\\\\.\\")) return path; - auto absolute = std::filesystem::absolute(path, error); - if (error) return {}; - absolute = absolute.lexically_normal().make_preferred(); - const auto& native = absolute.native(); - if (native.size() < MAX_PATH || native.starts_with(L"\\\\?\\") || native.starts_with(L"\\\\.\\")) return absolute; - return native.starts_with(L"\\\\") ? std::filesystem::path(L"\\\\?\\UNC\\" + native.substr(2)) - : std::filesystem::path(L"\\\\?\\" + native); + return NativeVerbatimPath_nid_no_patch(path, error); } static bool NativeSymbolicLinkLoop(const std::filesystem::path& path) { return File::WindowsSymbolicLinkLoop(path); @@ -211,6 +204,14 @@ static int SceIoErrorFromErrno(int error) { return static_cast(0x80020000u | static_cast(guest)); } +static int TrailingSlashCreationError(const std::filesystem::path& native, int flags) { + if ((flags & SCE_KERNEL_O_CREAT) == 0 || native.has_filename() || !native.has_relative_path()) return 0; + std::error_code error; + const auto status = std::filesystem::status(native, error); + if (error) return FilesystemError(error); + return status.type() == std::filesystem::file_type::not_found ? 2 : 0; +} + static int OpenNative(const std::filesystem::path& native, int flags, std::uint16_t mode) { if (AppArchiveHasDirectory_nid_no_patch(native)) { std::error_code error; @@ -231,13 +232,16 @@ static int OpenNative(const std::filesystem::path& native, int flags, std::uint1 return *archived; } #ifdef _WIN32 + std::error_code pathError; + const auto metadataOpenPath = NativeMetadataPath(native, pathError); + const auto& openNative = pathError ? native : metadataOpenPath; if ((flags & (SCE_KERNEL_O_DIRECTORY | SCE_KERNEL_O_CREAT)) == SCE_KERNEL_O_DIRECTORY) { std::error_code error; - const auto status = std::filesystem::status(native, error); + const auto status = std::filesystem::status(openNative, error); if (std::filesystem::exists(status) && !std::filesystem::is_directory(status)) return SceErrorFromErrno(ENOTDIR); } - if ((flags & (SCE_KERNEL_O_CREAT | SCE_KERNEL_O_EXCL)) == (SCE_KERNEL_O_CREAT | SCE_KERNEL_O_EXCL) && native.has_filename()) { - const DWORD attributes = GetFileAttributesW(native.c_str()); + if ((flags & (SCE_KERNEL_O_CREAT | SCE_KERNEL_O_EXCL)) == (SCE_KERNEL_O_CREAT | SCE_KERNEL_O_EXCL) && openNative.has_filename()) { + const DWORD attributes = GetFileAttributesW(openNative.c_str()); if (attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0) return SceErrorFromErrno(EEXIST); } #endif @@ -246,14 +250,18 @@ static int OpenNative(const std::filesystem::path& native, int flags, std::uint1 std::error_code error; if (std::filesystem::is_symlink(native, error)) return SceErrorFromErrno(GuestEmlink); #ifdef _WIN32 - const DWORD attributes = GetFileAttributesW(native.c_str()); + const DWORD attributes = GetFileAttributesW(openNative.c_str()); if (attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0) { std::string junctionTarget; - if (File::WindowsSymbolicLink::ReadJunction(native, junctionTarget, "open")) return SceErrorFromErrno(GuestEmlink); + if (File::WindowsSymbolicLink::ReadJunction(openNative, junctionTarget, "open")) return SceErrorFromErrno(GuestEmlink); } #endif } +#ifdef _WIN32 + int fd = NativeOpen(openNative, MapFlags(flags), mode); +#else int fd = NativeOpen(native, MapFlags(flags), mode); +#endif #ifdef __APPLE__ if (fd >= 0 && (flags & (SCE_KERNEL_O_CREAT | SCE_KERNEL_O_EXCL)) == SCE_KERNEL_O_CREAT) { struct stat opened{}; @@ -279,6 +287,12 @@ static int OpenNative(const std::filesystem::path& native, int flags, std::uint1 } } #endif + if (fd < 0 && (errno == EISDIR || errno == EINVAL)) { + const int openError = errno; + const int lookupError = TrailingSlashCreationError(native, flags); + errno = lookupError == 2 ? ENOENT : openError; + if (lookupError != 0 && lookupError != 2) return SceKernelError(lookupError); + } #ifdef _WIN32 if (fd < 0 && (errno == ENOENT || errno == ENOTDIR)) { std::error_code metadataError; @@ -392,6 +406,7 @@ int APS5_VABI sceKernelClose(int d) { std::int64_t APS5_VABI sceKernelRead(int d, void* buf, std::size_t nbytes) { if (File::IsDirectoryDescriptor(d)) return SceErrorFromErrno(EISDIR); + if (IsKqueueDescriptor_nid_no_patch(d)) return nbytes == 0 ? 0 : SCE_KERNEL_ERROR_EOPNOTSUPP; if (GuestSockets::IsOpen(d)) { const auto n = GuestSockets::Read(d, buf, nbytes); return n < 0 ? SceKernelError(*__error_nid_postfix()) : n; @@ -412,6 +427,7 @@ std::int64_t APS5_VABI sceKernelRead(int d, void* buf, std::size_t nbytes) { std::int64_t APS5_VABI sceKernelWrite(int d, const void* buf, std::size_t nbytes) { if (File::IsVirtualDirectoryDescriptor(d)) return SceErrorFromErrno(EISDIR); + if (IsKqueueDescriptor_nid_no_patch(d)) return SCE_KERNEL_ERROR_EOPNOTSUPP; if (GuestSockets::IsOpen(d)) { const auto n = GuestSockets::Write(d, buf, nbytes); return n < 0 ? SceKernelError(*__error_nid_postfix()) : n; @@ -441,7 +457,7 @@ static bool SeekOverflows(int d, std::int64_t offset, int whence) { } std::int64_t APS5_VABI sceKernelLseek(int d, std::int64_t offset, int whence) { - if (GuestSockets::IsOpen(d)) return SCE_KERNEL_ERROR_ESPIPE; + if (GuestSockets::IsOpen(d) || IsKqueueDescriptor_nid_no_patch(d)) return SCE_KERNEL_ERROR_ESPIPE; if (whence < 0 || whence > 2) { throw std::invalid_argument(std::string(__func__) + ": invalid whence=" + std::to_string(whence)); } @@ -494,7 +510,7 @@ static int StatNative(const std::filesystem::path& native, bool directory, FileS int APS5_VABI sceKernelStat(const char* path, FileStat* sb) { std::string copy; - if (const int error = CopyGuestPath_nid_no_patch(path, copy)) return SceErrorFromErrno(error); + if (const int error = CopyGuestPath_nid_no_patch(path, copy)) return SceKernelError(error); path = copy.c_str(); if (copy.empty()) return SceErrorFromErrno(2); std::filesystem::path native; @@ -515,38 +531,48 @@ int StatInDirectory_nid_no_patch(int directory, const char* path, FileStat* sb, int APS5_VABI sceKernelUnlink(const char* path) { std::string copy; - if (const int error = CopyGuestPath_nid_no_patch(path, copy)) return SceErrorFromErrno(error); + if (const int error = CopyGuestPath_nid_no_patch(path, copy)) return SceKernelError(error); path = copy.c_str(); if (copy.empty()) 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); +#ifdef _WIN32 + std::error_code unlinkPathError; + const auto metadataUnlinkPath = NativeMetadataPath(native, unlinkPathError); + const auto& unlinkNative = unlinkPathError ? native : metadataUnlinkPath; +#else + const auto& unlinkNative = native; +#endif constexpr int GuestEperm = 1; std::error_code error; - bool directory = std::filesystem::is_directory(std::filesystem::symlink_status(native, error)); + bool directory = std::filesystem::is_directory(std::filesystem::symlink_status(unlinkNative, error)); #ifdef _WIN32 constexpr DWORD DirectoryLink = FILE_ATTRIBUTE_DIRECTORY | FILE_ATTRIBUTE_REPARSE_POINT; - const DWORD linkAttributes = GetFileAttributesW(native.c_str()); + const DWORD linkAttributes = GetFileAttributesW(unlinkNative.c_str()); if (linkAttributes != INVALID_FILE_ATTRIBUTES && (linkAttributes & DirectoryLink) == DirectoryLink) directory = true; +#endif +#ifdef _WIN32 + if (unlinkNative.has_filename() && unlinkNative.filename().native().find_first_of(L"*?<>\"") == std::wstring::npos) { + WIN32_FIND_DATAW found{}; + if (const HANDLE find = FindFirstFileW(unlinkNative.c_str(), &found); find != INVALID_HANDLE_VALUE) { + FindClose(find); + constexpr DWORD DirectoryLink = FILE_ATTRIBUTE_REPARSE_POINT | FILE_ATTRIBUTE_DIRECTORY; + if ((found.dwFileAttributes & DirectoryLink) == DirectoryLink && found.dwReserved0 == IO_REPARSE_TAG_SYMLINK) { + if (!RemoveDirectoryW(unlinkNative.c_str())) return SceErrorFromErrno(GetLastError() == ERROR_ACCESS_DENIED ? 13 : 5); + RecordWrittenPath_nid_no_patch(native); + return 0; + } + } + } #endif if (directory) { #ifdef _WIN32 - if (native.has_filename()) { - WIN32_FIND_DATAW found{}; - if (const HANDLE find = FindFirstFileW(native.c_str(), &found); find != INVALID_HANDLE_VALUE) { - FindClose(find); - if ((found.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0 && found.dwReserved0 == IO_REPARSE_TAG_SYMLINK) { - if (!RemoveDirectoryW(native.c_str())) return SceErrorFromErrno(GetLastError() == ERROR_ACCESS_DENIED ? 13 : 5); - RecordWrittenPath_nid_no_patch(native); - return 0; - } - } - } - const DWORD attributes = GetFileAttributesW(native.c_str()); + const DWORD attributes = GetFileAttributesW(unlinkNative.c_str()); if (attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0) { std::string junctionTarget; - if (File::WindowsSymbolicLink::ReadJunction(native, junctionTarget, "unlink")) { - if (RemoveDirectoryW(native.c_str()) == 0) return SceErrorFromErrno(GuestEperm); + if (File::WindowsSymbolicLink::ReadJunction(unlinkNative, junctionTarget, "unlink")) { + if (RemoveDirectoryW(unlinkNative.c_str()) == 0) return SceErrorFromErrno(GuestEperm); RecordWrittenPath_nid_no_patch(native); return 0; } @@ -554,9 +580,9 @@ int APS5_VABI sceKernelUnlink(const char* path) { #endif return SceErrorFromErrno(GuestEperm); } - if (NativeUnlink(native) != 0) { + if (NativeUnlink(unlinkNative) != 0) { #ifdef _WIN32 - if (NativeSymbolicLinkLoop(native.parent_path())) return SCE_KERNEL_ERROR_ELOOP; + if (NativeSymbolicLinkLoop(unlinkNative.parent_path())) return SCE_KERNEL_ERROR_ELOOP; #endif return SceErrorFromErrno(errno == ENOENT ? NotFoundError_nid_no_patch(native) : errno); } @@ -593,4 +619,4 @@ int APS5_VABI sceKernelFcntl(int d, int command, ...) { return result < 0 ? SceKernelError(*__error_nid_postfix()) : result; } -} +} \ No newline at end of file diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 2049d994ff..5c682df352 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -8,6 +8,7 @@ #include "prx/libc/include/General.hpp" #include "prx/libc/include/FilesystemError.hpp" #include "prx/libc/include/RenameDot.hpp" +#include "prx/libc/include/WindowsVerbatimPath.hpp" #include "prx/libc/include/GuestArena.hpp" #include "prx/libkernel/DirectMemory/DirectMemory.hpp" #include "prx/libkernel/File/include/File.hpp" @@ -75,20 +76,49 @@ template static auto WithoutParameterHandler(TCall call) { return result; } static int NativeRmdir(const std::filesystem::path& path) { - return ::_wrmdir(path.wstring().c_str()); + std::error_code error; + const auto verbatim = NativeVerbatimPath_nid_no_patch(path, error, WindowsMaxDirectoryPathLength); + const auto& target = error ? path : verbatim; + if (::RemoveDirectoryW(target.c_str())) return 0; + errno = WindowsOpenError_nid_no_patch(GetLastError()); + return -1; +} +static bool IsDirectoryEmpty(const std::filesystem::path& path) { + if (path.empty()) return true; + const auto search = (path.native().back() == L'\\' || path.native().back() == L'/') ? (path.native() + L"*") : (path.native() + L"\\*"); + WIN32_FIND_DATAW data{}; + const HANDLE handle = ::FindFirstFileW(search.c_str(), &data); + if (handle == INVALID_HANDLE_VALUE) return true; + bool empty = true; + do { + const auto* name = data.cFileName; + if (name[0] == L'.' && (name[1] == L'\0' || (name[1] == L'.' && name[2] == L'\0'))) continue; + empty = false; + break; + } while (::FindNextFileW(handle, &data)); + ::FindClose(handle); + return empty; } static bool RemoveEmptyDirectory(const std::filesystem::path& path) { - struct _stat64 status{}; - const bool unlocked = ::_wstat64(path.c_str(), &status) == 0 && !(status.st_mode & _S_IWRITE) && - ::_wchmod(path.c_str(), _S_IREAD | _S_IWRITE) == 0; + std::error_code pathError; + const auto verbatim = NativeVerbatimPath_nid_no_patch(path, pathError, WindowsMaxDirectoryPathLength); + const auto& target = pathError ? path : verbatim; + const DWORD attributes = ::GetFileAttributesW(target.c_str()); + const bool unlocked = attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_READONLY) && + ::SetFileAttributesW(target.c_str(), attributes & ~FILE_ATTRIBUTE_READONLY); std::error_code error; - if (std::filesystem::remove(path, error)) return true; - if (unlocked) ::_wchmod(path.c_str(), _S_IREAD); - return false; + const bool removed = ::RemoveDirectoryW(target.c_str()) != 0 || std::filesystem::remove(target, error); + if (!removed && unlocked) ::SetFileAttributesW(target.c_str(), attributes); + return removed; } static int NativeMkdir(const std::filesystem::path& path, std::uint16_t mode) { (void)mode; - return ::_wmkdir(path.wstring().c_str()); + std::error_code error; + const auto verbatim = NativeVerbatimPath_nid_no_patch(path, error, WindowsMaxDirectoryPathLength); + const auto& target = error ? path : verbatim; + if (::CreateDirectoryW(target.c_str(), nullptr)) return 0; + errno = WindowsOpenError_nid_no_patch(GetLastError()); + return -1; } static std::optional HandlePath(HANDLE handle) { std::wstring path(MAX_PATH, L'\0'); @@ -119,12 +149,15 @@ std::optional File::WindowsDescriptorPath_nid_no_patch(in return HandlePath(handle); } static int NativeChmod(const std::filesystem::path& path, int mode) { - const DWORD attributes = GetFileAttributesW(path.c_str()); + std::error_code pathError; + const auto verbatim = NativeVerbatimPath_nid_no_patch(path, pathError); + const auto& checkPath = pathError ? path : verbatim; + const DWORD attributes = GetFileAttributesW(checkPath.c_str()); if (attributes == INVALID_FILE_ATTRIBUTES || (attributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0) { RecordWrittenPath_nid_no_patch(path); - return ::_wchmod(path.wstring().c_str(), mode); + return ::_wchmod(checkPath.wstring().c_str(), mode); } - const HANDLE handle = CreateFileW(path.c_str(), 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, + const HANDLE handle = CreateFileW(checkPath.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) { errno = WindowsOpenError_nid_no_patch(GetLastError()); @@ -134,7 +167,9 @@ static int NativeChmod(const std::filesystem::path& path, int mode) { CloseHandle(handle); if (!target) return -1; RecordWrittenPath_nid_no_patch(*target); - return ::_wchmod(target->wstring().c_str(), mode); + const auto verbatimTarget = NativeVerbatimPath_nid_no_patch(*target, pathError); + const auto& finalTarget = pathError ? *target : verbatimTarget; + return ::_wchmod(finalTarget.wstring().c_str(), mode); } static int NativeFchmod(int descriptor, int mode) { const auto path = File::WindowsDescriptorPath_nid_no_patch(descriptor); @@ -172,11 +207,14 @@ static int NativeFtruncate(int descriptor, std::int64_t length) { } } static int NativeTruncate(const std::filesystem::path& path, std::int64_t length) { - const int descriptor = ::_wopen(path.c_str(), _O_WRONLY | _O_BINARY); + std::error_code error; + const auto verbatim = NativeVerbatimPath_nid_no_patch(path, error); + const auto& target = error ? path : verbatim; + const int descriptor = ::_wopen(target.c_str(), _O_WRONLY | _O_BINARY); if (descriptor < 0) return errno; - const int error = NativeFtruncate(descriptor, length); + const int err = NativeFtruncate(descriptor, length); ::_close(descriptor); - return error; + return err; } static int SetTimes(HANDLE handle, const KernelTimeval* times) { FILETIME access{}, modified{}; @@ -195,7 +233,10 @@ static int SetTimes(HANDLE handle, const KernelTimeval* times) { return SetFileTime(handle, birth, &access, &modified) ? 0 : File::WindowsFileTime::Failure(GetLastError()); } static int NativeUtimes(const std::filesystem::path& path, const KernelTimeval* times, bool followLink = true) { - const auto handle = CreateFileW(path.c_str(), FILE_READ_ATTRIBUTES | FILE_WRITE_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + std::error_code error; + const auto verbatim = NativeVerbatimPath_nid_no_patch(path, error); + const auto& target = error ? path : verbatim; + const auto handle = CreateFileW(target.c_str(), FILE_READ_ATTRIBUTES | FILE_WRITE_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | (followLink ? 0 : FILE_FLAG_OPEN_REPARSE_POINT), nullptr); if (handle == INVALID_HANDLE_VALUE) return File::WindowsFileTime::Failure(GetLastError()); const int result = SetTimes(handle, times); @@ -677,7 +718,12 @@ static int PathError(const char* path, std::string& copy) { #ifdef _WIN32 bool DescriptorIsOpen_nid_no_patch(int descriptor) { - return descriptor >= 0 && WithoutParameterHandler([descriptor] { return ::_get_osfhandle(descriptor) == -1 ? -1 : 0; }) == 0; + return descriptor >= 0 && WithoutParameterHandler([descriptor] { + const auto handle = ::_get_osfhandle(descriptor); + if (handle == -1) return -1; + DWORD flags = 0; + return handle == -2 || GetHandleInformation(reinterpret_cast(handle), &flags) ? 0 : -1; + }) == 0; } #else bool DescriptorIsOpen_nid_no_patch(int descriptor) { @@ -722,20 +768,26 @@ int File::DuplicateFrom(int fd, int start) { static int LookupError(const std::filesystem::path& native) { #ifdef _WIN32 - const auto named = native.has_filename() ? native : WithoutTrailingSeparator(native); + std::error_code pathError; + const auto verbatim = NativeVerbatimPath_nid_no_patch(native, pathError); + const auto& checkNative = pathError ? native : verbatim; + const auto named = checkNative.has_filename() ? checkNative : WithoutTrailingSeparator(checkNative); const DWORD attributes = GetFileAttributesW(named.c_str()); if (attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0) { FileStat target{}; if (!File::TryFillFileStat(named, &target)) return SceErrorFromErrno(errno) & 0xffff; - return native.has_filename() || (target.st_mode & 0170000) == 0040000 ? 0 : GUEST_ENOTDIR; + return checkNative.has_filename() || (target.st_mode & 0170000) == 0040000 ? 0 : GUEST_ENOTDIR; } + const auto& statusNative = checkNative; +#else + const auto& statusNative = native; #endif std::error_code error; - const auto status = std::filesystem::status(native, error); - if (std::filesystem::exists(status)) return native.has_filename() || std::filesystem::is_directory(status) ? 0 : GUEST_ENOTDIR; + const auto status = std::filesystem::status(statusNative, error); + if (std::filesystem::exists(status)) return statusNative.has_filename() || std::filesystem::is_directory(status) ? 0 : GUEST_ENOTDIR; if (status.type() != std::filesystem::file_type::not_found) return SceErrorFromErrno(error.default_error_condition().value()) & 0xffff; - const bool fileWithSeparator = !native.has_filename() && std::filesystem::exists(WithoutTrailingSeparator(native), error); - return fileWithSeparator || HasNonDirectoryPrefix(native) ? GUEST_ENOTDIR : GUEST_ENOENT; + const bool fileWithSeparator = !statusNative.has_filename() && std::filesystem::exists(WithoutTrailingSeparator(statusNative), error); + return fileWithSeparator || HasNonDirectoryPrefix(statusNative) ? GUEST_ENOTDIR : GUEST_ENOENT; } static constexpr std::pair GUEST_FILE_FLAGS[] = { @@ -954,7 +1006,7 @@ int APS5_VABI dup2_nid_postfix(int from, int to) { int APS5_VABI flock_nid_postfix(int d, int operation) { if (File::IsVirtualDirectoryDescriptor(d)) return PosixFailure(GUEST_EISDIR); - if (GuestSockets::IsOpen(d)) return PosixFailure(GUEST_EOPNOTSUPP); + if (GuestSockets::IsOpen(d) || IsKqueueDescriptor_nid_no_patch(d)) return PosixFailure(GUEST_EOPNOTSUPP); File::RefuseArchivedFile(d, "flock"); #ifdef _WIN32 const auto handle = reinterpret_cast(WithoutParameterHandler([d] { return ::_get_osfhandle(d); })); @@ -1189,8 +1241,15 @@ int APS5_VABI sceKernelCheckReachability(const char* path) { std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native, true, true)) return SceKernelError(error); if (AppArchiveOnly_nid_no_patch(native)) return 0; +#ifdef _WIN32 + std::error_code pathError; + const auto verbatim = NativeVerbatimPath_nid_no_patch(native, pathError); + const auto& checkNative = pathError ? native : verbatim; +#else + const auto& checkNative = native; +#endif std::error_code error; - return std::filesystem::exists(native, error) ? 0 : SCE_KERNEL_ERROR_ENOENT; + return std::filesystem::exists(checkNative, error) ? 0 : SCE_KERNEL_ERROR_ENOENT; } int APS5_VABI sceKernelFstat(int d, FileStat* sb) { @@ -1212,7 +1271,7 @@ int APS5_VABI sceKernelFstat(int d, FileStat* sb) { int APS5_VABI sceKernelFsync(int fd) { if (File::IsVirtualDirectoryDescriptor(fd)) return SceErrorFromErrno(GUEST_EISDIR); - if (GuestSockets::IsOpen(fd)) return SceErrorFromErrno(GUEST_EINVAL); + if (GuestSockets::IsOpen(fd) || IsKqueueDescriptor_nid_no_patch(fd)) return SceErrorFromErrno(GUEST_EINVAL); File::RefuseArchivedFile(fd, "fsync"); #ifdef _WIN32 if (NativeSync(fd) != 0) return SceErrorFromErrno(errno); @@ -1224,7 +1283,7 @@ int APS5_VABI sceKernelFsync(int fd) { int APS5_VABI sceKernelFdatasync(int fd) { if (File::IsVirtualDirectoryDescriptor(fd)) return SceErrorFromErrno(GUEST_EISDIR); - if (GuestSockets::IsOpen(fd)) return SceErrorFromErrno(GUEST_EINVAL); + if (GuestSockets::IsOpen(fd) || IsKqueueDescriptor_nid_no_patch(fd)) return SceErrorFromErrno(GUEST_EINVAL); File::RefuseArchivedFile(fd, "fdatasync"); #ifdef _WIN32 if (NativeSync(fd) != 0) return SceErrorFromErrno(errno); @@ -1451,22 +1510,29 @@ int APS5_VABI getdents_nid_postfix(int fd, char* buf, int nbytes) { int APS5_VABI sceKernelMkdir(const char* path, uint16_t mode) { (void)mode; std::string copy; - if (const int error = CopyGuestPath_nid_no_patch(path, copy)) return SceErrorFromErrno(error); + if (const int error = CopyGuestPath_nid_no_patch(path, copy)) return SceKernelError(error); path = copy.c_str(); if (copy.empty()) return SceErrorFromErrno(GUEST_ENOENT); std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native)) return SceKernelError(error); while (!native.has_filename() && native.has_relative_path()) native = native.parent_path(); - std::error_code error; - if (std::filesystem::exists(native, error)) return SceErrorFromErrno(GUEST_EEXIST); - if (const int failure = PathLookupFailure(error)) return failure; - if (!std::filesystem::is_directory(WithoutTrailingSeparator(native).parent_path(), error)) { #ifdef _WIN32 - if (File::WindowsSymbolicLinkLoop(WithoutTrailingSeparator(native).parent_path())) return SceKernelError(GUEST_ELOOP); + std::error_code pathError; + const auto verbatimNative = NativeVerbatimPath_nid_no_patch(native, pathError, WindowsMaxDirectoryPathLength); + const auto& targetNative = pathError ? native : verbatimNative; +#else + const auto& targetNative = native; #endif - return SceErrorFromErrno(HasNonDirectoryPrefix(native) ? GUEST_ENOTDIR : GUEST_ENOENT); + std::error_code error; + if (std::filesystem::exists(targetNative, error)) return SceErrorFromErrno(GUEST_EEXIST); + if (const int failure = PathLookupFailure(error)) return failure; + if (!std::filesystem::is_directory(WithoutTrailingSeparator(targetNative).parent_path(), error)) { +#ifdef _WIN32 + if (File::WindowsSymbolicLinkLoop(WithoutTrailingSeparator(targetNative).parent_path())) return SceKernelError(GUEST_ELOOP); +#endif + return SceErrorFromErrno(HasNonDirectoryPrefix(targetNative) ? GUEST_ENOTDIR : GUEST_ENOENT); } - if (!std::filesystem::create_directory(native, error)) return SceErrorFromErrno(error.value() ? error.value() : GUEST_EIO); + if (!std::filesystem::create_directory(targetNative, error)) return SceErrorFromErrno(error.value() ? error.value() : GUEST_EIO); RecordWrittenPath_nid_no_patch(native); return 0; } @@ -1768,33 +1834,44 @@ int APS5_VABI sceKernelRename(const char* from, const char* to) { int APS5_VABI sceKernelRmdir(const char* path) { std::string copy; - if (const int error = CopyGuestPath_nid_no_patch(path, copy)) return SceErrorFromErrno(error); + if (const int error = CopyGuestPath_nid_no_patch(path, copy)) return SceKernelError(error); path = copy.c_str(); if (copy.empty()) 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); +#ifdef _WIN32 + std::error_code pathError; + const auto verbatimNative = NativeVerbatimPath_nid_no_patch(native, pathError, WindowsMaxDirectoryPathLength); + const auto& targetNative = pathError ? native : verbatimNative; +#else + const auto& targetNative = native; +#endif FileStat linkStatus{}; - if (native.has_filename() && File::FillLinkStat(native, &linkStatus) && (linkStatus.st_mode & 0170000) == 0120000) return SceErrorFromErrno(GUEST_ENOTDIR); + if (targetNative.has_filename() && File::FillLinkStat(targetNative, &linkStatus) && (linkStatus.st_mode & 0170000) == 0120000) return SceErrorFromErrno(GUEST_ENOTDIR); std::error_code error; - const auto status = std::filesystem::status(native, error); + const auto status = std::filesystem::status(targetNative, error); if (const int failure = PathLookupFailure(error)) return failure; #ifdef _WIN32 - const DWORD attributes = GetFileAttributesW(native.c_str()); + const DWORD attributes = GetFileAttributesW(targetNative.c_str()); if (attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0 && (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0) { std::string junctionTarget; - if (File::WindowsSymbolicLink::ReadJunction(native, junctionTarget, "rmdir")) return SceErrorFromErrno(GUEST_ENOTDIR); + if (File::WindowsSymbolicLink::ReadJunction(targetNative, junctionTarget, "rmdir")) return SceErrorFromErrno(GUEST_ENOTDIR); } #endif if (!std::filesystem::is_directory(status)) { #ifdef _WIN32 - if (File::WindowsSymbolicLinkLoop(WithoutTrailingSeparator(native).parent_path())) return SceKernelError(GUEST_ELOOP); + if (File::WindowsSymbolicLinkLoop(WithoutTrailingSeparator(targetNative).parent_path())) return SceKernelError(GUEST_ELOOP); #endif - const bool blockedByFile = HasNonDirectoryPrefix(native) || std::filesystem::exists(WithoutTrailingSeparator(native), error); + const bool blockedByFile = HasNonDirectoryPrefix(targetNative) || std::filesystem::exists(WithoutTrailingSeparator(targetNative), error); return SceErrorFromErrno(blockedByFile ? GUEST_ENOTDIR : GUEST_ENOENT); } - if (!std::filesystem::is_empty(native, error)) return SceErrorFromErrno(GUEST_ENOTEMPTY); - if (!RemoveEmptyDirectory(native)) return SceErrorFromErrno(GUEST_EIO); +#ifdef _WIN32 + if (!IsDirectoryEmpty(targetNative)) return SceErrorFromErrno(GUEST_ENOTEMPTY); +#else + if (!std::filesystem::is_empty(targetNative, error)) return SceErrorFromErrno(GUEST_ENOTEMPTY); +#endif + if (!RemoveEmptyDirectory(targetNative)) return SceErrorFromErrno(GUEST_EIO); RecordWrittenPath_nid_no_patch(native); return 0; } @@ -1812,7 +1889,7 @@ extern "C" { int APS5_VABI sceKernelChmod_nid_postfix(const char* path, std::uint16_t mode) { std::string copy; - if (const int error = CopyGuestPath_nid_no_patch(path, copy)) return SceErrorFromErrno(error); + if (const int error = CopyGuestPath_nid_no_patch(path, copy)) return SceKernelError(error); path = copy.c_str(); if (copy.empty()) return SceErrorFromErrno(GUEST_ENOENT); std::filesystem::path native; @@ -1852,24 +1929,26 @@ static bool CopyInTimes(const KernelTimeval*& times, KernelTimeval (&local)[2]) int APS5_VABI sceKernelTruncate_nid_postfix(const char* path, std::int64_t length) { if (length < 0) return SceErrorFromErrno(GUEST_EINVAL); std::string copy; - if (const int error = CopyGuestPath_nid_no_patch(path, copy)) return SceErrorFromErrno(error); + if (const int error = CopyGuestPath_nid_no_patch(path, copy)) return SceKernelError(error); path = copy.c_str(); if (copy.empty()) return SceErrorFromErrno(GUEST_ENOENT); std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native)) return SceKernelError(error); std::error_code error; - const auto status = std::filesystem::status(native, error); + const auto verbatim = NativeVerbatimPath_nid_no_patch(native, error); + const auto& target = error ? native : verbatim; + const auto status = std::filesystem::status(target, error); if (const int failure = PathLookupFailure(error)) return failure; if (!std::filesystem::exists(status)) { #ifdef _WIN32 - if (File::WindowsSymbolicLinkLoop(native)) return SceKernelError(GUEST_ELOOP); + if (File::WindowsSymbolicLinkLoop(target)) return SceKernelError(GUEST_ELOOP); #endif return SceErrorFromErrno(NotFoundError_nid_no_patch(native)); } if (std::filesystem::is_directory(status)) return SceErrorFromErrno(GUEST_EISDIR); - if (const int failure = NativeTruncate(native, length)) { + if (const int failure = NativeTruncate(target, length)) { #ifdef _WIN32 - if (File::WindowsSymbolicLinkLoop(native)) return SceKernelError(GUEST_ELOOP); + if (File::WindowsSymbolicLinkLoop(target)) return SceKernelError(GUEST_ELOOP); #endif return SceErrorFromErrno(failure); } @@ -1890,7 +1969,7 @@ int APS5_VABI sceKernelUtimes_nid_postfix(const char* path, const KernelTimeval* if (!CopyInTimes(times, local)) return SCE_KERNEL_ERROR_EFAULT; if (InvalidTimes(times)) return SCE_KERNEL_ERROR_EINVAL; std::string copy; - if (const int error = CopyGuestPath_nid_no_patch(path, copy)) return SceErrorFromErrno(error); + if (const int error = CopyGuestPath_nid_no_patch(path, copy)) return SceKernelError(error); path = copy.c_str(); if (copy.empty()) return SceErrorFromErrno(GUEST_ENOENT); std::filesystem::path native; diff --git a/core/libs/prx/libkernel/File/src/Terminal.cpp b/core/libs/prx/libkernel/File/src/Terminal.cpp index d8220e00bb..cdd4f8eec5 100644 --- a/core/libs/prx/libkernel/File/src/Terminal.cpp +++ b/core/libs/prx/libkernel/File/src/Terminal.cpp @@ -131,4 +131,6 @@ int APS5_VABI tcsetpgrp_nid_postfix(int descriptor, int group) { static_cast(group); return Fail(ProcessGroupError(descriptor, "tcsetpgrp: the SIGIO owner of a pipe is not supported")); } + +int APS5_VABI tcgetsid_nid_postfix(int descriptor) { return Fail(TerminalError(descriptor)); } } diff --git a/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp b/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp index c0b30ac399..b9b9beb672 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp @@ -40,35 +40,62 @@ static bool AcceptsSchedParam(int policy, int priority) { return (priority >= 256 && priority <= 767) || (priority >= 896 && priority <= 959); } static constexpr std::size_t GUEST_CPUSET_BYTES = 32; +static constexpr std::size_t GUEST_GETAFFINITY_ERANGE_BYTES = 24; static constexpr int GUEST_ERANGE = 34; extern "C" { int APS5_VABI cpuset_getaffinity_nid_postfix(int level, int which, std::int64_t id, std::size_t size, void* mask) { - constexpr int LevelWhich = 3, WhichThread = 1, WhichProcess = 2; + constexpr int LevelRoot = 1, LevelCpuset = 2, LevelWhich = 3; + constexpr int WhichThread = 1, WhichProcess = 2, WhichCpuset = 3, WhichIrq = 4, WhichJail = 5, WhichDomain = 6; constexpr int GuestEsrch = 3, GuestEfault = 14, GuestEinval = 22, GuestErange = 34; constexpr std::size_t MaximumSize = 256 / 8; + constexpr KernelCpumask RootCpumask = 0xffff; const auto fail = [](int error) { *__error_nid_postfix() = error; return -1; }; - if (size < sizeof(KernelCpumask) || size > MaximumSize) return fail(GuestErange); + if (size < sizeof(KernelCpumask) || size >= 24) return fail(GuestErange); + if (size > 16) NotImplemented_nid_no_patch("cpuset_getaffinity with a size of 17 to 23 bytes"); if (!mask) return fail(GuestEfault); auto* self = scePthreadSelf(); KernelCpumask affinity = 0; - if (which == WhichThread) { + switch (which) { + case WhichThread: { const auto tid = id == -1 ? self->tid : id; if (!GetLiveGuestThreadAffinity_nid_no_patch(tid, affinity)) return fail(GuestEsrch); - } else if (which == WhichProcess) { + break; + } + case WhichProcess: if (id != -1 && id != getpid_nid_postfix()) return fail(GuestEsrch); - } else if (which >= 3 && which <= 6) { - NotImplemented_nid_no_patch("cpuset_getaffinity other than the calling thread or process"); - } else { + break; + case WhichCpuset: + if (id != -1) NotImplemented_nid_no_patch("cpuset_getaffinity of a numbered cpuset"); + break; + case WhichJail: + NotImplemented_nid_no_patch("cpuset_getaffinity of a jail's cpuset"); + break; + case WhichIrq: + case WhichDomain: + break; + default: + return fail(GuestEinval); + } + switch (level) { + case LevelRoot: + case LevelCpuset: + if (which == WhichIrq || which == WhichDomain) return fail(GuestEinval); + affinity = level == LevelRoot ? RootCpumask : sceKernelGetAvailableCpumask(); + break; + case LevelWhich: + if (which == WhichIrq) NotImplemented_nid_no_patch("cpuset_getaffinity of an interrupt"); + if (which == WhichDomain) NotImplemented_nid_no_patch("cpuset_getaffinity of a memory domain"); + if (which == WhichProcess) affinity = LiveGuestThreadsAffinity(); + else if (which == WhichCpuset) affinity = sceKernelGetAvailableCpumask(); + break; + default: return fail(GuestEinval); } - if (level < 1 || level > 3) return fail(GuestEinval); - if (level != LevelWhich) NotImplemented_nid_no_patch("cpuset_getaffinity other than the calling thread or process"); - if (which == WhichProcess) affinity = LiveGuestThreadsAffinity(); std::array buffer{}; std::memcpy(buffer.data(), &affinity, sizeof(affinity)); if (!GuestCopyOut_nid_no_patch(mask, buffer.data(), size)) return fail(GuestEfault); @@ -76,14 +103,17 @@ int APS5_VABI cpuset_getaffinity_nid_postfix(int level, int which, std::int64_t } int APS5_VABI cpuset_setaffinity_nid_postfix(int level, int which, std::int64_t id, std::size_t size, const void* mask) { - constexpr int LevelWhich = 3, WhichThread = 1, WhichProcess = 2; + constexpr int LevelRoot = 1, LevelCpuset = 2, LevelWhich = 3; + constexpr int WhichThread = 1, WhichProcess = 2, WhichCpuset = 3, WhichIrq = 4, WhichJail = 5, WhichDomain = 6; + constexpr int WhichInterruptHandler = 7, WhichInterruptThread = 8; constexpr int GuestEsrch = 3, GuestEdeadlk = 11, GuestEfault = 14, GuestEinval = 22, GuestErange = 34; constexpr std::size_t MaximumSize = 256 / 8; const auto fail = [](int error) { *__error_nid_postfix() = error; return -1; }; - if (size < sizeof(KernelCpumask) || size > MaximumSize) return fail(GuestErange); + if (size < sizeof(KernelCpumask) || size >= 24) return fail(GuestErange); + if (size > 16) NotImplemented_nid_no_patch("cpuset_setaffinity with a size of 17 to 23 bytes"); if (!mask) return fail(GuestEfault); std::array buffer{}; if (!GuestCopyIn_nid_no_patch(buffer.data(), mask, size)) return fail(GuestEfault); @@ -91,17 +121,56 @@ int APS5_VABI cpuset_setaffinity_nid_postfix(int level, int which, std::int64_t if (buffer[index] != 0) return fail(GuestEinval); } auto* self = scePthreadSelf(); - if (which == WhichThread) { - if (id != -1 && id != self->tid && !LiveGuestThreadExists(id)) return fail(GuestEsrch); - } else if (which == WhichProcess) { + const auto threadExists = [&] { return id == -1 || id == self->tid || LiveGuestThreadExists(id); }; + switch (level) { + case LevelRoot: + case LevelCpuset: + switch (which) { + case WhichThread: + if (!threadExists()) return fail(GuestEsrch); + break; + case WhichProcess: + if (id != -1 && id != getpid_nid_postfix()) return fail(GuestEsrch); + break; + case WhichCpuset: + if (id != -1) NotImplemented_nid_no_patch("cpuset_setaffinity of a numbered cpuset"); + break; + case WhichJail: + NotImplemented_nid_no_patch("cpuset_setaffinity of a jail's cpuset"); + break; + case WhichIrq: + case WhichDomain: + default: + return fail(GuestEinval); + } + NotImplemented_nid_no_patch(level == LevelRoot ? "cpuset_setaffinity at CPU_LEVEL_ROOT" : "cpuset_setaffinity at CPU_LEVEL_CPUSET"); + return fail(GuestEinval); + case LevelWhich: + break; + default: + return fail(GuestEinval); + } + switch (which) { + case WhichThread: + if (!threadExists()) return fail(GuestEsrch); + break; + case WhichProcess: if (id != -1 && id != getpid_nid_postfix()) return fail(GuestEsrch); - } else if (which >= 3 && which <= 6) { - NotImplemented_nid_no_patch("cpuset_setaffinity other than the calling thread or process"); - } else { + break; + case WhichCpuset: + NotImplemented_nid_no_patch(id == -1 ? "cpuset_setaffinity of the calling thread's base cpuset" : "cpuset_setaffinity of a numbered cpuset"); + break; + case WhichJail: + NotImplemented_nid_no_patch("cpuset_setaffinity of a jail's cpuset"); + break; + case WhichIrq: + case WhichInterruptHandler: + case WhichInterruptThread: + NotImplemented_nid_no_patch("cpuset_setaffinity of an interrupt"); + break; + default: return fail(GuestEinval); } - if (level < 1 || level > 3) return fail(GuestEinval); - if (level != LevelWhich) NotImplemented_nid_no_patch("cpuset_setaffinity other than the calling thread or process"); KernelCpumask requested = 0; std::memcpy(&requested, buffer.data(), sizeof(requested)); const KernelCpumask affinity = requested & sceKernelGetAvailableCpumask(); @@ -271,8 +340,8 @@ int APS5_VABI pthread_getprio_nid_postfix(Pthread thread) { int APS5_VABI pthread_getaffinity_np_nid_postfix(Pthread thread, std::size_t cpusetSize, void* cpuset) { if (!thread) return PosixThread::GUEST_EINVAL; - if (cpusetSize >= GUEST_CPUSET_BYTES) return GUEST_ERANGE; - if (cpusetSize != 8 && cpusetSize != 16) NotImplemented_nid_no_patch("pthread_getaffinity_np with a set other than 8, 16 or 32 bytes and more"); + if (cpusetSize < sizeof(KernelCpumask) || cpusetSize >= GUEST_GETAFFINITY_ERANGE_BYTES) return GUEST_ERANGE; + if (cpusetSize > 16) NotImplemented_nid_no_patch("pthread_getaffinity_np with a set of 17 to 23 bytes"); if (!cpuset) return PosixThread::GUEST_EFAULT; KernelCpumask mask = 0; const int result = scePthreadGetaffinity(thread, &mask); diff --git a/core/libs/prx/libkernel/Pthread/include/Pthread.hpp b/core/libs/prx/libkernel/Pthread/include/Pthread.hpp index 46e1991860..a3928f8881 100644 --- a/core/libs/prx/libkernel/Pthread/include/Pthread.hpp +++ b/core/libs/prx/libkernel/Pthread/include/Pthread.hpp @@ -204,5 +204,8 @@ bool GetLiveGuestThreadSched_nid_no_patch(std::int64_t tid, int& policy, int& pr 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); +#if defined(__linux__) +extern "C" [[noreturn]] void APS5_VABI EnterGuestMainStack_nid_no_patch(void* arguments, void (APS5_VABI* entry)(void*)); +#endif #endif diff --git a/core/libs/prx/libkernel/Pthread/src/Thread.cpp b/core/libs/prx/libkernel/Pthread/src/Thread.cpp index 17b7cf277a..55eef3bf94 100644 --- a/core/libs/prx/libkernel/Pthread/src/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Thread.cpp @@ -7,7 +7,9 @@ #include "../include/WindowsThreadLocal.hpp" #include "../include/Cancel.hpp" #include "prx/libkernel/Time/include/Time.hpp" +#include "prx/libkernel/Time/include/TimedWait.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/GuestArena.hpp" #include "prx/libc/include/general/AsmMacros.hpp" #include "prx/libkernel/KernelErrors.hpp" #include "prx/libc/include/CpuTopology.hpp" @@ -35,6 +37,7 @@ #include #include #include +#include #include #endif @@ -59,6 +62,7 @@ static constexpr int GUEST_EINVAL = 22; 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; +static constexpr std::uint16_t GUEST_THREAD_FPU_CONTROL = 0x037f; #ifdef _WIN32 #include @@ -124,6 +128,11 @@ static void UnregisterGuestStackMapping(void* mapping) { std::lock_guard lock(guestStackMappingsLock); guestStackMappings.erase(reinterpret_cast(mapping)); } + +static constexpr std::uintptr_t GUEST_MAIN_STACK_TOP = 0x7eeffc000; +static constexpr int GUEST_MAP_FIXED = 0x10; +static std::atomic guestMainStackLow{0}; +static std::atomic guestMainStackHigh{0}; #endif #if defined(__linux__) @@ -214,6 +223,51 @@ static void ReleaseSpareStacks() { for (const auto& stack : release) ReleaseFinishedStack(stack); } +static std::size_t GuestMainStackBytes() { + constexpr std::size_t fallback = std::size_t{8} << 20; + constexpr std::size_t largest = std::size_t{1} << 30; + rlimit stack{}; + if (getrlimit(RLIMIT_STACK, &stack) != 0 || stack.rlim_cur == RLIM_INFINITY || stack.rlim_cur == 0) return fallback; + const auto bytes = static_cast(std::min(stack.rlim_cur, largest)); + return (bytes + PS5_PAGE_SIZE - 1) / PS5_PAGE_SIZE * PS5_PAGE_SIZE; +} + +extern "C" [[noreturn]] void APS5_VABI EnterGuestMainStack_nid_no_patch(void* arguments, void (APS5_VABI* entry)(void*)) { + if (entry == nullptr) throw std::invalid_argument("EnterGuestMainStack: null entry point"); + if (guestMainStackLow.load(std::memory_order_acquire) != 0) throw std::runtime_error("EnterGuestMainStack: the guest main stack was already entered"); + std::uintptr_t arenaBase = 0; + std::size_t arenaBytes = 0; + GuestArena::GuestArenaRange_nid_postfix(&arenaBase, &arenaBytes); + const std::uintptr_t top = GuestArena::GuestAddressOffset + GUEST_MAIN_STACK_TOP; + const std::size_t bytes = GuestMainStackBytes() + GUEST_STACK_GUARD; + if (top - bytes < arenaBase || top > arenaBase + arenaBytes) + throw std::runtime_error("EnterGuestMainStack: the guest arena does not cover the console's main stack range"); + void* mapping = reinterpret_cast(top - bytes); + const int mapped = DoMapAnon(&mapping, bytes, 3, GUEST_MAP_FIXED, PS5_PAGE_SIZE, true); + if (mapped == SCE_KERNEL_ERROR_ENOMEM) { + char message[160]; + std::snprintf(message, sizeof(message), "EnterGuestMainStack: the guest main stack [%#zx, %#zx) overlaps a guest mapping", static_cast(top - bytes), static_cast(top)); + throw std::runtime_error(message); + } + if (mapped != 0 || reinterpret_cast(mapping) + bytes != top) + throw std::runtime_error("EnterGuestMainStack: cannot map the guest main stack"); + if (DoMprotect(mapping, GUEST_STACK_GUARD, 0) != 0) throw std::runtime_error("EnterGuestMainStack: cannot protect the guest main stack guard"); + guestMainStackHigh.store(top, std::memory_order_release); + guestMainStackLow.store(reinterpret_cast(mapping) + GUEST_STACK_GUARD, std::memory_order_release); + __asm__ volatile( + "mov %0, %%rsp\n\t" + "pushq $0\n\t" + "pushq $0\n\t" + "mov %%rsp, %%rbp\n\t" + "xor %%esi, %%esi\n\t" + "call *%2\n\t" + "ud2" + : + : "r"(top), "D"(arguments), "a"(entry) + : "memory"); + __builtin_unreachable(); +} + static void AllocateGuestStack(PthreadPrivate* thread, std::size_t usableBytes) { const std::size_t bytes = (usableBytes + PS5_PAGE_SIZE - 1) / PS5_PAGE_SIZE * PS5_PAGE_SIZE + GUEST_STACK_GUARD; FinishedStack spare{}; @@ -305,6 +359,16 @@ static std::uintptr_t PrecommittedStackStart(std::uintptr_t low, std::uintptr_t #endif static bool HostStackLimits(std::uintptr_t* low, std::uintptr_t* high) { +#if defined(__linux__) + const auto mainLow = guestMainStackLow.load(std::memory_order_acquire); + const auto mainHigh = guestMainStackHigh.load(std::memory_order_acquire); + const auto frame = reinterpret_cast(__builtin_frame_address(0)); + if (mainLow != 0 && frame >= mainLow && frame < mainHigh) { + *low = mainLow; + *high = mainHigh; + return true; + } +#endif #ifdef _WIN32 ULONG_PTR lowLimit = 0; ULONG_PTR highLimit = 0; @@ -758,6 +822,9 @@ static void finishThread(PthreadPrivate* self, void* retval) { self->_finished.store(true, std::memory_order_release); } self->_join_cv.NotifyAll(); +#ifdef _WIN32 + TimedWait::ReapDeliveryDrain(); +#endif } bool ThreadCancel::Requested() { @@ -821,6 +888,7 @@ static void RunThread(std::unique_ptr args) { BindHostCpuClock(self); TimedWait::BindThreadWaitState(&self->waitCount); CurrentGuestState().current = self; + asm volatile("fldcw %0" : : "m"(GUEST_THREAD_FPU_CONTROL)); RegisterStack(self); #ifndef _WIN32 const sigset_t signalMask = args->signalMask; @@ -956,6 +1024,14 @@ struct NativeThreadArgs { }; #ifdef _WIN32 +static std::wstring ThreadDescription(const std::string& name) { + if (name.empty()) return {}; + const int length = MultiByteToWideChar(CP_UTF8, 0, name.data(), static_cast(name.size()), nullptr, 0); + std::wstring description(static_cast(length), L'\0'); + MultiByteToWideChar(CP_UTF8, 0, name.data(), static_cast(name.size()), description.data(), length); + return description; +} + static unsigned __stdcall StartNativeThread(void* opaque) { std::unique_ptr args(static_cast(opaque)); auto* self = args->guest->self; @@ -971,8 +1047,7 @@ static unsigned __stdcall StartNativeThread(void* opaque) { self->threadId = std::this_thread::get_id(); CurrentGuestState().current = self; if (!self->name.empty()) { - const std::wstring description(self->name.begin(), self->name.end()); - SetThreadDescription(GetCurrentThread(), description.c_str()); + SetThreadDescription(GetCurrentThread(), ThreadDescription(self->name).c_str()); ApplyJobAffinity(*self, nullptr); } args->initialized.set_value(); @@ -1412,8 +1487,7 @@ int APS5_VABI scePthreadRename(Pthread thread, const char* name) { std::lock_guard lock(thread->nameLock); thread->name = name; #ifdef _WIN32 - const std::wstring description(thread->name.begin(), thread->name.end()); - SetThreadDescription(static_cast(thread->nativeHandle), description.c_str()); + SetThreadDescription(static_cast(thread->nativeHandle), ThreadDescription(thread->name).c_str()); ApplyJobAffinity(*thread, thread->nativeHandle); #endif return SCE_OK; diff --git a/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp b/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp index 69ca450766..2d86ba2c66 100644 --- a/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp +++ b/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp @@ -88,6 +88,9 @@ int APS5_VABI sceKernelWaitSema(KernelSema sem, int need, KernelUseconds* time) if (need <= 0) { APS5_INVALID_ARG_EX; } + if (need > sem->maxCount) { + return SCE_KERNEL_ERROR_EINVAL; + } std::unique_lock lock(sem->mutex); const void* caller = __builtin_return_address(0); diff --git a/core/libs/prx/libkernel/Socket/include/SocketPoll.hpp b/core/libs/prx/libkernel/Socket/include/SocketPoll.hpp index 55956c9294..3b4b3a93b3 100644 --- a/core/libs/prx/libkernel/Socket/include/SocketPoll.hpp +++ b/core/libs/prx/libkernel/Socket/include/SocketPoll.hpp @@ -21,6 +21,13 @@ using Closer = bool (*)(int descriptor, int* error); using OptionSetter = bool (*)(int descriptor, int level, int option, const void* value, std::uint32_t length, int* error); using OptionGetter = bool (*)(int descriptor, int level, int option, void* value, std::uint32_t* length, int* error); using StatusControl = bool (*)(int descriptor, int command, int flags, int* result, int* error); +struct Forwards { + int (*family)(int descriptor); + int (*bind)(int descriptor, const void* address, std::uint32_t length); + int (*getsockname)(int descriptor, void* address, std::uint32_t* length); + std::int64_t (*sendto)(int descriptor, const void* buffer, std::uint64_t length, int flags, const void* address, std::uint32_t addressLength, int* error); + std::int64_t (*recvfrom)(int descriptor, void* buffer, std::uint64_t length, int flags, void* address, std::uint32_t* addressLength, int* error); +}; struct NativeOwner { int (*transfer)(NativeOwner* owner, bool send, void* buffer, int length) = nullptr; }; @@ -28,6 +35,7 @@ struct NativeOwner { using NativeLookup = bool (*)(int descriptor, std::shared_ptr* owner, std::uintptr_t* native); } +extern "C" void KernelSetSocketForwards_nid_no_patch(const KernelSocketPoll::Forwards* forwards); extern "C" void KernelSetSocketPoller_nid_no_patch(KernelSocketPoll::Poller poller); extern "C" void KernelSetSocketCloser_nid_no_patch(KernelSocketPoll::Closer closer); extern "C" bool KernelCloseSocket_nid_no_patch(int descriptor, int* error); diff --git a/core/libs/prx/libkernel/Socket/src/Socket.cpp b/core/libs/prx/libkernel/Socket/src/Socket.cpp index fa5a9d493e..b8f8ab3a47 100644 --- a/core/libs/prx/libkernel/Socket/src/Socket.cpp +++ b/core/libs/prx/libkernel/Socket/src/Socket.cpp @@ -268,8 +268,12 @@ int APS5_VABI select_nid_postfix(int nfds, void* readfds, void* writefds, void* return -1; } for (const auto& entry : hosts.entries) { - if ((entry.revents & KernelSocketPoll::Unknown) != 0) - throw std::runtime_error("select: descriptor " + std::to_string(entry.descriptor) + " is neither a socket, a pipe, a file, a directory nor a kqueue"); + if ((entry.revents & KernelSocketPoll::Unknown) == 0) continue; + if (!DescriptorIsOpen_nid_no_patch(entry.descriptor)) { + *__error_nid_postfix() = GuestEbadf; + return -1; + } + throw std::runtime_error("select: descriptor " + std::to_string(entry.descriptor) + " is neither a socket, a pipe, a file, a directory nor a kqueue"); } groups.push_back(std::move(hosts)); } diff --git a/core/libs/prx/libkernel/Socket/src/SocketOptions.cpp b/core/libs/prx/libkernel/Socket/src/SocketOptions.cpp index 027d43d4c1..e5e046868a 100644 --- a/core/libs/prx/libkernel/Socket/src/SocketOptions.cpp +++ b/core/libs/prx/libkernel/Socket/src/SocketOptions.cpp @@ -13,7 +13,6 @@ #endif #endif #include "prx/libkernel/Socket/include/SocketError.hpp" -#include "prx/libc/include/GuestArena.hpp" #include "prx/libkernel/Socket/include/SocketOptions.hpp" #include "prx/libc/include/General.hpp" #include @@ -158,14 +157,14 @@ int IpReceiveBit(int option) { } } +int ReadValue(void* destination, const void* source, std::size_t bytes) { + return GuestCopyIn_nid_no_patch(destination, source, bytes) ? 0 : ErrorFault; +} + int Copy(void* output, std::uint32_t* length, const void* value, std::uint32_t size) { const std::uint32_t count = output ? std::min(*length, size) : 0; - const GuestArena::HostWrite destination(count == 0 ? nullptr : output, count); - if (!destination.Open()) return ErrorFault; - const GuestArena::HostWrite lengthOutput(length, sizeof(*length)); - if (!lengthOutput.Open()) return ErrorFault; - if (count != 0) std::memcpy(output, value, count); - *length = count; + if (count != 0 && !GuestCopyOut_nid_no_patch(output, value, count)) return ErrorFault; + if (!GuestCopyOut_nid_no_patch(length, &count, sizeof(count))) return ErrorFault; return 0; } @@ -182,7 +181,7 @@ int SetSocketLevelOption(std::uintptr_t native, SocketOptionState& state, int fa if (option == GuestLinger) { if (length < sizeof(GuestLingerValue)) return ErrorInvalid; GuestLingerValue guest; - std::memcpy(&guest, value, sizeof(guest)); + if (const int fault = ReadValue(&guest, value, sizeof(guest))) return fault; if (guest.seconds < 0 || guest.seconds > MaximumLinger) return ErrorDomain; #ifdef _WIN32 linger request{}; @@ -199,7 +198,7 @@ int SetSocketLevelOption(std::uintptr_t native, SocketOptionState& state, int fa if (option == GuestSendTimeout || option == GuestReceiveTimeout) { if (length < sizeof(GuestTimeval)) return ErrorInvalid; GuestTimeval time; - std::memcpy(&time, value, sizeof(time)); + if (const int fault = ReadValue(&time, value, sizeof(time))) return fault; if (time.seconds < 0 || time.microseconds < 0 || time.microseconds >= 1000000) return ErrorDomain; (option == GuestSendTimeout ? state.sendTimeout : state.receiveTimeout) = TimeoutOf(time); return 0; @@ -207,7 +206,7 @@ int SetSocketLevelOption(std::uintptr_t native, SocketOptionState& state, int fa if (option == GuestNoSigpipe || option == GuestNonBlocking) { if (length < sizeof(int)) return ErrorInvalid; int number; - std::memcpy(&number, value, sizeof(number)); + if (const int fault = ReadValue(&number, value, sizeof(number))) return fault; if (option == GuestNoSigpipe) { state.noSignal = number != 0; return 0; @@ -228,7 +227,7 @@ int SetSocketLevelOption(std::uintptr_t native, SocketOptionState& state, int fa if (native_boolean || buffer || lowWater) { if (length < sizeof(int)) return ErrorInvalid; int number; - std::memcpy(&number, value, sizeof(number)); + if (const int fault = ReadValue(&number, value, sizeof(number))) return fault; if (buffer || lowWater) { if (number < 1) return ErrorInvalid; EnsureBuffers(state, family, type); @@ -253,7 +252,7 @@ int SetSocketLevelOption(std::uintptr_t native, SocketOptionState& state, int fa if ((option & (KeptFlags | WriteOnlyFlags)) != 0 && (option & (option - 1)) == 0) { if (length < sizeof(int)) return ErrorInvalid; int number; - std::memcpy(&number, value, sizeof(number)); + if (const int fault = ReadValue(&number, value, sizeof(number))) return fault; if ((option & KeptFlags) == 0) return 0; const int flags = number != 0 ? state.keptFlags | option : state.keptFlags & ~option; if (option == GuestReuseAddress || option == GuestReusePort) { @@ -355,7 +354,7 @@ int SetTcpLevelOption(std::uintptr_t native, SocketOptionState& state, bool tcp, return ErrorNoProtocolOption; if (length < sizeof(int)) return ErrorInvalid; int number; - std::memcpy(&number, value, sizeof(number)); + if (const int fault = ReadValue(&number, value, sizeof(number))) return fault; if (const int bit = TcpFlagBit(option); bit != 0) { state.tcpFlags = number != 0 ? state.tcpFlags | bit : state.tcpFlags & ~bit; return 0; @@ -413,7 +412,7 @@ int SetIpLevelOption(std::uintptr_t native, SocketOptionState& state, bool ipv4, if (option == GuestIpAddMembership || option == GuestIpDropMembership) { if (length < sizeof(ip_mreq)) return ErrorInvalid; ip_mreq request{}; - std::memcpy(&request, value, sizeof(request)); + if (const int fault = ReadValue(&request, value, sizeof(request))) return fault; const auto* group = reinterpret_cast(&request.imr_multiaddr); if ((group[0] & 0xf0) != 0xe0) return ErrorInvalid; std::uint32_t groupBits; @@ -441,14 +440,15 @@ int SetIpLevelOption(std::uintptr_t native, SocketOptionState& state, bool ipv4, if (length < sizeof(in_addr)) return ErrorInvalid; in_addr address{}; if (length == MulticastRequestWithIndex) { + unsigned char request[MulticastRequestWithIndex]; + if (const int fault = ReadValue(request, value, sizeof(request))) return fault; std::int32_t index; - std::memcpy(&index, static_cast(value) + 8, sizeof(index)); + std::memcpy(&index, request + 8, sizeof(index)); if (index != 0) throw std::runtime_error("IP_MULTICAST_IF: an interface index is not supported"); - const auto* addresses = static_cast(value); - if (std::any_of(addresses, addresses + 8, [](unsigned char byte) { return byte != 0; })) + if (std::any_of(request, request + 8, [](unsigned char byte) { return byte != 0; })) throw std::runtime_error("IP_MULTICAST_IF: an ip_mreqn with a non-zero address is not supported"); } else { - std::memcpy(&address, value, sizeof(address)); + if (const int fault = ReadValue(&address, value, sizeof(address))) return fault; } if (::setsockopt(handle, IPPROTO_IP, IP_MULTICAST_IF, reinterpret_cast(&address), sizeof(address))) return HostError(); std::memcpy(&state.multicastInterface, &address, sizeof(address)); @@ -457,14 +457,14 @@ int SetIpLevelOption(std::uintptr_t native, SocketOptionState& state, bool ipv4, if (const int bit = IpReceiveBit(option); bit != 0) { if (length < sizeof(int)) return ErrorInvalid; int number; - std::memcpy(&number, value, sizeof(number)); + if (const int fault = ReadValue(&number, value, sizeof(number))) return fault; state.ipReceiveOptions = number != 0 ? state.ipReceiveOptions | bit : state.ipReceiveOptions & ~bit; return 0; } if (option == GuestIpTimeToLive || option == GuestIpTypeOfService) { if (length < sizeof(int)) return ErrorInvalid; int number; - std::memcpy(&number, value, sizeof(number)); + if (const int fault = ReadValue(&number, value, sizeof(number))) return fault; const int stored = number & 0xff; if (option == GuestIpTypeOfService) { state.ipTypeOfService = stored; @@ -478,11 +478,11 @@ int SetIpLevelOption(std::uintptr_t native, SocketOptionState& state, bool ipv4, unsigned int number; if (length == 1) { unsigned char byte; - std::memcpy(&byte, value, 1); + if (const int fault = ReadValue(&byte, value, 1)) return fault; number = byte; } else { if (length < sizeof(number)) return ErrorInvalid; - std::memcpy(&number, value, sizeof(number)); + if (const int fault = ReadValue(&number, value, sizeof(number))) return fault; if (option == GuestIpMulticastTimeToLive && number > 255) return ErrorInvalid; } const int host = option == GuestIpMulticastLoop ? ((number & 0xff) != 0 ? 1 : 0) : static_cast(number); @@ -521,7 +521,7 @@ int SetIpv6LevelOption(std::uintptr_t native, bool ipv6, int option, const void* if (option != GuestIpv6Only) return ErrorNoProtocolOption; if (length != sizeof(int)) return ErrorInvalid; int number; - std::memcpy(&number, value, sizeof(number)); + if (const int fault = ReadValue(&number, value, sizeof(number))) return fault; const int enabled = number != 0 ? 1 : 0; if (::setsockopt(static_cast(native), IPPROTO_IPV6, IPV6_V6ONLY, reinterpret_cast(&enabled), sizeof(enabled))) return HostError(); return 0; diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 1408a68220..07d7885068 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -270,6 +270,16 @@ int Install(std::shared_ptr socket) { } return number; } +std::atomic socketForwards{nullptr}; +struct Forwarded { + const KernelSocketPoll::Forwards* forwards = nullptr; + int family = 0; +}; +Forwarded ForwardsFor(int descriptor) { + const auto* forwards = GuestSockets::IsOpen(descriptor) ? nullptr : socketForwards.load(std::memory_order_acquire); + const int family = forwards == nullptr ? 0 : forwards->family(descriptor); + return family == 0 ? Forwarded{} : Forwarded{forwards, family}; +} std::shared_ptr Lookup(int descriptor) { std::lock_guard lock(socketsMutex); const auto found = sockets.find(descriptor); @@ -331,6 +341,10 @@ bool PeerConnected(const Socket& socket) { socklen_t size = sizeof(peer); return ::getpeername(socket.value, reinterpret_cast(&peer), &size) == 0; } +bool Multicast(const sockaddr_storage& address) { + if (address.ss_family == AF_INET) return (reinterpret_cast(&reinterpret_cast(address).sin_addr)[0] & 0xf0) == 0xe0; + return address.ss_family == AF_INET6 && reinterpret_cast(&reinterpret_cast(address).sin6_addr)[0] == 0xff; +} 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; @@ -652,6 +666,10 @@ extern "C" void KernelReleaseSocketNumber_nid_no_patch(int number) { ReleaseNumber(number); } +extern "C" void KernelSetSocketForwards_nid_no_patch(const KernelSocketPoll::Forwards* forwards) { + socketForwards.store(forwards, std::memory_order_release); +} + namespace { #ifdef _WIN32 using NativePollDescriptor = WSAPOLLFD; @@ -759,8 +777,10 @@ bool QueryPipe(int descriptor, const char* function, ACCESS_MASK& access, PipeLo if (!query) throw std::runtime_error(std::string(function) + ": NtQueryInformationFile is unavailable"); PipeIoStatus status{}; if (query(handle, &status, &access, sizeof(access), FileAccessInformationClass) < 0 || - query(handle, &status, &pipe, sizeof(pipe), FilePipeLocalInformationClass) < 0) + query(handle, &status, &pipe, sizeof(pipe), FilePipeLocalInformationClass) < 0) { + if (reinterpret_cast(::_get_osfhandle(descriptor)) != handle || GetFileType(handle) != FILE_TYPE_PIPE) return false; throw std::runtime_error(std::string(function) + ": querying pipe descriptor " + std::to_string(descriptor) + " failed"); + } return true; } bool PipeReadiness(int descriptor, short events, short& revents) { @@ -1300,6 +1320,7 @@ int APS5_VABI _fcntl_nid_postfix(int descriptor, int command, ...) { int APS5_VABI setsockopt_nid_postfix(int descriptor, int level, int option, const void* value, std::uint32_t length) { if (!value && length != 0) return Fail(14); + if (static_cast(length) < 0) return Fail(22); int netError = 0; if (!GuestSockets::IsOpen(descriptor) && KernelSetSocketOption_nid_no_patch(descriptor, level, option, value, length, &netError)) { return netError == 0 ? 0 : Fail(netError); @@ -1317,26 +1338,25 @@ int APS5_VABI setsockopt_nid_postfix(int descriptor, int level, int option, } int APS5_VABI getsockopt_nid_postfix(int descriptor, int level, int option, void* value, std::uint32_t* length) { + std::uint32_t declared = 0; + if (value && !GuestCopyIn_nid_no_patch(&declared, length, sizeof(declared))) return Fail(14); + if (value && !GuestCopyOut_nid_no_patch(length, &declared, sizeof(declared))) return Fail(14); int netError = 0; - if (!GuestSockets::IsOpen(descriptor) && KernelGetSocketOption_nid_no_patch(descriptor, level, option, value, length, &netError)) { - return netError == 0 ? 0 : Fail(netError); + if (!GuestSockets::IsOpen(descriptor) && KernelGetSocketOption_nid_no_patch(descriptor, level, option, value, &declared, &netError)) { + if (netError != 0) return Fail(netError); + return GuestCopyOut_nid_no_patch(length, &declared, sizeof(declared)) ? 0 : Fail(14); } const auto socket = Lookup(descriptor); if (!socket) return -1; if (level != 0xffff && level != 0 && level != 6 && level != 41) return Fail(22); - if (!length) return Fail(22); - if (!value) { - const GuestArena::HostWrite lengthOutput(length, sizeof(*length)); - if (!lengthOutput.Open()) return Fail(14); - *length = 0; - } std::lock_guard lock(socket->modeMutex); const auto native = static_cast(socket->value); - const int error = level == 0 ? GuestSockets::GetIpLevelOption(socket->options, socket->family == 2, socket->type == 2, option, value, length) - : level == 6 ? GuestSockets::GetTcpLevelOption(native, socket->options, socket->family != GuestUnix && socket->type == 1, option, value, length) - : level == 41 ? GuestSockets::GetIpv6LevelOption(native, socket->family == 28, option, value, length) - : GuestSockets::GetSocketLevelOption(native, socket->options, socket->family, socket->type, option, value, length); - return error == 0 ? 0 : Fail(error); + const int error = level == 0 ? GuestSockets::GetIpLevelOption(socket->options, socket->family == 2, socket->type == 2, option, value, &declared) + : level == 6 ? GuestSockets::GetTcpLevelOption(native, socket->options, socket->family != GuestUnix && socket->type == 1, option, value, &declared) + : level == 41 ? GuestSockets::GetIpv6LevelOption(native, socket->family == 28, option, value, &declared) + : GuestSockets::GetSocketLevelOption(native, socket->options, socket->family, socket->type, option, value, &declared); + if (error != 0) return Fail(error); + return GuestCopyOut_nid_no_patch(length, &declared, sizeof(declared)) ? 0 : Fail(14); } int APS5_VABI socket_nid_postfix(int family, int type, int protocol) { const bool nonblocking = (type & GuestNonBlocking) != 0; @@ -1456,9 +1476,12 @@ int APS5_VABI connect_nid_postfix(int descriptor, const void* address, std::uint sockaddr_storage native{}; socklen_t size; if (!Address(socket->family, address, mapped ? 28 : length, native, size)) return -1; + if (native.ss_family == AF_INET6 && IN6_IS_ADDR_UNSPECIFIED(&reinterpret_cast(native).sin6_addr)) + reinterpret_cast(native).sin6_addr = in6addr_loopback; #ifdef _WIN32 if (socket->type == 1) RefuseClosedLoopbackAtOnce_nid_no_patch(socket->value, reinterpret_cast(&native)); #endif + if (socket->type == 1 && Multicast(native) && !PeerConnected(*socket)) return Fail(47); if (ZeroPortConnect(*socket, address, length)) return -1; std::unique_lock lock(socket->modeMutex); socket->options.reportedError = 0; @@ -1546,6 +1569,13 @@ std::int64_t APS5_VABI recv_nid_postfix(int descriptor, void* buffer, std::uint6 } int APS5_VABI bind_nid_postfix(int descriptor, const void* address, std::uint32_t length) { if (const int error = AddressLengthError(address, length)) return Fail(error); + if (const auto [forwards, family] = ForwardsFor(descriptor); forwards != nullptr) { + sockaddr_storage checked{}; + socklen_t checkedSize; + if (!Address(family, address, length, checked, checkedSize)) return -1; + const int error = forwards->bind(descriptor, address, length); + return error == 0 ? 0 : Fail(error); + } const auto socket = Lookup(descriptor); if (!socket) return -1; sockaddr_storage native{}; @@ -1553,6 +1583,7 @@ int APS5_VABI bind_nid_postfix(int descriptor, const void* address, std::uint32_ const int mapped = LongMappedDualStack(*socket, address, length); if (mapped < 0) return -1; if (!Address(socket->family, address, mapped ? 28 : length, native, size)) return -1; + if (socket->type == 1 && Multicast(native)) return Fail(47); std::unique_lock lock(socket->modeMutex, std::defer_lock); if (socket->type == 2) lock.lock(); if (::bind(socket->value, reinterpret_cast(&native), size)) return Fail(NativeError()); @@ -1608,6 +1639,13 @@ int CopyAddressOut(const sockaddr_storage& native, bool unnamed, void* address, int APS5_VABI getsockname_nid_postfix(int descriptor, void* address, std::uint32_t* length) { std::uint32_t capacity = 0; if (!GuestCopyIn_nid_no_patch(&capacity, length, sizeof(capacity))) return Fail(14); + if (const auto* forwards = ForwardsFor(descriptor).forwards; forwards != nullptr) { + unsigned char name[28]{}; + std::uint32_t size = std::min(capacity, sizeof(name)); + if (const int error = forwards->getsockname(descriptor, name, &size)) return Fail(error); + if (size != 0 && !GuestCopyOut_nid_no_patch(address, name, size)) return Fail(14); + return GuestCopyOut_nid_no_patch(length, &size, sizeof(size)) ? 0 : Fail(14); + } const auto socket = Lookup(descriptor); if (!socket) return -1; if (socket->family == GuestUnix) return CopyAddressOut({}, true, address, length, capacity); @@ -1641,7 +1679,14 @@ int APS5_VABI ioctl_nid_postfix(int descriptor, std::uint64_t request, void* arg if (File::IoctlRequestMalformed(request)) return Fail(25); const auto socket = Lookup(descriptor); if (!socket) return -1; - if (!argument) return Fail(14); + const auto command = static_cast(request); + if (command == 0x20006601 || command == 0x20006602) { + std::lock_guard registry(socketsMutex); + if (command == 0x20006601) closeOnExecDescriptors.insert(descriptor); + else closeOnExecDescriptors.erase(descriptor); + return 0; + } + if (!argument && (request & 0x1fff0000 || !(request & 0x20000000))) return Fail(14); if (request != 0x8004667e && request != 0x4004667f) return Fail(25); std::lock_guard lock(socket->modeMutex); if (request == 0x4004667f && socket->receiveShut) { @@ -1673,6 +1718,17 @@ std::int64_t APS5_VABI sendto_nid_postfix(int descriptor, const void* buffer, st if (!GuestCopyIn_nid_no_patch(destinationCopy.data(), address, addressLength)) return Fail(14); address = destinationCopy.data(); } + if (const auto [forwards, family] = ForwardsFor(descriptor); forwards != nullptr) { + if ((flags & ~GuestSendFlags) != 0) return Fail(45); + if (length > INT_MAX) return Fail(40); + if (!buffer && length) return Fail(14); + sockaddr_storage checked{}; + socklen_t checkedSize; + if (address && !Address(family, address, addressLength, checked, checkedSize)) return -1; + int error = 0; + const auto sent = forwards->sendto(descriptor, buffer, length, flags, address, addressLength, &error); + return error == 0 ? sent : Fail(error); + } const auto socket = Lookup(descriptor); if (!socket) return -1; if ((flags & ~GuestSendFlags) != 0) return Fail(45); @@ -1706,6 +1762,18 @@ std::int64_t APS5_VABI recvfrom_nid_postfix(int descriptor, void* buffer, std::u int flags, void* address, std::uint32_t* addressLength) { std::uint32_t capacity = 0; if (addressLength && !GuestCopyIn_nid_no_patch(&capacity, addressLength, sizeof(capacity))) return Fail(14); + if (const auto* forwards = ForwardsFor(descriptor).forwards; forwards != nullptr) { + if ((flags & ~GuestReceiveFlags) != 0) return Fail(45); + if (length > INT_MAX) return Fail(40); + if ((!buffer && length) || (address && !addressLength)) return Fail(14); + unsigned char name[28]{}; + std::uint32_t reported = std::min(capacity, sizeof(name)); + int error = 0; + const auto received = forwards->recvfrom(descriptor, buffer, length, flags, address ? name : nullptr, address ? &reported : nullptr, &error); + if (error != 0) return Fail(error); + if (!addressLength) return received; + return ReportAddress(address, addressLength, name, address ? reported : capacity) ? received : Fail(14); + } const auto socket = Lookup(descriptor); if (!socket) return -1; if ((flags & ~GuestReceiveFlags) != 0) return Fail(45); diff --git a/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp b/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp index cacf067d0b..efaf57f906 100644 --- a/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp +++ b/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp @@ -524,7 +524,7 @@ int MutexWake(UserMutex* mutex) { return 0; } -int ConditionDeadline(const UserCondition* condition, std::uint64_t flags, const void* timeArgument, std::optional& deadlineNanos) { +int ConditionDeadline(UserCondition* condition, std::uint64_t flags, const void* timeArgument, std::optional& deadlineNanos) { deadlineNanos.reset(); if (timeArgument == nullptr) { return 0; @@ -537,7 +537,7 @@ int ConditionDeadline(const UserCondition* condition, std::uint64_t flags, const if ((flags & CVWAIT_ABSTIME) == 0) { remaining = SaturatingNanos(timeout.tv_sec, timeout.tv_nsec); } else { - const int clock = (flags & CVWAIT_CLOCKID) != 0 ? static_cast(std::atomic_ref(condition->clockId).load()) : GUEST_CLOCK_REALTIME; + const int clock = (flags & CVWAIT_CLOCKID) != 0 ? static_cast(std::atomic_ref(condition->clockId).load()) : GUEST_CLOCK_REALTIME; KernelTimespec now{}; clock_gettime_nid_postfix(clock, &now); remaining = SaturatingNanos(timeout.tv_sec - now.tv_sec, timeout.tv_nsec - now.tv_nsec); diff --git a/core/libs/prx/libkernel/System/src/Exception.cpp b/core/libs/prx/libkernel/System/src/Exception.cpp index e26b1e26f2..095f8b5ad9 100644 --- a/core/libs/prx/libkernel/System/src/Exception.cpp +++ b/core/libs/prx/libkernel/System/src/Exception.cpp @@ -290,8 +290,11 @@ bool StackWritable(DWORD64 low, DWORD64 high) { struct QueuedDelivery { GuestExceptionHandler handler; int signum; + Pthread thread; std::atomic taken{false}; std::atomic waiting{false}; + std::atomic posted{false}; + std::atomic closed{false}; std::atomic references{1}; }; @@ -299,6 +302,22 @@ void ReleaseQueued(QueuedDelivery* queued) { if (queued != nullptr && queued->references.fetch_sub(1, std::memory_order_acq_rel) == 1) delete queued; } +void SettleQueued(QueuedDelivery* queued) { + if (queued != nullptr) queued->closed.store(true, std::memory_order_release); +} + +void DeferredWaitingEntry(void* parameter) { + auto* queued = static_cast(parameter); + const auto handler = queued->handler; + const int signum = queued->signum; + const bool claimed = !queued->closed.load(std::memory_order_acquire) && !queued->thread->_finished.load(std::memory_order_acquire) && !queued->taken.exchange(true, std::memory_order_acq_rel); + ReleaseQueued(queued); + if (!claimed) return; + CONTEXT context{}; + RtlCaptureContext(&context); + Deliver(handler, signum, context); +} + void CALLBACK WaitingEntry(ULONG_PTR parameter) { auto* queued = reinterpret_cast(parameter); const auto handler = queued->handler; @@ -735,7 +754,7 @@ Attempt TryDeliver(Pthread thread, HANDLE native, GuestExceptionHandler handler, continue; } if (thread->waitCount.load(std::memory_order_seq_cst) > 0) { - if (queued == nullptr) queued = new QueuedDelivery{handler, signum}; + if (queued == nullptr) queued = new QueuedDelivery{handler, signum, thread}; const bool accepted = QueueWaitingDelivery(native, queued); ResumeThread(native); if (!accepted) throw std::runtime_error("sceKernelRaiseException: cannot queue delivery to the waiting thread"); @@ -756,10 +775,16 @@ Attempt TryDeliver(Pthread thread, HANDLE native, GuestExceptionHandler handler, Redirect(native, delivery); return Attempt::Delivered; } - if (running == CodeKind::Host && queued == nullptr) queued = new QueuedDelivery{handler, signum}; + bool postAfterResume = false; + if (running == CodeKind::Host) { + if (queued == nullptr) queued = new QueuedDelivery{handler, signum, thread}; + postAfterResume = !queued->posted.exchange(true, std::memory_order_acq_rel); + if (postAfterResume) queued->references.fetch_add(1, std::memory_order_acq_rel); + } DWORD64 unknown = 0; if (arm != nullptr && running == CodeKind::Host && !restoring) ArmReturnBreak(native, delivery.context, queued, *arm, unknown); ResumeThread(native); + if (postAfterResume) TimedWait::QueueForDeliveryDrain(GetThreadId(native), &thread->_finished, &DeferredWaitingEntry, queued); if (running == CodeKind::Unknown) { static_cast(ClassifyImage(image)); continue; @@ -823,6 +848,7 @@ private: continue; } DisarmReturnBreak(raise.native, raise.arm); + SettleQueued(raise.queued); ReleaseQueued(raise.queued); CloseHandle(raise.native); } @@ -864,6 +890,7 @@ bool RaiseOn(Pthread thread, GuestExceptionHandler handler, int signum) { const auto attempt = TryDeliver(thread, native, handler, signum, queued, &arm); if (attempt == Attempt::Delivered || attempt == Attempt::Exited) { DisarmReturnBreak(native, arm); + SettleQueued(queued); ReleaseQueued(queued); return attempt == Attempt::Delivered; } @@ -871,6 +898,7 @@ bool RaiseOn(Pthread thread, GuestExceptionHandler handler, int signum) { if (attempt == Attempt::Restoring && now >= retryDeadline) { const bool taken = queued != nullptr && queued->taken.exchange(true, std::memory_order_acq_rel); DisarmReturnBreak(native, arm); + SettleQueued(queued); ReleaseQueued(queued); if (taken) return true; throw std::runtime_error("sceKernelRaiseException: the target thread stayed inside NtContinue for 1 s"); @@ -879,6 +907,7 @@ bool RaiseOn(Pthread thread, GuestExceptionHandler handler, int signum) { HANDLE owned = nullptr; if (!DuplicateHandle(GetCurrentProcess(), native, GetCurrentProcess(), &owned, 0, FALSE, DUPLICATE_SAME_ACCESS)) { DisarmReturnBreak(native, arm); + SettleQueued(queued); ReleaseQueued(queued); throw std::runtime_error("sceKernelRaiseException: cannot keep the target thread for a later delivery"); } diff --git a/core/libs/prx/libkernel/System/src/Signals.cpp b/core/libs/prx/libkernel/System/src/Signals.cpp index 56f806e9ec..70d6a45008 100644 --- a/core/libs/prx/libkernel/System/src/Signals.cpp +++ b/core/libs/prx/libkernel/System/src/Signals.cpp @@ -9,7 +9,6 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include #include -#include #include #include #include @@ -170,6 +169,9 @@ int NativeSignal(int guest) { case 6: return SIGABRT; case 8: return SIGFPE; case 11: return SIGSEGV; +#ifndef _WIN32 + case 13: return SIGPIPE; +#endif case 15: return SIGTERM; default: return 0; } @@ -211,7 +213,7 @@ void RunHandler(int guest, GuestHandler handler) { } void Dispatch(int native) { int guest = 0; - for (int candidate : {2, 4, 6, 8, 11, 15}) + for (int candidate : {2, 4, 6, 8, 11, 13, 15}) if (NativeSignal(candidate) == native) { guest = candidate; break; } if (!guest) return; #ifdef _WIN32 @@ -363,7 +365,7 @@ namespace { constexpr std::int64_t MaxItimerSeconds = 0x7fffffff / 2; struct AlarmState { std::mutex lock; - std::condition_variable changed; + TimedWait::Condition changed; std::optional deadline; std::chrono::microseconds interval{0}; bool threadStarted = false; @@ -411,18 +413,19 @@ void RunAlarms() { std::unique_lock lock(alarms.lock); for (;;) { if (!alarms.deadline) { - alarms.changed.wait(lock); + alarms.changed.Wait(lock); continue; } const auto deadline = *alarms.deadline; - if (alarms.changed.wait_until(lock, deadline) != std::cv_status::timeout || alarms.deadline != deadline) continue; + const auto now = std::chrono::steady_clock::now(); + if (now < deadline) { + const auto remaining = std::chrono::duration_cast(deadline - now).count(); + alarms.changed.WaitUntil(lock, TimedWait::NowNanos() + static_cast(remaining)); + continue; + } const auto interval = alarms.interval; if (interval.count() == 0) alarms.deadline.reset(); - else { - const auto now = std::chrono::steady_clock::now(); - const auto periods = now < deadline ? 1 : (now - deadline) / interval + 1; - alarms.deadline = deadline + periods * interval; - } + else alarms.deadline = deadline + ((now - deadline) / interval + 1) * interval; lock.unlock(); ExpireAlarm(interval); lock.lock(); @@ -439,7 +442,7 @@ void NotifyAlarms(AlarmState& alarms) { std::thread(RunAlarms).detach(); alarms.threadStarted = true; } - alarms.changed.notify_all(); + alarms.changed.NotifyAll(); } } diff --git a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp index a29ed2a43f..8c79416c18 100644 --- a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp +++ b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp @@ -601,6 +601,10 @@ int APS5_VABI sysctlbyname_nid_postfix(const char* name, void* oldValue, std::si static constexpr int mib[] = {1, 14, 12}; return sysctl_nid_postfix(mib, 3, oldValue, oldLength, newValue, newLength); } + if (std::strcmp(name, "kern.proc.proc") == 0) { + static constexpr int mib[] = {1, 14, 8}; + return sysctl_nid_postfix(mib, 3, oldValue, oldLength, newValue, newLength); + } if (std::strcmp(name, "kern.boottime") == 0) { static constexpr int mib[] = {1, 21}; return sysctl_nid_postfix(mib, 2, oldValue, oldLength, newValue, newLength); @@ -625,7 +629,10 @@ int APS5_VABI sysctlbyname_nid_postfix(const char* name, void* oldValue, std::si return CopyOut(block, sizeof(block), oldValue, oldLength); } if (std::strcmp(name, "vm.overcommit") == 0) { - if (newValue != nullptr) throw std::runtime_error(std::string(__func__) + ": setting vm.overcommit"); + if (newValue != nullptr) { + *__error_nid_postfix() = 1; + return -1; + } static constexpr int overcommit = 0; return CopyOut(&overcommit, sizeof(overcommit), oldValue, oldLength); } @@ -803,6 +810,7 @@ int APS5_VABI sysctlnametomib_nid_postfix(const char* name, int* mib, std::size_ {"kern.maxfilesperproc", {1, 27}, 2}, {"kern.maxproc", {1, 6}, 2}, {"kern.proc.pathname", {1, 14, 12}, 3}, + {"kern.proc.proc", {1, 14, 8}, 3}, {"kern.ps_strings", {1, 32}, 2}, {"kern.rng_pseudo", {1, 50}, 2}, {"vm.loadavg", {2, 2}, 2}, diff --git a/core/libs/prx/libkernel/System/src/UserContext.cpp b/core/libs/prx/libkernel/System/src/UserContext.cpp index ea580d7132..c984430564 100644 --- a/core/libs/prx/libkernel/System/src/UserContext.cpp +++ b/core/libs/prx/libkernel/System/src/UserContext.cpp @@ -226,7 +226,7 @@ int APS5_VABI Aps5GetcontextFinish_nid_no_patch(UserUcontext* context, const Reg machine.fpformat = FpFormatXmm; machine.ownedfp = FpOwnedFpu; std::memcpy(machine.fpstate, frame->fxsave, sizeof(frame->fxsave)); - std::memcpy(context, &captured, offsetof(UserUcontext, link)); + if (!GuestCopyOut_nid_no_patch(context, &captured, offsetof(UserUcontext, link))) return Fail(GuestEfault); return 0; } @@ -280,28 +280,32 @@ extern "C" { int APS5_VABI setcontext_nid_postfix(const UserUcontext* context) { if (context == nullptr) return Fail(GuestEinval); - const auto& machine = context->mcontext; + UserUcontext local{}; + if (!GuestCopyIn_nid_no_patch(&local, context, offsetof(UserUcontext, link))) return Fail(GuestEfault); + const auto& machine = local.mcontext; if (machine.len != sizeof(UserMcontext) || (machine.flags & ~McontextFlagMask) != 0) return Fail(GuestEinval); - RestoreContext(context, FpuSource::Context); + RestoreContext(&local, FpuSource::Context); } int SigreturnContext_nid_no_patch(const UserUcontext* context) { if (context == nullptr) return Fail(GuestEfault); - const auto& machine = context->mcontext; + UserUcontext local{}; + if (!GuestCopyIn_nid_no_patch(&local, context, sizeof(local))) return Fail(GuestEfault); + const auto& machine = local.mcontext; if ((machine.flags & ~McontextFlagMask) != 0) return Fail(GuestEinval); if (((machine.rflags ^ __builtin_ia32_readeflags_u64()) & ~UserChangeableFlags) != 0) return Fail(GuestEinval); if ((machine.cs & 3) != UserPrivilegeLevel) NotImplemented_nid_no_patch("sigreturn: a %cs that is not a user selector, for which FreeBSD raises SIGBUS"); if ((machine.flags & McontextHasFpxstate) != 0) NotImplemented_nid_no_patch("sigreturn: an extended FPU state (_MC_HASFPXSTATE)"); - if (machine.fpformat == FpFormatNodev) RestoreContext(context, FpuSource::Current); + if (machine.fpformat == FpFormatNodev) RestoreContext(&local, FpuSource::Current); if (machine.fpformat != FpFormatXmm) return Fail(GuestEinval); - if (machine.ownedfp == FpOwnedNone) RestoreContext(context, FpuSource::Initial); + if (machine.ownedfp == FpOwnedNone) RestoreContext(&local, FpuSource::Initial); if (machine.ownedfp != FpOwnedFpu && machine.ownedfp != FpOwnedPcb) return Fail(GuestEinval); - RestoreContext(context, FpuSource::Context); + RestoreContext(&local, FpuSource::Context); } int APS5_VABI Aps5SwapcontextFinish_nid_no_patch(UserUcontext* saved, const UserUcontext* next, const RegisterFrame* frame) { if (saved == nullptr || next == nullptr) return Fail(GuestEinval); - Aps5GetcontextFinish_nid_no_patch(saved, frame); + if (const int error = Aps5GetcontextFinish_nid_no_patch(saved, frame)) return error; return setcontext_nid_postfix(next); } diff --git a/core/libs/prx/libkernel/Time/Ktimer.cpp b/core/libs/prx/libkernel/Time/Ktimer.cpp index 933a20bc60..e3874ac020 100644 --- a/core/libs/prx/libkernel/Time/Ktimer.cpp +++ b/core/libs/prx/libkernel/Time/Ktimer.cpp @@ -118,6 +118,7 @@ int APS5_VABI ktimer_delete_nid_postfix(int id) { int APS5_VABI ktimer_settime_nid_postfix(int id, int flags, const GuestItimerspec* value, GuestItimerspec* oldValue) { if (value == nullptr) return static_cast(Fail(GuestEfault)); + if ((flags & ~TimerAbstime) != 0) return static_cast(Fail(GuestEinval)); std::lock_guard lock(g_ktimerLock); Ktimer* timer = FindTimer(id); if (timer == nullptr) return static_cast(Fail(GuestEinval)); diff --git a/core/libs/prx/libkernel/Time/Time.cpp b/core/libs/prx/libkernel/Time/Time.cpp index 2a2739d6a6..dcdb12d277 100644 --- a/core/libs/prx/libkernel/Time/Time.cpp +++ b/core/libs/prx/libkernel/Time/Time.cpp @@ -582,6 +582,10 @@ static int ReadClock(int clockId, KernelTimespec* tp) { } int APS5_VABI clock_gettime_nid_postfix(int clockId, KernelTimespec* tp) { + if (!IsCpuClock(clockId) && (clockId < 0 || clockId >= 32)) { + *__error_nid_postfix() = GUEST_EINVAL; + return -1; + } KernelTimespec value{}; if (ReadClock(clockId, &value) != 0) return -1; if (tp == nullptr) { diff --git a/core/libs/prx/libkernel/Time/TimedWait.cpp b/core/libs/prx/libkernel/Time/TimedWait.cpp index e49b8d43de..299ba46b1b 100644 --- a/core/libs/prx/libkernel/Time/TimedWait.cpp +++ b/core/libs/prx/libkernel/Time/TimedWait.cpp @@ -4,7 +4,9 @@ #include #include #include +#include #include +#include #include #ifdef _WIN32 @@ -50,6 +52,14 @@ struct ThreadWaitState { std::atomic own{0}; std::atomic* bound = &own; }; + +struct DeferredDrain { + void (*run)(void*); + void* argument; +}; + +std::mutex drainBoxesLock; +std::unordered_map> drainBoxes; ThreadWaitState& WaitState() { #ifdef _WIN32 return WindowsThreadLocal::Get(); @@ -72,6 +82,7 @@ void DrainApcs() { state->fetch_sub(1, std::memory_order_seq_cst); if (testAlert) testAlert(); else SleepEx(0, TRUE); + ReapDeliveryDrain(); } DWORD AlertableWait(DWORD count, const HANDLE* handles, DWORD milliseconds) { @@ -159,6 +170,48 @@ struct Waiter { } }; +std::atomic& DrainPostHook() { + static std::atomic hook{nullptr}; + return hook; +} + +void SetDeliveryDrainPostHook(void (*hook)()) { + DrainPostHook().store(hook, std::memory_order_release); +} + +void QueueForDeliveryDrain(unsigned long thread, const std::atomic* targetFinished, void (*run)(void*), void* argument) { + if (const auto hook = DrainPostHook().load(std::memory_order_acquire)) hook(); + bool queued = false; + { + const std::lock_guard lock(drainBoxesLock); + if (targetFinished == nullptr || !targetFinished->load(std::memory_order_acquire)) { + drainBoxes[thread].push_back({run, argument}); + queued = true; + } + } + if (!queued) run(argument); +} + +void ReapDeliveryDrain() { + std::vector deferred; + { + const std::lock_guard lock(drainBoxesLock); + const auto box = drainBoxes.find(GetCurrentThreadId()); + if (box != drainBoxes.end()) { + deferred = std::move(box->second); + drainBoxes.erase(box); + } + } + for (const auto& entry : deferred) entry.run(entry.argument); +} + +std::size_t DeliveryDrainPending() { + const std::lock_guard lock(drainBoxesLock); + std::size_t pending = 0; + for (const auto& box : drainBoxes) pending += box.second.size(); + return pending; +} + namespace { Waiter* ThisThreadWaiter() { diff --git a/core/libs/prx/libkernel/Time/include/TimedWait.hpp b/core/libs/prx/libkernel/Time/include/TimedWait.hpp index 9ad4e09f03..e072e1a860 100644 --- a/core/libs/prx/libkernel/Time/include/TimedWait.hpp +++ b/core/libs/prx/libkernel/Time/include/TimedWait.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -38,6 +39,11 @@ public: void DeliverDeferred(); #ifdef _WIN32 +void QueueForDeliveryDrain(unsigned long thread, const std::atomic* targetFinished, void (*run)(void*), void* argument); +void ReapDeliveryDrain(); +std::size_t DeliveryDrainPending(); +void SetDeliveryDrainPostHook(void (*hook)()); + constexpr unsigned PARK_SPINS = 1000; void SpinPause(); diff --git a/core/libs/prx/libkernel/Unimplemented.cpp b/core/libs/prx/libkernel/Unimplemented.cpp index 20c5d80c94..10b8e862d2 100644 --- a/core/libs/prx/libkernel/Unimplemented.cpp +++ b/core/libs/prx/libkernel/Unimplemented.cpp @@ -128,6 +128,11 @@ int APS5_VABI sceKernelGetProcessAbiType() { return 0; } +int APS5_VABI sceKernelProtectDirectMemoryForPID(int64_t offset, size_t len, int prot, int pid) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } extern "C" { diff --git a/core/libs/prx/libkernel/include/Api.hpp b/core/libs/prx/libkernel/include/Api.hpp index dc213606b5..9131f61a63 100644 --- a/core/libs/prx/libkernel/include/Api.hpp +++ b/core/libs/prx/libkernel/include/Api.hpp @@ -207,6 +207,8 @@ int APS5_VABI sceKernelWriteModifyMtypeProtectCommand(void* dst, std::uint64_t a int APS5_VABI sceKernelWriteRemapIntoPrtCommand(std::uint32_t* dst, std::uint64_t address, std::uint64_t source, std::uint64_t size, int protection, int opcode, std::uint64_t* outSize); int APS5_VABI sceKernelAllocateDirectMemory(int64_t searchStart, int64_t searchEnd, size_t len, size_t alignment, int memoryType, int64_t* physAddrOut); int APS5_VABI sceKernelAllocateMainDirectMemory(size_t len, size_t alignment, int memoryType, int64_t* physAddrOut); +int APS5_VABI sceKernelAllocateDirectMemoryForApp(int64_t searchStart, int64_t searchEnd, size_t len, size_t alignment, int memoryType, int64_t* physAddrOut); +int APS5_VABI sceKernelProtectDirectMemoryForPID(int64_t offset, size_t len, int prot, int pid); int APS5_VABI sceKernelAvailableDirectMemorySize(int64_t searchStart, int64_t searchEnd, size_t alignment, int64_t* physAddrOut, size_t* sizeOut); int APS5_VABI sceKernelDirectMemoryQuery(int64_t offset, int flags, void* info, size_t infoSize); int APS5_VABI sceKernelGetDirectMemoryType(int64_t offset, int* memoryType, int64_t* start, int64_t* end); @@ -703,6 +705,7 @@ int APS5_VABI sceKernelCreateUuidV4(std::uint8_t* uuid); void APS5_VABI sceKernelSync(void); void APS5_VABI sync_nid_postfix(void); int APS5_VABI sched_get_priority_max_nid_postfix(int policy); +int APS5_VABI tcgetsid_nid_postfix(int descriptor); int APS5_VABI sched_getparam_nid_postfix(int pid, KernelSchedParam* param); int APS5_VABI sched_getscheduler_nid_postfix(int pid); int APS5_VABI sched_setparam_nid_postfix(int pid, const KernelSchedParam* param); diff --git a/core/libs/prx/libkernel_sys/AdditionalExports.cpp b/core/libs/prx/libkernel_sys/AdditionalExports.cpp index 38cfeae140..0c574123e4 100644 --- a/core/libs/prx/libkernel_sys/AdditionalExports.cpp +++ b/core/libs/prx/libkernel_sys/AdditionalExports.cpp @@ -1,6 +1,15 @@ #include "prx/libkernel_sys/include/AdditionalExports.hpp" #include "prx/libc/include/General.hpp" +#include +#include + +namespace { + +std::atomic g_flag1WXQlxMpnNA{0}; + +} + extern "C" { int APS5_VABI __cfi_slowpath_diag_nid_postfix() { @@ -213,8 +222,9 @@ int APS5_VABI libkernel_sysUnknown_0CV40L3tznI_nid_no_patch() { APS5_EXPORT("0CV40L3tznI", libkernel_sysUnknown_0CV40L3tznI_nid_no_patch); -int APS5_VABI libkernel_sysUnknown_1WXQlxMpnNA_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); +std::uint64_t APS5_VABI libkernel_sysUnknown_1WXQlxMpnNA_nid_no_patch() { + g_flag1WXQlxMpnNA.store(1, std::memory_order_relaxed); + return 0; } APS5_EXPORT("1WXQlxMpnNA", libkernel_sysUnknown_1WXQlxMpnNA_nid_no_patch); @@ -428,8 +438,8 @@ int APS5_VABI libkernel_sysUnknown_C3xgqN95IoE_nid_no_patch() { APS5_EXPORT("C3xgqN95IoE", libkernel_sysUnknown_C3xgqN95IoE_nid_no_patch); -int APS5_VABI libkernel_sysUnknown_C49jelxiaVE_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); +std::uint64_t APS5_VABI libkernel_sysUnknown_C49jelxiaVE_nid_no_patch() { + return 0x20018u; } APS5_EXPORT("C49jelxiaVE", libkernel_sysUnknown_C49jelxiaVE_nid_no_patch); @@ -485,8 +495,8 @@ int APS5_VABI libkernel_sysUnknown_FIUPjhnoI_plus_o_nid_no_patch() { APS5_EXPORT("FIUPjhnoI+o", libkernel_sysUnknown_FIUPjhnoI_plus_o_nid_no_patch); -int APS5_VABI libkernel_sysUnknown_FR0odl35yQY_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); +std::uint64_t APS5_VABI libkernel_sysUnknown_FR0odl35yQY_nid_no_patch() { + return g_flag1WXQlxMpnNA.load(std::memory_order_relaxed); } APS5_EXPORT("FR0odl35yQY", libkernel_sysUnknown_FR0odl35yQY_nid_no_patch); @@ -619,8 +629,8 @@ int APS5_VABI libkernel_sysUnknown_MKlp_minus_bW1bz8_nid_no_patch() { APS5_EXPORT("MKlp-bW1bz8", libkernel_sysUnknown_MKlp_minus_bW1bz8_nid_no_patch); -int APS5_VABI libkernel_sysUnknown_N94a0T4F4_plus_U_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); +std::uint64_t APS5_VABI libkernel_sysUnknown_N94a0T4F4_plus_U_nid_no_patch() { + return 0x8002004eu; } APS5_EXPORT("N94a0T4F4+U", libkernel_sysUnknown_N94a0T4F4_plus_U_nid_no_patch); @@ -986,8 +996,8 @@ int APS5_VABI libkernel_sysUnknown_gBiaAGKEvHM_nid_no_patch() { APS5_EXPORT("gBiaAGKEvHM", libkernel_sysUnknown_gBiaAGKEvHM_nid_no_patch); -int APS5_VABI libkernel_sysUnknown_gJ5gpW45mU0_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); +std::uint64_t APS5_VABI libkernel_sysUnknown_gJ5gpW45mU0_nid_no_patch() { + return 0; } APS5_EXPORT("gJ5gpW45mU0", libkernel_sysUnknown_gJ5gpW45mU0_nid_no_patch); @@ -1142,8 +1152,8 @@ int APS5_VABI sceKernelInternalUnmapMapperSharedMemory() { } -int APS5_VABI libkernel_sysUnknown_pCbBddNl66Y_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); +std::uint64_t APS5_VABI libkernel_sysUnknown_pCbBddNl66Y_nid_no_patch() { + return 0; } APS5_EXPORT("pCbBddNl66Y", libkernel_sysUnknown_pCbBddNl66Y_nid_no_patch); @@ -1272,8 +1282,8 @@ int APS5_VABI libkernel_sysUnknown_vfYaxkxnJXQ_nid_no_patch() { APS5_EXPORT("vfYaxkxnJXQ", libkernel_sysUnknown_vfYaxkxnJXQ_nid_no_patch); -int APS5_VABI libkernel_sysUnknown_vq2_plus_HyP5U5I_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); +std::uint64_t APS5_VABI libkernel_sysUnknown_vq2_plus_HyP5U5I_nid_no_patch() { + return 0; } APS5_EXPORT("vq2+HyP5U5I", libkernel_sysUnknown_vq2_plus_HyP5U5I_nid_no_patch); @@ -1330,14 +1340,14 @@ int APS5_VABI libkernel_sysUnknown_z0qKN5kkVj0_nid_no_patch() { APS5_EXPORT("z0qKN5kkVj0", libkernel_sysUnknown_z0qKN5kkVj0_nid_no_patch); -int APS5_VABI libkernel_sysUnknown_zAloppPJxy0_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); +std::uint64_t APS5_VABI libkernel_sysUnknown_zAloppPJxy0_nid_no_patch() { + return 0; } APS5_EXPORT("zAloppPJxy0", libkernel_sysUnknown_zAloppPJxy0_nid_no_patch); -int APS5_VABI libkernel_sysUnknown_z_plus_e0vV_minus_tnH4_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); +std::uint64_t APS5_VABI libkernel_sysUnknown_z_plus_e0vV_minus_tnH4_nid_no_patch() { + return 0; } APS5_EXPORT("z+e0vV-tnH4", libkernel_sysUnknown_z_plus_e0vV_minus_tnH4_nid_no_patch); diff --git a/core/libs/prx/libkernel_sys/Cpuset.cpp b/core/libs/prx/libkernel_sys/Cpuset.cpp index 45b3b79b81..ececefdc6f 100644 --- a/core/libs/prx/libkernel_sys/Cpuset.cpp +++ b/core/libs/prx/libkernel_sys/Cpuset.cpp @@ -1,4 +1,5 @@ #include +#include #include #include @@ -91,18 +92,40 @@ int APS5_VABI cpuset_getid_nid_postfix(int level, int which, std::int64_t id, in if (which < WhichThread || which > WhichDomain || which == WhichIrq || which == WhichDomain) { return Fail(GuestEinval); } - if (which != WhichProcess) { - NotImplemented_nid_no_patch("cpuset_getid other than process"); - } - if (id != -1 && id != getpid_nid_postfix()) { - return Fail(GuestEsrch); + if (which != WhichProcess && which != WhichCpuset) { + NotImplemented_nid_no_patch("cpuset_getid other than process or cpuset"); } int resultId = 0; - if (level == LevelRoot) { - resultId = RootSetId; + if (which == WhichProcess) { + if (id != -1 && id != getpid_nid_postfix()) { + return Fail(GuestEsrch); + } + if (level == LevelRoot) { + resultId = RootSetId; + } else { + std::lock_guard lock(setMutex); + resultId = currentProcessSetId; + } } else { - std::lock_guard lock(setMutex); - resultId = currentProcessSetId; + if (id == -1) { + if (level == LevelRoot) { + resultId = RootSetId; + } else { + std::lock_guard lock(setMutex); + resultId = currentProcessSetId; + } + } else { + const int lookupId = static_cast(id); + std::lock_guard lock(setMutex); + if (validSets.find(lookupId) == validSets.end()) { + return Fail(GuestEsrch); + } + if (level == LevelRoot) { + resultId = RootSetId; + } else { + resultId = lookupId; + } + } } if (!GuestCopyOut_nid_no_patch(setid, &resultId, sizeof(resultId))) { return Fail(GuestEfault); diff --git a/core/libs/prx/libkernel_sys/Export.Discovery.cpp b/core/libs/prx/libkernel_sys/Export.Discovery.cpp index a7f810ab05..d3f44c6f4c 100644 --- a/core/libs/prx/libkernel_sys/Export.Discovery.cpp +++ b/core/libs/prx/libkernel_sys/Export.Discovery.cpp @@ -139,11 +139,6 @@ int APS5_VABI ipmimgr_call_nid_postfix(void) { return 0; } -int APS5_VABI is_in_sandbox_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI kmq_notify_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -417,11 +412,6 @@ int APS5_VABI sceKernelAllocateDirectMemory2(void) { return 0; } -int APS5_VABI sceKernelAllocateDirectMemoryForApp(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceKernelAllocateDirectMemoryForMiniApp(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -1338,11 +1328,6 @@ int APS5_VABI sceKernelIsGenuineN(void) { return 1; } -int APS5_VABI sceKernelIsInSandbox(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceKernelIsM2DeviceAttached(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -1502,11 +1487,6 @@ int APS5_VABI sceKernelProtectDirectMemory(void) { return 0; } -int APS5_VABI sceKernelProtectDirectMemoryForPID(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceKernelQueryToolMemory(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -2038,11 +2018,6 @@ int APS5_VABI scePthreadMutexInitForInternalLibc(PthreadMutex* mutex, const Pthr return SceResult(pthread_mutex_init_nid_postfix(mutex, attr)); } -int APS5_VABI scePthreadMutexIsowned(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI scePthreadMutexSetprioceiling(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -2154,26 +2129,11 @@ int APS5_VABI sysc_s00_nid_postfix(void) { return 0; } -int APS5_VABI tcgetsid_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI tcsetsid_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI unmount_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI utrace_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI vfork_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index f35a9e1e97..2ed2882ec6 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -10,12 +10,10 @@ #include #include #include -#include #include #include #include #include -#include #include #include #include @@ -86,6 +84,7 @@ constexpr int GuestEloop = 62; constexpr int GuestEnametoolong = 63; constexpr int GuestEnotempty = 66; constexpr int GuestEoverflow = 84; +constexpr std::uint64_t TerminalSetControlling = 0x20007461; constexpr int GuestAtFdcwd = -100; constexpr int GuestAtRemovedir = 0x800; constexpr std::size_t GuestPathMax = 1024; @@ -143,24 +142,6 @@ constexpr int KenvSet = 1; constexpr int KenvUnset = 2; constexpr int KenvDump = 3; constexpr std::size_t KenvNameLength = 128; -constexpr std::size_t KenvValueLength = 128; -constexpr std::size_t KenvEntries = 512; - -struct KernelEnvironment { - std::mutex mutex; - std::vector entries; -}; - -KernelEnvironment& KernelEnv() { - static auto* environment = new KernelEnvironment; - return *environment; -} - -std::vector::iterator FindKernelEnv(std::vector& entries, const std::string& name) { - return std::find_if(entries.begin(), entries.end(), [&](const std::string& entry) { - return entry.size() > name.size() && entry.compare(0, name.size(), name) == 0 && entry[name.size()] == '='; - }); -} int CopyKernelEnvString(const char* source, std::size_t limit, std::string& destination) { if (const int error = CopyGuestPath_nid_no_patch(source, destination)) return error; @@ -245,6 +226,56 @@ bool IsSymbolicLink(const std::filesystem::path& native) { #endif } +bool ReadSymbolicLinkTarget(const std::filesystem::path& native, std::filesystem::path& target) { +#ifdef _WIN32 + const DWORD attributes = GetFileAttributesW(native.c_str()); + if (attributes == INVALID_FILE_ATTRIBUTES || (attributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0) return false; + std::string text; + if (!File::WindowsSymbolicLink::Read(native, text, "link")) return false; + target = std::filesystem::path(reinterpret_cast(text.c_str())); + return true; +#else + std::error_code error; + if (!std::filesystem::is_symlink(std::filesystem::symlink_status(native, error))) return false; + target = std::filesystem::read_symlink(native, error); + if (error) throw std::system_error(error, "link: read_symlink"); + return true; +#endif +} + +int SymbolicLinkLimitError(const std::filesystem::path& native, bool followLast) { + constexpr int MaxSymbolicLinks = 32; + std::vector pending; + const auto push = [&pending](const std::filesystem::path& path) { + const std::vector parts(path.begin(), path.end()); + pending.insert(pending.end(), parts.rbegin(), parts.rend()); + }; + push(native.relative_path()); + std::filesystem::path current = native.root_path(); + int links = 0; + while (!pending.empty()) { + const auto part = pending.back(); + pending.pop_back(); + if (part.empty() || part == ".") continue; + if (part == "..") { + std::error_code error; + if (!std::filesystem::is_directory(current, error)) return 0; + current = current.parent_path(); + continue; + } + const auto next = current / part; + std::filesystem::path target; + if ((followLast || !pending.empty()) && ReadSymbolicLinkTarget(next, target)) { + if (++links > MaxSymbolicLinks) return GuestEloop; + if (target.has_root_path()) current = target.root_path(); + push(target.relative_path()); + continue; + } + current = next; + } + return 0; +} + std::int64_t ReadNativeLink(const std::filesystem::path& native, char* buffer, std::size_t size) { #ifdef _WIN32 const DWORD attributes = GetFileAttributesW(native.wstring().c_str()); @@ -384,6 +415,14 @@ int APS5_VABI sceKernelGetCompiledSdkVersion(std::int32_t* version) { return 0; } +int APS5_VABI is_in_sandbox_nid_postfix(void) { + return 1; +} + +int APS5_VABI sceKernelIsInSandbox(void) { + return is_in_sandbox_nid_postfix(); +} + int APS5_VABI fork_nid_postfix(void) { NotImplemented_nid_no_patch("fork: child processes"); return -1; @@ -497,58 +536,14 @@ int APS5_VABI fstatfs_nid_postfix(int d, KernelStatfs* buf) { } int APS5_VABI kenv_nid_postfix(int what, const char* name, char* value, int len) { - auto& environment = KernelEnv(); - if (what == KenvDump) { - std::size_t capacity = len > 0 ? static_cast(len) : 0; - if (capacity > KenvEntries * (KenvNameLength + KenvValueLength + 2)) capacity = KenvEntries * (KenvNameLength + KenvValueLength + 2); - const bool copy = len > 0 && value != nullptr; - std::string buffer; - std::size_t needed = 0; - { - const std::lock_guard lock(environment.mutex); - for (const auto& entry : environment.entries) { - const std::size_t size = entry.size() + 1; - needed += size; - if (copy) { - const std::size_t taken = std::min(size, capacity - buffer.size()); - buffer.append(entry.c_str(), taken); - } - } - } - if (copy && !GuestCopyOut_nid_no_patch(value, buffer.data(), buffer.size())) return static_cast(Fail(GuestEfault)); - return buffer.size() == needed ? 0 : static_cast(needed); - } + static_cast(value); + static_cast(len); + if (what == KenvDump) return 0; + if (what == KenvSet || what == KenvUnset) return static_cast(Fail(GuestEperm)); std::string key; if (const int error = CopyKernelEnvString(name, KenvNameLength + 1, key)) return static_cast(Fail(error)); - const std::lock_guard lock(environment.mutex); - auto& entries = environment.entries; - switch (what) { - case KenvGet: { - const auto found = FindKernelEnv(entries, key); - if (found == entries.end()) return static_cast(Fail(GuestEnoent)); - const std::size_t size = found->size() - key.size(); - const std::size_t limit = static_cast(static_cast(len)); - const std::size_t taken = std::min(size, limit); - if (!GuestCopyOut_nid_no_patch(value, found->c_str() + key.size() + 1, taken)) return static_cast(Fail(GuestEfault)); - return static_cast(taken); - } - case KenvSet: { - if (len == 0) return static_cast(Fail(GuestEinval)); - std::string text; - if (const int error = CopyKernelEnvString(value, std::min(len, KenvValueLength + 1), text)) return static_cast(Fail(error)); - const auto found = FindKernelEnv(entries, key); - if (found != entries.end()) *found = key + "=" + text; - else if (entries.size() < KenvEntries) entries.push_back(key + "=" + text); - return 0; - } - case KenvUnset: { - const auto found = FindKernelEnv(entries, key); - if (found == entries.end()) return static_cast(Fail(GuestEnoent)); - entries.erase(found); - return 0; - } - default: return static_cast(Fail(GuestEinval)); - } + if (what == KenvGet) return static_cast(Fail(GuestEnoent)); + return static_cast(Fail(GuestEinval)); } int GetModuleInfoExByHandle_nid_no_patch(KernelModule handle, ModuleInfoEx* info); @@ -730,9 +725,11 @@ int APS5_VABI link_nid_postfix(const char* existing, const char* created) { if (const int error = CopyPath(existing, existingCopy)) return static_cast(Fail(error)); std::filesystem::path source, destination; if (const int error = ResolvePathError_nid_no_patch(existing, source)) return static_cast(Fail(error)); + if (const int error = SymbolicLinkLimitError(source, true)) return static_cast(Fail(error)); if (const int error = LinkSourceError(source)) return static_cast(Fail(error)); if (const int error = CopyPath(created, createdCopy)) return static_cast(Fail(error)); if (const int error = ResolvePathError_nid_no_patch(created, destination)) return static_cast(Fail(error)); + if (const int error = SymbolicLinkLimitError(destination, false)) return static_cast(Fail(error)); return LinkNative(source, destination); } @@ -752,11 +749,13 @@ int APS5_VABI linkat_nid_postfix(int fd1, const char* path1, int fd2, const char if (const int error = CopyPath(path1, copy1)) return static_cast(Fail(error)); std::filesystem::path source, destination; if (const int error = ResolveAtPath(fd1, path1, source)) return static_cast(Fail(error)); + if (const int error = SymbolicLinkLimitError(source, flag != 0)) return static_cast(Fail(error)); const bool linkItself = flag == 0 && IsSymbolicLink(source); if (!linkItself) if (const int error = LinkSourceError(source)) return static_cast(Fail(error)); if (const int error = CopyPath(path2, copy2)) return static_cast(Fail(error)); if (const int error = ResolveAtPath(fd2, path2, destination)) return static_cast(Fail(error)); + if (const int error = SymbolicLinkLimitError(destination, false)) return static_cast(Fail(error)); if (linkItself) return LinkSymbolicLinkItself(source, destination); return LinkNative(source, destination); } @@ -1298,4 +1297,20 @@ int APS5_VABI sceKernelGetProcessType(int pid) { return 0; } +int APS5_VABI scePthreadMutexIsowned(PthreadMutex* mutex) { + const int owned = pthread_mutex_isowned_np_nid_postfix(mutex); + return owned == 0 ? 0 : SceKernelError(owned); +} + +int APS5_VABI utrace_nid_postfix(const void* address, std::size_t length) { + (void)address; + (void)length; + return 0; +} + +int APS5_VABI tcsetsid_nid_postfix(int fd, int pid) { + if (pid != getsid_nid_postfix(0)) return static_cast(Fail(GuestEinval)); + return ioctl_nid_postfix(fd, TerminalSetControlling, nullptr); +} + } diff --git a/core/libs/prx/libkernel_sys/include/AdditionalExports.hpp b/core/libs/prx/libkernel_sys/include/AdditionalExports.hpp index 75c2c5e523..5cee705589 100644 --- a/core/libs/prx/libkernel_sys/include/AdditionalExports.hpp +++ b/core/libs/prx/libkernel_sys/include/AdditionalExports.hpp @@ -2,6 +2,8 @@ #include "SceTypes.hpp" +#include + extern "C" { int APS5_VABI __cfi_slowpath_diag_nid_postfix(); @@ -56,7 +58,7 @@ int APS5_VABI _execvpe_nid_postfix(); int APS5_VABI _wait4_nid_postfix(); int APS5_VABI adjtime_nid_postfix(); int APS5_VABI libkernel_sysUnknown_0CV40L3tznI_nid_no_patch(); -int APS5_VABI libkernel_sysUnknown_1WXQlxMpnNA_nid_no_patch(); +std::uint64_t APS5_VABI libkernel_sysUnknown_1WXQlxMpnNA_nid_no_patch(); int APS5_VABI libkernel_sysUnknown_1v23w7Rnkic_nid_no_patch(); int APS5_VABI libkernel_sysUnknown_1v6DjTPunjs_nid_no_patch(); int APS5_VABI sceKernelInternalCreateMapperSharedMemory(); @@ -93,7 +95,7 @@ int APS5_VABI libkernel_sysUnknown_BJvWKKfmj4w_nid_no_patch(); int APS5_VABI libkernel_sysUnknown_BjmZn9pYSuk_nid_no_patch(); int APS5_VABI libkernel_sysUnknown_C3ToW2CA_plus_hM_nid_no_patch(); int APS5_VABI libkernel_sysUnknown_C3xgqN95IoE_nid_no_patch(); -int APS5_VABI libkernel_sysUnknown_C49jelxiaVE_nid_no_patch(); +std::uint64_t APS5_VABI libkernel_sysUnknown_C49jelxiaVE_nid_no_patch(); int APS5_VABI sceKernelSwitchToTrinityMode(); int APS5_VABI sceKernelIsGenuineCronosCex(); int APS5_VABI libkernel_sysUnknown_DWWduhQYA3I_nid_no_patch(); @@ -103,7 +105,7 @@ int APS5_VABI libkernel_sysUnknown_EIz_plus_fSlYCn8_nid_no_patch(); int APS5_VABI sceKernelTraceMarkerBookmark(); int APS5_VABI libkernel_sysUnknown_EaepepH_minus_LmI_nid_no_patch(); int APS5_VABI libkernel_sysUnknown_FIUPjhnoI_plus_o_nid_no_patch(); -int APS5_VABI libkernel_sysUnknown_FR0odl35yQY_nid_no_patch(); +std::uint64_t APS5_VABI libkernel_sysUnknown_FR0odl35yQY_nid_no_patch(); int APS5_VABI sceKernelIccGetExtBDDriveStatus(); int APS5_VABI libkernel_sysUnknown_GTyjVeZcdRM_nid_no_patch(); int APS5_VABI libkernel_sysUnknown_Gtncco_plus_mxio_nid_no_patch(); @@ -126,7 +128,7 @@ int APS5_VABI libkernel_sysUnknown_LVRydXoXt7U_nid_no_patch(); int APS5_VABI libkernel_sysUnknown_LwGBaXgvFaY_nid_no_patch(); int APS5_VABI libkernel_sysUnknown_MJ_minus_err8l_minus_XY_nid_no_patch(); int APS5_VABI libkernel_sysUnknown_MKlp_minus_bW1bz8_nid_no_patch(); -int APS5_VABI libkernel_sysUnknown_N94a0T4F4_plus_U_nid_no_patch(); +std::uint64_t APS5_VABI libkernel_sysUnknown_N94a0T4F4_plus_U_nid_no_patch(); int APS5_VABI libkernel_sysUnknown_NhFlyw_minus_77cY_nid_no_patch(); int APS5_VABI libkernel_sysUnknown_OZBJnWxt7d4_nid_no_patch(); int APS5_VABI libkernel_sysUnknown_PBq_minus_OUg5A5s_nid_no_patch(); @@ -189,7 +191,7 @@ int APS5_VABI sceKernelTraceMarkerPlot(); int APS5_VABI libkernel_sysUnknown_ftHBlCfIEQo_nid_no_patch(); int APS5_VABI libkernel_sysUnknown_g7MGwZ5UxBE_nid_no_patch(); int APS5_VABI libkernel_sysUnknown_gBiaAGKEvHM_nid_no_patch(); -int APS5_VABI libkernel_sysUnknown_gJ5gpW45mU0_nid_no_patch(); +std::uint64_t APS5_VABI libkernel_sysUnknown_gJ5gpW45mU0_nid_no_patch(); int APS5_VABI libkernel_sysUnknown_gSIiye3ERqo_nid_no_patch(); int APS5_VABI libkernel_sysUnknown_gpgi_minus_GwE2Is_nid_no_patch(); int APS5_VABI libkernel_sysUnknown_guq_plus_otVpgj0_nid_no_patch(); @@ -216,7 +218,7 @@ int APS5_VABI libkernel_sysUnknown_nsq826qQ2EI_nid_no_patch(); int APS5_VABI libkernel_sysUnknown_o141x2VDeVc_nid_no_patch(); int APS5_VABI sceKernelGetMainSocSubId(); int APS5_VABI sceKernelInternalUnmapMapperSharedMemory(); -int APS5_VABI libkernel_sysUnknown_pCbBddNl66Y_nid_no_patch(); +std::uint64_t APS5_VABI libkernel_sysUnknown_pCbBddNl66Y_nid_no_patch(); int APS5_VABI libkernel_sysUnknown_pOClGbxrx94_nid_no_patch(); int APS5_VABI sceKernelSetAppLibcData(); int APS5_VABI libkernel_sysUnknown_pp_plus_G_plus_UZhiqY_nid_no_patch(); @@ -238,7 +240,7 @@ int APS5_VABI libkernel_sysUnknown_uV_plus_YilcHqAs_nid_no_patch(); int APS5_VABI libkernel_sysUnknown_ulVjluL9TzM_nid_no_patch(); int APS5_VABI libkernel_sysUnknown_v_plus_qTlNjF6kE_nid_no_patch(); int APS5_VABI libkernel_sysUnknown_vfYaxkxnJXQ_nid_no_patch(); -int APS5_VABI libkernel_sysUnknown_vq2_plus_HyP5U5I_nid_no_patch(); +std::uint64_t APS5_VABI libkernel_sysUnknown_vq2_plus_HyP5U5I_nid_no_patch(); int APS5_VABI sceKernelSetCanvasHandle(); int APS5_VABI libkernel_sysUnknown_wn0XzOue4EM_nid_no_patch(); int APS5_VABI libkernel_sysUnknown_wwWv_plus_0MzhGM_nid_no_patch(); @@ -248,8 +250,8 @@ int APS5_VABI libkernel_sysUnknown_yWOJkfd_minus_DhA_nid_no_patch(); int APS5_VABI libkernel_sysUnknown_yn0b42DApAI_nid_no_patch(); int APS5_VABI libkernel_sysUnknown_yzvrk3YrPlE_nid_no_patch(); int APS5_VABI libkernel_sysUnknown_z0qKN5kkVj0_nid_no_patch(); -int APS5_VABI libkernel_sysUnknown_zAloppPJxy0_nid_no_patch(); -int APS5_VABI libkernel_sysUnknown_z_plus_e0vV_minus_tnH4_nid_no_patch(); +std::uint64_t APS5_VABI libkernel_sysUnknown_zAloppPJxy0_nid_no_patch(); +std::uint64_t APS5_VABI libkernel_sysUnknown_z_plus_e0vV_minus_tnH4_nid_no_patch(); int APS5_VABI mmap2_nid_postfix(); int APS5_VABI pthread_setschedother_np_nid_postfix(); int APS5_VABI sceKernelAddDynlibEvent(); diff --git a/core/libs/prx/libkernel_sys/include/Api.hpp b/core/libs/prx/libkernel_sys/include/Api.hpp index bd6afaccf4..0d1da8b729 100644 --- a/core/libs/prx/libkernel_sys/include/Api.hpp +++ b/core/libs/prx/libkernel_sys/include/Api.hpp @@ -178,7 +178,6 @@ int APS5_VABI sceKernelAddGpuExceptionEvent(void); int APS5_VABI sceKernelAddProcessToCanvasMap(void); int APS5_VABI sceKernelAddResource(void); int APS5_VABI sceKernelAllocateDirectMemory2(void); -int APS5_VABI sceKernelAllocateDirectMemoryForApp(void); int APS5_VABI sceKernelAllocateDirectMemoryForMiniApp(void); int APS5_VABI sceKernelAllocateToolMemory(void); int APS5_VABI sceKernelAllocateTraceDirectMemory(void); @@ -408,7 +407,6 @@ int APS5_VABI sceKernelPrepareDirectMemorySwap(void); int APS5_VABI sceKernelPrepareToResumeProcess(void); int APS5_VABI sceKernelPrepareToSuspendProcess(void); int APS5_VABI sceKernelProtectDirectMemory(void); -int APS5_VABI sceKernelProtectDirectMemoryForPID(void); int APS5_VABI sceKernelQueryToolMemory(void); int APS5_VABI sceKernelQueryTraceMemory(void); int APS5_VABI sceKernelRandomizedPath(void); @@ -522,7 +520,7 @@ int APS5_VABI scePthreadMain(void); int APS5_VABI scePthreadMulti(void); int APS5_VABI scePthreadMutexGetprioceiling(void); int APS5_VABI scePthreadMutexInitForInternalLibc(PthreadMutex* mutex, const PthreadMutexattr* attr); -int APS5_VABI scePthreadMutexIsowned(void); +int APS5_VABI scePthreadMutexIsowned(PthreadMutex* mutex); int APS5_VABI scePthreadMutexSetprioceiling(void); int APS5_VABI scePthreadMutexattrGetgen(const PthreadMutexattr* attr, int* generation); int APS5_VABI scePthreadMutexattrInitForInternalLibc(PthreadMutexattr* attr); @@ -546,10 +544,9 @@ int APS5_VABI symlinkat_nid_postfix(void); int APS5_VABI sysc_e00_nid_postfix(void); int APS5_VABI sysc_m00_nid_postfix(void); int APS5_VABI sysc_s00_nid_postfix(void); -int APS5_VABI tcgetsid_nid_postfix(void); -int APS5_VABI tcsetsid_nid_postfix(void); +int APS5_VABI tcsetsid_nid_postfix(int fd, int pid); int APS5_VABI unmount_nid_postfix(void); -int APS5_VABI utrace_nid_postfix(void); +int APS5_VABI utrace_nid_postfix(const void* address, std::size_t length); int APS5_VABI uuidgen_nid_postfix(GuestUuid* store, int count); int APS5_VABI vfork_nid_postfix(void); int APS5_VABI wait3_nid_postfix(void); diff --git a/core/libs/prx/libwebrtc/CMakeLists.txt b/core/libs/prx/libwebrtc/CMakeLists.txt index 9bc7d39e22..6dae662d85 100644 --- a/core/libs/prx/libwebrtc/CMakeLists.txt +++ b/core/libs/prx/libwebrtc/CMakeLists.txt @@ -2,10 +2,18 @@ add_library(libwebrtc SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Export target_include_directories(libwebrtc PRIVATE ${LIBS_INCLUDE_DIR}) configure_windows_unwind(libwebrtc) +target_sources(libwebrtc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/AudioDecoder.cpp) +target_sources(libwebrtc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/AudioDeviceGeneric.cpp) +target_sources(libwebrtc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/AudioEncoder.cpp) +target_sources(libwebrtc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/AudioFrame.cpp) +target_sources(libwebrtc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/AudioFrameOperations.cpp) target_sources(libwebrtc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/ExpFilter.cpp) +target_sources(libwebrtc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/IpAddress.cpp) target_sources(libwebrtc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/RtcpCommonHeader.cpp) +target_sources(libwebrtc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/CpuFeatures.cpp) target_sources(libwebrtc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/Opus.cpp) target_link_libraries(libwebrtc PRIVATE opus) +target_sources(libwebrtc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/TaskQueueBase.cpp) if(BUILD_TESTING) add_test_executable(guest_webrtc_fast_int_to_buffer_tests ${CMAKE_CURRENT_SOURCE_DIR}/tests/FastIntToBuffer.cpp) @@ -48,6 +56,16 @@ if(BUILD_TESTING) set_tests_properties(guest_webrtc_rtcp_common_header PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_webrtc_cpu_features_tests ${CMAKE_CURRENT_SOURCE_DIR}/tests/CpuFeatures.cpp) + target_include_directories(guest_webrtc_cpu_features_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_webrtc_cpu_features_tests PRIVATE libwebrtc libc) + configure_windows_unwind(guest_webrtc_cpu_features_tests) + add_test(NAME guest_webrtc_cpu_features COMMAND guest_webrtc_cpu_features_tests) + set_tests_properties(guest_webrtc_cpu_features PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_webrtc_cpu_features PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_webrtc_opus_tests ${CMAKE_CURRENT_SOURCE_DIR}/tests/Opus.cpp) target_include_directories(guest_webrtc_opus_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_webrtc_opus_tests PRIVATE libwebrtc libc opus) @@ -58,4 +76,64 @@ if(BUILD_TESTING) set_tests_properties(guest_webrtc_opus PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_webrtc_task_queue_base_tests ${CMAKE_CURRENT_SOURCE_DIR}/tests/TaskQueueBase.cpp) + target_include_directories(guest_webrtc_task_queue_base_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_webrtc_task_queue_base_tests PRIVATE libwebrtc libc) + configure_windows_unwind(guest_webrtc_task_queue_base_tests) + add_test(NAME guest_webrtc_task_queue_base COMMAND guest_webrtc_task_queue_base_tests) + set_tests_properties(guest_webrtc_task_queue_base PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_webrtc_task_queue_base PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_webrtc_ip_address_tests ${CMAKE_CURRENT_SOURCE_DIR}/tests/IpAddress.cpp) + target_include_directories(guest_webrtc_ip_address_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_webrtc_ip_address_tests PRIVATE libwebrtc libc) + configure_windows_unwind(guest_webrtc_ip_address_tests) + add_test(NAME guest_webrtc_ip_address COMMAND guest_webrtc_ip_address_tests) + set_tests_properties(guest_webrtc_ip_address PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_webrtc_ip_address PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_webrtc_audio_frame_tests ${CMAKE_CURRENT_SOURCE_DIR}/tests/AudioFrame.cpp) + target_include_directories(guest_webrtc_audio_frame_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_webrtc_audio_frame_tests PRIVATE libwebrtc libc) + configure_windows_unwind(guest_webrtc_audio_frame_tests) + add_test(NAME guest_webrtc_audio_frame COMMAND guest_webrtc_audio_frame_tests) + set_tests_properties(guest_webrtc_audio_frame PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_webrtc_audio_frame PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_webrtc_audio_encoder_tests ${CMAKE_CURRENT_SOURCE_DIR}/tests/AudioEncoder.cpp) + target_include_directories(guest_webrtc_audio_encoder_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_webrtc_audio_encoder_tests PRIVATE libwebrtc libc) + configure_windows_unwind(guest_webrtc_audio_encoder_tests) + add_test(NAME guest_webrtc_audio_encoder COMMAND guest_webrtc_audio_encoder_tests) + set_tests_properties(guest_webrtc_audio_encoder PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_webrtc_audio_encoder PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_webrtc_audio_device_generic_tests ${CMAKE_CURRENT_SOURCE_DIR}/tests/AudioDeviceGeneric.cpp) + target_include_directories(guest_webrtc_audio_device_generic_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_webrtc_audio_device_generic_tests PRIVATE libwebrtc libc) + configure_windows_unwind(guest_webrtc_audio_device_generic_tests) + add_test(NAME guest_webrtc_audio_device_generic COMMAND guest_webrtc_audio_device_generic_tests) + set_tests_properties(guest_webrtc_audio_device_generic PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_webrtc_audio_device_generic PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_webrtc_audio_decoder_tests ${CMAKE_CURRENT_SOURCE_DIR}/tests/AudioDecoder.cpp) + target_include_directories(guest_webrtc_audio_decoder_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_webrtc_audio_decoder_tests PRIVATE libwebrtc libc) + configure_windows_unwind(guest_webrtc_audio_decoder_tests) + add_test(NAME guest_webrtc_audio_decoder COMMAND guest_webrtc_audio_decoder_tests) + set_tests_properties(guest_webrtc_audio_decoder PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_webrtc_audio_decoder PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() diff --git a/core/libs/prx/libwebrtc/Export.cpp b/core/libs/prx/libwebrtc/Export.cpp index 78a99fbc55..0945106992 100644 --- a/core/libs/prx/libwebrtc/Export.cpp +++ b/core/libs/prx/libwebrtc/Export.cpp @@ -117,16 +117,6 @@ int APS5_VABI _ZNK3rtc13SocketAddress4portEv_nid_postfix(void) { return 0; } -int APS5_VABI _ZNK3rtc9IPAddress12ipv4_addressEv_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK3rtc9IPAddress12ipv6_addressEv_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI _ZN6webrtc8RmsLevel7AnalyzeEN3rtc9ArrayViewIKsLln4711EEE_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -232,11 +222,6 @@ int APS5_VABI _ZNK6webrtc20AudioSourceInterface7optionsEv_nid_postfix(void) { return 0; } -int APS5_VABI _ZNK3rtc9IPAddresseqERKS0__nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI _ZNK6webrtc8RTCError7messageEv_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -314,11 +299,6 @@ int APS5_VABI _ZN3rtc17CopyOnWriteBufferC1Emm_nid_postfix(void) { return 0; } -int APS5_VABI _ZN6webrtc13TaskQueueBase7CurrentEv_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI _ZN3rtc10PacketInfoD1Ev_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -465,16 +445,6 @@ int APS5_VABI _ZN3rtc15CurrentThreadIdEv_nid_postfix(void) { return 0; } -int APS5_VABI _ZN6webrtc13TaskQueueBase22CurrentTaskQueueSetterC1EPS0__nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZN6webrtc13TaskQueueBase22CurrentTaskQueueSetterD1Ev_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI _ZN6webrtc18EncodedImageBuffer6CreateEPKhm_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -735,11 +705,6 @@ int APS5_VABI _ZNK3rtc13SocketAddress10IsCompleteEv_nid_postfix(void) { return 0; } -int APS5_VABI _ZNK3rtc9IPAddress27v4AddressAsHostOrderIntegerEv_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - APS5_EXPORT("z-b1v5cik7s", libwebrtcUnknown19); int APS5_VABI libwebrtcUnknown19(void) { NotImplemented_nid_no_patch("z-b1v5cik7s"); @@ -756,11 +721,6 @@ int APS5_VABI _ZN3rtc10TruncateIPERKNS_9IPAddressEi_nid_postfix(void) { return 0; } -int APS5_VABI _ZN3rtc15CountIPMaskBitsERKNS_9IPAddressE_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI _ZN3rtc17AsyncPacketSocketD2Ev_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -1241,26 +1201,6 @@ int APS5_VABI _ZN6webrtc12AudioDecoder15DecodeRedundantEPKhmimPsPNS0_10SpeechTyp return 0; } -int APS5_VABI _ZNK6webrtc12AudioDecoder12HasDecodePlcEv_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZN6webrtc12AudioDecoder9DecodePlcEmPs_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZN6webrtc12AudioDecoder11GeneratePlcEmPN3rtc7BufferTIsLb0EEE_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZN6webrtc12AudioDecoder9ErrorCodeEv_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI _ZN6webrtc12AudioDecoder11ParseResultC1EOS1__nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -1287,16 +1227,6 @@ int APS5_VABI _ZN6webrtc12AudioEncoder6EncodeEjN3rtc9ArrayViewIKsLln4711EEEPNS1_ return 0; } -int APS5_VABI _ZNK6webrtc12AudioEncoder6GetDtxEv_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZN6webrtc12AudioEncoder16SetTargetBitrateEi_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - APS5_EXPORT("qli1Cjz9UIo", libwebrtcUnknown36); int APS5_VABI libwebrtcUnknown36(void) { NotImplemented_nid_no_patch("qli1Cjz9UIo"); @@ -1309,11 +1239,6 @@ int APS5_VABI libwebrtcUnknown37(void) { return 0; } -int APS5_VABI _ZN6webrtc12AudioEncoder26DisableAudioNetworkAdaptorEv_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI _ZN6webrtc12AudioEncoder45OnReceivedUplinkRecoverablePacketLossFractionEf_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -1329,31 +1254,6 @@ int APS5_VABI _ZN6webrtc12AudioEncoder26OnReceivedUplinkAllocationENS_23BitrateA return 0; } -int APS5_VABI _ZN6webrtc12AudioEncoder13OnReceivedRttEi_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZN6webrtc12AudioEncoder18OnReceivedOverheadEm_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZN6webrtc12AudioEncoder27SetReceiverFrameLengthRangeEii_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK6webrtc12AudioEncoder11GetANAStatsEv_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZN6webrtc8ANAStatsC1Ev_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI _ZN6webrtc12AudioEncoder11EncodedInfoC1Ev_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -1492,36 +1392,11 @@ int APS5_VABI _ZN6webrtc10AudioFrame8CopyFromERKS0__nid_postfix(void) { return 0; } -int APS5_VABI _ZNK6webrtc10AudioFrame5mutedEv_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI _ZNK6webrtc10AudioFrame9data_viewEv_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI _ZNK6webrtc10AudioFrame4dataEv_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZN6webrtc10AudioFrame12mutable_dataEv_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZN6webrtc10AudioFrame4MuteEv_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZN6webrtc10AudioFrameC1Ev_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI _ZN6webrtc20EchoCanceller3ConfigaSERKS0__nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -1613,41 +1488,6 @@ int APS5_VABI _ZN6webrtc17AudioDeviceBuffer22SetRecordingSampleRateEj_nid_postfi return 0; } -int APS5_VABI _ZNK6webrtc18AudioDeviceGeneric21BuiltInAECIsAvailableEv_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK6webrtc18AudioDeviceGeneric21BuiltInAGCIsAvailableEv_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK6webrtc18AudioDeviceGeneric20BuiltInNSIsAvailableEv_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZN6webrtc18AudioDeviceGeneric16EnableBuiltInAECEb_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZN6webrtc18AudioDeviceGeneric16EnableBuiltInAGCEb_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZN6webrtc18AudioDeviceGeneric15EnableBuiltInNSEb_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK6webrtc18AudioDeviceGeneric23GetPlayoutUnderrunCountEv_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI _ZN6webrtc17PushSincResamplerC1Emm_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -1734,11 +1574,6 @@ int APS5_VABI _ZN6webrtc11AudioBuffer6CopyToERKNS_12StreamConfigEPs_nid_postfix( return 0; } -int APS5_VABI _ZN6webrtc23GetAvailableCpuFeaturesEv_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI _ZN6webrtc7rnn_vad17FeaturesExtractorC1ERKNS_20AvailableCpuFeaturesE_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -1770,11 +1605,6 @@ int APS5_VABI _ZN6webrtc7rnn_vad6RnnVad21ComputeVadProbabilityEN3rtc9ArrayViewIK return 0; } -int APS5_VABI _ZN6webrtc20AudioFrameOperations4MuteEPNS_10AudioFrameE_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - APS5_EXPORT("GMnT0UEJDZY", libwebrtcUnknown44); int APS5_VABI libwebrtcUnknown44(void) { NotImplemented_nid_no_patch("GMnT0UEJDZY"); diff --git a/core/libs/prx/libwebrtc/include/Api.hpp b/core/libs/prx/libwebrtc/include/Api.hpp index 429f5b701a..e7ef3771b1 100644 --- a/core/libs/prx/libwebrtc/include/Api.hpp +++ b/core/libs/prx/libwebrtc/include/Api.hpp @@ -1,13 +1,20 @@ #pragma once #include "SceTypes.hpp" +#include "prx/libwebrtc/include/AudioDecoder.hpp" +#include "prx/libwebrtc/include/AudioDeviceGeneric.hpp" +#include "prx/libwebrtc/include/AudioEncoder.hpp" +#include "prx/libwebrtc/include/AudioFrame.hpp" #include "prx/libwebrtc/include/ExpFilter.hpp" +#include "prx/libwebrtc/include/IpAddress.hpp" #include "prx/libwebrtc/include/RtcpCommonHeader.hpp" #include "prx/libwebrtc/include/Opus.hpp" +#include "prx/libwebrtc/include/CpuFeatures.hpp" #include #include struct WebRtcVadInstance; +#include "prx/libwebrtc/include/TaskQueueBase.hpp" extern "C" { @@ -27,8 +34,8 @@ int APS5_VABI _ZN3rtc13SocketAddressaSERKS0__nid_postfix(void); int APS5_VABI _ZNK3rtc13SocketAddress6ipaddrEv_nid_postfix(void); int APS5_VABI _ZN3rtc13SocketAddressC1ERKNS_9IPAddressEi_nid_postfix(void); int APS5_VABI _ZNK3rtc13SocketAddress4portEv_nid_postfix(void); -int APS5_VABI _ZNK3rtc9IPAddress12ipv4_addressEv_nid_postfix(void); -int APS5_VABI _ZNK3rtc9IPAddress12ipv6_addressEv_nid_postfix(void); +WebRtcInAddr APS5_VABI _ZNK3rtc9IPAddress12ipv4_addressEv_nid_postfix(const WebRtcIpAddress* address); +WebRtcIn6Addr APS5_VABI _ZNK3rtc9IPAddress12ipv6_addressEv_nid_postfix(const WebRtcIpAddress* address); int APS5_VABI _ZN6webrtc8RmsLevel7AnalyzeEN3rtc9ArrayViewIKsLln4711EEE_nid_postfix(void); int APS5_VABI _ZN6webrtc8RmsLevel7AverageEv_nid_postfix(void); int APS5_VABI _ZN6webrtc8RmsLevelC1Ev_nid_postfix(void); @@ -50,7 +57,7 @@ int APS5_VABI _ZN6webrtc23PeerConnectionInterface9IceServerC1ERKS1__nid_postfix( int APS5_VABI _ZN6webrtc23PeerConnectionInterface9IceServerD1Ev_nid_postfix(void); int APS5_VABI _ZN3rtc14RTCCertificateD1Ev_nid_postfix(void); int APS5_VABI _ZNK6webrtc20AudioSourceInterface7optionsEv_nid_postfix(void); -int APS5_VABI _ZNK3rtc9IPAddresseqERKS0__nid_postfix(void); +bool APS5_VABI _ZNK3rtc9IPAddresseqERKS0__nid_postfix(const WebRtcIpAddress* address, const WebRtcIpAddress* other); int APS5_VABI _ZNK6webrtc8RTCError7messageEv_nid_postfix(void); int APS5_VABI _ZN6webrtc21PendingTaskSafetyFlag6CreateEv_nid_postfix(void); int APS5_VABI _ZN7cricket5CodecD1Ev_nid_postfix(void); @@ -66,7 +73,7 @@ int APS5_VABI _ZN6webrtc17RtpPacketReceivedC1ERKS0__nid_postfix(void); int APS5_VABI _ZN6webrtc17RtpPacketReceivedD1Ev_nid_postfix(void); int APS5_VABI _ZN3rtc17CopyOnWriteBuffer24UnshareAndEnsureCapacityEm_nid_postfix(void); int APS5_VABI _ZN3rtc17CopyOnWriteBufferC1Emm_nid_postfix(void); -int APS5_VABI _ZN6webrtc13TaskQueueBase7CurrentEv_nid_postfix(void); +WebRtcTaskQueueBase* APS5_VABI _ZN6webrtc13TaskQueueBase7CurrentEv_nid_postfix(void); int APS5_VABI _ZN3rtc10PacketInfoD1Ev_nid_postfix(void); int APS5_VABI _ZN6webrtc21RtpHeaderExtensionMapC1EN3rtc9ArrayViewIKNS_12RtpExtensionELln4711EEE_nid_postfix(void); int APS5_VABI _ZN6webrtc17RtpPacketReceivedC1EPKNS_21RtpHeaderExtensionMapENS_9TimestampE_nid_postfix(void); @@ -95,8 +102,8 @@ int APS5_VABI libwebrtcUnknown12(void); int APS5_VABI libwebrtcUnknown13(void); int APS5_VABI _ZN6webrtc32webrtc_sequence_checker_internal19SequenceCheckerImplC2Eb_nid_postfix(void); int APS5_VABI _ZN3rtc15CurrentThreadIdEv_nid_postfix(void); -int APS5_VABI _ZN6webrtc13TaskQueueBase22CurrentTaskQueueSetterC1EPS0__nid_postfix(void); -int APS5_VABI _ZN6webrtc13TaskQueueBase22CurrentTaskQueueSetterD1Ev_nid_postfix(void); +void APS5_VABI _ZN6webrtc13TaskQueueBase22CurrentTaskQueueSetterC1EPS0__nid_postfix(WebRtcCurrentTaskQueueSetter* setter, WebRtcTaskQueueBase* taskQueue); +void APS5_VABI _ZN6webrtc13TaskQueueBase22CurrentTaskQueueSetterD1Ev_nid_postfix(WebRtcCurrentTaskQueueSetter* setter); int APS5_VABI _ZN6webrtc18EncodedImageBuffer6CreateEPKhm_nid_postfix(void); int APS5_VABI _ZN6webrtc10ColorSpaceC1ERKS0__nid_postfix(void); int APS5_VABI _ZN6webrtc10ColorSpaceaSERKS0__nid_postfix(void); @@ -148,11 +155,11 @@ int APS5_VABI libwebrtcUnknown18(void); int APS5_VABI _ZN3rtc10LogMessageC1EPKciNS_15LoggingSeverityE_nid_postfix(void); int APS5_VABI _ZN3rtc10LogMessageD1Ev_nid_postfix(void); int APS5_VABI _ZNK3rtc13SocketAddress10IsCompleteEv_nid_postfix(void); -int APS5_VABI _ZNK3rtc9IPAddress27v4AddressAsHostOrderIntegerEv_nid_postfix(void); +std::uint32_t APS5_VABI _ZNK3rtc9IPAddress27v4AddressAsHostOrderIntegerEv_nid_postfix(const WebRtcIpAddress* address); int APS5_VABI libwebrtcUnknown19(void); int APS5_VABI _ZNK3rtc13SocketAddress7IsAnyIPEv_nid_postfix(void); int APS5_VABI _ZN3rtc10TruncateIPERKNS_9IPAddressEi_nid_postfix(void); -int APS5_VABI _ZN3rtc15CountIPMaskBitsERKNS_9IPAddressE_nid_postfix(void); +int APS5_VABI _ZN3rtc15CountIPMaskBitsERKNS_9IPAddressE_nid_postfix(const WebRtcIpAddress* mask); int APS5_VABI _ZN3rtc17AsyncPacketSocketD2Ev_nid_postfix(void); int APS5_VABI _ZN3rtc17AsyncPacketSocket20NotifyPacketReceivedERKNS_14ReceivedPacketE_nid_postfix(void); int APS5_VABI libwebrtcUnknown20(void); @@ -249,28 +256,28 @@ int APS5_VABI _ZN6webrtc4rtcp17TransportFeedbackD1Ev_nid_postfix(void); int APS5_VABI _ZN6webrtc9RtpPacket5ParseEPKhm_nid_postfix(void); int APS5_VABI _ZN6webrtc12AudioDecoder6DecodeEPKhmimPsPNS0_10SpeechTypeE_nid_postfix(void); int APS5_VABI _ZN6webrtc12AudioDecoder15DecodeRedundantEPKhmimPsPNS0_10SpeechTypeE_nid_postfix(void); -int APS5_VABI _ZNK6webrtc12AudioDecoder12HasDecodePlcEv_nid_postfix(void); -int APS5_VABI _ZN6webrtc12AudioDecoder9DecodePlcEmPs_nid_postfix(void); -int APS5_VABI _ZN6webrtc12AudioDecoder11GeneratePlcEmPN3rtc7BufferTIsLb0EEE_nid_postfix(void); -int APS5_VABI _ZN6webrtc12AudioDecoder9ErrorCodeEv_nid_postfix(void); +bool APS5_VABI _ZNK6webrtc12AudioDecoder12HasDecodePlcEv_nid_postfix(const WebRtcAudioDecoder* decoder); +std::size_t APS5_VABI _ZN6webrtc12AudioDecoder9DecodePlcEmPs_nid_postfix(WebRtcAudioDecoder* decoder, std::size_t numFrames, std::int16_t* decoded); +void APS5_VABI _ZN6webrtc12AudioDecoder11GeneratePlcEmPN3rtc7BufferTIsLb0EEE_nid_postfix(WebRtcAudioDecoder* decoder, std::size_t requestedSamplesPerChannel, WebRtcInt16Buffer* concealmentAudio); +int APS5_VABI _ZN6webrtc12AudioDecoder9ErrorCodeEv_nid_postfix(WebRtcAudioDecoder* decoder); int APS5_VABI _ZN6webrtc12AudioDecoder11ParseResultC1EOS1__nid_postfix(void); int APS5_VABI libwebrtcUnknown35(void); int APS5_VABI _ZN6webrtc12AudioDecoder11ParseResultD1Ev_nid_postfix(void); int APS5_VABI _ZN6webrtc12AudioEncoder11EncodedInfoaSERKS1__nid_postfix(void); int APS5_VABI _ZN6webrtc12AudioEncoder6EncodeEjN3rtc9ArrayViewIKsLln4711EEEPNS1_7BufferTIhLb0EEE_nid_postfix(void); -int APS5_VABI _ZNK6webrtc12AudioEncoder6GetDtxEv_nid_postfix(void); -int APS5_VABI _ZN6webrtc12AudioEncoder16SetTargetBitrateEi_nid_postfix(void); +bool APS5_VABI _ZNK6webrtc12AudioEncoder6GetDtxEv_nid_postfix(const WebRtcAudioEncoder* encoder); +void APS5_VABI _ZN6webrtc12AudioEncoder16SetTargetBitrateEi_nid_postfix(WebRtcAudioEncoder* encoder, int targetBps); int APS5_VABI libwebrtcUnknown36(void); int APS5_VABI libwebrtcUnknown37(void); -int APS5_VABI _ZN6webrtc12AudioEncoder26DisableAudioNetworkAdaptorEv_nid_postfix(void); +void APS5_VABI _ZN6webrtc12AudioEncoder26DisableAudioNetworkAdaptorEv_nid_postfix(WebRtcAudioEncoder* encoder); int APS5_VABI _ZN6webrtc12AudioEncoder45OnReceivedUplinkRecoverablePacketLossFractionEf_nid_postfix(void); int APS5_VABI _ZN6webrtc12AudioEncoder28OnReceivedTargetAudioBitrateEi_nid_postfix(void); int APS5_VABI _ZN6webrtc12AudioEncoder26OnReceivedUplinkAllocationENS_23BitrateAllocationUpdateE_nid_postfix(void); -int APS5_VABI _ZN6webrtc12AudioEncoder13OnReceivedRttEi_nid_postfix(void); -int APS5_VABI _ZN6webrtc12AudioEncoder18OnReceivedOverheadEm_nid_postfix(void); -int APS5_VABI _ZN6webrtc12AudioEncoder27SetReceiverFrameLengthRangeEii_nid_postfix(void); -int APS5_VABI _ZNK6webrtc12AudioEncoder11GetANAStatsEv_nid_postfix(void); -int APS5_VABI _ZN6webrtc8ANAStatsC1Ev_nid_postfix(void); +void APS5_VABI _ZN6webrtc12AudioEncoder13OnReceivedRttEi_nid_postfix(WebRtcAudioEncoder* encoder, int rttMs); +void APS5_VABI _ZN6webrtc12AudioEncoder18OnReceivedOverheadEm_nid_postfix(WebRtcAudioEncoder* encoder, std::size_t overheadBytesPerPacket); +void APS5_VABI _ZN6webrtc12AudioEncoder27SetReceiverFrameLengthRangeEii_nid_postfix(WebRtcAudioEncoder* encoder, int minFrameLengthMs, int maxFrameLengthMs); +WebRtcAnaStats* APS5_VABI _ZNK6webrtc12AudioEncoder11GetANAStatsEv_nid_postfix(WebRtcAnaStats* result, const WebRtcAudioEncoder* encoder); +void APS5_VABI _ZN6webrtc8ANAStatsC1Ev_nid_postfix(WebRtcAnaStats* stats); int APS5_VABI _ZN6webrtc12AudioEncoder11EncodedInfoC1Ev_nid_postfix(void); int APS5_VABI _ZN6webrtc12AudioEncoder11EncodedInfoC1ERKS1__nid_postfix(void); int APS5_VABI _ZN6webrtc12AudioEncoder11EncodedInfoD1Ev_nid_postfix(void); @@ -298,12 +305,12 @@ int APS5_VABI _ZN6webrtc15AudioSendStream6Config13SendCodecSpecC1EiRKNS_14SdpAud int APS5_VABI _ZN6webrtc15AudioSendStream6Config13SendCodecSpecD1Ev_nid_postfix(void); int APS5_VABI _ZN6webrtc10AudioFrame11UpdateFrameEjPKsmiNS0_10SpeechTypeENS0_11VADActivityEm_nid_postfix(void); int APS5_VABI _ZN6webrtc10AudioFrame8CopyFromERKS0__nid_postfix(void); -int APS5_VABI _ZNK6webrtc10AudioFrame5mutedEv_nid_postfix(void); +bool APS5_VABI _ZNK6webrtc10AudioFrame5mutedEv_nid_postfix(const WebRtcAudioFrame* frame); int APS5_VABI _ZNK6webrtc10AudioFrame9data_viewEv_nid_postfix(void); -int APS5_VABI _ZNK6webrtc10AudioFrame4dataEv_nid_postfix(void); -int APS5_VABI _ZN6webrtc10AudioFrame12mutable_dataEv_nid_postfix(void); -int APS5_VABI _ZN6webrtc10AudioFrame4MuteEv_nid_postfix(void); -int APS5_VABI _ZN6webrtc10AudioFrameC1Ev_nid_postfix(void); +const std::int16_t* APS5_VABI _ZNK6webrtc10AudioFrame4dataEv_nid_postfix(const WebRtcAudioFrame* frame); +std::int16_t* APS5_VABI _ZN6webrtc10AudioFrame12mutable_dataEv_nid_postfix(WebRtcAudioFrame* frame); +void APS5_VABI _ZN6webrtc10AudioFrame4MuteEv_nid_postfix(WebRtcAudioFrame* frame); +void APS5_VABI _ZN6webrtc10AudioFrameC1Ev_nid_postfix(WebRtcAudioFrame* frame); int APS5_VABI _ZN6webrtc20EchoCanceller3ConfigaSERKS0__nid_postfix(void); int APS5_VABI _ZN6webrtc20EchoCanceller3ConfigC1Ev_nid_postfix(void); int APS5_VABI _ZN6webrtc20EchoCanceller3ConfigC1ERKS0__nid_postfix(void); @@ -327,13 +334,13 @@ void APS5_VABI WebRtcVad_Free_nid_postfix(WebRtcVadInstance* handle); int APS5_VABI WebRtcVad_Init_nid_postfix(WebRtcVadInstance* handle); int APS5_VABI WebRtcVad_set_mode_nid_postfix(WebRtcVadInstance* handle, int mode); int APS5_VABI WebRtcVad_Process_nid_postfix(WebRtcVadInstance* handle, int sampleRate, const std::int16_t* audio, std::size_t length); -int APS5_VABI _ZNK6webrtc18AudioDeviceGeneric21BuiltInAECIsAvailableEv_nid_postfix(void); -int APS5_VABI _ZNK6webrtc18AudioDeviceGeneric21BuiltInAGCIsAvailableEv_nid_postfix(void); -int APS5_VABI _ZNK6webrtc18AudioDeviceGeneric20BuiltInNSIsAvailableEv_nid_postfix(void); -int APS5_VABI _ZN6webrtc18AudioDeviceGeneric16EnableBuiltInAECEb_nid_postfix(void); -int APS5_VABI _ZN6webrtc18AudioDeviceGeneric16EnableBuiltInAGCEb_nid_postfix(void); -int APS5_VABI _ZN6webrtc18AudioDeviceGeneric15EnableBuiltInNSEb_nid_postfix(void); -int APS5_VABI _ZNK6webrtc18AudioDeviceGeneric23GetPlayoutUnderrunCountEv_nid_postfix(void); +bool APS5_VABI _ZNK6webrtc18AudioDeviceGeneric21BuiltInAECIsAvailableEv_nid_postfix(const WebRtcAudioDeviceGeneric* device); +bool APS5_VABI _ZNK6webrtc18AudioDeviceGeneric21BuiltInAGCIsAvailableEv_nid_postfix(const WebRtcAudioDeviceGeneric* device); +bool APS5_VABI _ZNK6webrtc18AudioDeviceGeneric20BuiltInNSIsAvailableEv_nid_postfix(const WebRtcAudioDeviceGeneric* device); +std::int32_t APS5_VABI _ZN6webrtc18AudioDeviceGeneric16EnableBuiltInAECEb_nid_postfix(WebRtcAudioDeviceGeneric* device, bool enable); +std::int32_t APS5_VABI _ZN6webrtc18AudioDeviceGeneric16EnableBuiltInAGCEb_nid_postfix(WebRtcAudioDeviceGeneric* device, bool enable); +std::int32_t APS5_VABI _ZN6webrtc18AudioDeviceGeneric15EnableBuiltInNSEb_nid_postfix(WebRtcAudioDeviceGeneric* device, bool enable); +std::int32_t APS5_VABI _ZNK6webrtc18AudioDeviceGeneric23GetPlayoutUnderrunCountEv_nid_postfix(const WebRtcAudioDeviceGeneric* device); int APS5_VABI _ZN6webrtc17PushSincResamplerC1Emm_nid_postfix(void); int APS5_VABI _ZN6webrtc17PushSincResampler8ResampleEPKfmPfm_nid_postfix(void); int APS5_VABI _ZN6webrtc13PushResamplerIsEC1Ev_nid_postfix(void); @@ -351,14 +358,14 @@ int APS5_VABI libwebrtcUnknown42(void); int APS5_VABI _ZN6webrtc11AudioBufferC1Emmmmmm_nid_postfix(void); int APS5_VABI _ZN6webrtc11AudioBuffer8CopyFromEPKsRKNS_12StreamConfigE_nid_postfix(void); int APS5_VABI _ZN6webrtc11AudioBuffer6CopyToERKNS_12StreamConfigEPs_nid_postfix(void); -int APS5_VABI _ZN6webrtc23GetAvailableCpuFeaturesEv_nid_postfix(void); +WebRtcAvailableCpuFeatures APS5_VABI _ZN6webrtc23GetAvailableCpuFeaturesEv_nid_postfix(); int APS5_VABI _ZN6webrtc7rnn_vad17FeaturesExtractorC1ERKNS_20AvailableCpuFeaturesE_nid_postfix(void); int APS5_VABI _ZN6webrtc7rnn_vad6RnnVadC1ERKNS_20AvailableCpuFeaturesE_nid_postfix(void); int APS5_VABI _ZN6webrtc7rnn_vad6RnnVadD1Ev_nid_postfix(void); int APS5_VABI _ZN6webrtc7rnn_vad17FeaturesExtractorD1Ev_nid_postfix(void); int APS5_VABI libwebrtcUnknown43(void); int APS5_VABI _ZN6webrtc7rnn_vad6RnnVad21ComputeVadProbabilityEN3rtc9ArrayViewIKfLl42EEEb_nid_postfix(void); -int APS5_VABI _ZN6webrtc20AudioFrameOperations4MuteEPNS_10AudioFrameE_nid_postfix(void); +void APS5_VABI _ZN6webrtc20AudioFrameOperations4MuteEPNS_10AudioFrameE_nid_postfix(WebRtcAudioFrame* frame); int APS5_VABI libwebrtcUnknown44(void); int APS5_VABI libwebrtcUnknown45(void); int APS5_VABI _ZN3rtc11RateTrackerC1Elm_nid_postfix(void); diff --git a/core/libs/prx/libwebrtc/include/AudioDecoder.hpp b/core/libs/prx/libwebrtc/include/AudioDecoder.hpp new file mode 100644 index 0000000000..f058d27226 --- /dev/null +++ b/core/libs/prx/libwebrtc/include/AudioDecoder.hpp @@ -0,0 +1,4 @@ +#pragma once + +struct WebRtcAudioDecoder; +struct WebRtcInt16Buffer; diff --git a/core/libs/prx/libwebrtc/include/AudioDeviceGeneric.hpp b/core/libs/prx/libwebrtc/include/AudioDeviceGeneric.hpp new file mode 100644 index 0000000000..e508033696 --- /dev/null +++ b/core/libs/prx/libwebrtc/include/AudioDeviceGeneric.hpp @@ -0,0 +1,3 @@ +#pragma once + +struct WebRtcAudioDeviceGeneric; diff --git a/core/libs/prx/libwebrtc/include/AudioEncoder.hpp b/core/libs/prx/libwebrtc/include/AudioEncoder.hpp new file mode 100644 index 0000000000..4d207a9abe --- /dev/null +++ b/core/libs/prx/libwebrtc/include/AudioEncoder.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include +#include + +struct WebRtcAudioEncoder; + +struct WebRtcOptionalUint32 { + union { + char nullState; + std::uint32_t value; + } storage; + bool engaged; +}; + +struct WebRtcOptionalFloat { + union { + char nullState; + float value; + } storage; + bool engaged; +}; + +struct WebRtcAnaStats { + WebRtcOptionalUint32 bitrateActionCounter; + WebRtcOptionalUint32 channelActionCounter; + WebRtcOptionalUint32 dtxActionCounter; + WebRtcOptionalUint32 fecActionCounter; + WebRtcOptionalUint32 frameLengthIncreaseCounter; + WebRtcOptionalUint32 frameLengthDecreaseCounter; + WebRtcOptionalFloat uplinkPacketLossFraction; +}; + +static_assert(sizeof(WebRtcOptionalUint32) == 8); +static_assert(offsetof(WebRtcOptionalUint32, engaged) == 4); +static_assert(sizeof(WebRtcOptionalFloat) == 8); +static_assert(sizeof(WebRtcAnaStats) == 56); +static_assert(offsetof(WebRtcAnaStats, uplinkPacketLossFraction) == 48); diff --git a/core/libs/prx/libwebrtc/include/AudioFrame.hpp b/core/libs/prx/libwebrtc/include/AudioFrame.hpp new file mode 100644 index 0000000000..e462d1bea1 --- /dev/null +++ b/core/libs/prx/libwebrtc/include/AudioFrame.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include +#include + +constexpr std::size_t WebRtcAudioFrameMaxDataSizeSamples = 7680; +constexpr std::int32_t WebRtcAudioFrameSpeechUndefined = 4; +constexpr std::int32_t WebRtcAudioFrameVadUnknown = 2; +constexpr std::int32_t WebRtcChannelLayoutNone = 0; + +struct WebRtcAudioFrame { + std::uint32_t timestamp; + std::int64_t elapsedTimeMs; + std::int64_t ntpTimeMs; + std::size_t samplesPerChannel; + std::int32_t sampleRateHz; + std::size_t numChannels; + std::int32_t speechType; + std::int32_t vadActivity; + std::int64_t profileTimestampMs; + void* packetInfos; + std::int16_t data[WebRtcAudioFrameMaxDataSizeSamples]; + bool muted; + std::int32_t channelLayout; + union { + char nullState; + std::int64_t value; + } absoluteCaptureTimestampMs; + bool absoluteCaptureTimestampMsEngaged; +}; + +static_assert(sizeof(WebRtcAudioFrame) == 15456); +static_assert(offsetof(WebRtcAudioFrame, elapsedTimeMs) == 8); +static_assert(offsetof(WebRtcAudioFrame, ntpTimeMs) == 16); +static_assert(offsetof(WebRtcAudioFrame, samplesPerChannel) == 24); +static_assert(offsetof(WebRtcAudioFrame, sampleRateHz) == 32); +static_assert(offsetof(WebRtcAudioFrame, numChannels) == 40); +static_assert(offsetof(WebRtcAudioFrame, speechType) == 48); +static_assert(offsetof(WebRtcAudioFrame, vadActivity) == 52); +static_assert(offsetof(WebRtcAudioFrame, profileTimestampMs) == 56); +static_assert(offsetof(WebRtcAudioFrame, packetInfos) == 64); +static_assert(offsetof(WebRtcAudioFrame, data) == 72); +static_assert(offsetof(WebRtcAudioFrame, muted) == 15432); +static_assert(offsetof(WebRtcAudioFrame, channelLayout) == 15436); +static_assert(offsetof(WebRtcAudioFrame, absoluteCaptureTimestampMs) == 15440); +static_assert(offsetof(WebRtcAudioFrame, absoluteCaptureTimestampMsEngaged) == 15448); diff --git a/core/libs/prx/libwebrtc/include/CpuFeatures.hpp b/core/libs/prx/libwebrtc/include/CpuFeatures.hpp new file mode 100644 index 0000000000..4dd076d791 --- /dev/null +++ b/core/libs/prx/libwebrtc/include/CpuFeatures.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include + +struct WebRtcAvailableCpuFeatures { + bool sse2; + bool avx2; + bool neon; +}; + +static_assert(sizeof(WebRtcAvailableCpuFeatures) == 3); +static_assert(offsetof(WebRtcAvailableCpuFeatures, avx2) == 1); +static_assert(offsetof(WebRtcAvailableCpuFeatures, neon) == 2); diff --git a/core/libs/prx/libwebrtc/include/IpAddress.hpp b/core/libs/prx/libwebrtc/include/IpAddress.hpp new file mode 100644 index 0000000000..1142275a2a --- /dev/null +++ b/core/libs/prx/libwebrtc/include/IpAddress.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include +#include + +constexpr std::int32_t WebRtcAfUnspec = 0; +constexpr std::int32_t WebRtcAfInet = 2; +constexpr std::int32_t WebRtcAfInet6 = 28; + +struct WebRtcInAddr { + std::uint32_t sAddr; +}; + +struct WebRtcIn6Addr { + std::uint8_t bytes[16]; +}; + +struct WebRtcIpAddress { + const void* vtable; + std::int32_t family; + union { + WebRtcInAddr ip4; + WebRtcIn6Addr ip6; + } address; +}; + +static_assert(sizeof(WebRtcInAddr) == 4); +static_assert(sizeof(WebRtcIn6Addr) == 16); +static_assert(sizeof(WebRtcIpAddress) == 32); +static_assert(offsetof(WebRtcIpAddress, family) == 8); +static_assert(offsetof(WebRtcIpAddress, address) == 12); diff --git a/core/libs/prx/libwebrtc/include/TaskQueueBase.hpp b/core/libs/prx/libwebrtc/include/TaskQueueBase.hpp new file mode 100644 index 0000000000..100d970a67 --- /dev/null +++ b/core/libs/prx/libwebrtc/include/TaskQueueBase.hpp @@ -0,0 +1,9 @@ +#pragma once + +struct WebRtcTaskQueueBase; + +struct WebRtcCurrentTaskQueueSetter { + WebRtcTaskQueueBase* previous; +}; + +static_assert(sizeof(WebRtcCurrentTaskQueueSetter) == 8); diff --git a/core/libs/prx/libwebrtc/src/AudioDecoder.cpp b/core/libs/prx/libwebrtc/src/AudioDecoder.cpp new file mode 100644 index 0000000000..c552105486 --- /dev/null +++ b/core/libs/prx/libwebrtc/src/AudioDecoder.cpp @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2012 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +/* + * Copyright (c) 2011, The WebRTC project authors. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * * Neither the name of Google nor the names of its contributors may + * be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "prx/libwebrtc/libwebrtc.hpp" + +#include +#include + +extern "C" { + +bool APS5_VABI _ZNK6webrtc12AudioDecoder12HasDecodePlcEv_nid_postfix(const WebRtcAudioDecoder*) { + return false; +} + +std::size_t APS5_VABI _ZN6webrtc12AudioDecoder9DecodePlcEmPs_nid_postfix(WebRtcAudioDecoder*, std::size_t, std::int16_t*) { + return 0; +} + +void APS5_VABI _ZN6webrtc12AudioDecoder11GeneratePlcEmPN3rtc7BufferTIsLb0EEE_nid_postfix(WebRtcAudioDecoder*, std::size_t, WebRtcInt16Buffer*) {} + +int APS5_VABI _ZN6webrtc12AudioDecoder9ErrorCodeEv_nid_postfix(WebRtcAudioDecoder*) { + return 0; +} + +} diff --git a/core/libs/prx/libwebrtc/src/AudioDeviceGeneric.cpp b/core/libs/prx/libwebrtc/src/AudioDeviceGeneric.cpp new file mode 100644 index 0000000000..039e0d6ac7 --- /dev/null +++ b/core/libs/prx/libwebrtc/src/AudioDeviceGeneric.cpp @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2011 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +/* + * Copyright (c) 2011, The WebRTC project authors. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * * Neither the name of Google nor the names of its contributors may + * be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "prx/libwebrtc/libwebrtc.hpp" + +#include + +extern "C" { + +bool APS5_VABI _ZNK6webrtc18AudioDeviceGeneric21BuiltInAECIsAvailableEv_nid_postfix(const WebRtcAudioDeviceGeneric*) { + return false; +} + +std::int32_t APS5_VABI _ZN6webrtc18AudioDeviceGeneric16EnableBuiltInAECEb_nid_postfix(WebRtcAudioDeviceGeneric*, bool) { + return -1; +} + +bool APS5_VABI _ZNK6webrtc18AudioDeviceGeneric21BuiltInAGCIsAvailableEv_nid_postfix(const WebRtcAudioDeviceGeneric*) { + return false; +} + +std::int32_t APS5_VABI _ZN6webrtc18AudioDeviceGeneric16EnableBuiltInAGCEb_nid_postfix(WebRtcAudioDeviceGeneric*, bool) { + return -1; +} + +bool APS5_VABI _ZNK6webrtc18AudioDeviceGeneric20BuiltInNSIsAvailableEv_nid_postfix(const WebRtcAudioDeviceGeneric*) { + return false; +} + +std::int32_t APS5_VABI _ZN6webrtc18AudioDeviceGeneric15EnableBuiltInNSEb_nid_postfix(WebRtcAudioDeviceGeneric*, bool) { + return -1; +} + +std::int32_t APS5_VABI _ZNK6webrtc18AudioDeviceGeneric23GetPlayoutUnderrunCountEv_nid_postfix(const WebRtcAudioDeviceGeneric*) { + return -1; +} + +} diff --git a/core/libs/prx/libwebrtc/src/AudioEncoder.cpp b/core/libs/prx/libwebrtc/src/AudioEncoder.cpp new file mode 100644 index 0000000000..e745251521 --- /dev/null +++ b/core/libs/prx/libwebrtc/src/AudioEncoder.cpp @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2014 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +/* + * Copyright (c) 2011, The WebRTC project authors. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * * Neither the name of Google nor the names of its contributors may + * be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "prx/libwebrtc/libwebrtc.hpp" + +#include +#include + +namespace { + +void ClearOptional(WebRtcOptionalUint32& optional) { + optional.storage.nullState = 0; + optional.engaged = false; +} + +void ConstructAnaStats(WebRtcAnaStats& stats) { + ClearOptional(stats.bitrateActionCounter); + ClearOptional(stats.channelActionCounter); + ClearOptional(stats.dtxActionCounter); + ClearOptional(stats.fecActionCounter); + ClearOptional(stats.frameLengthIncreaseCounter); + ClearOptional(stats.frameLengthDecreaseCounter); + stats.uplinkPacketLossFraction.storage.nullState = 0; + stats.uplinkPacketLossFraction.engaged = false; +} + +} + +extern "C" { + +void APS5_VABI _ZN6webrtc8ANAStatsC1Ev_nid_postfix(WebRtcAnaStats* stats) { + if (!stats) + throw std::invalid_argument("ANAStats::ANAStats requires stats"); + ConstructAnaStats(*stats); +} + +WebRtcAnaStats* APS5_VABI _ZNK6webrtc12AudioEncoder11GetANAStatsEv_nid_postfix(WebRtcAnaStats* result, const WebRtcAudioEncoder*) { + if (!result) + throw std::invalid_argument("AudioEncoder::GetANAStats requires a result"); + ConstructAnaStats(*result); + return result; +} + +bool APS5_VABI _ZNK6webrtc12AudioEncoder6GetDtxEv_nid_postfix(const WebRtcAudioEncoder*) { + return false; +} + +void APS5_VABI _ZN6webrtc12AudioEncoder16SetTargetBitrateEi_nid_postfix(WebRtcAudioEncoder*, int) {} + +void APS5_VABI _ZN6webrtc12AudioEncoder26DisableAudioNetworkAdaptorEv_nid_postfix(WebRtcAudioEncoder*) {} + +void APS5_VABI _ZN6webrtc12AudioEncoder13OnReceivedRttEi_nid_postfix(WebRtcAudioEncoder*, int) {} + +void APS5_VABI _ZN6webrtc12AudioEncoder18OnReceivedOverheadEm_nid_postfix(WebRtcAudioEncoder*, std::size_t) {} + +void APS5_VABI _ZN6webrtc12AudioEncoder27SetReceiverFrameLengthRangeEii_nid_postfix(WebRtcAudioEncoder*, int, int) {} + +} diff --git a/core/libs/prx/libwebrtc/src/AudioFrame.cpp b/core/libs/prx/libwebrtc/src/AudioFrame.cpp new file mode 100644 index 0000000000..cbe7492c52 --- /dev/null +++ b/core/libs/prx/libwebrtc/src/AudioFrame.cpp @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2018 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +/* + * Copyright (c) 2011, The WebRTC project authors. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * * Neither the name of Google nor the names of its contributors may + * be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "prx/libwebrtc/libwebrtc.hpp" + +#include +#include +#include + +namespace { + +const std::int16_t ZeroedData[WebRtcAudioFrameMaxDataSizeSamples] = {}; + +} + +extern "C" { + +void APS5_VABI _ZN6webrtc10AudioFrameC1Ev_nid_postfix(WebRtcAudioFrame* frame) { + if (!frame) + throw std::invalid_argument("AudioFrame::AudioFrame requires a frame"); + frame->timestamp = 0; + frame->elapsedTimeMs = -1; + frame->ntpTimeMs = -1; + frame->samplesPerChannel = 0; + frame->sampleRateHz = 0; + frame->numChannels = 0; + frame->speechType = WebRtcAudioFrameSpeechUndefined; + frame->vadActivity = WebRtcAudioFrameVadUnknown; + frame->profileTimestampMs = 0; + frame->packetInfos = nullptr; + frame->muted = true; + frame->channelLayout = WebRtcChannelLayoutNone; + frame->absoluteCaptureTimestampMs.nullState = 0; + frame->absoluteCaptureTimestampMsEngaged = false; +} + +void APS5_VABI _ZN6webrtc10AudioFrame4MuteEv_nid_postfix(WebRtcAudioFrame* frame) { + if (!frame) + throw std::invalid_argument("AudioFrame::Mute requires a frame"); + frame->muted = true; +} + +bool APS5_VABI _ZNK6webrtc10AudioFrame5mutedEv_nid_postfix(const WebRtcAudioFrame* frame) { + if (!frame) + throw std::invalid_argument("AudioFrame::muted requires a frame"); + return frame->muted; +} + +const std::int16_t* APS5_VABI _ZNK6webrtc10AudioFrame4dataEv_nid_postfix(const WebRtcAudioFrame* frame) { + if (!frame) + throw std::invalid_argument("AudioFrame::data requires a frame"); + return frame->muted ? ZeroedData : frame->data; +} + +std::int16_t* APS5_VABI _ZN6webrtc10AudioFrame12mutable_dataEv_nid_postfix(WebRtcAudioFrame* frame) { + if (!frame) + throw std::invalid_argument("AudioFrame::mutable_data requires a frame"); + if (frame->muted) { + std::memset(frame->data, 0, sizeof(frame->data)); + frame->muted = false; + } + return frame->data; +} + +} diff --git a/core/libs/prx/libwebrtc/src/AudioFrameOperations.cpp b/core/libs/prx/libwebrtc/src/AudioFrameOperations.cpp new file mode 100644 index 0000000000..ebd4232b28 --- /dev/null +++ b/core/libs/prx/libwebrtc/src/AudioFrameOperations.cpp @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2012 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +/* + * Copyright (c) 2011, The WebRTC project authors. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * * Neither the name of Google nor the names of its contributors may + * be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "prx/libwebrtc/libwebrtc.hpp" +#include "prx/libc/include/General.hpp" + +#include + +extern "C" { + +void APS5_VABI _ZN6webrtc20AudioFrameOperations4MuteEPNS_10AudioFrameE_nid_postfix(WebRtcAudioFrame* frame) { + if (!frame) + throw std::invalid_argument("AudioFrameOperations::Mute requires a frame"); + if (frame->numChannels != 0 && frame->samplesPerChannel > WebRtcAudioFrameMaxDataSizeSamples / frame->numChannels) + NotImplemented_nid_no_patch(__func__); + frame->muted = true; +} + +} diff --git a/core/libs/prx/libwebrtc/src/CpuFeatures.cpp b/core/libs/prx/libwebrtc/src/CpuFeatures.cpp new file mode 100644 index 0000000000..4033c443e6 --- /dev/null +++ b/core/libs/prx/libwebrtc/src/CpuFeatures.cpp @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2020 The WebRTC project authors. All Rights Reserved. + * Copyright (c) 2011 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +/* + * Copyright (c) 2011, The WebRTC project authors. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * * Neither the name of Google nor the names of its contributors may + * be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "prx/libwebrtc/libwebrtc.hpp" + +#include +#include + +namespace { + +std::uint64_t ExtendedControlRegister0() { + std::uint32_t low = 0; + std::uint32_t high = 0; + __asm__ volatile("xgetbv" : "=a"(low), "=d"(high) : "c"(0)); + return (static_cast(high) << 32) | low; +} + +bool HasAvx2(unsigned int leaf1Ecx) { + unsigned int eax = 0; + unsigned int ebx = 0; + unsigned int ecx = 0; + unsigned int edx = 0; + __cpuid_count(0, 0, eax, ebx, ecx, edx); + if (static_cast(eax) < 7) + return false; + if ((leaf1Ecx & 0x10000000u) == 0 || (leaf1Ecx & 0x04000000u) == 0 || (leaf1Ecx & 0x08000000u) == 0) + return false; + if ((ExtendedControlRegister0() & 0x6u) != 0x6u) + return false; + __cpuid_count(7, 0, eax, ebx, ecx, edx); + return (ebx & 0x00000020u) != 0 && (ebx & 0x00000100u) != 0; +} + +} + +extern "C" { + +WebRtcAvailableCpuFeatures APS5_VABI _ZN6webrtc23GetAvailableCpuFeaturesEv_nid_postfix() { + unsigned int eax = 0; + unsigned int ebx = 0; + unsigned int ecx = 0; + unsigned int edx = 0; + __cpuid_count(1, 0, eax, ebx, ecx, edx); + return {(edx & 0x04000000u) != 0, HasAvx2(ecx), false}; +} + +} diff --git a/core/libs/prx/libwebrtc/src/IpAddress.cpp b/core/libs/prx/libwebrtc/src/IpAddress.cpp new file mode 100644 index 0000000000..db3cf1ff07 --- /dev/null +++ b/core/libs/prx/libwebrtc/src/IpAddress.cpp @@ -0,0 +1,117 @@ +/* + * Copyright 2004 The WebRTC Project Authors. All rights reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +/* + * Copyright (c) 2011, The WebRTC project authors. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * * Neither the name of Google nor the names of its contributors may + * be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "prx/libwebrtc/libwebrtc.hpp" + +#include +#include +#include +#include + +namespace { + +const WebRtcIpAddress& Checked(const WebRtcIpAddress* address, const char* function) { + if (!address) + throw std::invalid_argument(function); + return *address; +} + +std::uint32_t BigEndianWord(const std::uint8_t* bytes) { + return (static_cast(bytes[0]) << 24) | (static_cast(bytes[1]) << 16) | (static_cast(bytes[2]) << 8) | bytes[3]; +} + +std::uint32_t HostOrderIpv4(const WebRtcIpAddress& address) { + std::uint8_t bytes[4]; + std::memcpy(bytes, &address.address.ip4, sizeof(bytes)); + return BigEndianWord(bytes); +} + +} + +extern "C" { + +WebRtcInAddr APS5_VABI _ZNK3rtc9IPAddress12ipv4_addressEv_nid_postfix(const WebRtcIpAddress* address) { + return Checked(address, "IPAddress::ipv4_address requires an address").address.ip4; +} + +WebRtcIn6Addr APS5_VABI _ZNK3rtc9IPAddress12ipv6_addressEv_nid_postfix(const WebRtcIpAddress* address) { + return Checked(address, "IPAddress::ipv6_address requires an address").address.ip6; +} + +std::uint32_t APS5_VABI _ZNK3rtc9IPAddress27v4AddressAsHostOrderIntegerEv_nid_postfix(const WebRtcIpAddress* address) { + const WebRtcIpAddress& checked = Checked(address, "IPAddress::v4AddressAsHostOrderInteger requires an address"); + return checked.family == WebRtcAfInet ? HostOrderIpv4(checked) : 0; +} + +bool APS5_VABI _ZNK3rtc9IPAddresseqERKS0__nid_postfix(const WebRtcIpAddress* address, const WebRtcIpAddress* other) { + const WebRtcIpAddress& left = Checked(address, "IPAddress::operator== requires an address"); + const WebRtcIpAddress& right = Checked(other, "IPAddress::operator== requires another address"); + if (left.family != right.family) + return false; + if (left.family == WebRtcAfInet) + return std::memcmp(&left.address.ip4, &right.address.ip4, sizeof(WebRtcInAddr)) == 0; + if (left.family == WebRtcAfInet6) + return std::memcmp(&left.address.ip6, &right.address.ip6, sizeof(WebRtcIn6Addr)) == 0; + return left.family == WebRtcAfUnspec; +} + +int APS5_VABI _ZN3rtc15CountIPMaskBitsERKNS_9IPAddressE_nid_postfix(const WebRtcIpAddress* mask) { + const WebRtcIpAddress& checked = Checked(mask, "CountIPMaskBits requires a mask"); + std::uint32_t word = 0; + int bits = 0; + if (checked.family == WebRtcAfInet) { + word = HostOrderIpv4(checked); + } else if (checked.family == WebRtcAfInet6) { + const std::uint8_t* bytes = checked.address.ip6.bytes; + while (bits < 128 && BigEndianWord(bytes + bits / 8) == 0xFFFFFFFFu) + bits += 32; + if (bits < 128) + word = BigEndianWord(bytes + bits / 8); + } else { + return 0; + } + if (word == 0) + return bits; + return bits + 32 - std::countr_zero(word); +} + +} diff --git a/core/libs/prx/libwebrtc/src/TaskQueueBase.cpp b/core/libs/prx/libwebrtc/src/TaskQueueBase.cpp new file mode 100644 index 0000000000..f263fe3a35 --- /dev/null +++ b/core/libs/prx/libwebrtc/src/TaskQueueBase.cpp @@ -0,0 +1,72 @@ +/* + * Copyright 2019 The WebRTC Project Authors. All rights reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +/* + * Copyright (c) 2011, The WebRTC project authors. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * * Neither the name of Google nor the names of its contributors may + * be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "prx/libwebrtc/libwebrtc.hpp" + +#include + +namespace { + +thread_local WebRtcTaskQueueBase* currentTaskQueue = nullptr; + +} + +extern "C" { + +WebRtcTaskQueueBase* APS5_VABI _ZN6webrtc13TaskQueueBase7CurrentEv_nid_postfix(void) { + return currentTaskQueue; +} + +void APS5_VABI _ZN6webrtc13TaskQueueBase22CurrentTaskQueueSetterC1EPS0__nid_postfix(WebRtcCurrentTaskQueueSetter* setter, WebRtcTaskQueueBase* taskQueue) { + if (!setter) + throw std::invalid_argument("CurrentTaskQueueSetter requires a setter"); + setter->previous = currentTaskQueue; + currentTaskQueue = taskQueue; +} + +void APS5_VABI _ZN6webrtc13TaskQueueBase22CurrentTaskQueueSetterD1Ev_nid_postfix(WebRtcCurrentTaskQueueSetter* setter) { + if (!setter) + throw std::invalid_argument("~CurrentTaskQueueSetter requires a setter"); + currentTaskQueue = setter->previous; +} + +} diff --git a/core/libs/prx/libwebrtc/tests/AudioDecoder.cpp b/core/libs/prx/libwebrtc/tests/AudioDecoder.cpp new file mode 100644 index 0000000000..5ed74ba9f0 --- /dev/null +++ b/core/libs/prx/libwebrtc/tests/AudioDecoder.cpp @@ -0,0 +1,62 @@ +#include "prx/libwebrtc/libwebrtc.hpp" + +#include +#include +#include +#include +#include + +namespace { + +void Require(bool condition, std::source_location location = std::source_location::current()) { + if (condition) return; + std::fprintf(stderr, "libwebrtc AudioDecoder check failed at line %u\n", location.line()); + std::abort(); +} + +unsigned char decoderStorage[64]; +unsigned char bufferStorage[32]; +std::int16_t samples[64]; + +WebRtcAudioDecoder* Decoder() { + std::memset(decoderStorage, 0xC3, sizeof(decoderStorage)); + std::memset(bufferStorage, 0xA5, sizeof(bufferStorage)); + for (std::int16_t& sample : samples) + sample = 0x1234; + return reinterpret_cast(decoderStorage); +} + +bool Unchanged() { + for (unsigned char byte : decoderStorage) + if (byte != 0xC3) return false; + for (unsigned char byte : bufferStorage) + if (byte != 0xA5) return false; + for (std::int16_t sample : samples) + if (sample != 0x1234) return false; + return true; +} + +void TestPlc() { + WebRtcAudioDecoder* decoder = Decoder(); + Require(!_ZNK6webrtc12AudioDecoder12HasDecodePlcEv_nid_postfix(decoder)); + Require(_ZN6webrtc12AudioDecoder9DecodePlcEmPs_nid_postfix(decoder, 0, samples) == 0); + Require(_ZN6webrtc12AudioDecoder9DecodePlcEmPs_nid_postfix(decoder, 3, samples) == 0); + Require(_ZN6webrtc12AudioDecoder9DecodePlcEmPs_nid_postfix(decoder, 1, nullptr) == 0); + _ZN6webrtc12AudioDecoder11GeneratePlcEmPN3rtc7BufferTIsLb0EEE_nid_postfix(decoder, 480, reinterpret_cast(bufferStorage)); + _ZN6webrtc12AudioDecoder11GeneratePlcEmPN3rtc7BufferTIsLb0EEE_nid_postfix(decoder, 480, nullptr); + Require(Unchanged()); +} + +void TestErrorCode() { + WebRtcAudioDecoder* decoder = Decoder(); + Require(_ZN6webrtc12AudioDecoder9ErrorCodeEv_nid_postfix(decoder) == 0); + Require(Unchanged()); +} + +} + +int main() { + TestPlc(); + TestErrorCode(); + return 0; +} diff --git a/core/libs/prx/libwebrtc/tests/AudioDeviceGeneric.cpp b/core/libs/prx/libwebrtc/tests/AudioDeviceGeneric.cpp new file mode 100644 index 0000000000..05c1f1e42f --- /dev/null +++ b/core/libs/prx/libwebrtc/tests/AudioDeviceGeneric.cpp @@ -0,0 +1,62 @@ +#include "prx/libwebrtc/libwebrtc.hpp" + +#include +#include +#include +#include +#include + +namespace { + +void Require(bool condition, std::source_location location = std::source_location::current()) { + if (condition) return; + std::fprintf(stderr, "libwebrtc AudioDeviceGeneric check failed at line %u\n", location.line()); + std::abort(); +} + +unsigned char storage[64]; + +WebRtcAudioDeviceGeneric* Device() { + std::memset(storage, 0xC3, sizeof(storage)); + return reinterpret_cast(storage); +} + +bool DeviceUnchanged() { + for (unsigned char byte : storage) + if (byte != 0xC3) return false; + return true; +} + +void TestAvailability() { + WebRtcAudioDeviceGeneric* device = Device(); + Require(!_ZNK6webrtc18AudioDeviceGeneric21BuiltInAECIsAvailableEv_nid_postfix(device)); + Require(!_ZNK6webrtc18AudioDeviceGeneric21BuiltInAGCIsAvailableEv_nid_postfix(device)); + Require(!_ZNK6webrtc18AudioDeviceGeneric20BuiltInNSIsAvailableEv_nid_postfix(device)); + Require(DeviceUnchanged()); +} + +void TestEnable() { + WebRtcAudioDeviceGeneric* device = Device(); + for (bool enable : {true, false}) { + Require(_ZN6webrtc18AudioDeviceGeneric16EnableBuiltInAECEb_nid_postfix(device, enable) == -1); + Require(_ZN6webrtc18AudioDeviceGeneric16EnableBuiltInAGCEb_nid_postfix(device, enable) == -1); + Require(_ZN6webrtc18AudioDeviceGeneric15EnableBuiltInNSEb_nid_postfix(device, enable) == -1); + } + Require(!_ZNK6webrtc18AudioDeviceGeneric21BuiltInAECIsAvailableEv_nid_postfix(device)); + Require(DeviceUnchanged()); +} + +void TestUnderrunCount() { + WebRtcAudioDeviceGeneric* device = Device(); + Require(_ZNK6webrtc18AudioDeviceGeneric23GetPlayoutUnderrunCountEv_nid_postfix(device) == -1); + Require(DeviceUnchanged()); +} + +} + +int main() { + TestAvailability(); + TestEnable(); + TestUnderrunCount(); + return 0; +} diff --git a/core/libs/prx/libwebrtc/tests/AudioEncoder.cpp b/core/libs/prx/libwebrtc/tests/AudioEncoder.cpp new file mode 100644 index 0000000000..2b1141885f --- /dev/null +++ b/core/libs/prx/libwebrtc/tests/AudioEncoder.cpp @@ -0,0 +1,94 @@ +#include "prx/libwebrtc/libwebrtc.hpp" + +#include +#include +#include +#include +#include +#include + +namespace { + +void Require(bool condition, std::source_location location = std::source_location::current()) { + if (condition) return; + std::fprintf(stderr, "libwebrtc AudioEncoder check failed at line %u\n", location.line()); + std::abort(); +} + +template void RequireInvalidArgument(TAction action, std::source_location location = std::source_location::current()) { + try { + action(); + } catch (const std::invalid_argument&) { + return; + } + Require(false, location); +} + +void Fill(WebRtcAnaStats& stats) { + std::memset(&stats, 0x5A, sizeof(stats)); +} + +bool Cleared(const WebRtcOptionalUint32& optional) { + const auto* bytes = reinterpret_cast(&optional); + return !optional.engaged && bytes[0] == 0 && bytes[1] == 0x5A && bytes[2] == 0x5A && bytes[3] == 0x5A && bytes[5] == 0x5A && bytes[6] == 0x5A && bytes[7] == 0x5A; +} + +bool AllCleared(const WebRtcAnaStats& stats) { + WebRtcOptionalUint32 loss; + std::memcpy(&loss, &stats.uplinkPacketLossFraction, sizeof(loss)); + return Cleared(stats.bitrateActionCounter) && Cleared(stats.channelActionCounter) && Cleared(stats.dtxActionCounter) && Cleared(stats.fecActionCounter) && + Cleared(stats.frameLengthIncreaseCounter) && Cleared(stats.frameLengthDecreaseCounter) && Cleared(loss); +} + +WebRtcAudioEncoder* Encoder() { + static unsigned char storage[64]; + std::memset(storage, 0xC3, sizeof(storage)); + return reinterpret_cast(storage); +} + +bool EncoderUnchanged(const WebRtcAudioEncoder* encoder) { + const auto* bytes = reinterpret_cast(encoder); + for (int i = 0; i < 64; ++i) + if (bytes[i] != 0xC3) return false; + return true; +} + +void TestAnaStats() { + WebRtcAnaStats stats; + Fill(stats); + _ZN6webrtc8ANAStatsC1Ev_nid_postfix(&stats); + Require(AllCleared(stats)); + + WebRtcAudioEncoder* encoder = Encoder(); + WebRtcAnaStats result; + Fill(result); + Require(_ZNK6webrtc12AudioEncoder11GetANAStatsEv_nid_postfix(&result, encoder) == &result); + Require(AllCleared(result)); + Require(EncoderUnchanged(encoder)); +} + +void TestDefaults() { + WebRtcAudioEncoder* encoder = Encoder(); + Require(!_ZNK6webrtc12AudioEncoder6GetDtxEv_nid_postfix(encoder)); + _ZN6webrtc12AudioEncoder16SetTargetBitrateEi_nid_postfix(encoder, 32000); + _ZN6webrtc12AudioEncoder26DisableAudioNetworkAdaptorEv_nid_postfix(encoder); + _ZN6webrtc12AudioEncoder13OnReceivedRttEi_nid_postfix(encoder, 120); + _ZN6webrtc12AudioEncoder18OnReceivedOverheadEm_nid_postfix(encoder, 40); + _ZN6webrtc12AudioEncoder27SetReceiverFrameLengthRangeEii_nid_postfix(encoder, 20, 120); + Require(!_ZNK6webrtc12AudioEncoder6GetDtxEv_nid_postfix(encoder)); + Require(EncoderUnchanged(encoder)); +} + +void TestNullPointers() { + RequireInvalidArgument([] { _ZN6webrtc8ANAStatsC1Ev_nid_postfix(nullptr); }); + RequireInvalidArgument([] { _ZNK6webrtc12AudioEncoder11GetANAStatsEv_nid_postfix(nullptr, Encoder()); }); +} + +} + +int main() { + TestAnaStats(); + TestDefaults(); + TestNullPointers(); + return 0; +} diff --git a/core/libs/prx/libwebrtc/tests/AudioFrame.cpp b/core/libs/prx/libwebrtc/tests/AudioFrame.cpp new file mode 100644 index 0000000000..b8e6489e7b --- /dev/null +++ b/core/libs/prx/libwebrtc/tests/AudioFrame.cpp @@ -0,0 +1,143 @@ +#include "prx/libwebrtc/libwebrtc.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void Require(bool condition, std::source_location location = std::source_location::current()) { + if (condition) return; + std::fprintf(stderr, "libwebrtc AudioFrame check failed at line %u\n", location.line()); + std::abort(); +} + +template void RequireThrows(TAction action, std::source_location location = std::source_location::current()) { + try { + action(); + } catch (const TException&) { + return; + } + Require(false, location); +} + +std::unique_ptr Filled() { + auto frame = std::make_unique(); + std::memset(frame.get(), 0x5A, sizeof(WebRtcAudioFrame)); + return frame; +} + +bool AllSamples(const std::int16_t* data, std::int16_t value) { + for (std::size_t i = 0; i < WebRtcAudioFrameMaxDataSizeSamples; ++i) + if (data[i] != value) return false; + return true; +} + +void TestConstructor() { + auto frame = Filled(); + _ZN6webrtc10AudioFrameC1Ev_nid_postfix(frame.get()); + Require(frame->timestamp == 0); + Require(frame->elapsedTimeMs == -1); + Require(frame->ntpTimeMs == -1); + Require(frame->samplesPerChannel == 0); + Require(frame->sampleRateHz == 0); + Require(frame->numChannels == 0); + Require(frame->speechType == 4); + Require(frame->vadActivity == 2); + Require(frame->profileTimestampMs == 0); + Require(frame->packetInfos == nullptr); + Require(frame->muted); + Require(frame->channelLayout == 0); + Require(!frame->absoluteCaptureTimestampMsEngaged); + Require(AllSamples(frame->data, 0x5A5A)); + Require(_ZNK6webrtc10AudioFrame5mutedEv_nid_postfix(frame.get())); +} + +void TestMutedData() { + auto frame = Filled(); + _ZN6webrtc10AudioFrameC1Ev_nid_postfix(frame.get()); + const std::int16_t* zeroed = _ZNK6webrtc10AudioFrame4dataEv_nid_postfix(frame.get()); + Require(zeroed != frame->data); + Require(AllSamples(zeroed, 0)); + Require(AllSamples(frame->data, 0x5A5A)); + + std::int16_t* samples = _ZN6webrtc10AudioFrame12mutable_dataEv_nid_postfix(frame.get()); + Require(samples == frame->data); + Require(!frame->muted); + Require(AllSamples(frame->data, 0)); + Require(_ZNK6webrtc10AudioFrame4dataEv_nid_postfix(frame.get()) == frame->data); + + samples[0] = 1234; + samples[WebRtcAudioFrameMaxDataSizeSamples - 1] = -5; + Require(_ZN6webrtc10AudioFrame12mutable_dataEv_nid_postfix(frame.get()) == frame->data); + Require(frame->data[0] == 1234); + Require(frame->data[WebRtcAudioFrameMaxDataSizeSamples - 1] == -5); +} + +void TestMute() { + auto frame = Filled(); + _ZN6webrtc10AudioFrameC1Ev_nid_postfix(frame.get()); + _ZN6webrtc10AudioFrame12mutable_dataEv_nid_postfix(frame.get())[3] = 77; + Require(!_ZNK6webrtc10AudioFrame5mutedEv_nid_postfix(frame.get())); + _ZN6webrtc10AudioFrame4MuteEv_nid_postfix(frame.get()); + Require(_ZNK6webrtc10AudioFrame5mutedEv_nid_postfix(frame.get())); + Require(frame->data[3] == 77); + Require(AllSamples(_ZNK6webrtc10AudioFrame4dataEv_nid_postfix(frame.get()), 0)); + _ZN6webrtc10AudioFrame12mutable_dataEv_nid_postfix(frame.get()); + Require(frame->data[3] == 0); +} + +void TestOperationsMute() { + auto frame = Filled(); + _ZN6webrtc10AudioFrameC1Ev_nid_postfix(frame.get()); + frame->samplesPerChannel = 480; + frame->numChannels = 2; + _ZN6webrtc10AudioFrame12mutable_dataEv_nid_postfix(frame.get())[0] = 9; + _ZN6webrtc20AudioFrameOperations4MuteEPNS_10AudioFrameE_nid_postfix(frame.get()); + Require(frame->muted); + Require(frame->data[0] == 9); + Require(frame->samplesPerChannel == 480); + Require(frame->numChannels == 2); + + frame->muted = false; + frame->samplesPerChannel = 960; + frame->numChannels = 8; + _ZN6webrtc20AudioFrameOperations4MuteEPNS_10AudioFrameE_nid_postfix(frame.get()); + Require(frame->muted); + + frame->muted = false; + frame->samplesPerChannel = 7681; + frame->numChannels = 0; + _ZN6webrtc20AudioFrameOperations4MuteEPNS_10AudioFrameE_nid_postfix(frame.get()); + Require(frame->muted); + + frame->muted = false; + frame->samplesPerChannel = 961; + frame->numChannels = 8; + RequireThrows([&] { _ZN6webrtc20AudioFrameOperations4MuteEPNS_10AudioFrameE_nid_postfix(frame.get()); }); + Require(!frame->muted); +} + +void TestNullFrame() { + RequireThrows([] { _ZN6webrtc10AudioFrameC1Ev_nid_postfix(nullptr); }); + RequireThrows([] { _ZN6webrtc10AudioFrame4MuteEv_nid_postfix(nullptr); }); + RequireThrows([] { _ZNK6webrtc10AudioFrame5mutedEv_nid_postfix(nullptr); }); + RequireThrows([] { _ZNK6webrtc10AudioFrame4dataEv_nid_postfix(nullptr); }); + RequireThrows([] { _ZN6webrtc10AudioFrame12mutable_dataEv_nid_postfix(nullptr); }); + RequireThrows([] { _ZN6webrtc20AudioFrameOperations4MuteEPNS_10AudioFrameE_nid_postfix(nullptr); }); +} + +} + +int main() { + TestConstructor(); + TestMutedData(); + TestMute(); + TestOperationsMute(); + TestNullFrame(); + return 0; +} diff --git a/core/libs/prx/libwebrtc/tests/CpuFeatures.cpp b/core/libs/prx/libwebrtc/tests/CpuFeatures.cpp new file mode 100644 index 0000000000..b488e6ca66 --- /dev/null +++ b/core/libs/prx/libwebrtc/tests/CpuFeatures.cpp @@ -0,0 +1,28 @@ +#include "prx/libwebrtc/libwebrtc.hpp" + +#include +#include +#include + +namespace { + +void Require(bool condition, std::source_location location = std::source_location::current()) { + if (condition) return; + std::fprintf(stderr, "libwebrtc GetAvailableCpuFeatures check failed at line %u\n", location.line()); + std::abort(); +} + +} + +int main() { + __builtin_cpu_init(); + WebRtcAvailableCpuFeatures features = _ZN6webrtc23GetAvailableCpuFeaturesEv_nid_postfix(); + Require(features.sse2); + Require(features.sse2 == (__builtin_cpu_supports("sse2") != 0)); + Require(features.avx2 == (__builtin_cpu_supports("avx2") != 0 && __builtin_cpu_supports("bmi2") != 0)); + Require(!features.neon); + WebRtcAvailableCpuFeatures again = _ZN6webrtc23GetAvailableCpuFeaturesEv_nid_postfix(); + Require(again.sse2 == features.sse2 && again.avx2 == features.avx2 && again.neon == features.neon); + std::printf("sse2=%d avx2=%d neon=%d\n", features.sse2, features.avx2, features.neon); + return 0; +} diff --git a/core/libs/prx/libwebrtc/tests/IpAddress.cpp b/core/libs/prx/libwebrtc/tests/IpAddress.cpp new file mode 100644 index 0000000000..237768f008 --- /dev/null +++ b/core/libs/prx/libwebrtc/tests/IpAddress.cpp @@ -0,0 +1,137 @@ +#include "prx/libwebrtc/libwebrtc.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void Require(bool condition, std::source_location location = std::source_location::current()) { + if (condition) return; + std::fprintf(stderr, "libwebrtc rtc::IPAddress check failed at line %u\n", location.line()); + std::abort(); +} + +template void RequireInvalidArgument(TAction action, std::source_location location = std::source_location::current()) { + try { + action(); + } catch (const std::invalid_argument&) { + return; + } + Require(false, location); +} + +WebRtcIpAddress Address(std::int32_t family, std::initializer_list bytes) { + WebRtcIpAddress address; + std::memset(&address, 0x5A, sizeof(address)); + address.vtable = &address; + address.family = family; + std::memset(&address.address, 0, sizeof(address.address)); + std::size_t index = 0; + for (std::uint8_t byte : bytes) + address.address.ip6.bytes[index++] = byte; + return address; +} + +WebRtcIpAddress V4(std::uint8_t a, std::uint8_t b, std::uint8_t c, std::uint8_t d) { + return Address(WebRtcAfInet, {a, b, c, d}); +} + +WebRtcIpAddress V6Prefix(int ones) { + WebRtcIpAddress address = Address(WebRtcAfInet6, {}); + for (int bit = 0; bit < ones; ++bit) + address.address.ip6.bytes[bit / 8] |= static_cast(0x80u >> (bit % 8)); + return address; +} + +bool Equal(const WebRtcIpAddress& left, const WebRtcIpAddress& right) { + return _ZNK3rtc9IPAddresseqERKS0__nid_postfix(&left, &right); +} + +int MaskBits(const WebRtcIpAddress& mask) { + return _ZN3rtc15CountIPMaskBitsERKNS_9IPAddressE_nid_postfix(&mask); +} + +void TestAccessors() { + WebRtcIpAddress v4 = V4(192, 168, 1, 10); + WebRtcInAddr ip4 = _ZNK3rtc9IPAddress12ipv4_addressEv_nid_postfix(&v4); + Require(std::memcmp(&ip4, v4.address.ip6.bytes, 4) == 0); + Require(_ZNK3rtc9IPAddress27v4AddressAsHostOrderIntegerEv_nid_postfix(&v4) == 0xC0A8010Au); + + WebRtcIpAddress v6 = Address(WebRtcAfInet6, {0x20, 0x01, 0x0D, 0xB8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x01}); + WebRtcIn6Addr ip6 = _ZNK3rtc9IPAddress12ipv6_addressEv_nid_postfix(&v6); + Require(std::memcmp(ip6.bytes, v6.address.ip6.bytes, 16) == 0); + WebRtcInAddr v6AsV4 = _ZNK3rtc9IPAddress12ipv4_addressEv_nid_postfix(&v6); + Require(std::memcmp(&v6AsV4, v6.address.ip6.bytes, 4) == 0); + WebRtcIn6Addr v4AsV6 = _ZNK3rtc9IPAddress12ipv6_addressEv_nid_postfix(&v4); + Require(std::memcmp(v4AsV6.bytes, v4.address.ip6.bytes, 16) == 0); + Require(_ZNK3rtc9IPAddress27v4AddressAsHostOrderIntegerEv_nid_postfix(&v6) == 0); + + WebRtcIpAddress unspec = Address(WebRtcAfUnspec, {1, 2, 3, 4}); + Require(_ZNK3rtc9IPAddress27v4AddressAsHostOrderIntegerEv_nid_postfix(&unspec) == 0); +} + +void TestEquality() { + Require(Equal(V4(10, 0, 0, 1), V4(10, 0, 0, 1))); + Require(!Equal(V4(10, 0, 0, 1), V4(10, 0, 0, 2))); + + WebRtcIpAddress v4 = V4(10, 0, 0, 1); + WebRtcIpAddress v4Tail = V4(10, 0, 0, 1); + v4Tail.address.ip6.bytes[15] = 0xFF; + Require(Equal(v4, v4Tail)); + + WebRtcIpAddress v6 = Address(WebRtcAfInet6, {10, 0, 0, 1}); + Require(!Equal(v4, v6)); + Require(Equal(v6, Address(WebRtcAfInet6, {10, 0, 0, 1}))); + WebRtcIpAddress v6Tail = v6; + v6Tail.address.ip6.bytes[15] = 1; + Require(!Equal(v6, v6Tail)); + + Require(Equal(Address(WebRtcAfUnspec, {1}), Address(WebRtcAfUnspec, {2}))); + WebRtcIpAddress other = Address(1, {}); + Require(!Equal(other, other)); +} + +void TestMaskBits() { + Require(MaskBits(V4(255, 255, 255, 0)) == 24); + Require(MaskBits(V4(255, 255, 255, 255)) == 32); + Require(MaskBits(V4(128, 0, 0, 0)) == 1); + Require(MaskBits(V4(0, 0, 0, 0)) == 0); + Require(MaskBits(V4(255, 0, 255, 0)) == 24); + Require(MaskBits(V4(0, 0, 0, 1)) == 32); + + for (int ones : {0, 1, 31, 32, 33, 60, 64, 96, 127, 128}) + Require(MaskBits(V6Prefix(ones)) == ones); + WebRtcIpAddress gap = V6Prefix(64); + gap.address.ip6.bytes[9] = 0x01; + Require(MaskBits(gap) == 80); + WebRtcIpAddress lateBit = Address(WebRtcAfInet6, {0, 0, 0, 0, 0, 0, 0, 0x01}); + Require(MaskBits(lateBit) == 0); + + Require(MaskBits(Address(WebRtcAfUnspec, {255, 255, 255, 0})) == 0); + Require(MaskBits(Address(1, {255, 255, 255, 0})) == 0); +} + +void TestNullAddress() { + WebRtcIpAddress v4 = V4(10, 0, 0, 1); + RequireInvalidArgument([] { _ZNK3rtc9IPAddress12ipv4_addressEv_nid_postfix(nullptr); }); + RequireInvalidArgument([] { _ZNK3rtc9IPAddress12ipv6_addressEv_nid_postfix(nullptr); }); + RequireInvalidArgument([] { _ZNK3rtc9IPAddress27v4AddressAsHostOrderIntegerEv_nid_postfix(nullptr); }); + RequireInvalidArgument([&] { _ZNK3rtc9IPAddresseqERKS0__nid_postfix(nullptr, &v4); }); + RequireInvalidArgument([&] { _ZNK3rtc9IPAddresseqERKS0__nid_postfix(&v4, nullptr); }); + RequireInvalidArgument([] { _ZN3rtc15CountIPMaskBitsERKNS_9IPAddressE_nid_postfix(nullptr); }); +} + +} + +int main() { + TestAccessors(); + TestEquality(); + TestMaskBits(); + TestNullAddress(); + return 0; +} diff --git a/core/libs/prx/libwebrtc/tests/TaskQueueBase.cpp b/core/libs/prx/libwebrtc/tests/TaskQueueBase.cpp new file mode 100644 index 0000000000..dbad7cad58 --- /dev/null +++ b/core/libs/prx/libwebrtc/tests/TaskQueueBase.cpp @@ -0,0 +1,126 @@ +#include "prx/libwebrtc/libwebrtc.hpp" + +#include +#include +#include +#include +#include + +namespace { + +void Require(bool condition, std::source_location location = std::source_location::current()) { + if (condition) return; + std::fprintf(stderr, "libwebrtc TaskQueueBase check failed at line %u\n", location.line()); + std::abort(); +} + +template void RequireInvalidArgument(TAction action, std::source_location location = std::source_location::current()) { + try { + action(); + } catch (const std::invalid_argument&) { + return; + } + Require(false, location); +} + +WebRtcTaskQueueBase* Queue(int index) { + static char storage[3]; + return reinterpret_cast(&storage[index]); +} + +WebRtcTaskQueueBase* Current() { + return _ZN6webrtc13TaskQueueBase7CurrentEv_nid_postfix(); +} + +void Set(WebRtcCurrentTaskQueueSetter& setter, WebRtcTaskQueueBase* queue) { + _ZN6webrtc13TaskQueueBase22CurrentTaskQueueSetterC1EPS0__nid_postfix(&setter, queue); +} + +void Restore(WebRtcCurrentTaskQueueSetter& setter) { + _ZN6webrtc13TaskQueueBase22CurrentTaskQueueSetterD1Ev_nid_postfix(&setter); +} + +void TestNoQueue() { + Require(Current() == nullptr); +} + +void TestSetAndRestore() { + WebRtcCurrentTaskQueueSetter setter {Queue(2)}; + Set(setter, Queue(0)); + Require(setter.previous == nullptr); + Require(Current() == Queue(0)); + Restore(setter); + Require(Current() == nullptr); +} + +void TestNested() { + WebRtcCurrentTaskQueueSetter outer {}; + WebRtcCurrentTaskQueueSetter inner {}; + WebRtcCurrentTaskQueueSetter cleared {}; + Set(outer, Queue(0)); + Set(inner, Queue(1)); + Require(inner.previous == Queue(0)); + Require(Current() == Queue(1)); + Set(cleared, nullptr); + Require(cleared.previous == Queue(1)); + Require(Current() == nullptr); + Restore(cleared); + Require(Current() == Queue(1)); + Restore(inner); + Require(Current() == Queue(0)); + Restore(outer); + Require(Current() == nullptr); +} + +void TestRestoreUsesTheSetter() { + WebRtcCurrentTaskQueueSetter setter {}; + Set(setter, Queue(0)); + setter.previous = Queue(2); + Restore(setter); + Require(Current() == Queue(2)); + WebRtcCurrentTaskQueueSetter reset {nullptr}; + Restore(reset); + Require(Current() == nullptr); +} + +void TestPerThread() { + WebRtcCurrentTaskQueueSetter setter {}; + Set(setter, Queue(0)); + WebRtcTaskQueueBase* seenBefore = Queue(2); + WebRtcTaskQueueBase* seenWhileSet = nullptr; + WebRtcTaskQueueBase* seenAfter = Queue(2); + std::thread worker([&] { + seenBefore = Current(); + WebRtcCurrentTaskQueueSetter workerSetter {}; + Set(workerSetter, Queue(1)); + seenWhileSet = Current(); + Restore(workerSetter); + seenAfter = Current(); + }); + worker.join(); + Require(seenBefore == nullptr); + Require(seenWhileSet == Queue(1)); + Require(seenAfter == nullptr); + Require(Current() == Queue(0)); + Restore(setter); + Require(Current() == nullptr); +} + +void TestNullSetter() { + RequireInvalidArgument([] { _ZN6webrtc13TaskQueueBase22CurrentTaskQueueSetterC1EPS0__nid_postfix(nullptr, Queue(0)); }); + Require(Current() == nullptr); + RequireInvalidArgument([] { _ZN6webrtc13TaskQueueBase22CurrentTaskQueueSetterD1Ev_nid_postfix(nullptr); }); + Require(Current() == nullptr); +} + +} + +int main() { + TestNoQueue(); + TestSetAndRestore(); + TestNested(); + TestRestoreUsesTheSetter(); + TestPerThread(); + TestNullSetter(); + return 0; +} diff --git a/core/libs/tests/EdgeWatch.cpp b/core/libs/tests/EdgeWatch.cpp new file mode 100644 index 0000000000..d22f5f8a72 --- /dev/null +++ b/core/libs/tests/EdgeWatch.cpp @@ -0,0 +1,174 @@ +#include "prx/libkernel/Equeue/include/EdgeWatch.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" [[noreturn]] void NotImplemented_nid_no_patch(const char* name) { + throw std::runtime_error(name); +} + +namespace { + +void Require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +struct Pair { + explicit Pair(bool socket) { + Require((socket ? ::socketpair(AF_UNIX, SOCK_STREAM, 0, descriptors.data()) : ::pipe(descriptors.data())) == 0, "cannot create descriptor pair"); + } + ~Pair() { + for (int descriptor : descriptors) if (descriptor >= 0) ::close(descriptor); + } + std::array descriptors{-1, -1}; +}; + +bool Wait(int descriptor, int milliseconds) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(milliseconds); + for (;;) { + const auto remaining = std::chrono::duration_cast(deadline - std::chrono::steady_clock::now()).count(); + if (remaining < 0) return false; + pollfd entry{descriptor, POLLIN, 0}; + const int result = ::poll(&entry, 1, static_cast(remaining + 1)); + if (result < 0 && errno == EINTR) continue; + Require(result >= 0, "poll failed"); + Require((entry.revents & (POLLERR | POLLNVAL | POLLHUP)) == 0, "watch descriptor failed"); + return (entry.revents & POLLIN) != 0; + } +} + +void Edge(const std::shared_ptr& watch) { + Require(Wait(watch->Descriptor(), 5000), "edge did not wake the watch descriptor"); + Require(watch->TakeEdge(), "readable watch did not yield an edge"); +} + +void Quiet(const std::shared_ptr& watch) { + Require(!watch->TakeEdge(), "edge repeated without new activity"); + Require(!Wait(watch->Descriptor(), 50), "idle watch descriptor remained readable"); +} + +void Send(int descriptor, std::size_t count) { + std::array bytes{}; + Require(::write(descriptor, bytes.data(), count) == static_cast(count), "write failed"); +} + +void Read(int descriptor, std::size_t count) { + std::array bytes{}; + Require(::read(descriptor, bytes.data(), count) == static_cast(count), "read failed"); +} + +void ReadEdges(bool socket) { + Pair pair(socket); + auto watch = EdgeWatch::Watch::Open(pair.descriptors[0], false); + Quiet(watch); + Send(pair.descriptors[1], 2); + Edge(watch); + Quiet(watch); + Send(pair.descriptors[1], 1); + Edge(watch); + Quiet(watch); + Read(pair.descriptors[0], 3); + Quiet(watch); + std::promise entered; + auto started = entered.get_future(); + auto waiter = std::async(std::launch::async, [&] { + entered.set_value(); + return Wait(watch->Descriptor(), 5000); + }); + started.wait(); + const bool blocked = waiter.wait_for(std::chrono::milliseconds(100)) == std::future_status::timeout; + Send(pair.descriptors[1], 1); + const bool woke = waiter.get(); + Require(blocked, "blocking wait returned before new activity"); + Require(woke, "blocking wait did not wake after new activity"); + Require(watch->TakeEdge(), "blocking wait lost the edge"); + Require(::close(pair.descriptors[1]) == 0, "close writer failed"); + pair.descriptors[1] = -1; + Edge(watch); + Quiet(watch); + Read(pair.descriptors[0], 1); + watch->TakeEdge(); + char byte; + Require(::read(pair.descriptors[0], &byte, 1) == 0, "read did not report EOF"); + watch->TakeEdge(); + Quiet(watch); +} + +void WriteEdges() { + Pair pair(true); + Require(::fcntl(pair.descriptors[1], F_SETFL, O_NONBLOCK) == 0, "cannot make socket nonblocking"); + auto watch = EdgeWatch::Watch::Open(pair.descriptors[1], true); + Edge(watch); + Quiet(watch); + std::array bytes{}; + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); + ssize_t result; + do { + result = ::write(pair.descriptors[1], bytes.data(), bytes.size()); + Require(std::chrono::steady_clock::now() < deadline, "socket did not fill"); + } while (result >= 0 || errno == EINTR); + Require(errno == EAGAIN || errno == EWOULDBLOCK, "socket fill failed"); + while (watch->TakeEdge()) Require(std::chrono::steady_clock::now() < deadline, "write edge did not clear"); + Quiet(watch); + Require(::fcntl(pair.descriptors[0], F_SETFL, O_NONBLOCK) == 0, "cannot make reader nonblocking"); + do { + result = ::read(pair.descriptors[0], bytes.data(), bytes.size()); + Require(std::chrono::steady_clock::now() < deadline, "socket did not drain"); + } while (result > 0 || (result < 0 && errno == EINTR)); + Require(result < 0 && (errno == EAGAIN || errno == EWOULDBLOCK), "socket drain failed"); + Edge(watch); + Quiet(watch); +} + +void Lifecycle() { + Pair pair(false); + int descriptor; + { + auto watch = EdgeWatch::Watch::Open(pair.descriptors[0], false); + descriptor = watch->Descriptor(); + const int flags = ::fcntl(descriptor, F_GETFD); + Require(flags >= 0 && (flags & FD_CLOEXEC) != 0, "watch lacks close-on-exec"); + } + Require(::fcntl(descriptor, F_GETFD) == -1 && errno == EBADF, "watch descriptor leaked"); + Send(pair.descriptors[1], 1); + Read(pair.descriptors[0], 1); +} + +void Errors() { + bool threw = false; + try { + EdgeWatch::Watch::Open(-1, false); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw, "invalid descriptor was accepted"); +} + +} + +int main(int argc, char** argv) { + try { + Require(argc == 2, "expected test mode"); + const std::string_view mode(argv[1]); + if (mode == "socket") ReadEdges(true); + else if (mode == "pipe") ReadEdges(false); + else if (mode == "write") WriteEdges(); + else if (mode == "lifecycle") Lifecycle(); + else if (mode == "errors") Errors(); + else throw std::runtime_error("unknown test mode"); + std::printf("PASS: %s\n", argv[1]); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "FAIL: %s\n", error.what()); + return 1; + } +} diff --git a/core/libs/tests/ExportLookup.hpp b/core/libs/tests/ExportLookup.hpp new file mode 100644 index 0000000000..235b066e32 --- /dev/null +++ b/core/libs/tests/ExportLookup.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include +#include +#ifdef _WIN32 +#include +#else +#include +#endif + +inline bool LibraryExportsName(void* library, const char* name) { +#ifdef _WIN32 + return GetProcAddress(static_cast(library), name) != nullptr; +#else + return dlsym(library, name) != nullptr; +#endif +} + +template +bool ExportsResolveByName(const char* path, const Export (&exports)[Count], const char* absentName) { + if (path == nullptr) { + std::fprintf(stderr, "no library path was given\n"); + return false; + } +#ifdef _WIN32 + void* library = LoadLibraryA(path); +#else + void* library = dlopen(path, RTLD_NOW | RTLD_LOCAL); +#endif + if (library == nullptr) { + std::fprintf(stderr, "%s did not load\n", path); + return false; + } + for (const auto& entry : exports) { + auto* volatile function = entry.function; + if (function == nullptr || !LibraryExportsName(library, entry.name)) { + std::fprintf(stderr, "%s is not exported\n", entry.name); + return false; + } + } + if (LibraryExportsName(library, absentName)) { + std::fprintf(stderr, "%s resolved although nothing exports it\n", absentName); + return false; + } + return true; +} diff --git a/core/libs/tests/FilePosition.cpp b/core/libs/tests/FilePosition.cpp index 50fcb3006c..57268ea049 100644 --- a/core/libs/tests/FilePosition.cpp +++ b/core/libs/tests/FilePosition.cpp @@ -49,6 +49,23 @@ int main() { Require(fgetpos_nid_postfix(&file, &position) == 0 && position == 1); file.Close(); + FileStream reread(std::tmpfile()); + Require(std::fputs("position", reread.GetHandle()) >= 0); + Require(fsetpos_nid_postfix(&reread, &beginning) == 0); + for (const char expected : {'p', 'o', 's'}) Require(fgetc_nid_postfix(&reread) == expected); + Require(fgetpos_nid_postfix(&reread, &position) == 0 && position == 3); + const std::int64_t third = position; + while (fgetc_nid_postfix(&reread) != EOF) {} + Require(feof_nid_postfix(&reread) != 0); + Require(fsetpos_nid_postfix(&reread, &third) == 0); + Require(feof_nid_postfix(&reread) == 0); + Require(fgetc_nid_postfix(&reread) == 'i'); + const std::int64_t negative = -1; + errno = 0; + Require(fsetpos_nid_postfix(&reread, &negative) != 0); + Require(errno == EINVAL); + reread.Close(); + int descriptors[2]; #ifdef _WIN32 Require(_pipe(descriptors, 4096, _O_BINARY) == 0); diff --git a/core/libs/tests/FileStreams.cpp b/core/libs/tests/FileStreams.cpp index 8a0603b2e0..451e092682 100644 --- a/core/libs/tests/FileStreams.cpp +++ b/core/libs/tests/FileStreams.cpp @@ -400,6 +400,35 @@ static void CheckWriteAfterRead(const std::string& path, const char* mode, const Require(ReadWhole(path) == expected, "Write after read wrote other bytes"); } +static void CheckReadsOnWriteOnlyStreams(const std::string& path) { + for (const char* mode : {"wb", "ab"}) { + auto* stream = fopen_nid_postfix(path.c_str(), mode); + Require(stream != nullptr && fwrite_nid_postfix("abc", 1, 3, stream) == 3, "fwrite on a write-only stream"); + const auto failed = [stream](bool result, const char* message) { + Require(result && errno == EBADF && ferror_nid_postfix(stream) != 0, message); + clearerr_nid_postfix(stream); + errno = 0; + }; + std::array buffer{}; + errno = 0; + failed(fread_nid_postfix(buffer.data(), 1, buffer.size(), stream) == 0, "fread on a write-only stream"); + failed(fgetc_nid_postfix(stream) == EOF, "fgetc on a write-only stream"); + failed(getc_unlocked_nid_postfix(stream) == EOF, "getc_unlocked on a write-only stream"); + failed(fgets_nid_postfix(buffer.data(), static_cast(buffer.size()), stream) == nullptr, "fgets on a write-only stream"); + int value = 0; + Require(fscanf_nid_postfix(stream, "%d", &value) == EOF, "fscanf on a write-only stream"); +#ifdef _WIN32 + Require(errno == EBADF && ferror_nid_postfix(stream) != 0, "fscanf on a write-only stream left errno or the error indicator unset"); + clearerr_nid_postfix(stream); + errno = 0; + Require(ungetc_nid_postfix('q', stream) == EOF && errno == 0 && ferror_nid_postfix(stream) == 0, "ungetc on a write-only stream"); +#endif + Require(fclose_nid_postfix(stream) == 0, "fclose after reads on a write-only stream"); + Require(ReadWhole(path) == "abc", "Reads on a write-only stream changed the file"); + Require(std::remove(path.c_str()) == 0); + } +} + static void CheckStreamWritesAfterReads(const std::string& base) { CheckCharacterScanLengths(); const std::string path = base + ".written"; @@ -455,6 +484,7 @@ static void CheckStreamWritesAfterReads(const std::string& base) { errno = 0; Require(fprintf_nid_postfix(stream, "%s", "XY") == -1 && errno == EBADF && ferror_nid_postfix(stream) != 0, "fprintf after fgetc on a read-only stream"); }); + CheckReadsOnWriteOnlyStreams(base + ".write-only"); Require(std::remove(path.c_str()) == 0); } diff --git a/core/libs/tests/Formatting.cpp b/core/libs/tests/Formatting.cpp index 87e27c19cd..6687df8d06 100644 --- a/core/libs/tests/Formatting.cpp +++ b/core/libs/tests/Formatting.cpp @@ -421,9 +421,47 @@ __attribute__((noinline)) static void APS5_VABI RunChecks() { Require(ScanList(" -0B11", "%i", &binary) == 1 && binary == 0); Require(ScanListS("0b1", "%i", &binary) == 1 && binary == 0); Require(sscanf_nid_postfix("0x1f", "%i", &binary) == 1 && binary == 31); + for (const char* format : {"%hls", "%lhs", "%llls", "%jls", "%lLs", "%5y", "%k"}) { + for (const char* input : {"ab", ""}) { + char bytes[8]; + std::fill(bytes, bytes + sizeof(bytes), '!'); + Require(ScanListS(input, format, bytes, 8u) == 0); + Require(std::all_of(bytes, bytes + sizeof(bytes), [](char value) { return value == '!'; })); + } + } #ifdef _WIN32 int scanned = -1; int position = -1; + { + const struct { + const char* input; + const char* format; + int result; + const char* stored; + } sets[] = { + {"za", "%[z-a]", 0, ""}, {"b", "%[a-a-c]", 0, ""}, {"a-c", "%[a-a-c]", 1, "a-c"}, {"-d", "%[a-c-e]", 1, "-"}, + {"+", "%[*--]", 1, "+"}, {"-", "%[a--]", 0, ""}, {"z", "%[-^]", 0, ""}, {"-^z", "%[-^]", 1, "-^"}, + {"b]", "%[]-c]", 1, "b]"}, {"-]a", "%[^]-]", 0, ""}, {"ab", "%[^]", 0, ""}, {"ab", "%lls", 1, "ab"}, + {"ab", "%hhs", 1, "ab"}, {"ab", "%ll[a-z]", 1, "ab"}, {"ab", "%y", 0, ""}, {"", "%y", 0, ""}, + {"ab", "%5y", 0, ""}, {"ab", "%k", 0, ""}, {"ab", "%hls", 0, ""}, {"ab", "%llls", 0, ""}, + {"ab", "%l", 0, ""}, {"", "%l", 0, ""}, {"ab", "%ll", 0, ""}, {"ab", "%h", 0, ""}, {"ab", "%hl", 0, ""}, + }; + for (const auto& item : sets) { + char bytes[8] = {'!', '!', '!', '!', '!', '!', '!', '!'}; + Require(sscanf_nid_postfix(item.input, item.format, bytes) == item.result); + Require(item.result == 0 ? bytes[0] == '!' : std::strcmp(bytes, item.stored) == 0); + std::memset(bytes, '!', sizeof(bytes)); + Require(ScanListS(item.input, item.format, bytes, 8u) == item.result); + Require(item.result == 0 ? bytes[0] == '!' : std::strcmp(bytes, item.stored) == 0); + } + char pair[4] = {'!', '!', '!', '!'}; + Require(sscanf_nid_postfix("abc", "%2llc", pair) == 1 && pair[0] == 'a' && pair[1] == 'b' && pair[2] == '!'); + Require(sscanf_nid_postfix("ab", "%s%y", pair) == 1 && std::strcmp(pair, "ab") == 0); + char16_t wide[3] = {u'!', u'!', u'!'}; + Require(sscanf_nid_postfix("ab", "%ls", wide) == 1 && wide[0] == u'a' && wide[1] == u'b' && wide[2] == 0); + std::fill(wide, wide + 3, u'!'); + Require(ScanListS("ab", "%ls", wide, 3u) == 1 && wide[0] == u'a' && wide[1] == u'b' && wide[2] == 0); + } Require(sscanf_nid_postfix("12", "%*d%d", &scanned) == 0 && scanned == -1); Require(sscanf_nid_postfix("12 ", "%*d %d", &scanned) == 0); Require(sscanf_nid_postfix("abc", "%*s%d", &scanned) == 0); @@ -436,25 +474,35 @@ __attribute__((noinline)) static void APS5_VABI RunChecks() { Require(sscanf_nid_postfix("", "%*d", &scanned) == EOF); Require(sscanf_nid_postfix(" ", "%*d%d", &scanned) == EOF); Require(sscanf_nid_postfix("", "%d", &scanned) == EOF); -#else +#endif { char16_t first[8] = {}; char16_t letter = u'!'; Require(sscanf_nid_postfix("ab cd", "%ls %lc", first, &letter) == 2); Require(std::u16string(first) == u"ab" && letter == u'c'); - Require(sscanf_nid_postfix(" xy z", "%S %C", first, &letter) == 2 && std::u16string(first) == u"xy" && letter == u'z'); + letter = u'!'; + Require(sscanf_nid_postfix(" xy z", "%S %C", first, &letter) == 1 && std::u16string(first) == u"xy" && letter == u'!'); + Require(sscanf_nid_postfix("abc", "%C", &letter) == 0 && sscanf_nid_postfix("abc", "%2C", &letter) == 0 && letter == u'!'); + int afterWide = -7; + int countAfterWide = -7; + Require(sscanf_nid_postfix("x5", "%C%d", &letter, &afterWide) == 0 && letter == u'!' && afterWide == -7); + Require(sscanf_nid_postfix("xy", "%C%n", &letter, &countAfterWide) == 0 && countAfterWide == -7); + Require(sscanf_nid_postfix("5 x", "%d %C%d", &afterWide, &letter, &countAfterWide) == 1 && afterWide == 5 && letter == u'!' && countAfterWide == -7); + afterWide = -7; + Require(sscanf_nid_postfix("", "%C", &letter) == 0 && sscanf_nid_postfix("x5", "%*C%d", &afterWide) == 0 && afterWide == -7); + Require(sscanf_nid_postfix("xy", "%lC", &letter) == 0 && sscanf_nid_postfix("xy", "%hC", &letter) == 0 && letter == u'!'); + afterWide = -7; + Require(ScanListS("x5", "%C%d", &letter, 4u, &afterWide) == 0 && letter == u'!' && afterWide == -7); + Require(ScanListS("xy", "%C%n", &letter, 4u, &countAfterWide) == 0 && countAfterWide == -7); + Require(ScanListS("", "%C", &letter, 4u) == 0); + Require(sscanf_nid_postfix("abc", "%hS", first) == 1 && std::u16string(first) == u"abc"); char16_t three[5] = {u'!', u'!', u'!', u'!', u'!'}; Require(sscanf_nid_postfix("abcdef", "%3lc", three) == 1); Require(three[0] == u'a' && three[1] == u'b' && three[2] == u'c' && three[3] == u'!'); Require(sscanf_nid_postfix("abcz", "%l[a-c]", first) == 1 && std::u16string(first) == u"abc"); Require(sscanf_nid_postfix("ab cd", "%l[^ ]", first) == 1 && std::u16string(first) == u"ab"); Require(sscanf_nid_postfix("]x", "%l[]x]", first) == 1 && std::u16string(first) == u"]x"); - char16_t second[8] = {}; - Require(sscanf_nid_postfix("\xc5\xbelu\xc5\xa5 k\xc5\xaf\xc5\x88", "%ls %ls", first, second) == 2); - Require(std::u16string(first) == u"\u017elu\u0165" && std::u16string(second) == u"k\u016f\u0148"); int number = 0; - Require(sscanf_nid_postfix("\xc5\xbelu\xc5\xa5 7", "%2ls%*ls %d", first, &number) == 2); - Require(std::u16string(first) == u"\u017el" && number == 7); char narrowWord[8] = {}; Require(sscanf_nid_postfix("12 ab cd", "%d %ls %7s", &number, first, narrowWord) == 3); Require(number == 12 && std::u16string(first) == u"ab" && std::strcmp(narrowWord, "cd") == 0); @@ -473,14 +521,40 @@ __attribute__((noinline)) static void APS5_VABI RunChecks() { small[0] = u'!'; Require(ScanListS("xyz", "%3lc", small, 2u) == 0 && small[0] == 0); Require(ScanListS("ab cd", "%*ls %l[a-z]", fits, 6u) == 1 && std::u16string(fits) == u"cd"); - char16_t untouched[4] = {u'!', u'!', u'!', u'!'}; - const auto rejects = [&](const char* input, const char* format) { - try { sscanf_nid_postfix(input, format, untouched); } catch (const std::runtime_error&) { return untouched[0] == u'!'; } - return false; + struct Case { + const char* input; + const char* format; + int result; + const char16_t* stored; + int error; }; - Require(rejects("\xff", "%ls") && rejects("a b", "%2$ls") && rejects("\xc5\xbe", "%l[\xc5\xbe]")); - Require(rejects("\xf0\x9f\x98\x80", "%ls")); + const Case cases[] = { + {"\xc3\xa9", "%ls", 0, u"", 86}, {"a\xc3\xa9", "%ls", 0, u"a", 86}, {"\x80", "%ls", EOF, u"", 86}, + {" \x80", "%ls", EOF, u"", 86}, {"\x7f\x80", "%ls", 0, u"\x7f", 86}, {"\xf0\x9f\x98\x80", "%ls", EOF, u"", 86}, + {"\xc3", "%ls", EOF, u"", 0}, {"\xc3 x", "%ls", EOF, u"", 0}, {"a\xc3" "b", "%2ls", EOF, u"a", 0}, + {"\xc3\xa9", "%lc", EOF, u"", 0}, {"\xc3" "a", "%2lc", 0, u"", 86}, {"\x80", "%lc", EOF, u"", 86}, + {"\xc3\xa9", "%S", 0, u"", 86}, {"a\xc3\xa9", "%l[^ ]", 0, u"a", 86}, {"\x80", "%l[^ ]", EOF, u"", 86}, + {"\xc3" "a", "%l[a-z]", 0, u"", 0}, {"a\xc3" "b", "%l[a-z]", 1, u"a", 0}, {"\xc3\xa9", "%l[\xc3\xa9]", 0, u"", 86}, + {"za", "%l[z-a]", 0, u"", 0}, {"b", "%l[a-a-c]", 0, u"", 0}, {"a-c", "%l[a-a-c]", 1, u"a-c", 0}, {"+", "%l[*--]", 1, u"+", 0}, + {"\x80", "%l[\x80-\xff]", EOF, u"", 86}, {"a\x80" "b", "%l[a\x80]", 0, u"a", 86}, {"a\xc3" "b", "%l[^\xc3]", 1, u"a", 0}, + }; + for (const auto& item : cases) { + char16_t units[8] = {u'!', u'!', u'!', u'!', u'!', u'!', u'!', u'!'}; + *__error_nid_postfix() = 0; + Require(sscanf_nid_postfix(item.input, item.format, units) == item.result && *__error_nid_postfix() == item.error); + const std::u16string stored(item.stored); + Require(std::u16string(units, stored.size()) == stored && units[stored.size()] == (item.result == 1 ? u'\0' : u'!')); + } + Require(sscanf_nid_postfix("5 \xc3\xa9", "%d %ls", &number, first) == 1 && number == 5); + Require(sscanf_nid_postfix("x \x80", "%ls %ls", first, fits) == 1 && std::u16string(first) == u"x"); + Require(sscanf_nid_postfix("\xc3\xa9", "%*ls") == 0 && sscanf_nid_postfix("\x80", "%*ls") == EOF); + Require(sscanf_nid_postfix("\xc3\xa9", "%*lc") == EOF && sscanf_nid_postfix("a\xc3", "%*ls") == EOF); + char16_t untouched[4] = {u'!', u'!', u'!', u'!'}; + bool positional = false; + try { sscanf_nid_postfix("a b", "%2$ls", untouched); } catch (const std::runtime_error&) { positional = untouched[0] == u'!'; } + Require(positional); } +#ifndef _WIN32 char narrow[8] = {}; Require(sscanf_nid_postfix("ab%", "%7[^%ls]", narrow) == 1 && std::strcmp(narrow, "ab") == 0); #endif diff --git a/core/libs/tests/GuestAio.cpp b/core/libs/tests/GuestAio.cpp index 83fc668a6e..dff172fd1a 100644 --- a/core/libs/tests/GuestAio.cpp +++ b/core/libs/tests/GuestAio.cpp @@ -19,7 +19,10 @@ #include #ifdef _WIN32 #include +#include extern "C" _invalid_parameter_handler _set_thread_local_invalid_parameter_handler(_invalid_parameter_handler); +#else +#include #endif static void Require(bool value, int line) { @@ -35,6 +38,25 @@ static void DeleteAio(std::int32_t id) { Check(sceKernelAioDeleteRequest(id, &removed) == 0 && removed == 0); } +static void* MapReadOnlyPage() { +#ifdef _WIN32 + void* page = VirtualAlloc(nullptr, 4096, MEM_COMMIT | MEM_RESERVE, PAGE_READONLY); + Check(page != nullptr); +#else + void* page = mmap(nullptr, 4096, PROT_READ, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Check(page != MAP_FAILED); +#endif + return page; +} + +static void UnmapReadOnlyPage(void* page) { +#ifdef _WIN32 + Check(VirtualFree(page, 0, MEM_RELEASE) != 0); +#else + Check(munmap(page, 4096) == 0); +#endif +} + #ifdef _WIN32 static int g_parameterReports = 0; static void CountParameterReport(const wchar_t*, const wchar_t*, const wchar_t*, unsigned int, std::uintptr_t) { @@ -400,6 +422,55 @@ int main(int argc, char** argv) { Check(sceKernelAioDeleteRequests(badIds, 2, untouched) == 0); Check(untouched[0] == 0 && untouched[1] == SCE_KERNEL_ERROR_ESRCH); Check(sceKernelAioDeleteRequests(writeIds, 2, nullptr) == SCE_KERNEL_ERROR_EFAULT); + const int faultFd = sceKernelOpen(file.string().c_str(), SCE_KERNEL_O_RDONLY, 0); + Check(faultFd >= 0); + std::array faultBuffer{}; + KernelAioResult faultResult{-1, 0}; + KernelAioRwRequest faultRequest{0, faultBuffer.size(), faultBuffer.data(), &faultResult, faultFd}; + const auto finished = [&] { + std::int32_t requestId = 0; + Check(sceKernelAioSubmitReadCommands(&faultRequest, 1, 1, &requestId) == 0 && faultResult.state == 3); + return requestId; + }; + auto* unreadable = reinterpret_cast(0x10); + std::int32_t kept[2] = {7, 7}; + Check(sceKernelAioPollRequests(unreadable, 0, kept) == SCE_KERNEL_ERROR_EINVAL); + Check(sceKernelAioPollRequests(unreadable, 2, kept) == SCE_KERNEL_ERROR_EFAULT); + Check(sceKernelAioCancelRequests(unreadable, 2, kept) == SCE_KERNEL_ERROR_EFAULT); + Check(sceKernelAioDeleteRequests(unreadable, 2, kept) == SCE_KERNEL_ERROR_EFAULT); + Check(kept[0] == 7 && kept[1] == 7); + void* readOnly = MapReadOnlyPage(); + for (void* bad : {static_cast(unreadable), readOnly}) { + auto* badState = static_cast(bad); + const std::int32_t pollId = finished(); + Check(sceKernelAioPollRequest(pollId, badState) == SCE_KERNEL_ERROR_EFAULT); + Check(sceKernelAioPollRequest(pollId, &polled) == 0 && polled == 0x10003); + const std::int32_t cancelId = finished(); + Check(sceKernelAioCancelRequest(cancelId, badState) == SCE_KERNEL_ERROR_EFAULT); + Check(sceKernelAioPollRequest(cancelId, &polled) == 0 && polled == 3); + const std::int32_t deleteId = finished(); + Check(sceKernelAioDeleteRequest(deleteId, badState) == SCE_KERNEL_ERROR_EFAULT); + std::int32_t removed = 7; + Check(sceKernelAioDeleteRequest(deleteId, &removed) == 0 && removed == SCE_KERNEL_ERROR_ESRCH); + std::int32_t batchIds[2] = {finished(), finished()}; + std::int32_t batchStates[2] = {7, 7}; + Check(sceKernelAioPollRequests(batchIds, 2, badState) == SCE_KERNEL_ERROR_EFAULT); + Check(sceKernelAioCancelRequests(batchIds, 2, badState) == SCE_KERNEL_ERROR_EFAULT); + Check(sceKernelAioPollRequests(batchIds, 2, batchStates) == 0 && batchStates[0] == 0x10003 && batchStates[1] == 0x10003); + Check(sceKernelAioDeleteRequests(batchIds, 2, badState) == SCE_KERNEL_ERROR_EFAULT); + Check(sceKernelAioDeleteRequests(batchIds, 2, batchStates) == 0); + Check(batchStates[0] == SCE_KERNEL_ERROR_ESRCH && batchStates[1] == SCE_KERNEL_ERROR_ESRCH); + DeleteAio(pollId); + DeleteAio(cancelId); + } + Check(*static_cast(readOnly) == 0); + UnmapReadOnlyPage(readOnly); + const std::int32_t twiceId = finished(); + std::int32_t twice[2] = {twiceId, twiceId}; + std::int32_t twiceStates[2] = {7, 7}; + Check(sceKernelAioPollRequests(twice, 2, twiceStates) == 0 && twiceStates[0] == 3 && twiceStates[1] == 0x10003); + Check(sceKernelAioDeleteRequests(twice, 2, twiceStates) == 0 && twiceStates[0] == 0 && twiceStates[1] == SCE_KERNEL_ERROR_ESRCH); + Check(sceKernelClose(faultFd) == 0); const auto shared = root / "shared.bin"; std::string sharedContents; for (char letter : {'A', 'B', 'C', 'D'}) sharedContents.append(1024, letter); diff --git a/core/libs/tests/GuestAioRegistry.cpp b/core/libs/tests/GuestAioRegistry.cpp index 1b165ec7ca..e6e6324361 100644 --- a/core/libs/tests/GuestAioRegistry.cpp +++ b/core/libs/tests/GuestAioRegistry.cpp @@ -160,8 +160,10 @@ static std::condition_variable g_aioGateChanged; static bool g_aioGateEntered = false; static bool g_aioGateReleased = false; static bool g_aioGateFails = false; +static std::uintptr_t g_aioGateSource = 0; -static void HoldAioSource(std::uintptr_t, std::size_t) { +static void HoldAioSource(std::uintptr_t address, std::size_t) { + if (address != g_aioGateSource) return; std::unique_lock lock(g_aioGateMutex); g_aioGateEntered = true; g_aioGateChanged.notify_all(); @@ -180,6 +182,7 @@ static void CheckAioProcessingOwnership(bool fail) { KernelAioRwRequest request{0, 1, &byte, &result, -1}; std::int32_t submittedId = -7; bool submitFailed = false; + g_aioGateSource = reinterpret_cast(&byte); GuestArena::GuestArenaSetHostReadHook_nid_no_patch(HoldAioSource); std::thread submitter([&] { try { Check(sceKernelAioSubmitWriteCommands(&request, 1, 1, &submittedId) == 0); } diff --git a/core/libs/tests/GuestAjm.cpp b/core/libs/tests/GuestAjm.cpp index 35ad71e346..007a6beaf0 100644 --- a/core/libs/tests/GuestAjm.cpp +++ b/core/libs/tests/GuestAjm.cpp @@ -851,6 +851,23 @@ void TestGetInfo(std::uint32_t context) { Require(sceAjmInstanceDestroy(context, instance) == 0); } +void TestGetStatistics(std::uint32_t context) { + std::uint32_t instance = 0; + Require(sceAjmInstanceCreate(context, 1, 0, &instance) == 0); + std::vector batch(4096); + AjmBatchInfo info{}; + std::vector statistics(32, 0xaa); + FormatSideband format{-1, -1, 0xaaaaaaaau, 0xaaaaaaaau, 0xaaaaaaaau, 0xaaaaaaaau, 0xaaaaaaaau, 0xaaaaaaaau}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobGetStatistics(&info, 1.0f, statistics.data()) == 0); + Require(sceAjmBatchJobGetInfo(&info, instance, &format) == 0); + Submit(context, info); + Require(std::all_of(statistics.begin(), statistics.begin() + 24, [](std::uint8_t value) { return value == 0; })); + Require(std::all_of(statistics.begin() + 24, statistics.end(), [](std::uint8_t value) { return value == 0xaa; })); + Require(format.result == 1 && format.numChannels == 0xaaaaaaaau); + Require(sceAjmInstanceDestroy(context, instance) == 0); +} + void TestBatchBounds(std::uint32_t context) { constexpr int invalidParameter = static_cast(0x80930005); std::vector storage(128); @@ -2587,6 +2604,7 @@ int main(int argc, char** argv) { TestGaplessDecode(context); TestCodecInfo(context); TestGetInfo(context); + TestGetStatistics(context); TestBatchBounds(context); TestBatchWaitRelease(context); TestInstanceHandleLifetime(context); diff --git a/core/libs/tests/GuestAmpr.cpp b/core/libs/tests/GuestAmpr.cpp index 05e617348e..308c01e66b 100644 --- a/core/libs/tests/GuestAmpr.cpp +++ b/core/libs/tests/GuestAmpr.cpp @@ -1457,6 +1457,50 @@ void TestAmmRemapAndProtect() { ReleaseRecorderBuffer(exact); } +struct ObservedMapping { + std::uintptr_t address = 0; + std::size_t bytes = 0; + std::uint64_t generation = 0; +}; + +std::vector observedMappings; + +void ObserveMapping(std::uintptr_t address, std::size_t bytes, std::uint64_t generation) { + observedMappings.push_back({address, bytes, generation}); +} + +void RequireZeroMappingObserved(std::uint64_t address, std::uint64_t size, std::uint64_t before) { +#ifdef _WIN32 + bool found = false; + for (const auto& mapping : observedMappings) { + if (mapping.address <= address && mapping.address + mapping.bytes >= address + size && mapping.generation > before) found = true; + } + Require(found); + Require(GuestArena::GuestArenaCommitGeneration_nid_postfix() > before); +#else + (void)address; + (void)size; + (void)before; +#endif +} + +void RequireReadOnlyZeroPages(std::uint64_t address, std::uint64_t size) { +#ifdef _WIN32 + for (std::uint64_t offset = 0; offset < size; offset += page) { + auto* pointer = reinterpret_cast(address + offset); + MEMORY_BASIC_INFORMATION info{}; + Require(VirtualQuery(pointer, &info, sizeof(info)) == sizeof(info)); + Require(info.Type == MEM_MAPPED && info.AllocationProtect == PAGE_READONLY && info.Protect == PAGE_READONLY); + DWORD previous = 0; + Require(!VirtualProtect(pointer, page, PAGE_READWRITE, &previous)); + Require(At(address + offset) == 0); + } +#else + (void)address; + (void)size; +#endif +} + void TestAmmPrt() { std::uint64_t start = 0; std::uint64_t end = 0; @@ -1478,6 +1522,7 @@ void TestAmmPrt() { SubmitAmm(reserve); Require(Protection(prt) == 0x10 && Protection(prt + 3 * page) == 0x10); Require(At(prt) == 0 && At(prt + 4 * page - 8) == 0); + RequireReadOnlyZeroPages(prt, 4 * page); At(source) = 0x88; Recorder back; @@ -1498,8 +1543,20 @@ void TestAmmPrt() { commands = release.Commands(); Require(sceAmprAmmCommandBufferUnmapToPrt(&release.buffer, prt + page, 2 * page) == 0); RequireAppended(release, offset, commands, Apr::Opcode::AmmUnmapToPrt, sceAmprAmmMeasureAmmCommandSizeUnmap(prt + page, 2 * page)); +#ifdef _WIN32 + const auto beforeRelease = GuestArena::GuestArenaCommitGeneration_nid_postfix(); + observedMappings.clear(); + GuestArena::GuestArenaSetPrivateMappingObserver_nid_postfix(&ObserveMapping); +#else + const std::uint64_t beforeRelease = 0; +#endif SubmitAmm(release); +#ifdef _WIN32 + GuestArena::GuestArenaSetPrivateMappingObserver_nid_postfix(nullptr); +#endif + RequireZeroMappingObserved(prt + page, 2 * page, beforeRelease); Require(Protection(prt + page) == 0x10 && At(prt + page) == 0 && At(prt + 2 * page) == 0 && At(prt + 3 * page) == 0x88); + RequireReadOnlyZeroPages(prt + page, 2 * page); Recorder rebuild; Require(sceAmprAmmCommandBufferAllocatePaForPrt(&rebuild.buffer, prt, 3 * page, 0, cpuReadWrite) == 0); diff --git a/core/libs/tests/GuestAppInstUtil.cpp b/core/libs/tests/GuestAppInstUtil.cpp new file mode 100644 index 0000000000..33f02a5f53 --- /dev/null +++ b/core/libs/tests/GuestAppInstUtil.cpp @@ -0,0 +1,37 @@ +#include "prx/libSceAppInstUtil/include/Api.hpp" + +#include +#include +#include + +namespace { + +void Require(bool condition, const char* message) { + if (!condition) { + std::fprintf(stderr, "AppInstUtil: %s\n", message); + std::abort(); + } +} + +} + +int main() { + Require(sceAppInstUtilInitialize() == 0, "initialization failed"); + + std::int32_t exists = 0; + Require(sceAppInstUtilAppExists("PPSA00000", &exists) == 0 && exists == 1, "the running title is not installed"); + exists = 7; + Require(sceAppInstUtilAppExists("PPSA00001", &exists) == 0 && exists == 0, "another title is installed"); + exists = 7; + Require(sceAppInstUtilAppExists("PPSA0000", &exists) == 0 && exists == 0, "a title id prefix matched"); + exists = 7; + Require(sceAppInstUtilAppExists("", &exists) == 0 && exists == 0, "an empty title id matched"); + + Require(sceAppInstUtilTerminate() == 0, "termination failed"); + Require(sceAppInstUtilInitialize() == 0, "initialization after termination failed"); + exists = 0; + Require(sceAppInstUtilAppExists("PPSA00000", &exists) == 0 && exists == 1, "the running title is not installed after a restart"); + Require(sceAppInstUtilTerminate() == 0, "the second termination failed"); + std::puts("AppInstUtil tests passed"); + return 0; +} diff --git a/core/libs/tests/GuestAprRead.cpp b/core/libs/tests/GuestAprRead.cpp index da7ee7df7c..f086665bbd 100644 --- a/core/libs/tests/GuestAprRead.cpp +++ b/core/libs/tests/GuestAprRead.cpp @@ -14,6 +14,8 @@ #include #ifdef _WIN32 #include +#else +#include #endif namespace { @@ -176,6 +178,7 @@ void TestStreams(const Fixture& fixture) { void TestQueries() { constexpr int GuestEinval = 22; constexpr int GuestEnoent = 2; + constexpr int GuestEfault = 14; const char* names[] = {"generated.bin", "missing.bin"}; std::array ids{0, 0}; std::array sizes{1, 1}; @@ -193,6 +196,28 @@ void TestQueries() { FileStat stat{}; Require(sceKernelAprGetFileStat(id, nullptr) == -1 && *__error_nid_postfix() == GuestEinval, "APR accepted a null file stat"); Require(sceKernelAprGetFileStat(id, &stat) == 0 && stat.st_size == static_cast(FileBytes), "APR reported a wrong file stat size"); + for (const bool readable : {true, false}) { +#ifdef _WIN32 + void* page = VirtualAlloc(nullptr, 4096, MEM_COMMIT | MEM_RESERVE, readable ? PAGE_READONLY : PAGE_NOACCESS); + Require(page != nullptr, "cannot map an unwritable APR destination"); +#else + void* page = mmap(nullptr, 4096, readable ? PROT_READ : PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(page != MAP_FAILED, "cannot map an unwritable APR destination"); +#endif + *__error_nid_postfix() = 0; + Require(sceKernelAprGetFileSize(id, static_cast(page)) == -1 && *__error_nid_postfix() == GuestEfault, "APR accepted an unwritable file size"); + *__error_nid_postfix() = 0; + Require(sceKernelAprGetFileStat(id, static_cast(page)) == -1 && *__error_nid_postfix() == GuestEfault, "APR accepted an unwritable file stat"); + if (readable) { + const auto* bytes = static_cast(page); + Require(std::all_of(bytes, bytes + 4096, [](std::uint8_t byte) { return byte == 0; }), "APR wrote into an unwritable destination"); + } +#ifdef _WIN32 + VirtualFree(page, 0, MEM_RELEASE); +#else + munmap(page, 4096); +#endif + } std::vector data(4096, 0xCC); const Apr::ReadFileCommand command{{Apr::Opcode::ReadFile, sizeof(Apr::ReadFileCommand)}, id, 0, reinterpret_cast(data.data()), data.size(), 100}; alignas(8) std::array memory{}; diff --git a/core/libs/tests/GuestAudioOut.cpp b/core/libs/tests/GuestAudioOut.cpp new file mode 100644 index 0000000000..b80d9741f9 --- /dev/null +++ b/core/libs/tests/GuestAudioOut.cpp @@ -0,0 +1,22 @@ +#include "prx/libSceAudioOut/include/AudioOut.hpp" +#include "SceTypes.hpp" +#include "prx/libc/include/Shutdown.hpp" +#include + +static void Require(bool value) { + if (!value) { + std::abort(); + } +} + +int main() { + constexpr int invalidPointer = static_cast(0x80260004); + constexpr int invalidHandle = static_cast(0x80260001); + + Require(sceAudioOutInit() == 0); + Require(sceAudioOutGetPortState(invalidHandle, nullptr) == invalidPointer); + Require(sceAudioOutGetPortState(0, nullptr) == invalidPointer); + + LibcRunShutdown_nid_postfix(); + return 0; +} diff --git a/core/libs/tests/GuestAudiodec.cpp b/core/libs/tests/GuestAudiodec.cpp index b01a959654..9d7053a504 100644 --- a/core/libs/tests/GuestAudiodec.cpp +++ b/core/libs/tests/GuestAudiodec.cpp @@ -267,6 +267,25 @@ void TestAac(bool adts, std::uint32_t maxChannels = 2, std::uint32_t paramSize = Require(sceAudiodecDeleteDecoder(handle) == 0); } +void TestAacUnopenableLayout() { + AudiodecParamM4aac param{sizeof(param), 1, 1, 3, 2, 0}; + AudiodecM4aacInfo info{sizeof(info), 0, 0, 0, 0}; + AudiodecCtrl ctrl{¶m, &info, nullptr, nullptr}; + const std::int32_t handle = sceAudiodecCreateDecoder(&ctrl, 3); + Require(handle > 0); + Streams streams; + const std::size_t length = AdtsLength(AAC_ADTS); + std::vector reserved(AAC_ADTS, AAC_ADTS + length); + reserved[2] = static_cast((reserved[2] & 0xC3) | 13 << 2); + Require(Decode(handle, ctrl, streams, reserved.data(), reserved.size()) == errorApiFail && info.i_result == -4); + Require(streams.au.ui_au_size == 0 && streams.pcm.ui_pcm_size == 0); + Require(Decode(handle, ctrl, streams, reserved.data(), reserved.size()) == errorApiFail && info.i_result == -4); + Require(sceAudiodecClearContext(handle) == 0); + Require(Decode(handle, ctrl, streams, AAC_ADTS, length) == 0); + Require(streams.au.ui_au_size == length && streams.pcm.ui_pcm_size == 1024 * 2 * 2 && info.i_result == 0); + Require(sceAudiodecDeleteDecoder(handle) == 0); +} + void TestAacRaw51(std::uint32_t maxChannels) { AudiodecParamM4aac param{sizeof(param), 1, 2, 3, maxChannels, 0}; AudiodecM4aacInfo info{sizeof(info), 0, 0, 0, 0}; @@ -483,6 +502,7 @@ int main() { TestHeAac(HEAAC_STEREO, sizeof(HEAAC_STEREO), 2, adts, highEfficiency); } } + TestAacUnopenableLayout(); TestMp3(); TestAt9(); for (std::uint32_t type = 1; type <= 3; ++type) Require(sceAudiodecTermLibrary(type) == 0); diff --git a/core/libs/tests/GuestAudiodecNative.cpp b/core/libs/tests/GuestAudiodecNative.cpp new file mode 100644 index 0000000000..3d16686ffd --- /dev/null +++ b/core/libs/tests/GuestAudiodecNative.cpp @@ -0,0 +1,31 @@ +#include "prx/libSceAudiodec.native/include/Api.hpp" +#include "SceTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "tests/ExportLookup.hpp" +#include + +int main(int argc, char** argv) { + struct Export { + int (APS5_VABI* function)(void); + const char* name; + }; + const Export exports[] = { + {sceAudiodecClearContextEx, "sceAudiodecClearContextEx"}, + {sceAudiodecCreateDecoderEx, "sceAudiodecCreateDecoderEx"}, + {sceAudiodecDecode2, "sceAudiodecDecode2"}, + {sceAudiodecDecode2Ex, "sceAudiodecDecode2Ex"}, + {sceAudiodecDecode2WithPriority, "sceAudiodecDecode2WithPriority"}, + {sceAudiodecDecode2WithPriorityEx, "sceAudiodecDecode2WithPriorityEx"}, + {sceAudiodecDecodeEx, "sceAudiodecDecodeEx"}, + {sceAudiodecDecodeWithPriority, "sceAudiodecDecodeWithPriority"}, + {sceAudiodecDecodeWithPriorityEx, "sceAudiodecDecodeWithPriorityEx"}, + {sceAudiodecDeleteDecoderEx, "sceAudiodecDeleteDecoderEx"}, + {sceAudiodecGetContext, "sceAudiodecGetContext"}, + {sceAudiodecInitialize, "sceAudiodecInitialize"}, + {sceAudiodecRegisterCodec, "sceAudiodecRegisterCodec"}, + {sceAudiodecTerminate, "sceAudiodecTerminate"}, + }; + if (!ExportsResolveByName(argc > 1 ? argv[1] : nullptr, exports, "sceAudiodecNotExported")) return 1; + std::puts("libSceAudiodec.native pending exports are exported"); + return 0; +} diff --git a/core/libs/tests/GuestAvPlayerNative.cpp b/core/libs/tests/GuestAvPlayerNative.cpp new file mode 100644 index 0000000000..bf694893fa --- /dev/null +++ b/core/libs/tests/GuestAvPlayerNative.cpp @@ -0,0 +1,19 @@ +#include "prx/libSceAvPlayer.native/include/Api.hpp" +#include "SceTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "tests/ExportLookup.hpp" +#include + +int main(int argc, char** argv) { + struct Export { + int (APS5_VABI* function)(void); + const char* name; + }; + const Export exports[] = { + {sceAvPlayerPrintf, "sceAvPlayerPrintf"}, + {sceAvPlayerVprintf, "sceAvPlayerVprintf"}, + }; + if (!ExportsResolveByName(argc > 1 ? argv[1] : nullptr, exports, "sceAvPlayerNotExported")) return 1; + std::puts("libSceAvPlayer.native pending exports are exported"); + return 0; +} diff --git a/core/libs/tests/GuestAvSettingPredicates.cpp b/core/libs/tests/GuestAvSettingPredicates.cpp new file mode 100644 index 0000000000..1d2196a649 --- /dev/null +++ b/core/libs/tests/GuestAvSettingPredicates.cpp @@ -0,0 +1,111 @@ +#include "prx/libSceAvSetting/include/Api.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestHeap.hpp" + +#include +#include +#include +#include +#include +#include + +namespace { + +void Require(bool condition, const char* message) { + if (condition) return; + std::fprintf(stderr, "AVSETTING PREDICATES: %s\n", message); + std::abort(); +} + +void Write(void* destination, const void* source, std::size_t bytes) { + const GuestArena::HostWrite access(destination, bytes); + Require(access.Open(), "guest fixture memory was not writable"); + std::memcpy(destination, source, bytes); +} + +template +void WriteField(void* object, std::size_t offset, TValue value) { + Write(static_cast(object) + offset, &value, sizeof(value)); +} + +void* Allocate(std::size_t bytes) { + void* memory = GuestHeap::GuestHeapAllocate_nid_postfix(bytes); + Require(memory != nullptr, "guest fixture allocation failed"); + const std::array zeros{}; + auto* destination = static_cast(memory); + for (std::size_t offset = 0; offset < bytes; offset += zeros.size()) { + const auto count = bytes - offset < zeros.size() ? bytes - offset : zeros.size(); + Write(destination + offset, zeros.data(), count); + } + return memory; +} + +void TestColorimetry() { + Require(!sceAvControlIsColorimetryHdr(8), "colorimetry below the table range was accepted"); + for (std::uint32_t value = 9; value <= 20; ++value) { + Require(sceAvControlIsColorimetryHdr(value) == (value != 18), "colorimetry table result is wrong"); + } + Require(!sceAvControlIsColorimetryHdr(21), "colorimetry above the table range was accepted"); +} + +void TestDeepColor(void* monitorInfo) { + Require(!sceAvControlIsDeepColorSupportedByMonitorInfo(nullptr), "null monitor info was accepted"); + Require(!sceAvControlIsDeepColorSupportedByMonitorInfo(reinterpret_cast(std::uintptr_t{1})), + "unmapped monitor info was accepted"); + Require(!sceAvControlIsDeepColorSupportedByMonitorInfo(monitorInfo), "empty monitor info was accepted"); + + WriteField(monitorInfo, 0x74, std::uint8_t{0x02}); + Require(sceAvControlIsDeepColorSupportedByMonitorInfo(monitorInfo), "first deep-color mask was ignored"); + WriteField(monitorInfo, 0x74, std::uint8_t{0}); + WriteField(monitorInfo, 0x75, std::uint8_t{0x08}); + Require(sceAvControlIsDeepColorSupportedByMonitorInfo(monitorInfo), "second deep-color mask was ignored"); + WriteField(monitorInfo, 0x75, std::uint8_t{0}); + WriteField(monitorInfo, 0x352, std::uint8_t{0x02}); + Require(sceAvControlIsDeepColorSupportedByMonitorInfo(monitorInfo), "extended deep-color mask was ignored"); + WriteField(monitorInfo, 0x352, std::uint8_t{0x04}); + Require(!sceAvControlIsDeepColorSupportedByMonitorInfo(monitorInfo), "unrelated extended bits were accepted"); +} + +void TestHdr(void* monitorInfo) { + Require(!sceAvControlIsHdrSupportedByMonitorInfo(nullptr), "null monitor info was accepted"); + Require(!sceAvControlIsHdrSupportedByMonitorInfo(reinterpret_cast(std::uintptr_t{1})), + "unmapped monitor info was accepted"); + Require(!sceAvControlIsHdrSupportedByMonitorInfo(monitorInfo), "empty monitor info was accepted"); + + WriteField(monitorInfo, 0x78, std::uint8_t{0x10}); + Require(!sceAvControlIsHdrSupportedByMonitorInfo(monitorInfo), "HDR was accepted without the capability bit"); + WriteField(monitorInfo, 0x358, std::uint8_t{0x04}); + Require(sceAvControlIsHdrSupportedByMonitorInfo(monitorInfo), "HDR flags were not combined correctly"); + WriteField(monitorInfo, 0x78, std::uint8_t{0x40}); + Require(!sceAvControlIsHdrSupportedByMonitorInfo(monitorInfo), "unrelated HDR flags were accepted"); +} + +void TestModeFlags(void* videoOutMode) { + Require(!sceAvControlIsModeGen1Vr(nullptr), "null video mode was accepted by Gen1 VR query"); + Require(!sceAvControlIsModeSystemDefault(nullptr), "null video mode was accepted by default-mode query"); + Require(!sceAvControlIsModeGen1Vr(reinterpret_cast(std::uintptr_t{1})), + "unmapped video mode was accepted by Gen1 VR query"); + Require(!sceAvControlIsModeSystemDefault(reinterpret_cast(std::uintptr_t{1})), + "unmapped video mode was accepted by default-mode query"); + Require(sceAvControlIsModeSystemDefault(videoOutMode), "clear system-default bit was not recognized"); + Require(!sceAvControlIsModeGen1Vr(videoOutMode), "clear Gen1 VR bit was accepted"); + + WriteField(videoOutMode, 0x30, std::uint32_t{0x02}); + Require(sceAvControlIsModeGen1Vr(videoOutMode), "Gen1 VR bit was ignored"); + Require(sceAvControlIsModeSystemDefault(videoOutMode), "unrelated Gen1 VR bit changed default-mode result"); + WriteField(videoOutMode, 0x30, std::uint32_t{0x03}); + Require(!sceAvControlIsModeSystemDefault(videoOutMode), "set system-default bit was accepted"); +} + +} + +int main() { + auto* monitorInfo = Allocate(0x360); + auto* videoOutMode = Allocate(0x34); + TestColorimetry(); + TestDeepColor(monitorInfo); + TestHdr(monitorInfo); + TestModeFlags(videoOutMode); + std::puts("AvSetting predicate tests passed"); + return 0; +} diff --git a/core/libs/tests/GuestAvailableCpumask.cpp b/core/libs/tests/GuestAvailableCpumask.cpp index 25b13a9f3b..9f2a6ea956 100644 --- a/core/libs/tests/GuestAvailableCpumask.cpp +++ b/core/libs/tests/GuestAvailableCpumask.cpp @@ -83,8 +83,8 @@ static void* APS5_VABI ReportCpuset(void* arg) { result[0] = cpuset_getaffinity_nid_postfix(3, 1, -1, 8, set); result[1] = set[0]; result[2] = set[1]; - result[3] = cpuset_getaffinity_nid_postfix(3, 1, -1, sizeof(set), set); - result[4] = set[0] | (set[1] != 0 || set[2] != 0 || set[3] != 0 ? 1ull << 63 : 0); + result[3] = cpuset_getaffinity_nid_postfix(3, 1, -1, 16, set); + result[4] = set[0] | (set[1] != 0 ? 1ull << 63 : 0); result[5] = cpuset_getaffinity_nid_postfix(3, 1, -1, 4, set) == -1 && *__error_nid_postfix() == 34; result[6] = cpuset_getaffinity_nid_postfix(3, 1, -1, 33, set) == -1 && *__error_nid_postfix() == 34; result[7] = cpuset_getaffinity_nid_postfix(3, 1, -1, 8, nullptr) == -1 && *__error_nid_postfix() == 14; @@ -107,7 +107,7 @@ static void* APS5_VABI SetCpuset(void* arg) { set[1] = 1; result->results[1] = cpuset_setaffinity_nid_postfix(3, 1, -1, 16, set) == -1 && *__error_nid_postfix() == 22; set[1] = 0; - result->results[2] = cpuset_setaffinity_nid_postfix(3, 1, -1, sizeof(set), set); + result->results[2] = cpuset_setaffinity_nid_postfix(3, 1, -1, 16, set); result->results[3] = cpuset_setaffinity_nid_postfix(3, 1, -1, 4, set) == -1 && *__error_nid_postfix() == 34; result->results[4] = cpuset_setaffinity_nid_postfix(3, 1, -1, 33, set) == -1 && *__error_nid_postfix() == 34; result->results[5] = cpuset_setaffinity_nid_postfix(3, 1, -1, 8, nullptr) == -1 && *__error_nid_postfix() == 14; @@ -220,8 +220,8 @@ int main() { } std::uint64_t processSet[4] = {~0ull, ~0ull, ~0ull, ~0ull}; - Require(cpuset_getaffinity_nid_postfix(3, 2, -1, sizeof(processSet), processSet) == 0); - Require(processSet[0] == available && processSet[1] == 0 && processSet[2] == 0 && processSet[3] == 0); + Require(cpuset_getaffinity_nid_postfix(3, 2, -1, 16, processSet) == 0); + Require(processSet[0] == available && processSet[1] == 0 && processSet[2] == ~0ull && processSet[3] == ~0ull); processSet[0] = 0; Require(cpuset_getaffinity_nid_postfix(3, 2, getpid_nid_postfix(), 8, processSet) == 0 && processSet[0] == available); Require(cpuset_getaffinity_nid_postfix(3, 2, -1, 4, processSet) == -1 && *__error_nid_postfix() == GUEST_ERANGE); @@ -280,8 +280,18 @@ int main() { Require(ReadCpuset(pthread_self_nid_postfix()) == available); std::uint8_t rawSet[GUEST_CPUSET_BYTES]{}; - for (const std::size_t size : {std::size_t{32}, std::size_t{33}, std::size_t{64}}) + for (const std::size_t size : {std::size_t{24}, std::size_t{32}, std::size_t{33}, std::size_t{64}}) Require(pthread_getaffinity_np_nid_postfix(pthread_self_nid_postfix(), size, rawSet) == GUEST_ERANGE); + for (const std::size_t size : {std::size_t{0}, std::size_t{1}, std::size_t{4}, std::size_t{7}}) + Require(pthread_getaffinity_np_nid_postfix(pthread_self_nid_postfix(), size, rawSet) == GUEST_ERANGE); + for (const std::size_t size : {std::size_t{9}, std::size_t{12}, std::size_t{15}}) { + std::memset(rawSet, 0xff, sizeof(rawSet)); + Require(pthread_getaffinity_np_nid_postfix(pthread_self_nid_postfix(), size, rawSet) == 0); + KernelCpumask narrow = 0; + std::memcpy(&narrow, rawSet, sizeof(narrow)); + Require(narrow == available); + for (std::size_t index = sizeof(narrow); index < sizeof(rawSet); ++index) Require(rawSet[index] == (index < size ? 0 : 0xff)); + } Require(pthread_getaffinity_np_nid_postfix(nullptr, GUEST_CPUSET_BYTES, rawSet) == GUEST_EINVAL); Require(pthread_getaffinity_np_nid_postfix(nullptr, 0, rawSet) == GUEST_EINVAL); Require(pthread_getaffinity_np_nid_postfix(pthread_self_nid_postfix(), 16, nullptr) == GUEST_EFAULT); @@ -301,8 +311,8 @@ int main() { Require(cpuset_setaffinity_nid_postfix(3, 1, 999999, 8, processSet) == -1 && *__error_nid_postfix() == GUEST_ESRCH); Require(cpuset_setaffinity_nid_postfix(3, 2, 999999, 8, processSet) == -1 && *__error_nid_postfix() == GUEST_ESRCH); Require(cpuset_setaffinity_nid_postfix(99, 1, -1, 8, processSet) == -1 && *__error_nid_postfix() == GUEST_EINVAL); - Require(cpuset_setaffinity_nid_postfix(99, 1, 999999, 8, processSet) == -1 && *__error_nid_postfix() == GUEST_ESRCH); - Require(cpuset_setaffinity_nid_postfix(99, 2, 999999, 8, processSet) == -1 && *__error_nid_postfix() == GUEST_ESRCH); + Require(cpuset_setaffinity_nid_postfix(99, 1, 999999, 8, processSet) == -1 && *__error_nid_postfix() == GUEST_EINVAL); + Require(cpuset_setaffinity_nid_postfix(99, 2, 999999, 8, processSet) == -1 && *__error_nid_postfix() == GUEST_EINVAL); Require(cpuset_setaffinity_nid_postfix(3, 99, -1, 8, processSet) == -1 && *__error_nid_postfix() == GUEST_EINVAL); const auto throws = [](auto&& call) { try { @@ -312,14 +322,71 @@ int main() { return true; } }; - Require(throws([&] { cpuset_getaffinity_nid_postfix(1, 1, -1, 8, processSet); })); - Require(throws([&] { cpuset_getaffinity_nid_postfix(2, 1, -1, 8, processSet); })); - Require(throws([&] { cpuset_getaffinity_nid_postfix(3, 3, -1, 8, processSet); })); Require(throws([&] { cpuset_getaffinity_nid_postfix(3, 4, -1, 8, processSet); })); + std::uint64_t pinnedSelf[4] = {available & (~available + 1), 0, 0, 0}; + Require(cpuset_setaffinity_nid_postfix(3, 1, -1, 8, pinnedSelf) == 0); + for (const int which : {1, 2}) { + for (const std::size_t size : {std::size_t{8}, std::size_t{16}}) { + std::uint64_t base[4] = {~0ull, ~0ull, ~0ull, ~0ull}; + Require(cpuset_getaffinity_nid_postfix(2, which, -1, size, base) == 0); + Require(base[0] == available && (size == 8 || base[1] == 0)); + Require(base[2] == ~0ull && base[3] == ~0ull); + } + for (const int level : {1, 2}) { + std::uint64_t set[4] = {0, ~0ull, 0, 0}; + Require(cpuset_getaffinity_nid_postfix(level, which, -1, 16, set) == 0); + Require(set[0] == (level == 1 ? 0xffffull : available) && set[1] == 0); + } + } + for (const std::size_t size : {std::size_t{0}, std::size_t{7}, std::size_t{24}, std::size_t{32}, std::size_t{64}}) { + std::uint64_t sized[8]{}; + *__error_nid_postfix() = 0; + Require(cpuset_getaffinity_nid_postfix(2, 1, -1, size, sized) == -1 && *__error_nid_postfix() == GUEST_ERANGE); + *__error_nid_postfix() = 0; + Require(cpuset_setaffinity_nid_postfix(3, 1, -1, size, sized) == -1 && *__error_nid_postfix() == GUEST_ERANGE); + } + for (const std::size_t size : {std::size_t{9}, std::size_t{12}}) { + std::uint64_t sized[4] = {0, ~0ull, ~0ull, ~0ull}; + Require(cpuset_getaffinity_nid_postfix(2, 1, -1, size, sized) == 0 && sized[0] == available && sized[2] == ~0ull); + } + { + std::uint64_t root = 0; + Require(cpuset_getaffinity_nid_postfix(1, 3, -1, 8, &root) == 0 && root == 0xffff); + root = 0; + Require(cpuset_getaffinity_nid_postfix(1, 1, scePthreadGetthreadid(), 8, &root) == 0 && root == 0xffff); + *__error_nid_postfix() = 0; + Require(cpuset_getaffinity_nid_postfix(1, 1, 999999, 8, &root) == -1 && *__error_nid_postfix() == GUEST_ESRCH); + } + std::uint64_t base[4]{}; + Require(cpuset_getaffinity_nid_postfix(2, 1, scePthreadGetthreadid(), 8, base) == 0 && base[0] == available); + Require(cpuset_getaffinity_nid_postfix(2, 3, -1, 8, base) == 0 && base[0] == available); + Require(cpuset_getaffinity_nid_postfix(3, 3, -1, 8, base) == 0 && base[0] == available); + Require(cpuset_getaffinity_nid_postfix(3, 1, -1, 8, base) == 0 && base[0] == pinnedSelf[0]); + Require(cpuset_getaffinity_nid_postfix(2, 1, 999999, 8, base) == -1 && *__error_nid_postfix() == GUEST_ESRCH); + for (const int level : {1, 2}) { + for (const int which : {4, 6}) { + *__error_nid_postfix() = 0; + Require(cpuset_getaffinity_nid_postfix(level, which, -1, 8, base) == -1 && *__error_nid_postfix() == GUEST_EINVAL); + } + } + Require(cpuset_getaffinity_nid_postfix(3, 7, -1, 8, base) == -1 && *__error_nid_postfix() == GUEST_EINVAL); + Require(cpuset_getaffinity_nid_postfix(3, 8, -1, 8, base) == -1 && *__error_nid_postfix() == GUEST_EINVAL); + pinnedSelf[0] = available; + Require(cpuset_setaffinity_nid_postfix(3, 1, -1, 8, pinnedSelf) == 0); Require(throws([&] { cpuset_setaffinity_nid_postfix(1, 1, -1, 8, processSet); })); Require(throws([&] { cpuset_setaffinity_nid_postfix(2, 1, -1, 8, processSet); })); Require(throws([&] { cpuset_setaffinity_nid_postfix(3, 3, -1, 8, processSet); })); Require(throws([&] { cpuset_setaffinity_nid_postfix(3, 4, -1, 8, processSet); })); + for (const int level : {1, 2}) { + for (const int which : {4, 6, 7, 8, 99}) { + *__error_nid_postfix() = 0; + Require(cpuset_setaffinity_nid_postfix(level, which, -1, 8, processSet) == -1 && *__error_nid_postfix() == GUEST_EINVAL); + } + *__error_nid_postfix() = 0; + Require(cpuset_setaffinity_nid_postfix(level, 1, 999999, 8, processSet) == -1 && *__error_nid_postfix() == GUEST_ESRCH); + } + *__error_nid_postfix() = 0; + Require(cpuset_setaffinity_nid_postfix(3, 6, 0, 8, processSet) == -1 && *__error_nid_postfix() == GUEST_EINVAL); std::promise holdOther; auto heldOther = holdOther.get_future(); RemoteThread otherArg; @@ -329,14 +396,14 @@ int main() { while (otherArg.tid.load(std::memory_order_acquire) == 0) std::this_thread::yield(); const int otherTid = otherArg.tid.load(std::memory_order_acquire); std::uint64_t remoteSet[5] = {~0ull, ~0ull, ~0ull, ~0ull, ~0ull}; - Require(cpuset_getaffinity_nid_postfix(3, 1, otherTid, GUEST_CPUSET_BYTES, remoteSet) == 0); - Require(remoteSet[0] == available && remoteSet[1] == 0 && remoteSet[2] == 0 && remoteSet[3] == 0 && remoteSet[4] == ~0ull); + Require(cpuset_getaffinity_nid_postfix(3, 1, otherTid, 16, remoteSet) == 0); + Require(remoteSet[0] == available && remoteSet[1] == 0 && remoteSet[2] == ~0ull && remoteSet[3] == ~0ull && remoteSet[4] == ~0ull); const std::uint64_t remoteMask[4] = {last | (1ull << 40), 0, 0, 0}; - Require(cpuset_setaffinity_nid_postfix(3, 1, otherTid, sizeof(remoteMask), remoteMask) == 0); + Require(cpuset_setaffinity_nid_postfix(3, 1, otherTid, 16, remoteMask) == 0); Require(ReadCpuset(otherThread) == last); Require(ReadCpuset(pthread_self_nid_postfix()) == available); Require(cpuset_getaffinity_nid_postfix(3, 1, otherTid, 8, remoteSet) == 0 && remoteSet[0] == last); - const std::uint64_t invalidRemote[4] = {first, 0, 0, 1}; + const std::uint64_t invalidRemote[2] = {first, 1}; Require(cpuset_setaffinity_nid_postfix(3, 1, otherTid, sizeof(invalidRemote), invalidRemote) == -1 && *__error_nid_postfix() == GUEST_EINVAL); const std::uint64_t unavailableRemote[1] = {1ull << 40}; Require(cpuset_setaffinity_nid_postfix(3, 1, otherTid, sizeof(unavailableRemote), unavailableRemote) == -1 && *__error_nid_postfix() == GUEST_EDEADLK); diff --git a/core/libs/tests/GuestBadOptionalAccess.cpp b/core/libs/tests/GuestBadOptionalAccess.cpp index 3dd9f1a1c5..07709bab83 100644 --- a/core/libs/tests/GuestBadOptionalAccess.cpp +++ b/core/libs/tests/GuestBadOptionalAccess.cpp @@ -7,6 +7,21 @@ struct Vtable { std::ptrdiff_t offset; const std::type_info* type; const void* destroy; const void* deleteObject; const char* (APS5_VABI *what)(const LibcException::ExceptionObject*); }; +struct AbiRecord { const void* const* vtable; const char* name; const void* base; }; +struct PointerRecord { const void* const* vtable; const char* name; unsigned flags; const void* pointee; }; +struct AbiVtable { + std::ptrdiff_t offset; + const void* type; + const void* destroy; + const void* deleteObject; + const void* isPointer; + const void* isFunction; + bool (APS5_VABI *catchType)(const void*, const void*, void**, unsigned); +}; + +struct SiTypeRecord { const void* const* vtable; const char* name; const std::type_info* base; }; +struct PointerTypeRecord { const void* const* vtable; const char* name; unsigned flags; const std::type_info* pointee; }; + extern "C" { extern __cxxabiv1::__class_type_info _ZTISt9exception_nid_postfix; extern __cxxabiv1::__si_class_type_info _ZTISt19bad_optional_access_nid_postfix; @@ -16,6 +31,21 @@ extern std::type_info _ZTIj_nid_postfix; extern std::type_info _ZTIf_nid_postfix; extern std::type_info _ZTIDn_nid_postfix; extern void* const _ZTVN10__cxxabiv123__fundamental_type_infoE_nid_postfix[]; +extern SiTypeRecord _ZTISt9type_info_nid_postfix; +extern SiTypeRecord _ZTIN10__cxxabiv116__shim_type_infoE_nid_postfix; +extern SiTypeRecord _ZTIN10__cxxabiv117__class_type_infoE_nid_postfix; +extern SiTypeRecord _ZTIN10__cxxabiv117__pbase_type_infoE_nid_postfix; +extern SiTypeRecord _ZTIN10__cxxabiv119__pointer_type_infoE_nid_postfix; +extern SiTypeRecord _ZTIN10__cxxabiv120__si_class_type_infoE_nid_postfix; +extern PointerTypeRecord _ZTIPi_nid_postfix; +extern PointerTypeRecord _ZTIPKi_nid_postfix; +extern PointerTypeRecord _ZTIPf_nid_postfix; +extern PointerTypeRecord _ZTIPv_nid_postfix; +extern PointerTypeRecord _ZTIPKv_nid_postfix; +extern void* const _ZTVN10__cxxabiv117__class_type_infoE_nid_postfix[]; +extern void* const _ZTVN10__cxxabiv120__si_class_type_infoE_nid_postfix[]; +extern void* const _ZTVN10__cxxabiv119__pointer_type_infoE_nid_postfix[]; +extern void* const _ZTVN10__cxxabiv117__pbase_type_infoE_nid_postfix[]; } static void Require(bool value) { if (!value) std::abort(); } @@ -53,6 +83,48 @@ static void TestFundamentalRecords() { Require(caught); } +static void TestAbiAndPointerRecords() { + const void* const* classAddressPoint = _ZTVN10__cxxabiv117__class_type_infoE_nid_postfix + 2; + const void* const* siAddressPoint = _ZTVN10__cxxabiv120__si_class_type_infoE_nid_postfix + 2; + const void* const* pointerAddressPoint = _ZTVN10__cxxabiv119__pointer_type_infoE_nid_postfix + 2; + Require(_ZTISt9type_info_nid_postfix.vtable == classAddressPoint && _ZTISt9type_info_nid_postfix.base == nullptr && + std::strcmp(_ZTISt9type_info_nid_postfix.name, "St9type_info") == 0); + Require(_ZTIN10__cxxabiv116__shim_type_infoE_nid_postfix.vtable == siAddressPoint && + _ZTIN10__cxxabiv116__shim_type_infoE_nid_postfix.base == + reinterpret_cast(&_ZTISt9type_info_nid_postfix)); + Require(_ZTIN10__cxxabiv117__class_type_infoE_nid_postfix.base == + reinterpret_cast(&_ZTIN10__cxxabiv116__shim_type_infoE_nid_postfix)); + Require(_ZTIN10__cxxabiv120__si_class_type_infoE_nid_postfix.base == + reinterpret_cast(&_ZTIN10__cxxabiv117__class_type_infoE_nid_postfix)); + Require(_ZTIN10__cxxabiv119__pointer_type_infoE_nid_postfix.base == + reinterpret_cast(&_ZTIN10__cxxabiv117__pbase_type_infoE_nid_postfix)); + Require(_ZTIPi_nid_postfix.vtable == pointerAddressPoint && _ZTIPKi_nid_postfix.vtable == pointerAddressPoint && + _ZTIPv_nid_postfix.vtable == pointerAddressPoint && _ZTIPKv_nid_postfix.vtable == pointerAddressPoint); + Require(std::strcmp(_ZTIPi_nid_postfix.name, "Pi") == 0 && std::strcmp(_ZTIPKi_nid_postfix.name, "PKi") == 0); + Require(_ZTIPi_nid_postfix.flags == 0 && _ZTIPKi_nid_postfix.flags == 1 && _ZTIPKv_nid_postfix.flags == 1); + Require(_ZTIPi_nid_postfix.pointee == &_ZTIi_nid_postfix && _ZTIPKi_nid_postfix.pointee == &_ZTIi_nid_postfix); + Require(_ZTIPi_nid_postfix.vtable[-1] == reinterpret_cast(&_ZTIN10__cxxabiv119__pointer_type_infoE_nid_postfix)); + Require(_ZTVN10__cxxabiv119__pointer_type_infoE_nid_postfix[1] == + reinterpret_cast(&_ZTIN10__cxxabiv119__pointer_type_infoE_nid_postfix)); + Require(_ZTVN10__cxxabiv117__pbase_type_infoE_nid_postfix[1] == + reinterpret_cast(&_ZTIN10__cxxabiv117__pbase_type_infoE_nid_postfix)); + + using CatchType = bool (APS5_VABI*)(const std::type_info*, const std::type_info*, void**, unsigned); + const auto catchType = reinterpret_cast(const_cast(_ZTIPi_nid_postfix.vtable[4])); + const auto* pi = reinterpret_cast(&_ZTIPi_nid_postfix); + const auto* pki = reinterpret_cast(&_ZTIPKi_nid_postfix); + const auto* pf = reinterpret_cast(&_ZTIPf_nid_postfix); + const auto* pv = reinterpret_cast(&_ZTIPv_nid_postfix); + const auto* pkv = reinterpret_cast(&_ZTIPKv_nid_postfix); + void* pointerObject = nullptr; + Require(catchType(pki, pi, &pointerObject, 0)); + Require(!catchType(pi, pki, &pointerObject, 0)); + Require(catchType(pkv, pi, &pointerObject, 0)); + Require(catchType(pv, pi, &pointerObject, 0)); + Require(!catchType(pi, pf, &pointerObject, 0)); + Require(catchType(pi, &_ZTIDn_nid_postfix, &pointerObject, 0)); +} + int main() { const auto& type = _ZTISt19bad_optional_access_nid_postfix; Require(std::strcmp(type.name(), "St19bad_optional_access") == 0); @@ -72,5 +144,6 @@ int main() { } Require(thrown); TestFundamentalRecords(); + TestAbiAndPointerRecords(); return 0; } diff --git a/core/libs/tests/GuestBeisobmfBoxes.cpp b/core/libs/tests/GuestBeisobmfBoxes.cpp index 665b07248a..076d37acb8 100644 --- a/core/libs/tests/GuestBeisobmfBoxes.cpp +++ b/core/libs/tests/GuestBeisobmfBoxes.cpp @@ -125,18 +125,37 @@ int main() { "null member-type result is wrong"); Require(isobmf_box_getmember_ptr_nid_postfix(box, nullptr) == nullptr, "null member name result is wrong"); + Require(isobmf_box_getmember_length_nid_postfix(box, nullptr) == 0, + "null member name length result is wrong"); + Require(isobmf_box_getmember_type_nid_postfix(box, nullptr) == 0, + "null member name type result is wrong"); auto *badPointer = reinterpret_cast(std::uintptr_t{1}); Require(isobmf_box_gettype_nid_postfix(badPointer) == 0, "unmapped box result is wrong"); + Require(isobmf_box_getchildbox_nid_postfix(badPointer) == nullptr, "unmapped child box result is wrong"); + Require(isobmf_box_getdiskoffset_nid_postfix(badPointer) == 0, "unmapped disk-offset result is wrong"); + Require(isobmf_box_getnextbox_nid_postfix(badPointer) == nullptr, "unmapped next box result is wrong"); + Require(isobmf_box_getstate_nid_postfix(badPointer) == 0, "unmapped state result is wrong"); + Require(isobmf_box_getflags32b_nid_postfix(badPointer) == 0, "unmapped flags box result is wrong"); Require(isobmf_decoder_next_disk_offset_nid_postfix(badPointer) == 0xffffffff, "unmapped decoder did not return its safe sentinel"); auto *badName = reinterpret_cast(std::uintptr_t{1}); Require(isobmf_box_getmember_ptr_nid_postfix(box, badName) == nullptr, "unmapped member name result is wrong"); + Require(isobmf_box_getmember_length_nid_postfix(box, badName) == 0, + "unmapped member name length result is wrong"); + Require(isobmf_box_getmember_type_nid_postfix(box, badName) == 0, + "unmapped member name type result is wrong"); auto *badListBox = static_cast(Allocate(0xa0)); WriteField(badListBox, 0x40, std::uintptr_t{1}); Require(isobmf_box_getmember_ptr_nid_postfix(badListBox, "missing") == nullptr, "unmapped member list result is wrong"); + Require(isobmf_box_getmember_length_nid_postfix(badListBox, "missing") == 0, + "unmapped member list length result is wrong"); + Require(isobmf_box_getmember_type_nid_postfix(badListBox, "missing") == 0, + "unmapped member list type result is wrong"); + Require(isobmf_box_getflags32b_nid_postfix(badListBox) == 0, + "unmapped flags list result is wrong"); auto *cycleBox = static_cast(Allocate(0xa0)); auto *cycleMember = static_cast(Allocate(0x70)); @@ -144,6 +163,12 @@ int main() { WriteField(cycleMember, 0x10, cycleMember); Require(isobmf_box_getmember_ptr_nid_postfix(cycleBox, "missing") == nullptr, "cyclic member list did not fail safely"); + Require(isobmf_box_getmember_length_nid_postfix(cycleBox, "missing") == 0, + "cyclic member list length did not fail safely"); + Require(isobmf_box_getmember_type_nid_postfix(cycleBox, "missing") == 0, + "cyclic member list type did not fail safely"); + Require(isobmf_box_getflags32b_nid_postfix(cycleBox) == 0, + "cyclic flags list did not fail safely"); std::puts("BEISOBMF BOXES tests passed"); return 0; diff --git a/core/libs/tests/GuestBoundedSearchGuards.cpp b/core/libs/tests/GuestBoundedSearchGuards.cpp new file mode 100644 index 0000000000..366deab375 --- /dev/null +++ b/core/libs/tests/GuestBoundedSearchGuards.cpp @@ -0,0 +1,86 @@ +#include "prx/libc/include/Api.hpp" +#include +#include +#include +#include +#include +#include +#include + +static void Require(bool value, std::source_location location = std::source_location::current()) { + if (!value) { + std::fprintf(stderr, "check failed at line %u\n", location.line()); + std::abort(); + } +} + +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#include +#endif + +class Guarded { +public: + unsigned char* data = nullptr; + std::size_t size = 0; + Guarded() { +#ifdef _WIN32 + SYSTEM_INFO info{}; + GetSystemInfo(&info); + size = info.dwPageSize; + data = static_cast(VirtualAlloc(nullptr, size * 2, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + Require(data != nullptr); + DWORD previous = 0; + Require(VirtualProtect(data + size, size, PAGE_NOACCESS, &previous) != 0); +#else + const auto pageSize = sysconf(_SC_PAGESIZE); + Require(pageSize > 0); + size = static_cast(pageSize); + auto* mapping = mmap(nullptr, size * 2, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(mapping != MAP_FAILED); + data = static_cast(mapping); + Require(mprotect(data + size, size, PROT_NONE) == 0); +#endif + } + ~Guarded() { +#ifdef _WIN32 + Require(VirtualFree(data, 0, MEM_RELEASE) != 0); +#else + Require(munmap(data, size * 2) == 0); +#endif + } + Guarded(const Guarded&) = delete; + Guarded& operator=(const Guarded&) = delete; +}; + +int main() { + Guarded memory; + for (const std::size_t length : {0, 1, 2, 15, 16, 17, 31, 32, 33, 63, 64, 65, 127, 128, 129, 255}) { + auto* input = memory.data + memory.size - std::max(length, 1); + std::fill_n(input, std::max(length, 1), static_cast(0x81)); + for (const bool matches : {false, true}) { + if (matches && length) { input[0] = 0xfe; input[length - 1] = 0xfe; } + const std::vector before(input, input + std::max(length, 1)); + const void* first = nullptr; + const void* last = nullptr; + for (std::size_t i = 0; i < length; ++i) { + if (input[i] == 0xfe) { if (!first) first = input + i; last = input + i; } + } + Require(memchr_nid_postfix(input, 0xfe, length) == first); + Require(memrchr_nid_postfix(input, 0xfe, length) == last); + Require(strnlen_nid_postfix(reinterpret_cast(input), length) == length); + Require(std::equal(before.begin(), before.end(), input)); + } + if (length) { + input[length - 1] = 0; + Require(strnlen_nid_postfix(reinterpret_cast(input), length) == length - 1); + input[0] = 0; + Require(strnlen_nid_postfix(reinterpret_cast(input), length) == 0); + } + } +} diff --git a/core/libs/tests/GuestBrokenPipe.cpp b/core/libs/tests/GuestBrokenPipe.cpp new file mode 100644 index 0000000000..e11bc6219b --- /dev/null +++ b/core/libs/tests/GuestBrokenPipe.cpp @@ -0,0 +1,62 @@ +#include "prx/libkernel/include/Api.hpp" +#include "prx/libc/include/Api.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include + +namespace { + +constexpr int Unix = 1; +constexpr int Stream = 1; +constexpr int BrokenPipe = 13; +constexpr int PipeError = 32; +constexpr int TryAgain = 35; +volatile std::sig_atomic_t brokenPipeSignals = 0; + +void Require(bool value) { + if (!value) std::abort(); +} + +void APS5_VABI CountBrokenPipe(int signal) { + if (signal == BrokenPipe) brokenPipeSignals = brokenPipeSignals + 1; +} + +int WriteToClosedPeer() { + int pair[2] = {-1, -1}; + Require(socketpair_nid_postfix(Unix, Stream, 0, pair) == 0); + Require(close_nid_postfix(pair[1]) == 0); + *__error_nid_postfix() = 0; + const bool failed = write_nid_postfix(pair[0], "x", 1) == -1; + const int error = *__error_nid_postfix(); + Require(close_nid_postfix(pair[0]) == 0); + return failed ? error : 0; +} + +} + +int main() { + const auto ignore = reinterpret_cast(std::uintptr_t{1}); + Require(signal_nid_postfix(BrokenPipe, ignore) == nullptr); + Require(WriteToClosedPeer() == PipeError); + Require(signal_nid_postfix(BrokenPipe, CountBrokenPipe) == ignore); + Require(WriteToClosedPeer() == PipeError && brokenPipeSignals == 1); + GuestSignalSet blocked{}; + blocked.bits[0] = 1u << (BrokenPipe - 1); + Require(_sigprocmask_nid_postfix(1, &blocked, nullptr) == 0); + Require(WriteToClosedPeer() == PipeError && brokenPipeSignals == 1); + GuestSignalSet pending{}; + Require(sigpending_nid_postfix(&pending) == 0 && (pending.bits[0] & blocked.bits[0]) != 0); + Require(_sigprocmask_nid_postfix(2, &blocked, nullptr) == 0 && brokenPipeSignals == 2); + const KernelTimespec poll{0, 0}; + Require(_sigprocmask_nid_postfix(1, &blocked, nullptr) == 0); + Require(WriteToClosedPeer() == PipeError); + Require(sigtimedwait_nid_postfix(&blocked, nullptr, &poll) == BrokenPipe); + Require(signal_nid_postfix(BrokenPipe, ignore) == CountBrokenPipe); + Require(WriteToClosedPeer() == PipeError); + *__error_nid_postfix() = 0; + Require(sigtimedwait_nid_postfix(&blocked, nullptr, &poll) == -1 && *__error_nid_postfix() == TryAgain); + Require(_sigprocmask_nid_postfix(2, &blocked, nullptr) == 0 && brokenPipeSignals == 2); + Require(signal_nid_postfix(BrokenPipe, nullptr) == ignore); + return 0; +} diff --git a/core/libs/tests/GuestBsearchPacked.cpp b/core/libs/tests/GuestBsearchPacked.cpp new file mode 100644 index 0000000000..a8a4ecb915 --- /dev/null +++ b/core/libs/tests/GuestBsearchPacked.cpp @@ -0,0 +1,41 @@ +#include "prx/libc/include/Api.hpp" +#include +#include +#include +#include +#include +#include +#include + +static void Require(bool value, std::source_location location = std::source_location::current()) { + if (!value) { + std::fprintf(stderr, "check failed at line %u\n", location.line()); + std::abort(); + } +} + +static int APS5_VABI Compare(const void* key, const void* record) { + const auto left = *static_cast(key); + const auto right = *static_cast(record); + return (left > right) - (left < right); +} + +int main() { + const std::array keys{1, 3, 3, 7, 9, 11, 13, 17, 21}; + for (std::size_t stride : {1, 3, 5, 7, 17}) { + for (std::size_t offset : {0, 1, 2, 5}) { + std::vector records(offset + keys.size() * stride + 7, 0xa5); + auto* base = records.data() + offset; + for (std::size_t i = 0; i < keys.size(); ++i) base[i * stride] = keys[i]; + const auto before = records; + for (unsigned char key = 0; key < 24; ++key) { + auto* found = static_cast(bsearch_nid_postfix(&key, base, keys.size(), stride, Compare)); + const bool present = std::find(keys.begin(), keys.end(), key) != keys.end(); + Require((found != nullptr) == present); + if (found) Require(found >= base && found < base + keys.size() * stride && + (found - base) % stride == 0 && *found == key); + Require(records == before); + } + } + } +} diff --git a/core/libs/tests/GuestBzeroExtent.cpp b/core/libs/tests/GuestBzeroExtent.cpp new file mode 100644 index 0000000000..c53a4c8d41 --- /dev/null +++ b/core/libs/tests/GuestBzeroExtent.cpp @@ -0,0 +1,81 @@ +#include "prx/libc/include/Api.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static void Require(bool condition) { + if (!condition) { + std::fputs("GuestBzeroExtent check failed\n", stderr); + std::abort(); + } +} + +#ifdef _WIN32 +#include +#else +#include +#include +#endif + +class Buffer { + unsigned char* allocation; + std::size_t page; +public: + Buffer() { +#ifdef _WIN32 + SYSTEM_INFO info{}; + GetSystemInfo(&info); + page = info.dwPageSize; + allocation = static_cast(VirtualAlloc(nullptr, page * 3, MEM_RESERVE | MEM_COMMIT, PAGE_NOACCESS)); + Require(allocation != nullptr); + DWORD previous; + Require(VirtualProtect(allocation + page, page, PAGE_READWRITE, &previous) != 0); +#else + const long size = sysconf(_SC_PAGESIZE); + Require(size > 0); + page = static_cast(size); + allocation = static_cast(mmap(nullptr, page * 3, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)); + Require(allocation != MAP_FAILED); + Require(mprotect(allocation + page, page, PROT_READ | PROT_WRITE) == 0); +#endif + std::memset(allocation + page, 0xa5, page); + } + ~Buffer() { +#ifdef _WIN32 + Require(VirtualFree(allocation, 0, MEM_RELEASE) != 0); +#else + Require(munmap(allocation, page * 3) == 0); +#endif + } + Buffer(const Buffer&) = delete; + Buffer& operator=(const Buffer&) = delete; + template + T* At(std::size_t count) { + Require(count <= (page - 32) / sizeof(T)); + return reinterpret_cast(allocation + page * 2 - (std::max)(count, std::size_t{1}) * sizeof(T)); + } +}; + +template +static void Prefix(const T* data) { + for (int index = 1; index <= 8; ++index) Require(data[-index] == static_cast(sizeof(T) == 1 ? 0xa5 : 0xa5a5)); +} + +int main() { + Buffer output; + for (std::size_t length = 0; length <= 256; ++length) { + auto* destination = output.At(length); + const auto extent = (std::max)(length, std::size_t{1}); + std::memset(destination - 16, 0xa5, extent + 16); + bzero_nid_postfix(destination, length); + for (std::size_t i = 0; i < length; ++i) Require(destination[i] == 0); + if (!length) Require(destination[0] == 0xa5); + Prefix(destination); + } +} diff --git a/core/libs/tests/GuestC23Bits.cpp b/core/libs/tests/GuestC23Bits.cpp new file mode 100644 index 0000000000..7b6706b69b --- /dev/null +++ b/core/libs/tests/GuestC23Bits.cpp @@ -0,0 +1,140 @@ +#include "prx/libc/include/Api.hpp" +#include +#include +#include +#include +#include + +static void Require(bool condition) { + if (condition) return; + std::abort(); +} + +static std::uint64_t Reference(std::uint64_t value, unsigned width, unsigned operation) { + if (operation < 4) { + const bool leading = operation < 2; + const unsigned wanted = operation & 1u; + for (unsigned position = 1; position <= width; ++position) { + const unsigned bit = leading ? width - position : position - 1; + if (((value >> bit) & 1u) == wanted) return position; + } + return 0; + } + if (operation == 4) { + unsigned ones = 0; + for (unsigned bit = 0; bit < width; ++bit) ones += (value >> bit) & 1u; + return ones == 1; + } + if (operation == 5) { + unsigned bits = 0; + for (auto remaining = value; remaining; remaining >>= 1) ++bits; + return bits; + } + if (operation == 6) { + if (!value) return 0; + std::uint64_t power = 1; + for (auto remaining = value; remaining > 1; remaining >>= 1) power *= 2; + return power; + } + const auto maximum = width == 64 ? UINT64_MAX : (std::uint64_t{1} << width) - 1; + std::uint64_t power = 1; + while (power < value) { + if (power > maximum / 2) return 0; + power *= 2; + } + return power; +} + +template +static void Check(TFunction function, unsigned operation, const char* name) { + constexpr unsigned width = std::numeric_limits::digits; + const auto maximum = std::numeric_limits::max(); + const auto check = [&](TValue value) { + const auto expected = Reference(value, width, operation); + const auto actual = static_cast(function(value)); + if (actual == expected) return; + std::fprintf(stderr, "%s input %llx expected %llx got %llx\n", name, + static_cast(value), static_cast(expected), + static_cast(actual)); + std::abort(); + }; + check(0); + check(1); + check(maximum); + check(static_cast(0xaaaaaaaaaaaaaaaaULL)); + check(static_cast(0x5555555555555555ULL)); + if constexpr (width <= 16) { + for (std::uint32_t value = 0; value <= maximum; ++value) check(static_cast(value)); + } + for (unsigned bit = 0; bit < width; ++bit) { + const auto one = static_cast(TValue{1} << bit); + check(one); + check(static_cast(one - 1)); + check(static_cast(one + 1)); + check(static_cast(~one)); + for (unsigned other = 0; other < width; ++other) { + const auto pair = static_cast(one | (TValue{1} << other)); + check(pair); + check(static_cast(~pair)); + } + } + if constexpr (width > 16) { + std::uint64_t state = 0x8c314fb296ea570dULL; + for (unsigned index = 0; index < 100000; ++index) { + state ^= state << 13; + state ^= state >> 7; + state ^= state << 17; + check(static_cast(state)); + check(static_cast(~state)); + } + } +} + +int main(int argc, char** argv) { + Require(argc == 2); + const int saved = *__error_nid_postfix(); + *__error_nid_postfix() = 87; + if (std::strcmp(argv[1], "first_leading_zero_uc") == 0) Check(stdc_first_leading_zero_uc_nid_postfix, 0, argv[1]); + else if (std::strcmp(argv[1], "first_leading_zero_us") == 0) Check(stdc_first_leading_zero_us_nid_postfix, 0, argv[1]); + else if (std::strcmp(argv[1], "first_leading_zero_ui") == 0) Check(stdc_first_leading_zero_ui_nid_postfix, 0, argv[1]); + else if (std::strcmp(argv[1], "first_leading_zero_ul") == 0) Check(stdc_first_leading_zero_ul_nid_postfix, 0, argv[1]); + else if (std::strcmp(argv[1], "first_leading_zero_ull") == 0) Check(stdc_first_leading_zero_ull_nid_postfix, 0, argv[1]); + else if (std::strcmp(argv[1], "first_leading_one_uc") == 0) Check(stdc_first_leading_one_uc_nid_postfix, 1, argv[1]); + else if (std::strcmp(argv[1], "first_leading_one_us") == 0) Check(stdc_first_leading_one_us_nid_postfix, 1, argv[1]); + else if (std::strcmp(argv[1], "first_leading_one_ui") == 0) Check(stdc_first_leading_one_ui_nid_postfix, 1, argv[1]); + else if (std::strcmp(argv[1], "first_leading_one_ul") == 0) Check(stdc_first_leading_one_ul_nid_postfix, 1, argv[1]); + else if (std::strcmp(argv[1], "first_leading_one_ull") == 0) Check(stdc_first_leading_one_ull_nid_postfix, 1, argv[1]); + else if (std::strcmp(argv[1], "first_trailing_zero_uc") == 0) Check(stdc_first_trailing_zero_uc_nid_postfix, 2, argv[1]); + else if (std::strcmp(argv[1], "first_trailing_zero_us") == 0) Check(stdc_first_trailing_zero_us_nid_postfix, 2, argv[1]); + else if (std::strcmp(argv[1], "first_trailing_zero_ui") == 0) Check(stdc_first_trailing_zero_ui_nid_postfix, 2, argv[1]); + else if (std::strcmp(argv[1], "first_trailing_zero_ul") == 0) Check(stdc_first_trailing_zero_ul_nid_postfix, 2, argv[1]); + else if (std::strcmp(argv[1], "first_trailing_zero_ull") == 0) Check(stdc_first_trailing_zero_ull_nid_postfix, 2, argv[1]); + else if (std::strcmp(argv[1], "first_trailing_one_uc") == 0) Check(stdc_first_trailing_one_uc_nid_postfix, 3, argv[1]); + else if (std::strcmp(argv[1], "first_trailing_one_us") == 0) Check(stdc_first_trailing_one_us_nid_postfix, 3, argv[1]); + else if (std::strcmp(argv[1], "first_trailing_one_ui") == 0) Check(stdc_first_trailing_one_ui_nid_postfix, 3, argv[1]); + else if (std::strcmp(argv[1], "first_trailing_one_ul") == 0) Check(stdc_first_trailing_one_ul_nid_postfix, 3, argv[1]); + else if (std::strcmp(argv[1], "first_trailing_one_ull") == 0) Check(stdc_first_trailing_one_ull_nid_postfix, 3, argv[1]); + else if (std::strcmp(argv[1], "has_single_bit_uc") == 0) Check(stdc_has_single_bit_uc_nid_postfix, 4, argv[1]); + else if (std::strcmp(argv[1], "has_single_bit_us") == 0) Check(stdc_has_single_bit_us_nid_postfix, 4, argv[1]); + else if (std::strcmp(argv[1], "has_single_bit_ui") == 0) Check(stdc_has_single_bit_ui_nid_postfix, 4, argv[1]); + else if (std::strcmp(argv[1], "has_single_bit_ul") == 0) Check(stdc_has_single_bit_ul_nid_postfix, 4, argv[1]); + else if (std::strcmp(argv[1], "has_single_bit_ull") == 0) Check(stdc_has_single_bit_ull_nid_postfix, 4, argv[1]); + else if (std::strcmp(argv[1], "bit_width_uc") == 0) Check(stdc_bit_width_uc_nid_postfix, 5, argv[1]); + else if (std::strcmp(argv[1], "bit_width_us") == 0) Check(stdc_bit_width_us_nid_postfix, 5, argv[1]); + else if (std::strcmp(argv[1], "bit_width_ui") == 0) Check(stdc_bit_width_ui_nid_postfix, 5, argv[1]); + else if (std::strcmp(argv[1], "bit_width_ul") == 0) Check(stdc_bit_width_ul_nid_postfix, 5, argv[1]); + else if (std::strcmp(argv[1], "bit_width_ull") == 0) Check(stdc_bit_width_ull_nid_postfix, 5, argv[1]); + else if (std::strcmp(argv[1], "bit_floor_uc") == 0) Check(stdc_bit_floor_uc_nid_postfix, 6, argv[1]); + else if (std::strcmp(argv[1], "bit_floor_us") == 0) Check(stdc_bit_floor_us_nid_postfix, 6, argv[1]); + else if (std::strcmp(argv[1], "bit_floor_ui") == 0) Check(stdc_bit_floor_ui_nid_postfix, 6, argv[1]); + else if (std::strcmp(argv[1], "bit_floor_ul") == 0) Check(stdc_bit_floor_ul_nid_postfix, 6, argv[1]); + else if (std::strcmp(argv[1], "bit_floor_ull") == 0) Check(stdc_bit_floor_ull_nid_postfix, 6, argv[1]); + else if (std::strcmp(argv[1], "bit_ceil_uc") == 0) Check(stdc_bit_ceil_uc_nid_postfix, 7, argv[1]); + else if (std::strcmp(argv[1], "bit_ceil_us") == 0) Check(stdc_bit_ceil_us_nid_postfix, 7, argv[1]); + else if (std::strcmp(argv[1], "bit_ceil_ui") == 0) Check(stdc_bit_ceil_ui_nid_postfix, 7, argv[1]); + else if (std::strcmp(argv[1], "bit_ceil_ul") == 0) Check(stdc_bit_ceil_ul_nid_postfix, 7, argv[1]); + else if (std::strcmp(argv[1], "bit_ceil_ull") == 0) Check(stdc_bit_ceil_ull_nid_postfix, 7, argv[1]); + else Require(false); + Require(*__error_nid_postfix() == 87); + *__error_nid_postfix() = saved; +} diff --git a/core/libs/tests/GuestCairoMatrix.cpp b/core/libs/tests/GuestCairoMatrix.cpp new file mode 100644 index 0000000000..de02f4f39a --- /dev/null +++ b/core/libs/tests/GuestCairoMatrix.cpp @@ -0,0 +1,121 @@ +#include "prx/libcairo/include/Matrix.hpp" +#include +#include +#include +#include +#include +#include + +using MatrixInit = void (APS5_VABI*)(CairoMatrix*, double, double, double, double, double, double); +using MatrixMultiply = void (APS5_VABI*)(CairoMatrix*, const CairoMatrix*, const CairoMatrix*); +using MatrixInvert = int (APS5_VABI*)(CairoMatrix*); +static_assert(std::is_same_v); +static_assert(std::is_same_v); +static_assert(std::is_same_v); + +namespace { +void Check(bool ok, int line) { + if (!ok) { + std::fprintf(stderr, "Cairo matrix guest test failed at line %d\n", line); + std::abort(); + } +} +#define Require(condition) Check((condition), __LINE__) + +bool Close(double a, double b) { + return std::abs(a - b) <= 1e-11 * (1 + std::abs(a) + std::abs(b)); +} + +void Matches(const CairoMatrix& a, const std::array& expected) { + const std::array actual{a.xx, a.yx, a.xy, a.yy, a.x0, a.y0}; + for (std::size_t i = 0; i < 6; ++i) Require(Close(actual[i], expected[i])); +} + +void TestConstruction() { + CairoMatrix a{}; + cairo_matrix_init(&a, 2, 3, 4, 5, 6, 7); + Matches(a, {2, 3, 4, 5, 6, 7}); + cairo_matrix_init_identity(&a); + Matches(a, {1, 0, 0, 1, 0, 0}); + cairo_matrix_init_translate(&a, 8, -3); + Matches(a, {1, 0, 0, 1, 8, -3}); +} + +void TestMultiplicationAndAliasing() { + const CairoMatrix a{2, 3, 4, 5, 6, 7}; + const CairoMatrix b{11, 13, 17, 19, 23, 29}; + CairoMatrix out{}; + cairo_matrix_multiply(&out, &a, &b); + Matches(out, {73, 83, 129, 147, 208, 240}); + CairoMatrix inA = a; + cairo_matrix_multiply(&inA, &inA, &b); + Matches(inA, {73, 83, 129, 147, 208, 240}); + CairoMatrix inB = b; + cairo_matrix_multiply(&inB, &a, &inB); + Matches(inB, {73, 83, 129, 147, 208, 240}); + CairoMatrix square = a; + cairo_matrix_multiply(&square, &square, &square); + Matches(square, {16, 21, 28, 37, 46, 60}); +} + +void TestTransforms() { + CairoMatrix m{}; + cairo_matrix_init_identity(&m); + cairo_matrix_translate(&m, 5, -2); + cairo_matrix_scale(&m, 2, 3); + Matches(m, {2, 0, 0, 3, 5, -2}); + double x = 1, y = 2; + cairo_matrix_transform_point(&m, &x, &y); + Require(Close(x, 7) && Close(y, 4)); + x = 1; + y = 2; + cairo_matrix_transform_distance(&m, &x, &y); + Require(Close(x, 2) && Close(y, 6)); + + cairo_matrix_init_identity(&m); + cairo_matrix_scale(&m, 2, 3); + cairo_matrix_translate(&m, 5, -2); + Matches(m, {2, 0, 0, 3, 10, -6}); + + constexpr double pi = 3.1415926535897932384626433832795; + cairo_matrix_init_identity(&m); + cairo_matrix_rotate(&m, pi / 2); + Matches(m, {0, 1, -1, 0, 0, 0}); + x = 2; + y = 1; + cairo_matrix_transform_point(&m, &x, &y); + Require(Close(x, -1) && Close(y, 2)); +} + +void TestInverse() { + CairoMatrix m{2, 0, 0, 4, 6, 8}; + Require(cairo_matrix_invert(&m) == 0); + Matches(m, {0.5, 0, 0, 0.25, -3, -2}); + Require(cairo_matrix_invert(&m) == 0); + Matches(m, {2, 0, 0, 4, 6, 8}); + + const CairoMatrix shear{1, 2, 3, 7, 5, 11}; + m = shear; + Require(cairo_matrix_invert(&m) == 0); + CairoMatrix identity{}; + cairo_matrix_multiply(&identity, &shear, &m); + Matches(identity, {1, 0, 0, 1, 0, 0}); + + m = CairoMatrix{1, 2, 2, 4, 3, 4}; + Require(cairo_matrix_invert(&m) == 5); + Matches(m, {1, 2, 2, 4, 3, 4}); + m = CairoMatrix{0, 0, 0, 1, 3, 4}; + Require(cairo_matrix_invert(&m) == 5); + Matches(m, {0, 0, 0, 1, -3, -4}); + m = CairoMatrix{1, 1, 0, std::numeric_limits::infinity(), 1, 1}; + Require(cairo_matrix_invert(&m) == 5); +} +} + +int main() { + TestConstruction(); + TestMultiplicationAndAliasing(); + TestTransforms(); + TestInverse(); + std::puts("guest cairo matrix tests passed"); +} diff --git a/core/libs/tests/GuestCallTrace.cpp b/core/libs/tests/GuestCallTrace.cpp index 6721aaddac..658a7f5e08 100644 --- a/core/libs/tests/GuestCallTrace.cpp +++ b/core/libs/tests/GuestCallTrace.cpp @@ -60,6 +60,10 @@ static Block APS5_VABI Blocks(std::uint64_t seed) { return {{seed, seed + 1, seed + 2, seed + 3}}; } +static std::uint64_t APS5_VABI Unlabeled(std::uint64_t value) { + return value + 3; +} + static std::uint64_t CurrentThread() { #ifdef _WIN32 return GetCurrentThreadId(); @@ -215,4 +219,25 @@ int main(int argc, char** argv) { Require(!WriteCallCounts_nid_no_patch(reinterpret_cast(unwritable.c_str()))); Require(!WriteCallCounts_nid_no_patch(nullptr) && !WriteCallCounts_nid_no_patch("")); std::filesystem::remove(path); + + Require(Traced(&Unlabeled, "Unlabeled", "") == &Unlabeled); + const auto executable = std::filesystem::path(argv[0]).filename().string(); +#ifdef _WIN32 + Require(_putenv_s("APS5_TRACE_CALLS", executable.c_str()) == 0); +#else + Require(setenv("APS5_TRACE_CALLS", executable.c_str(), 1) == 0); +#endif + const auto unlabeled = Traced(&Unlabeled, "Unlabeled", ""); + Require(unlabeled != &Unlabeled && unlabeled(39) == 42); + const auto tracedCount = CopyCallCounts_nid_no_patch(counts, 8, &total); + Require(tracedCount == 6 && total == 9 + Workers * Calls); + bool inferred = false; + for (std::size_t index = 0; index < tracedCount; ++index) { + const auto& count = counts[index]; + if (count.name != nullptr && std::strcmp(count.name, "Unlabeled") == 0) { + Require(count.calls == 1 && executable == count.library); + inferred = true; + } + } + Require(inferred); } diff --git a/core/libs/tests/GuestCompanionHttpd.cpp b/core/libs/tests/GuestCompanionHttpd.cpp index 05aad8a7d1..7a712c7b14 100644 --- a/core/libs/tests/GuestCompanionHttpd.cpp +++ b/core/libs/tests/GuestCompanionHttpd.cpp @@ -87,6 +87,14 @@ void CheckStates() { Require(sceCompanionHttpdInitialize(reinterpret_cast(param)) == 0, "initializing again after the termination failed"); Require(sceCompanionHttpdRegisterRequestCallback(reinterpret_cast(callback), nullptr) == 0, "the termination kept the callback"); Require(sceCompanionHttpdTerminate() == 0, "the last termination failed"); + Require(sceCompanionHttpdGetEvent(nullptr) == InvalidParam, "a null event after the last termination"); + Require(sceCompanionHttpdGetEvent(event) == NotInitialized, "an event after the last termination"); + Require(sceCompanionHttpdStart() == NotInitialized, "a start after the last termination"); + Require(sceCompanionHttpdStop() == NotInitialized, "a stop after the last termination"); + Require(sceCompanionHttpdRegisterRequestCallback(nullptr, nullptr) == InvalidParam, "a null callback after the last termination"); + Require(sceCompanionHttpdRegisterRequestCallback(reinterpret_cast(callback), nullptr) == NotInitialized, "a callback after the last termination"); + Require(sceCompanionHttpdUnregisterRequestCallback() == NotInitialized, "an unregistration after the last termination"); + Require(sceCompanionHttpdInitialize(nullptr) == InvalidParam, "a null parameter on reinit after termination"); } void CheckResponseBuilders() { diff --git a/core/libs/tests/GuestContentExport.cpp b/core/libs/tests/GuestContentExport.cpp index 07036b1d9c..1936c153db 100644 --- a/core/libs/tests/GuestContentExport.cpp +++ b/core/libs/tests/GuestContentExport.cpp @@ -27,7 +27,21 @@ void Export(std::int32_t expId) { ContentExportCallbackParam content{}; ContentExportCallbackParam thumbnail{}; sceContentExportFromDataWithThumbnail(expId, ¶m, &content, "image/png", &thumbnail, path, sizeof(path)); +} +void ExportData(std::int32_t expId) { + ContentExportParam param{}; + sceContentExportFromData(expId, ¶m, 0, nullptr, nullptr, path, sizeof(path)); +} + +void ExportFile(std::int32_t expId) { + ContentExportParam param{}; + sceContentExportFromFile(expId, ¶m, "source", path, sizeof(path)); +} + +void ExportFileWithThumbnail(std::int32_t expId) { + ContentExportParam param{}; + sceContentExportFromFileWithThumbnail(expId, ¶m, "source", "thumb", path, sizeof(path)); } } @@ -38,6 +52,9 @@ int main() { Require(sceContentExportTerm() == static_cast(0x809D3004u)); Require(sceContentExportStart() == static_cast(0x809D3004u)); Require(Throws([] { sceContentExportFinish(1); })); + Require(Throws([] { ExportData(1); })); + Require(Throws([] { ExportFile(1); })); + Require(Throws([] { ExportFileWithThumbnail(1); })); ContentExportInitParam2 missing = param; missing.free_func = nullptr; Require(Throws([&] { sceContentExportInit2(&missing); })); @@ -65,6 +82,15 @@ int main() { Require(Throws([&] { sceContentExportGetProgress(first); })); Require(Throws([&] { sceContentExportCancel(first); })); Require(Throws([&] { Export(first); })); + Require(Throws([&] { ExportData(first); })); + Require(Throws([&] { ExportFile(first); })); + Require(Throws([&] { ExportFileWithThumbnail(first); })); + Require(Throws([] { ExportData(0); })); + Require(Throws([] { ExportFile(0); })); + Require(Throws([] { ExportFileWithThumbnail(0); })); + Require(Throws([] { ExportData(-1); })); + Require(Throws([] { ExportFile(-1); })); + Require(Throws([] { ExportFileWithThumbnail(-1); })); Require(Throws([] { sceContentExportFinish(0); })); Require(Throws([] { sceContentExportFinish(-1); })); sessions.erase(first); @@ -80,17 +106,8 @@ int main() { Require(sceContentExportTerm() == 0); Require(sceContentExportStart() == static_cast(0x809D3004u)); Require(Throws([&] { sceContentExportFinish(reopened); })); - Require(Throws([] { sceContentExportAddToFavorites(0); })); - Require(Throws([] { sceContentExportDataProvideFunction(); })); - Require(Throws([] { sceContentExportFree(); })); - Require(Throws([] { sceContentExportFrom(); })); - Require(Throws([] { sceContentExportFromFileForSf(0, nullptr, nullptr, nullptr, nullptr, nullptr, 0); })); - Require(Throws([] { sceContentExportFromFileWithContentIdList(0, nullptr, nullptr, nullptr, nullptr, 0, nullptr, 0); })); - Require(Throws([] { sceContentExportFromFileWithTitleIdList(0, nullptr, nullptr, nullptr, nullptr, nullptr, 0); })); - Require(Throws([] { sceContentExportInit(nullptr); })); - Require(Throws([] { sceContentExportMalloc(); })); - Require(Throws([] { sceContentExportRemoveFromFavorites(0); })); - Require(Throws([] { sceContentExportRenameContent(0, nullptr); })); - Require(Throws([] { sceContentExportValidateContents(nullptr, 0); })); + Require(Throws([&] { ExportData(reopened); })); + Require(Throws([&] { ExportFile(reopened); })); + Require(Throws([&] { ExportFileWithThumbnail(reopened); })); } diff --git a/core/libs/tests/GuestConvertKeycode.cpp b/core/libs/tests/GuestConvertKeycode.cpp index 70ba11927f..a68c5deedc 100644 --- a/core/libs/tests/GuestConvertKeycode.cpp +++ b/core/libs/tests/GuestConvertKeycode.cpp @@ -195,6 +195,16 @@ int main() { } SetLanguage("30"); Require(sceConvertKeycodeGetImeKeyboardType(0x10000000, &type) == 0 && type == 4); +#if defined(__linux__) + for (const char* name : {"LC_MESSAGES", "LANG", "LANGUAGE"}) Require(::unsetenv(name) == 0); + Require(::setenv("LC_ALL", "de_DE.UTF-8", 1) == 0); + SetLanguage("auto"); + type = 0xFFFFFFFFu; + Require(sceConvertKeycodeGetImeKeyboardType(0x10000000, &type) == 0 && type == imeTypeByLanguage[4]); + Require(::setenv("LC_ALL", "ja_JP.UTF-8", 1) == 0); + type = 0xFFFFFFFFu; + Require(sceConvertKeycodeGetImeKeyboardType(0x10000000, &type) == 0 && type == imeTypeByLanguage[4]); +#endif SetLanguage(nullptr); Require(ImeKeyboardType::IsPs4TitleId("CUSA00000")); @@ -250,16 +260,26 @@ int main() { char16_t kbdChar = u'?'; Require(sceConvertKeycodeGetCharacterFromKeyboardData(nullptr, 0, 0, 4, &kbdChar) == static_cast(0x80BC0031u)); Require(sceConvertKeycodeGetCharacterFromKeyboardData(&keyData, 0, 0, 4, nullptr) == static_cast(0x80BC0031u)); + constexpr int invalidParameter = static_cast(0x80BC0030u); for (std::int32_t badIndex : {-1, 1, 16}) { kbdChar = u'?'; - bool rejected = false; - try { - sceConvertKeycodeGetCharacterFromKeyboardData(&keyData, badIndex, 0, 4, &kbdChar); - } catch (const std::runtime_error&) { - rejected = true; - } - Require(rejected && kbdChar == u'?'); + Require(sceConvertKeycodeGetCharacterFromKeyboardData(&keyData, badIndex, 0, 4, &kbdChar) == invalidParameter && kbdChar == u'?'); } + for (std::uint32_t badState : {0x1u, 0x200u, 0x20000u}) { + kbdChar = u'?'; + Require(sceConvertKeycodeGetCharacterFromKeyboardData(&keyData, 0, badState, 4, &kbdChar) == invalidParameter && kbdChar == u'?'); + } + for (std::int32_t badLength : {0, 17, -1}) { + KeyboardData badData = keyData; + badData.length = badLength; + Require(sceConvertKeycodeGetCharacterFromKeyboardData(&badData, 0, 0, 4, &kbdChar) == invalidParameter); + } + KeyboardData badLed = keyData; + badLed.led = 8; + Require(sceConvertKeycodeGetCharacterFromKeyboardData(&badLed, 0, 0, 4, &kbdChar) == invalidParameter); + KeyboardData badModifier = keyData; + badModifier.modifier_key = 0x100; + Require(sceConvertKeycodeGetCharacterFromKeyboardData(&badModifier, 0, 0, 40, &kbdChar) == invalidParameter); for (std::uint32_t layout : {31u, 39u}) { kbdChar = u'?'; Require(sceConvertKeycodeGetCharacterFromKeyboardData(&keyData, 0, 0, layout, &kbdChar) == 0 && kbdChar == u'a'); @@ -267,6 +287,42 @@ int main() { Require(sceConvertKeycodeGetCharacterFromKeyboardData(&keyData, 0, 0, 0, &kbdChar) == static_cast(0x80BC001Du)); Require(sceConvertKeycodeGetCharacterFromKeyboardData(&keyData, 0, 0, 40, &kbdChar) == static_cast(0x80BC001Du)); Require(sceConvertKeycodeGetCharacterFromKeyboardData(&keyData, 0, 0, 4, &kbdChar) == 0 && kbdChar == u'a'); - Require(sceConvertKeycodeGetCharacterFromKeyboardData(&keyData, 0, 0x200, 4, &kbdChar) == 0 && kbdChar == u'A'); + Require(sceConvertKeycodeGetCharacterFromKeyboardData(&keyData, 0, 0x80, 4, &kbdChar) == 0 && kbdChar == u'a'); + KeyboardData shifted = keyData; + shifted.modifier_key = 0x02; + Require(sceConvertKeycodeGetCharacterFromKeyboardData(&shifted, 0, 0, 4, &kbdChar) == 0 && kbdChar == u'A'); + KeyboardData capsLock = keyData; + capsLock.led = 2; + Require(sceConvertKeycodeGetCharacterFromKeyboardData(&capsLock, 0, 0, 4, &kbdChar) == 0 && kbdChar == u'A'); + KeyboardData numLock = keyData; + numLock.led = 1; + numLock.key_code[0] = 89; + Require(sceConvertKeycodeGetCharacterFromKeyboardData(&numLock, 0, 0, 4, &kbdChar) == 0 && kbdChar == u'1'); + KeyboardData full = keyData; + full.length = 16; + full.key_code[15] = 5; + full.led = 7; + full.modifier_key = 0xFF; + char16_t expected = u'?'; + Require(sceConvertKeycodeGetCharacter(5, 0x7FF00, 4, &expected) == 0); + kbdChar = u'?'; + Require(sceConvertKeycodeGetCharacterFromKeyboardData(&full, 15, 0, 4, &kbdChar) == 0 && kbdChar == expected); + for (std::uint32_t goodState : {0x4000000u, 0x8000000u, 0x10000000u, 0x1C000080u}) { + for (std::uint32_t layout : {4u, 24u, 30u}) { + Require(sceConvertKeycodeGetCharacter(4, goodState, layout, &expected) == 0); + kbdChar = u'?'; + Require(sceConvertKeycodeGetCharacterFromKeyboardData(&keyData, 0, goodState, layout, &kbdChar) == 0 && kbdChar == expected); + } + } + Require(sceConvertKeycodeGetCharacterFromKeyboardData(&keyData, 0, 0x4000000, 24, &kbdChar) == 0 && kbdChar == 0x3141); + for (std::uint32_t badState : {0x40u, 0x100u, 0x20000000u, 0x80000000u}) { + kbdChar = u'?'; + Require(sceConvertKeycodeGetCharacterFromKeyboardData(&keyData, 0, badState, 4, &kbdChar) == invalidParameter && kbdChar == u'?'); + } + const std::uint32_t oldSdkVersion = 0x03000000u; + std::memcpy(process.data() + 16, &oldSdkVersion, sizeof(oldSdkVersion)); + Require(sceConvertKeycodeGetCharacterFromKeyboardData(&keyData, 0, 0, 38, &kbdChar) == static_cast(0x80BC001Du)); + Require(sceConvertKeycodeGetCharacterFromKeyboardData(&badLed, 0, 0, 38, &kbdChar) == invalidParameter); + std::memcpy(process.data() + 16, &sdkVersion, sizeof(sdkVersion)); return 0; } diff --git a/core/libs/tests/GuestConvertKeycodeTitle.cpp b/core/libs/tests/GuestConvertKeycodeTitle.cpp index 267ff00453..1c7ee9402b 100644 --- a/core/libs/tests/GuestConvertKeycodeTitle.cpp +++ b/core/libs/tests/GuestConvertKeycodeTitle.cpp @@ -55,5 +55,6 @@ int main(int argc, char** argv) { if (sceConvertKeycodeGetCharacter(28, 0, 39, &character) != rejected) return 1; setSdkVersion(sdkVersion); } + if (sceConvertKeycodeGetImeKeyboardType(-1, &type) != static_cast(0x80BC0010u)) return 1; return sceConvertKeycodeGetImeKeyboardType(0x10000000, &type) == 0 && type == 4 ? 0 : 1; } diff --git a/core/libs/tests/GuestDecimalBoundary.cpp b/core/libs/tests/GuestDecimalBoundary.cpp new file mode 100644 index 0000000000..51490bbbd8 --- /dev/null +++ b/core/libs/tests/GuestDecimalBoundary.cpp @@ -0,0 +1,88 @@ +#include "prx/libc/include/Api.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static void Require(bool condition) { + if (!condition) { + std::fputs("GuestDecimalBoundary check failed\n", stderr); + std::abort(); + } +} + +#ifdef _WIN32 +#include +#else +#include +#include +#endif + +class Buffer { + unsigned char* allocation; + std::size_t page; +public: + Buffer() { +#ifdef _WIN32 + SYSTEM_INFO info{}; + GetSystemInfo(&info); + page = info.dwPageSize; + allocation = static_cast(VirtualAlloc(nullptr, page * 3, MEM_RESERVE | MEM_COMMIT, PAGE_NOACCESS)); + Require(allocation != nullptr); + DWORD previous; + Require(VirtualProtect(allocation + page, page, PAGE_READWRITE, &previous) != 0); +#else + const long size = sysconf(_SC_PAGESIZE); + Require(size > 0); + page = static_cast(size); + allocation = static_cast(mmap(nullptr, page * 3, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)); + Require(allocation != MAP_FAILED); + Require(mprotect(allocation + page, page, PROT_READ | PROT_WRITE) == 0); +#endif + std::memset(allocation + page, 0xa5, page); + } + ~Buffer() { +#ifdef _WIN32 + Require(VirtualFree(allocation, 0, MEM_RELEASE) != 0); +#else + Require(munmap(allocation, page * 3) == 0); +#endif + } + Buffer(const Buffer&) = delete; + Buffer& operator=(const Buffer&) = delete; + template + T* At(std::size_t count) { + Require(count <= (page - 32) / sizeof(T)); + return reinterpret_cast(allocation + page * 2 - (std::max)(count, std::size_t{1}) * sizeof(T)); + } +}; + +template +static void Prefix(const T* data) { + for (int index = 1; index <= 8; ++index) Require(data[-index] == static_cast(sizeof(T) == 1 ? 0xa5 : 0xa5a5)); +} + +int main() { + Buffer input; + for (long long value : {-2147483648LL, -9999LL, -1LL, 0LL, 1LL, 9999LL, 2147483647LL}) { + const auto number = std::to_string(value); + for (std::size_t spaces : {0u, 1u, 15u, 64u, 128u}) { + for (const std::string suffix : {std::string{}, std::string{"tail"}}) { + const auto text = std::string(spaces, ' ') + "\t" + (value >= 0 ? "+" : "") + number + suffix; + auto* source = input.At(text.size() + 1); + std::copy(text.begin(), text.end(), source); + source[text.size()] = 0; + const std::string saved(source, text.size() + 1); + Require(atoi_nid_postfix(source) == value); + Require(atol_nid_postfix(source) == value); + Require(atoll_nid_postfix(source) == value); + Require(std::equal(saved.begin(), saved.end(), source)); + } + } + } +} diff --git a/core/libs/tests/GuestDeviceName.cpp b/core/libs/tests/GuestDeviceName.cpp new file mode 100644 index 0000000000..e34dfcf57d --- /dev/null +++ b/core/libs/tests/GuestDeviceName.cpp @@ -0,0 +1,95 @@ +#include "prx/libc/include/Api.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +static void Require(bool condition, int line) { + if (!condition) { + std::fprintf(stderr, "Device name check failed at line %d\n", line); + std::abort(); + } +} +#define Check(value) Require((value), __LINE__) + +namespace { +constexpr std::uint16_t Character = 0020000; +constexpr std::uint16_t Block = 0060000; +constexpr std::uint16_t Regular = 0100000; +constexpr std::uint16_t Directory = 0040000; +constexpr std::uint32_t NoDevice = 0xFFFFFFFFu; + +std::string Name(std::uint32_t device, std::uint16_t type) { + std::array buffer; + buffer.fill('#'); + char* result = devname_r_nid_postfix(device, type, buffer.data(), static_cast(buffer.size())); + Check(result == buffer.data()); + return result; +} +} + +int main() { + Check(Name(NoDevice, Character) == "#NODEV"); + Check(Name(NoDevice, Block) == "#NODEV"); + Check(Name(0x1234, Regular) == "#NODEV"); + Check(Name(0x1234, Directory) == "#NODEV"); + Check(Name(0x1234, 0) == "#NODEV"); + Check(Name(0x1234, Character | 0644) == "#C:0x1234"); + Check(Name(0, Character) == "#C:0"); + Check(Name(1, Character) == "#C:0x1"); + Check(Name(0xF, Character) == "#C:0xf"); + Check(Name(0x10, Character) == "#C:0x10"); + Check(Name(0xDEADBEEFu, Character) == "#C:0xdeadbeef"); + Check(Name(0xFFFFFFFEu, Character) == "#C:0xfffffffe"); + Check(Name(0x61006001u, Block) == "#B:0x61006001"); + Check(Name(0x6001u, Block) == "#B:0x6001"); + Check(Name(0x1234, Block) == "#NODEV"); + Check(Name(0x1234, Block | 0644) == "#NODEV"); + + for (std::size_t length = 0; length <= 16; ++length) { + std::array buffer; + buffer.fill('#'); + char* result = devname_r_nid_postfix(0xABCDu, Character, buffer.data() + 1, static_cast(length)); + Check(result == buffer.data() + 1); + const std::string expected = "#C:0xabcd"; + Check(buffer.front() == '#'); + for (std::size_t index = length + 1; index < buffer.size(); ++index) Check(buffer[index] == '#'); + if (length != 0) Check(std::string(buffer.data() + 1, std::min(length - 1, expected.size())) == expected.substr(0, length - 1)); + if (length != 0 && length <= expected.size()) Check(buffer[length] == '\0'); + if (length > expected.size()) Check(std::strcmp(buffer.data() + 1, expected.c_str()) == 0); + } + + for (std::size_t length = 0; length <= 8; ++length) { + std::array buffer; + buffer.fill('#'); + devname_r_nid_postfix(NoDevice, Character, buffer.data() + 1, static_cast(length)); + const std::string expected = "#NODEV"; + if (length == 0) Check(buffer[1] == '#'); + else { + Check(std::strncmp(buffer.data() + 1, expected.c_str(), std::min(length - 1, expected.size())) == 0); + Check(buffer[1 + std::min(length - 1, expected.size())] == '\0'); + } + } + + std::array negative; + negative.fill('#'); + *__error_nid_postfix() = 0; + Check(devname_r_nid_postfix(0x12, Character, negative.data(), -1) == negative.data()); + Check(*__error_nid_postfix() == 84 && negative[0] == '\0' && negative[1] == '#'); + *__error_nid_postfix() = 0; + Check(devname_r_nid_postfix(0x12, Character, negative.data(), std::numeric_limits::min()) == negative.data()); + Check(*__error_nid_postfix() == 84 && negative[0] == '\0'); + + char* first = devname_nid_postfix(0x77, Character); + Check(first != nullptr && std::strcmp(first, "#C:0x77") == 0); + char* second = devname_nid_postfix(NoDevice, Character); + Check(second == first && std::strcmp(second, "#NODEV") == 0); + char* third = devname_nid_postfix(0x6001u, Block); + Check(third == first && std::strcmp(third, "#B:0x6001") == 0); + Check(std::strlen(devname_nid_postfix(0xFFFFFFFEu, Character)) == 13); +} diff --git a/core/libs/tests/GuestDinkumwareDivision.cpp b/core/libs/tests/GuestDinkumwareDivision.cpp new file mode 100644 index 0000000000..946ab9926a --- /dev/null +++ b/core/libs/tests/GuestDinkumwareDivision.cpp @@ -0,0 +1,192 @@ +#include "prx/libc/include/Api.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace { + +struct Case { + unsigned function; + unsigned rounding; + unsigned denormals; + std::uint64_t first; + std::uint64_t second; + std::uint64_t result; + unsigned flags; +}; + +constexpr Case Cases[] = { + {0, 0, 0, 0x3ff0000000000000ull, 0x4008000000000000ull, 0x3fd5555555555555ull, 32u}, + {0, 0, 0, 0x0000000000000000ull, 0x0000000000000000ull, 0xfff8000000000000ull, 1u}, + {0, 0, 1, 0x3ff0000000000000ull, 0x4008000000000000ull, 0x3fd5555555555555ull, 32u}, + {0, 0, 1, 0x3ff0000000000000ull, 0x0000000000000000ull, 0x7ff0000000000000ull, 4u}, + {0, 0, 2, 0x3ff0000000000000ull, 0x4008000000000000ull, 0x3fd5555555555555ull, 32u}, + {0, 0, 2, 0xbff0000000000000ull, 0x0000000000000000ull, 0xfff0000000000000ull, 4u}, + {0, 0, 3, 0x3ff0000000000000ull, 0x4008000000000000ull, 0x3fd5555555555555ull, 32u}, + {0, 0, 3, 0x0000000000000000ull, 0xbff0000000000000ull, 0x8000000000000000ull, 0u}, + {0, 1, 0, 0x3ff0000000000000ull, 0x4008000000000000ull, 0x3fd5555555555555ull, 32u}, + {0, 1, 0, 0x7ff0000000000001ull, 0x3ff0000000000000ull, 0x7ff8000000000001ull, 1u}, + {0, 1, 1, 0x3ff0000000000000ull, 0x4008000000000000ull, 0x3fd5555555555555ull, 32u}, + {0, 1, 1, 0x7ff8000000001234ull, 0xfff8000000005678ull, 0x7ff8000000001234ull, 0u}, + {0, 1, 2, 0x3ff0000000000000ull, 0x4008000000000000ull, 0x3fd5555555555555ull, 32u}, + {0, 1, 2, 0xfff8000000005678ull, 0x7ff8000000001234ull, 0xfff8000000005678ull, 0u}, + {0, 1, 3, 0x3ff0000000000000ull, 0x4008000000000000ull, 0x3fd5555555555555ull, 32u}, + {0, 1, 3, 0x0000000000000001ull, 0xbff0000000000000ull, 0x8000000000000000ull, 0u}, + {0, 2, 0, 0x3ff0000000000000ull, 0x4008000000000000ull, 0x3fd5555555555556ull, 32u}, + {0, 2, 0, 0x7fefffffffffffffull, 0x3fd0000000000000ull, 0x7ff0000000000000ull, 40u}, + {0, 2, 1, 0x3ff0000000000000ull, 0x4008000000000000ull, 0x3fd5555555555556ull, 32u}, + {0, 2, 1, 0x3fd0000000000000ull, 0x7fefffffffffffffull, 0x0001000000000001ull, 48u}, + {0, 2, 2, 0x3ff0000000000000ull, 0x4008000000000000ull, 0x3fd5555555555556ull, 32u}, + {0, 2, 2, 0x0000000000000000ull, 0x0000000000000000ull, 0xfff8000000000000ull, 1u}, + {0, 2, 3, 0x3ff0000000000000ull, 0x4008000000000000ull, 0x3fd5555555555556ull, 32u}, + {0, 2, 3, 0x3ff0000000000000ull, 0x0000000000000000ull, 0x7ff0000000000000ull, 4u}, + {0, 3, 0, 0x3ff0000000000000ull, 0x4008000000000000ull, 0x3fd5555555555555ull, 32u}, + {0, 3, 0, 0xbff0000000000000ull, 0x0000000000000000ull, 0xfff0000000000000ull, 4u}, + {0, 3, 1, 0x3ff0000000000000ull, 0x4008000000000000ull, 0x3fd5555555555555ull, 32u}, + {0, 3, 1, 0x0000000000000000ull, 0xbff0000000000000ull, 0x8000000000000000ull, 0u}, + {0, 3, 2, 0x3ff0000000000000ull, 0x4008000000000000ull, 0x3fd5555555555555ull, 32u}, + {0, 3, 2, 0x7ff0000000000001ull, 0x3ff0000000000000ull, 0x7ff8000000000001ull, 1u}, + {0, 3, 3, 0x3ff0000000000000ull, 0x4008000000000000ull, 0x3fd5555555555555ull, 32u}, + {0, 3, 3, 0x7ff8000000001234ull, 0xfff8000000005678ull, 0x7ff8000000001234ull, 0u}, + {1, 0, 0, 0x000000003f800000ull, 0x0000000040400000ull, 0x000000003eaaaaabull, 32u}, + {1, 0, 0, 0x0000000000000000ull, 0x0000000000000000ull, 0x00000000ffc00000ull, 1u}, + {1, 0, 1, 0x000000003f800000ull, 0x0000000040400000ull, 0x000000003eaaaaabull, 32u}, + {1, 0, 1, 0x000000003f800000ull, 0x0000000000000000ull, 0x000000007f800000ull, 4u}, + {1, 0, 2, 0x000000003f800000ull, 0x0000000040400000ull, 0x000000003eaaaaabull, 32u}, + {1, 0, 2, 0x00000000bf800000ull, 0x0000000000000000ull, 0x00000000ff800000ull, 4u}, + {1, 0, 3, 0x000000003f800000ull, 0x0000000040400000ull, 0x000000003eaaaaabull, 32u}, + {1, 0, 3, 0x0000000000000000ull, 0x00000000bf800000ull, 0x0000000080000000ull, 0u}, + {1, 1, 0, 0x000000003f800000ull, 0x0000000040400000ull, 0x000000003eaaaaaaull, 32u}, + {1, 1, 0, 0x000000007f800001ull, 0x000000003f800000ull, 0x000000007fc00001ull, 1u}, + {1, 1, 1, 0x000000003f800000ull, 0x0000000040400000ull, 0x000000003eaaaaaaull, 32u}, + {1, 1, 1, 0x000000007fc01234ull, 0x00000000ffc05678ull, 0x000000007fc01234ull, 0u}, + {1, 1, 2, 0x000000003f800000ull, 0x0000000040400000ull, 0x000000003eaaaaaaull, 32u}, + {1, 1, 2, 0x00000000ffc05678ull, 0x000000007fc01234ull, 0x00000000ffc05678ull, 0u}, + {1, 1, 3, 0x000000003f800000ull, 0x0000000040400000ull, 0x000000003eaaaaaaull, 32u}, + {1, 1, 3, 0x0000000000000001ull, 0x00000000bf800000ull, 0x0000000080000000ull, 0u}, + {1, 2, 0, 0x000000003f800000ull, 0x0000000040400000ull, 0x000000003eaaaaabull, 32u}, + {1, 2, 0, 0x000000007f7fffffull, 0x000000003e800000ull, 0x000000007f800000ull, 40u}, + {1, 2, 1, 0x000000003f800000ull, 0x0000000040400000ull, 0x000000003eaaaaabull, 32u}, + {1, 2, 1, 0x000000003e800000ull, 0x000000007f7fffffull, 0x0000000000080001ull, 48u}, + {1, 2, 2, 0x000000003f800000ull, 0x0000000040400000ull, 0x000000003eaaaaabull, 32u}, + {1, 2, 2, 0x0000000000000000ull, 0x0000000000000000ull, 0x00000000ffc00000ull, 1u}, + {1, 2, 3, 0x000000003f800000ull, 0x0000000040400000ull, 0x000000003eaaaaabull, 32u}, + {1, 2, 3, 0x000000003f800000ull, 0x0000000000000000ull, 0x000000007f800000ull, 4u}, + {1, 3, 0, 0x000000003f800000ull, 0x0000000040400000ull, 0x000000003eaaaaaaull, 32u}, + {1, 3, 0, 0x00000000bf800000ull, 0x0000000000000000ull, 0x00000000ff800000ull, 4u}, + {1, 3, 1, 0x000000003f800000ull, 0x0000000040400000ull, 0x000000003eaaaaaaull, 32u}, + {1, 3, 1, 0x0000000000000000ull, 0x00000000bf800000ull, 0x0000000080000000ull, 0u}, + {1, 3, 2, 0x000000003f800000ull, 0x0000000040400000ull, 0x000000003eaaaaaaull, 32u}, + {1, 3, 2, 0x000000007f800001ull, 0x000000003f800000ull, 0x000000007fc00001ull, 1u}, + {1, 3, 3, 0x000000003f800000ull, 0x0000000040400000ull, 0x000000003eaaaaaaull, 32u}, + {1, 3, 3, 0x000000007fc01234ull, 0x00000000ffc05678ull, 0x000000007fc01234ull, 0u}, + {2, 0, 0, 0x4008000000000000ull, 0x0000000000000000ull, 0x3fd5555555555555ull, 32u}, + {2, 0, 0, 0x0000000000000000ull, 0x0000000000000000ull, 0x7ff0000000000000ull, 4u}, + {2, 0, 1, 0x4008000000000000ull, 0x0000000000000000ull, 0x3fd5555555555555ull, 32u}, + {2, 0, 1, 0x8000000000000000ull, 0x0000000000000000ull, 0xfff0000000000000ull, 4u}, + {2, 0, 2, 0x4008000000000000ull, 0x0000000000000000ull, 0x3fd5555555555555ull, 32u}, + {2, 0, 2, 0x0000000000000001ull, 0x0000000000000000ull, 0x7ff0000000000000ull, 42u}, + {2, 0, 3, 0x4008000000000000ull, 0x0000000000000000ull, 0x3fd5555555555555ull, 32u}, + {2, 0, 3, 0x000fffffffffffffull, 0x0000000000000000ull, 0x7ff0000000000000ull, 4u}, + {2, 1, 0, 0x4008000000000000ull, 0x0000000000000000ull, 0x3fd5555555555555ull, 32u}, + {2, 1, 0, 0x0010000000000000ull, 0x0000000000000000ull, 0x7fd0000000000000ull, 0u}, + {2, 1, 1, 0x4008000000000000ull, 0x0000000000000000ull, 0x3fd5555555555555ull, 32u}, + {2, 1, 1, 0x7fefffffffffffffull, 0x0000000000000000ull, 0x0004000000000000ull, 48u}, + {2, 1, 2, 0x4008000000000000ull, 0x0000000000000000ull, 0x3fd5555555555555ull, 32u}, + {2, 1, 2, 0x7ff0000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0u}, + {2, 1, 3, 0x4008000000000000ull, 0x0000000000000000ull, 0x3fd5555555555555ull, 32u}, + {2, 1, 3, 0x7ff0000000000001ull, 0x0000000000000000ull, 0x7ff8000000000001ull, 1u}, + {2, 2, 0, 0x4008000000000000ull, 0x0000000000000000ull, 0x3fd5555555555556ull, 32u}, + {2, 2, 0, 0x7ff8000000001234ull, 0x0000000000000000ull, 0x7ff8000000001234ull, 0u}, + {2, 2, 1, 0x4008000000000000ull, 0x0000000000000000ull, 0x3fd5555555555556ull, 32u}, + {2, 2, 1, 0xfff8000000005678ull, 0x0000000000000000ull, 0xfff8000000005678ull, 0u}, + {2, 2, 2, 0x4008000000000000ull, 0x0000000000000000ull, 0x3fd5555555555556ull, 32u}, + {2, 2, 2, 0x0000000000000000ull, 0x0000000000000000ull, 0x7ff0000000000000ull, 4u}, + {2, 2, 3, 0x4008000000000000ull, 0x0000000000000000ull, 0x3fd5555555555556ull, 32u}, + {2, 2, 3, 0x8000000000000000ull, 0x0000000000000000ull, 0xfff0000000000000ull, 4u}, + {2, 3, 0, 0x4008000000000000ull, 0x0000000000000000ull, 0x3fd5555555555555ull, 32u}, + {2, 3, 0, 0x0000000000000001ull, 0x0000000000000000ull, 0x7fefffffffffffffull, 42u}, + {2, 3, 1, 0x4008000000000000ull, 0x0000000000000000ull, 0x3fd5555555555555ull, 32u}, + {2, 3, 1, 0x000fffffffffffffull, 0x0000000000000000ull, 0x7ff0000000000000ull, 4u}, + {2, 3, 2, 0x4008000000000000ull, 0x0000000000000000ull, 0x3fd5555555555555ull, 32u}, + {2, 3, 2, 0x0010000000000000ull, 0x0000000000000000ull, 0x7fd0000000000000ull, 0u}, + {2, 3, 3, 0x4008000000000000ull, 0x0000000000000000ull, 0x3fd5555555555555ull, 32u}, + {2, 3, 3, 0x7fefffffffffffffull, 0x0000000000000000ull, 0x0000000000000000ull, 48u}, + {3, 0, 0, 0x0000000040400000ull, 0x0000000000000000ull, 0x000000003eaaaaabull, 32u}, + {3, 0, 0, 0x0000000000000000ull, 0x0000000000000000ull, 0x000000007f800000ull, 4u}, + {3, 0, 1, 0x0000000040400000ull, 0x0000000000000000ull, 0x000000003eaaaaabull, 32u}, + {3, 0, 1, 0x0000000080000000ull, 0x0000000000000000ull, 0x00000000ff800000ull, 4u}, + {3, 0, 2, 0x0000000040400000ull, 0x0000000000000000ull, 0x000000003eaaaaabull, 32u}, + {3, 0, 2, 0x0000000000000001ull, 0x0000000000000000ull, 0x000000007f800000ull, 42u}, + {3, 0, 3, 0x0000000040400000ull, 0x0000000000000000ull, 0x000000003eaaaaabull, 32u}, + {3, 0, 3, 0x00000000007fffffull, 0x0000000000000000ull, 0x000000007f800000ull, 4u}, + {3, 1, 0, 0x0000000040400000ull, 0x0000000000000000ull, 0x000000003eaaaaaaull, 32u}, + {3, 1, 0, 0x0000000000800000ull, 0x0000000000000000ull, 0x000000007e800000ull, 0u}, + {3, 1, 1, 0x0000000040400000ull, 0x0000000000000000ull, 0x000000003eaaaaaaull, 32u}, + {3, 1, 1, 0x000000007f7fffffull, 0x0000000000000000ull, 0x0000000000200000ull, 48u}, + {3, 1, 2, 0x0000000040400000ull, 0x0000000000000000ull, 0x000000003eaaaaaaull, 32u}, + {3, 1, 2, 0x000000007f800000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0u}, + {3, 1, 3, 0x0000000040400000ull, 0x0000000000000000ull, 0x000000003eaaaaaaull, 32u}, + {3, 1, 3, 0x000000007f800001ull, 0x0000000000000000ull, 0x000000007fc00001ull, 1u}, + {3, 2, 0, 0x0000000040400000ull, 0x0000000000000000ull, 0x000000003eaaaaabull, 32u}, + {3, 2, 0, 0x000000007fc01234ull, 0x0000000000000000ull, 0x000000007fc01234ull, 0u}, + {3, 2, 1, 0x0000000040400000ull, 0x0000000000000000ull, 0x000000003eaaaaabull, 32u}, + {3, 2, 1, 0x00000000ffc05678ull, 0x0000000000000000ull, 0x00000000ffc05678ull, 0u}, + {3, 2, 2, 0x0000000040400000ull, 0x0000000000000000ull, 0x000000003eaaaaabull, 32u}, + {3, 2, 2, 0x0000000000000000ull, 0x0000000000000000ull, 0x000000007f800000ull, 4u}, + {3, 2, 3, 0x0000000040400000ull, 0x0000000000000000ull, 0x000000003eaaaaabull, 32u}, + {3, 2, 3, 0x0000000080000000ull, 0x0000000000000000ull, 0x00000000ff800000ull, 4u}, + {3, 3, 0, 0x0000000040400000ull, 0x0000000000000000ull, 0x000000003eaaaaaaull, 32u}, + {3, 3, 0, 0x0000000000000001ull, 0x0000000000000000ull, 0x000000007f7fffffull, 42u}, + {3, 3, 1, 0x0000000040400000ull, 0x0000000000000000ull, 0x000000003eaaaaaaull, 32u}, + {3, 3, 1, 0x00000000007fffffull, 0x0000000000000000ull, 0x000000007f800000ull, 4u}, + {3, 3, 2, 0x0000000040400000ull, 0x0000000000000000ull, 0x000000003eaaaaaaull, 32u}, + {3, 3, 2, 0x0000000000800000ull, 0x0000000000000000ull, 0x000000007e800000ull, 0u}, + {3, 3, 3, 0x0000000040400000ull, 0x0000000000000000ull, 0x000000003eaaaaaaull, 32u}, + {3, 3, 3, 0x000000007f7fffffull, 0x0000000000000000ull, 0x0000000000000000ull, 48u}, +}; + +int StandardFlags(unsigned flags) { + return ((flags & 1u) ? FE_INVALID : 0) | ((flags & 4u) ? FE_DIVBYZERO : 0) | + ((flags & 8u) ? FE_OVERFLOW : 0) | ((flags & 16u) ? FE_UNDERFLOW : 0) | + ((flags & 32u) ? FE_INEXACT : 0); +} + +} + +int main() { + std::fenv_t saved; + if (std::fegetenv(&saved) != 0) return 1; + const auto savedCsr = _mm_getcsr(); + const int savedError = *__error_nid_postfix(); + unsigned index = 0; + for (const auto& test : Cases) { + for (unsigned sticky : {0u, 5u, 32u}) { + if (std::fesetenv(FE_DFL_ENV) != 0) return 1; + const unsigned control = 0x1f80u | (test.rounding << 13u) | ((test.denormals & 1u) ? 0x40u : 0u) | ((test.denormals & 2u) ? 0x8000u : 0u); + *__error_nid_postfix() = 97; + _mm_setcsr(control | sticky); + std::uint64_t result = 0; + if (test.function == 0) result = std::bit_cast(_Divide_nid_postfix(std::bit_cast(test.first), std::bit_cast(test.second))); + else if (test.function == 1) result = std::bit_cast(_FDivide_nid_postfix(std::bit_cast(static_cast(test.first)), std::bit_cast(static_cast(test.second)))); + else if (test.function == 2) result = std::bit_cast(_Recip_nid_postfix(std::bit_cast(test.first))); + else result = std::bit_cast(_FRecip_nid_postfix(std::bit_cast(static_cast(test.first)))); + const unsigned csr = _mm_getcsr(); + const int flags = std::fetestexcept(FE_ALL_EXCEPT); + const int error = *__error_nid_postfix(); + if (result != test.result || csr != (control | test.flags | sticky) || flags != StandardFlags(test.flags | sticky) || error != 97) { + std::fprintf(stderr, "Dinkumware division case %u sticky %u: bits %016llx/%016llx csr %x/%x flags %x/%x errno %d\n", index, sticky, + static_cast(result), static_cast(test.result), csr, control | test.flags | sticky, flags, StandardFlags(test.flags | sticky), error); + return 1; + } + } + ++index; + } + if (std::fesetenv(&saved) != 0) return 1; + _mm_setcsr(savedCsr); + *__error_nid_postfix() = savedError; +} diff --git a/core/libs/tests/GuestDirectMemoryForApp.cpp b/core/libs/tests/GuestDirectMemoryForApp.cpp new file mode 100644 index 0000000000..94ac293efc --- /dev/null +++ b/core/libs/tests/GuestDirectMemoryForApp.cpp @@ -0,0 +1,38 @@ +#include "prx/libkernel/include/Api.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/DirectMemory/DirectMemory.hpp" +#include "prx/libkernel/KernelErrors.hpp" +#include "SceTypes.hpp" +#include +#include +#include +#include +#include + +static void Require(bool condition, std::source_location location = std::source_location::current()) { + if (!condition) { + std::fprintf(stderr, "Direct memory for-app check failed at %s:%u\n", location.file_name(), static_cast(location.line())); + std::abort(); + } +} + +int main() { + constexpr std::size_t page = PS5_PAGE_SIZE; + const auto size = static_cast(sceKernelGetDirectMemorySize()); + int64_t phys = 0; + + Require(sceKernelAllocateDirectMemoryForApp(0, size, 0, 0, 0, &phys) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelAllocateDirectMemoryForApp(0, size, page + 1, 0, 0, &phys) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelAllocateDirectMemoryForApp(0, size, page, 0, 0, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelAllocateDirectMemoryForApp(size, 0, page, 0, 0, &phys) == SCE_KERNEL_ERROR_EINVAL); + + phys = -1; + Require(sceKernelAllocateDirectMemoryForApp(0, size, page * 4, 0, 0, &phys) == 0); + Require(phys >= 0); + Require((phys % static_cast(page)) == 0); + Require(phys + static_cast(page * 4) <= size); + std::printf("allocated for-app phys=%lld len=%zu\n", static_cast(phys), page * 4); + Require(sceKernelReleaseDirectMemory(phys, page * 4) == 0); + + return 0; +} diff --git a/core/libs/tests/GuestDivisionPrxAbi.cpp b/core/libs/tests/GuestDivisionPrxAbi.cpp new file mode 100644 index 0000000000..c0f1a58936 --- /dev/null +++ b/core/libs/tests/GuestDivisionPrxAbi.cpp @@ -0,0 +1,159 @@ +#include "prx/libc/include/Api.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static void Require(bool value, std::source_location location = std::source_location::current()) { + if (!value) { + std::fprintf(stderr, "check failed at line %u\n", location.line()); + std::abort(); + } +} + +#ifdef _WIN32 +#include +#else +#include +#endif + +class Module { +#ifdef _WIN32 + HMODULE handle; +#else + void* handle; +#endif +public: + explicit Module(const std::filesystem::path& path) { +#ifdef _WIN32 + handle = LoadLibraryExW(path.c_str(), nullptr, LOAD_WITH_ALTERED_SEARCH_PATH); + if (!handle) std::fprintf(stderr, "LoadLibraryExW failed: %lu\n", GetLastError()); +#else + handle = dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL); + if (!handle) std::fprintf(stderr, "%s\n", dlerror()); +#endif + Require(handle != nullptr); + } + ~Module() { +#ifdef _WIN32 + Require(FreeLibrary(handle) != 0); +#else + Require(dlclose(handle) == 0); +#endif + } + template TFunction Resolve(const std::string& name) { +#ifdef _WIN32 + const auto symbol = GetProcAddress(handle, name.c_str()); +#else + const auto symbol = dlsym(handle, name.c_str()); +#endif + Require(symbol != nullptr); + static_assert(sizeof(TFunction) == sizeof(symbol)); + TFunction function; + std::memcpy(&function, &symbol, sizeof(function)); + return function; + } +}; + +struct Answer { long long numerator, denominator, quotient, remainder; }; +static constexpr Answer small[] = { + {0LL, 1LL, 0LL, 0LL}, + {0LL, 2LL, 0LL, 0LL}, + {0LL, -3LL, 0LL, 0LL}, + {0LL, 17LL, 0LL, 0LL}, + {1LL, 1LL, 1LL, 0LL}, + {1LL, 2LL, 0LL, 1LL}, + {1LL, -3LL, 0LL, 1LL}, + {1LL, 17LL, 0LL, 1LL}, + {-1LL, 1LL, -1LL, 0LL}, + {-1LL, 2LL, 0LL, -1LL}, + {-1LL, -3LL, 0LL, -1LL}, + {-1LL, 17LL, 0LL, -1LL}, + {7LL, 1LL, 7LL, 0LL}, + {7LL, 2LL, 3LL, 1LL}, + {7LL, -3LL, -2LL, 1LL}, + {7LL, 17LL, 0LL, 7LL}, + {-7LL, 1LL, -7LL, 0LL}, + {-7LL, 2LL, -3LL, -1LL}, + {-7LL, -3LL, 2LL, -1LL}, + {-7LL, 17LL, 0LL, -7LL}, + {2147483647LL, 1LL, 2147483647LL, 0LL}, + {2147483647LL, 2LL, 1073741823LL, 1LL}, + {2147483647LL, -3LL, -715827882LL, 1LL}, + {2147483647LL, 17LL, 126322567LL, 8LL}, + {-2147483648LL, 1LL, -2147483648LL, 0LL}, + {-2147483648LL, 2LL, -1073741824LL, 0LL}, + {-2147483648LL, -3LL, 715827882LL, -2LL}, + {-2147483648LL, 17LL, -126322567LL, -9LL} +}; +static constexpr Answer wide[] = { + {0LL, 1LL, 0LL, 0LL}, + {0LL, 2LL, 0LL, 0LL}, + {0LL, -3LL, 0LL, 0LL}, + {0LL, 17LL, 0LL, 0LL}, + {1LL, 1LL, 1LL, 0LL}, + {1LL, 2LL, 0LL, 1LL}, + {1LL, -3LL, 0LL, 1LL}, + {1LL, 17LL, 0LL, 1LL}, + {-1LL, 1LL, -1LL, 0LL}, + {-1LL, 2LL, 0LL, -1LL}, + {-1LL, -3LL, 0LL, -1LL}, + {-1LL, 17LL, 0LL, -1LL}, + {7LL, 1LL, 7LL, 0LL}, + {7LL, 2LL, 3LL, 1LL}, + {7LL, -3LL, -2LL, 1LL}, + {7LL, 17LL, 0LL, 7LL}, + {-7LL, 1LL, -7LL, 0LL}, + {-7LL, 2LL, -3LL, -1LL}, + {-7LL, -3LL, 2LL, -1LL}, + {-7LL, 17LL, 0LL, -7LL}, + {2147483647LL, 1LL, 2147483647LL, 0LL}, + {2147483647LL, 2LL, 1073741823LL, 1LL}, + {2147483647LL, -3LL, -715827882LL, 1LL}, + {2147483647LL, 17LL, 126322567LL, 8LL}, + {-2147483648LL, 1LL, -2147483648LL, 0LL}, + {-2147483648LL, 2LL, -1073741824LL, 0LL}, + {-2147483648LL, -3LL, 715827882LL, -2LL}, + {-2147483648LL, 17LL, -126322567LL, -9LL}, + {1099511627809LL, 2LL, 549755813904LL, 1LL}, + {1099511627809LL, -7LL, -157073089687LL, 0LL}, + {1099511627809LL, 4294967297LL, 255LL, 4294967074LL}, + {-1099511627809LL, 2LL, -549755813904LL, -1LL}, + {-1099511627809LL, -7LL, 157073089687LL, 0LL}, + {-1099511627809LL, 4294967297LL, -255LL, -4294967074LL}, + {9223372036854775807LL, 2LL, 4611686018427387903LL, 1LL}, + {9223372036854775807LL, -7LL, -1317624576693539401LL, 0LL}, + {9223372036854775807LL, 4294967297LL, 2147483647LL, 2147483648LL}, + {-9223372036854775807LL, 2LL, -4611686018427387903LL, -1LL}, + {-9223372036854775807LL, -7LL, 1317624576693539401LL, 0LL}, + {-9223372036854775807LL, 4294967297LL, -2147483647LL, -2147483648LL} +}; + +template +static void Check(TFunction function, const Answer (&answers)[TSize]) { + for (const auto& answer : answers) { + using Result = decltype(function(answer.numerator, answer.denominator)); + struct GuardedResult { std::uint64_t before; Result value; std::uint64_t after; }; + GuardedResult result{0x123456789abcdef0ull, {}, 0xfedcba9876543210ull}; + result.value = function(answer.numerator, answer.denominator); + Require(result.value.quot == answer.quotient && result.value.rem == answer.remainder); + Require(result.before == 0x123456789abcdef0ull && result.after == 0xfedcba9876543210ull); + } +} + +int main(int argc, char** argv) { + Require(argc == 3); + for (int index = 1; index <= 2; ++index) { + Module module(std::filesystem::absolute(argv[index])); + const auto name = [&](const char* symbol) { return index == 1 ? std::string(symbol) + "_nid_postfix" : Nid::ComputeNid(symbol, "libc"); }; + Check(module.Resolve(name("div")), small); + Check(module.Resolve(name("ldiv")), wide); + Check(module.Resolve(name("lldiv")), wide); + Check(module.Resolve(name("imaxdiv")), wide); + } +} diff --git a/core/libs/tests/GuestDownloadDataMount.cpp b/core/libs/tests/GuestDownloadDataMount.cpp index 1c3abe8c03..9b8299d9f1 100644 --- a/core/libs/tests/GuestDownloadDataMount.cpp +++ b/core/libs/tests/GuestDownloadDataMount.cpp @@ -1,4 +1,5 @@ #include "prx/libkernel/AppMetadata/include/AppMetadata.hpp" +#include "prx/libc/include/General.hpp" #include #include #include @@ -51,6 +52,12 @@ int main(int argc, char** argv) { Require(rejected); return 0; } - Require(std::strcmp(GetAppTitleId_nid_postfix().value, mode == "sfo-child" ? "CUSA00000" : "PPSA00000") == 0); + const std::string titleId = GetAppTitleId_nid_postfix().value; + Require(std::strcmp(titleId.c_str(), mode == "sfo-child" ? "CUSA00000" : "PPSA00000") == 0); + const std::string sandboxApp0 = std::string("/mnt/sandbox/") + titleId + "_000/app0"; + Require(ResolvePath_nid_no_patch(sandboxApp0.c_str()) == (std::filesystem::current_path() / "app0").make_preferred()); + const std::string metadataFile = mode == "sfo-child" ? "param.sfo" : "param.json"; + Require(ResolvePath_nid_no_patch((sandboxApp0 + "/sce_sys/" + metadataFile).c_str()) == + (std::filesystem::current_path() / "app0" / "sce_sys" / metadataFile).make_preferred()); std::filesystem::remove_all("download0"); } diff --git a/core/libs/tests/GuestEntitlements.cpp b/core/libs/tests/GuestEntitlements.cpp index 2bf1b61c4e..d43bd30d05 100644 --- a/core/libs/tests/GuestEntitlements.cpp +++ b/core/libs/tests/GuestEntitlements.cpp @@ -12,9 +12,80 @@ #include #include #include +#ifdef _WIN32 +#include +#else +#include +#endif static void Require(bool value) { if (!value) std::abort(); } +static std::string CaptureStderr(void (*body)()) { + std::fflush(stderr); + std::FILE* file = std::tmpfile(); + Require(file != nullptr); +#ifdef _WIN32 + const int saved = _dup(_fileno(stderr)); + _dup2(_fileno(file), _fileno(stderr)); +#else + const int saved = dup(fileno(stderr)); + dup2(fileno(file), fileno(stderr)); +#endif + body(); + std::fflush(stderr); +#ifdef _WIN32 + _dup2(saved, _fileno(stderr)); + _close(saved); +#else + dup2(saved, fileno(stderr)); + close(saved); +#endif + std::rewind(file); + std::string text; + char buffer[4096]; + for (std::size_t read = 0; (read = std::fread(buffer, 1, sizeof(buffer), file)) != 0;) text.append(buffer, read); + std::fclose(file); + return text; +} + +static void QueryUnlistedTwiceAndListedOnce() { + NpEntitlementAccessAddcontEntitlementInfo info{}; + NpUnifiedEntitlementLabel unlisted{}; + std::memcpy(unlisted.data, "LOGGEDADDON", 11); + Require(sceNpEntitlementAccessGetAddcontEntitlementInfo(0, &unlisted, &info) == static_cast(0x80558007)); + Require(sceNpEntitlementAccessGetAddcontEntitlementInfo(0, &unlisted, &info) == static_cast(0x80558007)); + NpUnifiedEntitlementLabel listed{}; + std::memcpy(listed.data, "SECONDADDON", 11); + Require(sceNpEntitlementAccessGetAddcontEntitlementInfo(0, &listed, &info) == 0); +} + +static void QueryUnlisted(const char* text, std::size_t length) { + NpEntitlementAccessAddcontEntitlementInfo info{}; + NpUnifiedEntitlementLabel label{}; + std::memcpy(label.data, text, length); + Require(sceNpEntitlementAccessGetAddcontEntitlementInfo(0, &label, &info) == static_cast(0x80558007)); +} + +static void QueryUnprintable() { + QueryUnlisted("BAD\nNAME\x1b[1m", 13); + QueryUnlisted("SLASH\\ADDON", 11); +} + +static void QueryPastTheLimit() { + for (int i = 0; i < 70; ++i) { + char text[17]{}; + std::snprintf(text, sizeof(text), "CAPADDON%02d", i); + QueryUnlisted(text, std::strlen(text)); + } + QueryUnlisted("LOGGEDADDON", 11); +} + +static std::size_t Occurrences(const std::string& text, std::string_view needle) { + std::size_t count = 0; + for (auto at = text.find(needle); at != std::string::npos; at = text.find(needle, at + needle.size())) ++count; + return count; +} + static void SetEntitlements(const std::filesystem::path& file) { #ifdef _WIN32 Require(_wputenv_s(L"ANYPS5_ENTITLEMENTS", file.wstring().c_str()) == 0); @@ -69,6 +140,18 @@ int main(int argc, char** argv) { NpEntitlementAccessAddcontEntitlementInfo info{}; Require(sceNpEntitlementAccessGetAddcontEntitlementInfo(0, &label, &info) == 0); Require(std::strcmp(info.entitlement_label.data, "SECONDADDON") == 0 && info.package_type == 3 && info.download_status == 4); + const auto log = CaptureStderr(QueryUnlistedTwiceAndListedOnce); + Require(Occurrences(log, "[entitlements] the title asked for the add-on LOGGEDADDON, which ") == 1); + Require(log.find(file.filename().string() + " does not list, so it is reported as not installed\n") != std::string::npos); + Require(Occurrences(log, "[entitlements]") == 1); + const auto escaped = CaptureStderr(QueryUnprintable); + Require(escaped.find("the add-on BAD\\x0aNAME\\x1b[1m, which ") != std::string::npos); + Require(escaped.find("the add-on SLASH\\x5cADDON, which ") != std::string::npos); + Require(Occurrences(escaped, "\n") == 2 && escaped.find('\x1b') == std::string::npos); + const auto capped = CaptureStderr(QueryPastTheLimit); + Require(Occurrences(capped, "the add-on CAPADDON") == 61); + Require(Occurrences(capped, "[entitlements] the title asked for more than 64 add-ons that ") == 1); + Require(Occurrences(capped, "[entitlements]") == 62); for (std::int32_t userId : {1, 0x10000000, std::numeric_limits::min()}) { NpEntitlementAccessAddcontEntitlementInfo individual{}; Require(sceNpEntitlementAccessGetAddcontEntitlementInfoIndividual(userId, 0, &label, &individual) == 0); diff --git a/core/libs/tests/GuestEqueueEvents.cpp b/core/libs/tests/GuestEqueueEvents.cpp index bc00cf8317..ec5b994361 100644 --- a/core/libs/tests/GuestEqueueEvents.cpp +++ b/core/libs/tests/GuestEqueueEvents.cpp @@ -106,6 +106,32 @@ static void VerifyPeriodicTimer() { Require(sceKernelDeleteEqueue(eq) == SCE_OK); } +static void VerifyHRTimerDelete() { + using namespace std::chrono_literals; + int udata = 0; + KernelEvent events[2]{}; + int count = 0; + const KernelUseconds poll = 0; + const KernelTimespec far{3600, 0}; + const KernelTimespec soon{0, 1000000}; + Require(sceKernelDeleteHRTimerEvent(0, 12) == SCE_KERNEL_ERROR_EBADF); + + KernelEqueue eq = 0; + Require(sceKernelCreateEqueue(&eq, "hrtimer delete") == SCE_OK); + Require(sceKernelDeleteHRTimerEvent(eq, 12) == SCE_KERNEL_ERROR_ENOENT); + Require(sceKernelAddHRTimerEvent(eq, 12, &far, &udata) == SCE_OK); + Require(sceKernelAddTimerEvent(eq, 12, 3000000000u, &udata) == SCE_OK); + Require(sceKernelDeleteHRTimerEvent(eq, 12) == SCE_OK); + Require(sceKernelDeleteHRTimerEvent(eq, 12) == SCE_KERNEL_ERROR_ENOENT); + Require(sceKernelDeleteTimerEvent(eq, 12) == SCE_OK); + + Require(sceKernelAddHRTimerEvent(eq, 13, &soon, &udata) == SCE_OK); + SleepAtLeast(5ms); + Require(sceKernelDeleteHRTimerEvent(eq, 13) == SCE_OK); + Require(sceKernelWaitEqueue(eq, events, 2, &count, &poll) == SCE_KERNEL_ERROR_ETIMEDOUT); + Require(sceKernelDeleteEqueue(eq) == SCE_OK); +} + static void VerifyUserEventReportedOncePerWait() { KernelEqueue eq = 0; Require(sceKernelCreateEqueue(&eq, "user") == SCE_OK); @@ -368,8 +394,27 @@ static void VerifyReportedEventsMoveBehindWaitingOnes() { Require(sceKernelDeleteEqueue(eq) == SCE_OK); } +static void VerifyInvalidQueueAttributes() { + KernelEqueue eq = 0; + Require(sceKernelCreateEqueue(nullptr, "invalid") == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelCreateEqueue(&eq, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelCreateEqueue(nullptr, nullptr) == SCE_KERNEL_ERROR_EINVAL); + + Require(sceKernelCreateEqueue(&eq, "bounds") == SCE_OK); + KernelEvent event{}; + int count = -1; + const KernelUseconds timeout = 0; + Require(sceKernelWaitEqueue(eq, &event, 0, &count, &timeout) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelWaitEqueue(eq, &event, -1, &count, &timeout) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelWaitEqueue(eq, &event, 1, nullptr, &timeout) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelWaitEqueue(eq, nullptr, 1, &count, &timeout) == SCE_KERNEL_ERROR_EFAULT); + Require(sceKernelDeleteEqueue(eq) == SCE_OK); +} + int main() { + VerifyInvalidQueueAttributes(); VerifyPeriodicTimer(); + VerifyHRTimerDelete(); VerifyUserEventReportedOncePerWait(); VerifyBlockingWaitReportsLevelEventOnce(); VerifyEdgeUserEventReportedOnce(); diff --git a/core/libs/tests/GuestExplicitMemset.cpp b/core/libs/tests/GuestExplicitMemset.cpp new file mode 100644 index 0000000000..b54f1a441d --- /dev/null +++ b/core/libs/tests/GuestExplicitMemset.cpp @@ -0,0 +1,129 @@ +#include "prx/libc/include/Api.hpp" +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#else +#include +#include +#endif + +static void Check(bool condition, int line) { + if (condition) return; + std::fprintf(stderr, "memset_explicit check failed at line %d\n", line); + std::abort(); +} +#define Require(condition) Check((condition), __LINE__) + +class GuardedPage { + unsigned char* allocation; +public: + std::size_t size; + unsigned char* data; + + GuardedPage() { +#ifdef _WIN32 + SYSTEM_INFO info{}; + GetSystemInfo(&info); + size = info.dwPageSize; + allocation = static_cast(VirtualAlloc(nullptr, size * 3, MEM_RESERVE | MEM_COMMIT, PAGE_NOACCESS)); + Require(allocation != nullptr); + DWORD previous = 0; + Require(VirtualProtect(allocation + size, size, PAGE_READWRITE, &previous) != 0); +#else + const long pageSize = sysconf(_SC_PAGESIZE); + Require(pageSize > 0); + size = static_cast(pageSize); + allocation = static_cast(mmap(nullptr, size * 3, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)); + Require(allocation != MAP_FAILED); + Require(mprotect(allocation + size, size, PROT_READ | PROT_WRITE) == 0); +#endif + data = allocation + size; + } + + GuardedPage(const GuardedPage&) = delete; + GuardedPage& operator=(const GuardedPage&) = delete; + + ~GuardedPage() { +#ifdef _WIN32 + VirtualFree(allocation, 0, MEM_RELEASE); +#else + munmap(allocation, size * 3); +#endif + } +}; + +static void Invoke(void* destination, int value, std::size_t count) { + *__error_nid_postfix() = 97; + Require(memset_explicit_nid_postfix(destination, value, count) == destination); + Require(*__error_nid_postfix() == 97); +} + +static void Verify(unsigned char* bytes, std::size_t size, std::size_t offset, int value, std::size_t count) { + Require(offset <= size && count <= size - offset); + for (std::size_t index = 0; index < size; ++index) bytes[index] = 0xA7; + Invoke(bytes + offset, value, count); + for (std::size_t index = 0; index < size; ++index) { + const auto expected = index >= offset && index - offset < count ? static_cast(value) : 0xA7; + Require(bytes[index] == expected); + } +} + +static void Empty() { + GuardedPage page; + unsigned char byte = 0x35; + for (const int value : {0, -1, INT_MIN, INT_MAX}) { + Invoke(nullptr, value, 0); + Invoke(&byte, value, 0); + Invoke(page.data - 1, value, 0); + Invoke(page.data + page.size, value, 0); + Require(byte == 0x35); + } +} + +static void Values() { + std::array bytes; + constexpr std::array lengths{0, 1, 2, 3, 7, 8, 15, 16, 17, 31, 32, 63, + 64, 65, 127, 128, 255, 256, 257, 511, 512, 1023, 1024, 1056}; + const auto checkValue = [&](int value) { + for (const auto count : lengths) { + for (std::size_t offset = 0; offset < 32; ++offset) Verify(bytes.data(), bytes.size(), offset, value, count); + } + }; + for (int value = 0; value <= 255; ++value) checkValue(value); + for (const int value : {-1025, -513, -256, -1, 256, 511, INT_MIN, INT_MAX}) checkValue(value); +} + +static void Ranges() { + std::array bytes; + for (std::size_t offset = 0; offset < 64; ++offset) { + for (std::size_t count = 0; count <= 256; ++count) { + for (const int value : {0, 0xA7, -1}) Verify(bytes.data(), bytes.size(), offset, value, count); + } + } +} + +static void GuardPages() { + GuardedPage page; + for (std::size_t count = 0; count <= page.size; ++count) { + for (const int value : {0, 0x35, -1}) { + Verify(page.data, page.size, 0, value, count); + Verify(page.data, page.size, page.size - count, value, count); + } + } +} + +int main(int argc, char** argv) { + Require(argc == 2); + if (std::strcmp(argv[1], "empty") == 0) Empty(); + else if (std::strcmp(argv[1], "values") == 0) Values(); + else if (std::strcmp(argv[1], "ranges") == 0) Ranges(); + else if (std::strcmp(argv[1], "guard_pages") == 0) GuardPages(); + else Require(false); + std::puts("memset_explicit return values, bytes, boundaries and errno passed"); +} diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index 548d3d5f71..ab3f86d9b3 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -5,7 +5,9 @@ #include "prx/libc/include/General.hpp" #include "prx/libc/include/FileStream.hpp" #include "prx/libc/include/RenameDot.hpp" +#include "prx/libc/include/WindowsVerbatimPath.hpp" #include "prx/libkernel/File/include/File.hpp" +#include "prx/libkernel/File/include/FileFlags.hpp" #include "prx/libkernel/File/include/NativeStat.hpp" #include "prx/libkernel/KernelErrors.hpp" #include "tests/TemporaryFile.hpp" @@ -173,6 +175,10 @@ static void TestParentTraversal() { checkInvalid("/" + root.generic_string() + "/missing/..", 2); checkInvalid(root.generic_string() + "/missing/../../" + root.generic_string(), 2); AddPathAlias_nid_no_patch("parent-test-mount", std::filesystem::absolute(root).string().c_str()); + const char* missingCreateAlias = "/parent-test-mount/missing-create/"; + Require(sceKernelOpen(missingCreateAlias, SCE_KERNEL_O_WRONLY | SCE_KERNEL_O_CREAT, 0600) == SCE_KERNEL_ERROR_ENOENT); + Require(open_nid_postfix(missingCreateAlias, SCE_KERNEL_O_WRONLY | SCE_KERNEL_O_CREAT, 0600) == -1 && *__error_nid_postfix() == 2); + Require(!std::filesystem::exists(root / "missing-create")); checkInvalid("/PARENT-TEST-MOUNT/missing/..", 2); checkInvalid("/parent-test-mount/present.txt/..", 20); FileStat status{}; @@ -392,8 +398,324 @@ static void TestLongHostPathStat() { for (auto path = directories.rbegin(); path != directories.rend(); ++path) Require(::RemoveDirectoryW(ExtendedPath(*path).c_str()) != 0); } + +static void TestWindowsVerbatimPathLive() { + auto root = std::filesystem::absolute(std::filesystem::temp_directory_path()).lexically_normal(); + if (root.filename().empty()) root = root.parent_path(); + root /= L"anyps5-verbatim-" + std::to_wstring(std::chrono::steady_clock::now().time_since_epoch().count()); + Require(::CreateDirectoryW(root.c_str(), nullptr) != 0); + const auto host = root.string(); + AddPathAlias_nid_no_patch("/verbatim-test", host.c_str()); + + std::error_code error; + const auto dosPath = NativeVerbatimPath_nid_no_patch(root / L"nul.txt", error); + Require(!error); + Require(dosPath.native().starts_with(L"\\\\?\\")); + + const auto dotPath = NativeVerbatimPath_nid_no_patch(root / L"file.", error); + Require(!error); + Require(dotPath.native().starts_with(L"\\\\?\\")); + + const auto spacePath = NativeVerbatimPath_nid_no_patch(root / L"file ", error); + Require(!error); + Require(spacePath.native().starts_with(L"\\\\?\\")); + + const auto normalPath = NativeVerbatimPath_nid_no_patch(root / L"regular.txt", error); + Require(!error); + Require(!normalPath.native().starts_with(L"\\\\?\\")); + + const auto alreadyVerbatim = NativeVerbatimPath_nid_no_patch(dosPath, error); + Require(!error); + Require(alreadyVerbatim == dosPath); + + const int fdDot = sceKernelOpen("/verbatim-test/file.", SCE_KERNEL_O_CREAT | SCE_KERNEL_O_RDWR, 0666); + Require(fdDot >= 0); + sceKernelClose(fdDot); + + FileStat sbDot{}; + Require(sceKernelStat("/verbatim-test/file.", &sbDot) == 0); + Require(sceKernelUnlink("/verbatim-test/file.") == 0); + Require(sceKernelStat("/verbatim-test/file.", &sbDot) == static_cast(0x80020002u)); + + const int fdNul = sceKernelOpen("/verbatim-test/nul", SCE_KERNEL_O_CREAT | SCE_KERNEL_O_RDWR, 0666); + Require(fdNul >= 0); + sceKernelClose(fdNul); + + FileStat sbNul{}; + Require(sceKernelStat("/verbatim-test/nul", &sbNul) == 0); + Require(sceKernelUnlink("/verbatim-test/nul") == 0); + Require(sceKernelStat("/verbatim-test/nul", &sbNul) == static_cast(0x80020002u)); + + const int fdPlain = sceKernelOpen("/verbatim-test/file", SCE_KERNEL_O_CREAT | SCE_KERNEL_O_RDWR, 0666); + Require(fdPlain >= 0); + Require(sceKernelWrite(fdPlain, "A", 1) == 1); + sceKernelClose(fdPlain); + + const int fdSibling = sceKernelOpen("/verbatim-test/file.", SCE_KERNEL_O_CREAT | SCE_KERNEL_O_RDWR, 0666); + Require(fdSibling >= 0); + Require(sceKernelWrite(fdSibling, "BB", 2) == 2); + sceKernelClose(fdSibling); + + FileStat sbPlain{}, sbSibling{}; + Require(sceKernelStat("/verbatim-test/file", &sbPlain) == 0); + Require(sceKernelStat("/verbatim-test/file.", &sbSibling) == 0); + Require(sbPlain.st_size == 1); + Require(sbSibling.st_size == 2); + Require(sceKernelUnlink("/verbatim-test/file") == 0); + Require(sceKernelUnlink("/verbatim-test/file.") == 0); + + Require(sceKernelMkdir("/verbatim-test/d.", 0777) == 0); + const int fdSave = sceKernelOpen("/verbatim-test/d./save", SCE_KERNEL_O_CREAT | SCE_KERNEL_O_RDWR, 0666); + Require(fdSave >= 0); + sceKernelClose(fdSave); + FileStat sbSave{}; + Require(sceKernelStat("/verbatim-test/d./save", &sbSave) == 0); + Require(sceKernelUnlink("/verbatim-test/d./save") == 0); + Require(sceKernelRmdir("/verbatim-test/d.") == 0); + + Require(mkdir_nid_postfix("/verbatim-test/posix_d.", 0777) == 0); + const int fdPosixSave = open_nid_postfix("/verbatim-test/posix_d./save", SCE_KERNEL_O_CREAT | SCE_KERNEL_O_RDWR, 0666); + Require(fdPosixSave >= 0); + close_nid_postfix(fdPosixSave); + FileStat sbPosixSave{}; + Require(sceKernelStat("/verbatim-test/posix_d./save", &sbPosixSave) == 0); + Require(unlink_nid_postfix("/verbatim-test/posix_d./save") == 0); + Require(rmdir_nid_postfix("/verbatim-test/posix_d.") == 0); + + const int fdOrig = sceKernelOpen("/verbatim-test/orig.", SCE_KERNEL_O_CREAT | SCE_KERNEL_O_RDWR, 0666); + Require(fdOrig >= 0); + sceKernelClose(fdOrig); + Require(rename_nid_postfix("/verbatim-test/orig.", "/verbatim-test/renamed.") == 0); + FileStat sbRename{}; + Require(sceKernelStat("/verbatim-test/orig.", &sbRename) == static_cast(0x80020002u)); + Require(sceKernelStat("/verbatim-test/renamed.", &sbRename) == 0); + Require(sceKernelRename("/verbatim-test/renamed.", "/verbatim-test/kernel_renamed.") == 0); + Require(sceKernelStat("/verbatim-test/renamed.", &sbRename) == static_cast(0x80020002u)); + Require(sceKernelStat("/verbatim-test/kernel_renamed.", &sbRename) == 0); + Require(sceKernelUnlink("/verbatim-test/kernel_renamed.") == 0); + + const int fdReach = sceKernelOpen("/verbatim-test/reach.", SCE_KERNEL_O_CREAT | SCE_KERNEL_O_RDWR, 0666); + Require(fdReach >= 0); + sceKernelClose(fdReach); + Require(sceKernelCheckReachability("/verbatim-test/reach.") == 0); + Require(sceKernelUnlink("/verbatim-test/reach.") == 0); + Require(sceKernelCheckReachability("/verbatim-test/reach.") == static_cast(0x80020002u)); + + const int fdTrunc = sceKernelOpen("/verbatim-test/trunc.", SCE_KERNEL_O_CREAT | SCE_KERNEL_O_RDWR, 0666); + Require(fdTrunc >= 0); + Require(sceKernelWrite(fdTrunc, "12345678", 8) == 8); + sceKernelClose(fdTrunc); + Require(sceKernelTruncate_nid_postfix("/verbatim-test/trunc.", 4) == 0); + FileStat sbTrunc{}; + Require(sceKernelStat("/verbatim-test/trunc.", &sbTrunc) == 0); + Require(sbTrunc.st_size == 4); + Require(sceKernelChmod_nid_postfix("/verbatim-test/trunc.", 0644) == 0); + Require(sceKernelUnlink("/verbatim-test/trunc.") == 0); + + Require(sceKernelMkdir("/verbatim-test/aux.dir", 0777) == 0); + const int fdAux = sceKernelOpen("/verbatim-test/aux.dir/file", SCE_KERNEL_O_CREAT | SCE_KERNEL_O_RDWR, 0666); + Require(fdAux >= 0); + sceKernelClose(fdAux); + Require(sceKernelUnlink("/verbatim-test/aux.dir/file") == 0); + Require(sceKernelRmdir("/verbatim-test/aux.dir") == 0); + + const auto rootLength = root.native().size(); + if (rootLength + 2 <= 250) { + const std::string dir250Name(250 - rootLength - 1, 'd'); + const std::string dir250Guest = "/verbatim-test/" + dir250Name; + Require(sceKernelMkdir(dir250Guest.c_str(), 0777) == 0); + Require(sceKernelRmdir(dir250Guest.c_str()) == 0); + } + + RemovePathAlias_nid_no_patch("/verbatim-test"); + ::RemoveDirectoryW(root.c_str()); +} #endif +static void TestWindowsVerbatimPath() { + Require(IsDosDeviceStem_nid_no_patch(std::string_view("nul"))); + Require(IsDosDeviceStem_nid_no_patch(std::string_view("NUL"))); + Require(IsDosDeviceStem_nid_no_patch(std::string_view("con"))); + Require(IsDosDeviceStem_nid_no_patch(std::string_view("CON"))); + Require(IsDosDeviceStem_nid_no_patch(std::string_view("prn"))); + Require(IsDosDeviceStem_nid_no_patch(std::string_view("PRN"))); + Require(IsDosDeviceStem_nid_no_patch(std::string_view("aux"))); + Require(IsDosDeviceStem_nid_no_patch(std::string_view("AUX"))); + for (char digit = '1'; digit <= '9'; ++digit) { + Require(IsDosDeviceStem_nid_no_patch(std::string("com") + digit)); + Require(IsDosDeviceStem_nid_no_patch(std::string("COM") + digit)); + Require(IsDosDeviceStem_nid_no_patch(std::string("lpt") + digit)); + Require(IsDosDeviceStem_nid_no_patch(std::string("LPT") + digit)); + } + Require(IsDosDeviceStem_nid_no_patch(std::string_view("com\xC2\xB9"))); + Require(IsDosDeviceStem_nid_no_patch(std::string_view("COM\xC2\xB9"))); + Require(IsDosDeviceStem_nid_no_patch(std::string_view("com\xC2\xB2"))); + Require(IsDosDeviceStem_nid_no_patch(std::string_view("COM\xC2\xB2"))); + Require(IsDosDeviceStem_nid_no_patch(std::string_view("com\xC2\xB3"))); + Require(IsDosDeviceStem_nid_no_patch(std::string_view("COM\xC2\xB3"))); + Require(IsDosDeviceStem_nid_no_patch(std::string_view("lpt\xC2\xB9"))); + Require(IsDosDeviceStem_nid_no_patch(std::string_view("LPT\xC2\xB9"))); + Require(IsDosDeviceStem_nid_no_patch(std::string_view("lpt\xC2\xB2"))); + Require(IsDosDeviceStem_nid_no_patch(std::string_view("LPT\xC2\xB2"))); + Require(IsDosDeviceStem_nid_no_patch(std::string_view("lpt\xC2\xB3"))); + Require(IsDosDeviceStem_nid_no_patch(std::string_view("LPT\xC2\xB3"))); + Require(IsDosDeviceStem_nid_no_patch(std::wstring_view(L"com\u00b9"))); + Require(IsDosDeviceStem_nid_no_patch(std::wstring_view(L"COM\u00b9"))); + Require(IsDosDeviceStem_nid_no_patch(std::wstring_view(L"com\u00b2"))); + Require(IsDosDeviceStem_nid_no_patch(std::wstring_view(L"COM\u00b2"))); + Require(IsDosDeviceStem_nid_no_patch(std::wstring_view(L"com\u00b3"))); + Require(IsDosDeviceStem_nid_no_patch(std::wstring_view(L"COM\u00b3"))); + Require(IsDosDeviceStem_nid_no_patch(std::wstring_view(L"lpt\u00b9"))); + Require(IsDosDeviceStem_nid_no_patch(std::wstring_view(L"LPT\u00b9"))); + Require(IsDosDeviceStem_nid_no_patch(std::wstring_view(L"lpt\u00b2"))); + Require(IsDosDeviceStem_nid_no_patch(std::wstring_view(L"LPT\u00b2"))); + Require(IsDosDeviceStem_nid_no_patch(std::wstring_view(L"lpt\u00b3"))); + Require(IsDosDeviceStem_nid_no_patch(std::wstring_view(L"LPT\u00b3"))); + Require(!IsDosDeviceStem_nid_no_patch(std::string_view("com\xC2\xB4"))); + Require(!IsDosDeviceStem_nid_no_patch(std::wstring_view(L"com\u00b4"))); + Require(IsDosDeviceStem_nid_no_patch(std::string_view("conin$"))); + Require(IsDosDeviceStem_nid_no_patch(std::string_view("CONIN$"))); + Require(IsDosDeviceStem_nid_no_patch(std::string_view("conout$"))); + Require(IsDosDeviceStem_nid_no_patch(std::string_view("CONOUT$"))); + Require(IsDosDeviceStem_nid_no_patch(std::string_view("con. "))); + Require(IsDosDeviceStem_nid_no_patch(std::string_view("nul "))); + Require(!IsDosDeviceStem_nid_no_patch(std::string_view(""))); + Require(!IsDosDeviceStem_nid_no_patch(std::string_view("null"))); + Require(!IsDosDeviceStem_nid_no_patch(std::string_view("conflict"))); + Require(!IsDosDeviceStem_nid_no_patch(std::string_view("auxiliary"))); + Require(!IsDosDeviceStem_nid_no_patch(std::string_view("com10"))); + Require(!IsDosDeviceStem_nid_no_patch(std::string_view("lpt10"))); + Require(!IsDosDeviceStem_nid_no_patch(std::string_view("regular"))); + Require(!IsDosDeviceStem_nid_no_patch(std::string_view("clock$"))); + + Require(ComponentNeedsVerbatimPrefix_nid_no_patch(std::string_view("nul"))); + Require(ComponentNeedsVerbatimPrefix_nid_no_patch(std::string_view("nul.txt"))); + Require(ComponentNeedsVerbatimPrefix_nid_no_patch(std::string_view("NUL.TXT"))); + Require(ComponentNeedsVerbatimPrefix_nid_no_patch(std::string_view("con.dat"))); + Require(ComponentNeedsVerbatimPrefix_nid_no_patch(std::string_view("aux.bin"))); + Require(ComponentNeedsVerbatimPrefix_nid_no_patch(std::string_view("com1.log"))); + Require(ComponentNeedsVerbatimPrefix_nid_no_patch(std::string_view("lpt9.out"))); + Require(ComponentNeedsVerbatimPrefix_nid_no_patch(std::string_view("conin$.txt"))); + Require(ComponentNeedsVerbatimPrefix_nid_no_patch(std::string_view("CONOUT$.dat"))); + Require(ComponentNeedsVerbatimPrefix_nid_no_patch(std::string_view("con .txt"))); + Require(ComponentNeedsVerbatimPrefix_nid_no_patch(std::string_view("file."))); + Require(ComponentNeedsVerbatimPrefix_nid_no_patch(std::string_view("file "))); + Require(ComponentNeedsVerbatimPrefix_nid_no_patch(std::string_view("name.txt."))); + Require(ComponentNeedsVerbatimPrefix_nid_no_patch(std::string_view("name.txt "))); + Require(ComponentNeedsVerbatimPrefix_nid_no_patch(std::string_view("..."))); + Require(ComponentNeedsVerbatimPrefix_nid_no_patch(std::string_view("com\xC2\xB9.txt"))); + Require(ComponentNeedsVerbatimPrefix_nid_no_patch(std::string_view("LPT\xC2\xB3.out"))); + Require(ComponentNeedsVerbatimPrefix_nid_no_patch(std::wstring_view(L"COM\u00b9.txt"))); + Require(ComponentNeedsVerbatimPrefix_nid_no_patch(std::wstring_view(L"LPT\u00b3.log"))); + + Require(!ComponentNeedsVerbatimPrefix_nid_no_patch(std::string_view(""))); + Require(!ComponentNeedsVerbatimPrefix_nid_no_patch(std::string_view("."))); + Require(!ComponentNeedsVerbatimPrefix_nid_no_patch(std::string_view(".."))); + Require(!ComponentNeedsVerbatimPrefix_nid_no_patch(std::string_view("regular.txt"))); + Require(!ComponentNeedsVerbatimPrefix_nid_no_patch(std::string_view("file.nul"))); + Require(!ComponentNeedsVerbatimPrefix_nid_no_patch(std::string_view(".nul"))); + Require(!ComponentNeedsVerbatimPrefix_nid_no_patch(std::string_view("null.txt"))); + Require(!ComponentNeedsVerbatimPrefix_nid_no_patch(std::string_view("conflict.dat"))); + + Require(PathNeedsVerbatimPrefix_nid_no_patch(std::string_view(R"(C:\games\title\nul)"))); + Require(PathNeedsVerbatimPrefix_nid_no_patch(std::string_view(R"(C:\games\title\nul.txt)"))); + Require(PathNeedsVerbatimPrefix_nid_no_patch(std::string_view(R"(C:\games\title\file.)"))); + Require(PathNeedsVerbatimPrefix_nid_no_patch(std::string_view(R"(C:\games\title\file )"))); + Require(PathNeedsVerbatimPrefix_nid_no_patch(std::string_view(R"(C:\games\nul\save.dat)"))); + Require(PathNeedsVerbatimPrefix_nid_no_patch(std::string_view(R"(C:\games\dir.\save.dat)"))); + Require(PathNeedsVerbatimPrefix_nid_no_patch(std::string_view(R"(C:\games\dir \save.dat)"))); + Require(PathNeedsVerbatimPrefix_nid_no_patch(std::string_view(R"(\\server\share\nul)"))); + Require(PathNeedsVerbatimPrefix_nid_no_patch(std::string_view(R"(\\server\share\file.)"))); + Require(PathNeedsVerbatimPrefix_nid_no_patch(std::string_view("C:/games/title/nul"))); + Require(PathNeedsVerbatimPrefix_nid_no_patch(std::string_view("C:/games/title/file."))); + Require(PathNeedsVerbatimPrefix_nid_no_patch(std::string_view("//server/share/nul"))); + Require(PathNeedsVerbatimPrefix_nid_no_patch(std::string_view("C:nul"))); + Require(PathNeedsVerbatimPrefix_nid_no_patch(std::string_view("C:file."))); + Require(PathNeedsVerbatimPrefix_nid_no_patch(std::string_view("C:file "))); + Require(PathNeedsVerbatimPrefix_nid_no_patch(std::wstring_view(L"COM\u00b9.txt"))); + Require(PathNeedsVerbatimPrefix_nid_no_patch(std::wstring_view(L"LPT\u00b3.log"))); + Require(PathNeedsVerbatimPrefix_nid_no_patch(std::string_view("C:/games/title/com\xC2\xB9.txt"))); + Require(PathNeedsVerbatimPrefix_nid_no_patch(std::wstring_view(L"C:/games/title/COM\u00b9.txt"))); + Require(!PathNeedsVerbatimPrefix_nid_no_patch(std::string_view(R"(C:\games\title\file.txt)"))); + Require(!PathNeedsVerbatimPrefix_nid_no_patch(std::string_view(R"(\\server\share\file.txt)"))); + Require(!PathNeedsVerbatimPrefix_nid_no_patch(std::string_view("C:/games/title/file.txt"))); + Require(!PathNeedsVerbatimPrefix_nid_no_patch(std::string_view("//server/share/file.txt"))); + Require(!PathNeedsVerbatimPrefix_nid_no_patch(std::string_view("C:file.txt"))); + Require(!PathNeedsVerbatimPrefix_nid_no_patch(std::string_view("C:/games/title/com10.txt"))); + Require(!PathNeedsVerbatimPrefix_nid_no_patch(std::wstring_view(L"C:/games/title/COM10.txt"))); + Require(!PathNeedsVerbatimPrefix_nid_no_patch(std::string_view(R"(\\?\C:\games\title\nul)"))); + Require(!PathNeedsVerbatimPrefix_nid_no_patch(std::string_view(R"(\\.\C:\games\title\nul)"))); + Require(!PathNeedsVerbatimPrefix_nid_no_patch(std::string_view("//?/C:/games/title/nul"))); + + Require(FormatVerbatimWindowsPath_nid_no_patch(std::string(R"(C:\games\title\nul)")) == R"(\\?\C:\games\title\nul)"); + Require(FormatVerbatimWindowsPath_nid_no_patch(std::string("C:/games/title/nul")) == R"(\\?\C:\games\title\nul)"); + Require(FormatVerbatimWindowsPath_nid_no_patch(std::string(R"(C:\games\title\file.)")) == R"(\\?\C:\games\title\file.)"); + Require(FormatVerbatimWindowsPath_nid_no_patch(std::string("C:/games/title/file.")) == R"(\\?\C:\games\title\file.)"); + Require(FormatVerbatimWindowsPath_nid_no_patch(std::string(R"(C:\games\title\file )")) == R"(\\?\C:\games\title\file )"); + Require(FormatVerbatimWindowsPath_nid_no_patch(std::string(R"(\\server\share\nul)")) == R"(\\?\UNC\server\share\nul)"); + Require(FormatVerbatimWindowsPath_nid_no_patch(std::string("//server/share/nul")) == R"(\\?\UNC\server\share\nul)"); + Require(FormatVerbatimWindowsPath_nid_no_patch(std::string(R"(\\server\share\file.)")) == R"(\\?\UNC\server\share\file.)"); + Require(FormatVerbatimWindowsPath_nid_no_patch(std::string(R"(\\?\C:\games\title\nul)")) == R"(\\?\C:\games\title\nul)"); + Require(FormatVerbatimWindowsPath_nid_no_patch(std::string(R"(\\?\UNC\server\share\nul)")) == R"(\\?\UNC\server\share\nul)"); + Require(FormatVerbatimWindowsPath_nid_no_patch(std::string("//?/C:/games/title/nul")) == R"(\\?\C:\games\title\nul)"); + Require(FormatVerbatimWindowsPath_nid_no_patch(std::wstring(L"C:\\games\\title\\nul")) == L"\\\\?\\C:\\games\\title\\nul"); + Require(FormatVerbatimWindowsPath_nid_no_patch(std::wstring(L"C:/games/title/nul")) == L"\\\\?\\C:\\games\\title\\nul"); + Require(FormatVerbatimWindowsPath_nid_no_patch(std::wstring(L"\\\\server\\share\\nul")) == L"\\\\?\\UNC\\server\\share\\nul"); + Require(FormatVerbatimWindowsPath_nid_no_patch(std::wstring(L"//server/share/nul")) == L"\\\\?\\UNC\\server\\share\\nul"); + Require(FormatVerbatimWindowsPath_nid_no_patch(std::wstring(L"\\\\?\\C:\\games\\title\\nul")) == L"\\\\?\\C:\\games\\title\\nul"); + + Require(MakeVerbatimWindowsPathForTest_nid_no_patch(std::string_view(R"(C:\games\title\file.txt)")) == R"(C:\games\title\file.txt)"); + Require(MakeVerbatimWindowsPathForTest_nid_no_patch(std::string_view(R"(C:\games\title\nul)")) == R"(\\?\C:\games\title\nul)"); + Require(MakeVerbatimWindowsPathForTest_nid_no_patch(std::string_view("C:/games/title/nul")) == R"(\\?\C:\games\title\nul)"); + Require(MakeVerbatimWindowsPathForTest_nid_no_patch(std::string_view(R"(C:\games\title\con.dat)")) == R"(\\?\C:\games\title\con.dat)"); + Require(MakeVerbatimWindowsPathForTest_nid_no_patch(std::string_view(R"(C:\games\title\aux.bin)")) == R"(\\?\C:\games\title\aux.bin)"); + Require(MakeVerbatimWindowsPathForTest_nid_no_patch(std::string_view(R"(C:\games\title\com1.log)")) == R"(\\?\C:\games\title\com1.log)"); + Require(MakeVerbatimWindowsPathForTest_nid_no_patch(std::string_view(R"(C:\games\title\lpt9.out)")) == R"(\\?\C:\games\title\lpt9.out)"); + Require(MakeVerbatimWindowsPathForTest_nid_no_patch(std::string_view(R"(C:\games\title\file.)")) == R"(\\?\C:\games\title\file.)"); + Require(MakeVerbatimWindowsPathForTest_nid_no_patch(std::string_view("C:/games/title/file.")) == R"(\\?\C:\games\title\file.)"); + Require(MakeVerbatimWindowsPathForTest_nid_no_patch(std::string_view(R"(C:\games\title\file )")) == R"(\\?\C:\games\title\file )"); + Require(MakeVerbatimWindowsPathForTest_nid_no_patch(std::string_view(R"(\\server\share\nul)")) == R"(\\?\UNC\server\share\nul)"); + Require(MakeVerbatimWindowsPathForTest_nid_no_patch(std::string_view("//server/share/nul")) == R"(\\?\UNC\server\share\nul)"); + Require(MakeVerbatimWindowsPathForTest_nid_no_patch(std::string_view(R"(\\server\share\file.)")) == R"(\\?\UNC\server\share\file.)"); + Require(MakeVerbatimWindowsPathForTest_nid_no_patch(std::string_view("C:nul")) == R"(\\?\C:nul)"); + Require(MakeVerbatimWindowsPathForTest_nid_no_patch(std::string_view("C:file.")) == R"(\\?\C:file.)"); + Require(MakeVerbatimWindowsPathForTest_nid_no_patch(std::wstring_view(L"COM\u00b9.txt")) == L"\\\\?\\COM\u00b9.txt"); + Require(MakeVerbatimWindowsPathForTest_nid_no_patch(std::wstring_view(L"LPT\u00b3.log")) == L"\\\\?\\LPT\u00b3.log"); + Require(MakeVerbatimWindowsPathForTest_nid_no_patch(std::string_view("C:/games/title/com\xC2\xB9.txt")) == R"(\\?\C:\games\title\com)" "\xC2\xB9.txt"); + Require(MakeVerbatimWindowsPathForTest_nid_no_patch(std::wstring_view(L"C:/games/title/COM\u00b9.txt")) == L"\\\\?\\C:\\games\\title\\COM\u00b9.txt"); + Require(MakeVerbatimWindowsPathForTest_nid_no_patch(std::string_view(R"(C:\games\title\null.txt)")) == R"(C:\games\title\null.txt)"); + Require(MakeVerbatimWindowsPathForTest_nid_no_patch(std::string_view(R"(C:\games\title\file.nul)")) == R"(C:\games\title\file.nul)"); + Require(MakeVerbatimWindowsPathForTest_nid_no_patch(std::string_view(R"(C:\games\title\com10.txt)")) == R"(C:\games\title\com10.txt)"); + Require(MakeVerbatimWindowsPathForTest_nid_no_patch(std::string_view(R"(\\?\C:\games\title\nul)")) == R"(\\?\C:\games\title\nul)"); + Require(MakeVerbatimWindowsPathForTest_nid_no_patch(std::string_view(R"(\\?\UNC\server\share\nul)")) == R"(\\?\UNC\server\share\nul)"); + const std::string path247 = "C:\\" + std::string(232, 'a') + "\\regular.txt"; + Require(path247.size() == 247); + Require(MakeVerbatimWindowsPathForTest_nid_no_patch(std::string_view(path247)) == path247); + const std::string path248 = "C:\\" + std::string(233, 'a') + "\\regular.txt"; + Require(path248.size() == 248); + Require(MakeVerbatimWindowsPathForTest_nid_no_patch(std::string_view(path248)) == R"(\\?\)" + path248); + const std::string path259 = "C:\\" + std::string(244, 'a') + "\\regular.txt"; + Require(path259.size() == 259); + Require(MakeVerbatimWindowsPathForTest_nid_no_patch(std::string_view(path259)) == R"(\\?\)" + path259); + const std::string path260 = "C:\\" + std::string(245, 'a') + "\\regular.txt"; + Require(path260.size() == 260); + Require(MakeVerbatimWindowsPathForTest_nid_no_patch(std::string_view(path260)) == R"(\\?\)" + path260); + Require(MakeVerbatimWindowsPathForTest_nid_no_patch(std::string_view(path248), WindowsMaxPathLength) == path248); + Require(MakeVerbatimWindowsPathForTest_nid_no_patch(std::string_view(path260), WindowsMaxPathLength) == R"(\\?\)" + path260); + const std::wstring wPath247 = L"C:\\" + std::wstring(232, L'a') + L"\\regular.txt"; + Require(wPath247.size() == 247); + Require(MakeVerbatimWindowsPathForTest_nid_no_patch(std::wstring_view(wPath247)) == wPath247); + const std::wstring wPath248 = L"C:\\" + std::wstring(233, L'a') + L"\\regular.txt"; + Require(wPath248.size() == 248); + Require(MakeVerbatimWindowsPathForTest_nid_no_patch(std::wstring_view(wPath248)) == L"\\\\?\\" + wPath248); + const std::string longPath = "C:\\" + std::string(300, 'a') + "\\regular.txt"; + Require(MakeVerbatimWindowsPathForTest_nid_no_patch(std::string_view(longPath)) == R"(\\?\)" + longPath); + +#ifdef _WIN32 + TestWindowsVerbatimPathLive(); +#endif +} + #ifdef __linux__ static int DirectoryOpens(int watch) { alignas(inotify_event) char buffer[4096]; @@ -436,6 +758,7 @@ int main() { TestParentTraversal(); TestRelinkedModuleNames(); TestRelinkedModuleNameCase(); + TestWindowsVerbatimPath(); #ifdef _WIN32 TestCaseSensitiveHostDirectory(); TestLongHostPathStat(); @@ -1645,6 +1968,7 @@ int main() { Require(sceKernelRmdir(directoryLink.string().c_str()) == static_cast(0x80020014u)); Require(rmdir_nid_postfix(danglingDirectoryLink.string().c_str()) == -1 && *__error_nid_postfix() == 20); Require((GetFileAttributesW(directoryLink.c_str()) & FILE_ATTRIBUTE_REPARSE_POINT) != 0 && std::filesystem::is_directory(linkedDirectory)); + Require(unlink_nid_postfix((root / "directory-li*").string().c_str()) == -1 && *__error_nid_postfix() != 5 && (GetFileAttributesW(directoryLink.c_str()) & FILE_ATTRIBUTE_REPARSE_POINT) != 0); Require(unlink_nid_postfix(directoryLink.string().c_str()) == 0 && unlink_nid_postfix(danglingDirectoryLink.string().c_str()) == 0); } #else diff --git a/core/libs/tests/GuestFindLastTrivial.cpp b/core/libs/tests/GuestFindLastTrivial.cpp new file mode 100644 index 0000000000..4e11d2dd85 --- /dev/null +++ b/core/libs/tests/GuestFindLastTrivial.cpp @@ -0,0 +1,174 @@ +#include "prx/libc/include/Api.hpp" +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#else +#include +#include +#endif + +static void Check(bool condition, int line) { + if (condition) return; + std::fprintf(stderr, "find-last check failed at line %d\n", line); + std::abort(); +} +#define Require(condition) Check((condition), __LINE__) + +class GuardedPage { + unsigned char* allocation; +public: + std::size_t size; + unsigned char* data; + + GuardedPage() { +#ifdef _WIN32 + SYSTEM_INFO info{}; + GetSystemInfo(&info); + size = info.dwPageSize; + allocation = static_cast(VirtualAlloc(nullptr, size * 3, MEM_RESERVE | MEM_COMMIT, PAGE_NOACCESS)); + Require(allocation != nullptr); + DWORD previous; + Require(VirtualProtect(allocation + size, size, PAGE_READWRITE, &previous) != 0); +#else + const long pageSize = sysconf(_SC_PAGESIZE); + Require(pageSize > 0); + size = static_cast(pageSize); + allocation = static_cast(mmap(nullptr, size * 3, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)); + Require(allocation != MAP_FAILED); + Require(mprotect(allocation + size, size, PROT_READ | PROT_WRITE) == 0); +#endif + data = allocation + size; + std::memset(data, 0xff, size); +#ifdef _WIN32 + Require(VirtualProtect(data, size, PAGE_READONLY, &previous) != 0); +#else + Require(mprotect(data, size, PROT_READ) == 0); +#endif + } + + GuardedPage(const GuardedPage&) = delete; + GuardedPage& operator=(const GuardedPage&) = delete; + + ~GuardedPage() { +#ifdef _WIN32 + VirtualFree(allocation, 0, MEM_RELEASE); +#else + munmap(allocation, size * 3); +#endif + } +}; + +template +static void Invoke(TFunction function, const void* first, const void* last, TValue value, const void* expected) { + *__error_nid_postfix() = 97; + Require(function(first, last, value) == expected); + Require(*__error_nid_postfix() == 97); +} + +template +static void Empty(TFunction function) { + GuardedPage page; + for (const void* address : {static_cast(nullptr), static_cast(page.data), + static_cast(page.data - 1), static_cast(page.data + page.size)}) { + for (const TValue value : {TValue{}, TValue{1}, std::numeric_limits::max()}) { + Invoke(function, address, address, value, address); + } + } +} + +template +static void Positions(TFunction function) { + std::array storage; + const TValue zero{}, maximum = std::numeric_limits::max(); + for (std::size_t alignment = 0; alignment < 32; ++alignment) { + auto* first = storage.data() + alignment; + for (std::size_t count = 1; count <= 129; ++count) { + auto* last = first + count * sizeof(TValue); + storage.fill(0xff); + Invoke(function, first, last, zero, last); + Invoke(function, first, last, maximum, last - sizeof(TValue)); + for (std::size_t index = 0; index < count; ++index) { + auto* match = first + index * sizeof(TValue); + std::memcpy(match, &zero, sizeof(zero)); + const auto before = storage; + Invoke(function, first, last, zero, match); + Require(storage == before); + } + Invoke(function, first, last, maximum, last); + } + } +} + +template +static void Values(TFunction function) { + std::array storage{}; + std::uint64_t state = 0xac431bd5708ef269ULL; + const auto checkValue = [&](TValue value) { + for (std::size_t alignment = 0; alignment < 32; ++alignment) { + auto* first = storage.data() + alignment; + auto* last = first + 64 * sizeof(TValue); + const TValue other = static_cast(~value); + for (std::size_t index = 0; index < 64; ++index) std::memcpy(first + index * sizeof(TValue), &other, sizeof(other)); + Invoke(function, first, last, value, last); + for (const std::size_t index : {std::size_t{0}, std::size_t{15}, std::size_t{31}, std::size_t{63}}) { + auto* match = first + index * sizeof(TValue); + std::memcpy(match, &value, sizeof(value)); + const auto before = storage; + Invoke(function, first, last, value, match); + Require(storage == before); + } + } + }; + if constexpr (sizeof(TValue) <= 2) { + for (std::uint32_t value = 0; value <= std::numeric_limits::max(); ++value) checkValue(static_cast(value)); + } else { + for (unsigned bit = 0; bit < sizeof(TValue) * 8; ++bit) { + const auto value = static_cast(TValue{1} << bit); + checkValue(value); + checkValue(static_cast(~value)); + } + for (unsigned index = 0; index < 1024; ++index) { + state ^= state << 13; + state ^= state >> 7; + state ^= state << 17; + checkValue(static_cast(state)); + } + } +} + +template +static void GuardPages(TFunction function) { + GuardedPage page; + for (std::size_t count = 1; count <= page.size / sizeof(TValue); ++count) { + for (const auto* first : {page.data, page.data + page.size - count * sizeof(TValue)}) { + const auto* last = first + count * sizeof(TValue); + Invoke(function, first, last, TValue{}, last); + Invoke(function, first, last, std::numeric_limits::max(), last - sizeof(TValue)); + } + } +} + +template +static void Run(TFunction function, const char* mode) { + if (std::strcmp(mode, "empty") == 0) Empty(function); + else if (std::strcmp(mode, "positions") == 0) Positions(function); + else if (std::strcmp(mode, "values") == 0) Values(function); + else if (std::strcmp(mode, "guard_pages") == 0) GuardPages(function); + else Require(false); +} + +int main(int argc, char** argv) { + Require(argc == 3); + if (std::strcmp(argv[1], "1") == 0) Run(__std_find_last_trivial_1_nid_postfix, argv[2]); + else if (std::strcmp(argv[1], "2") == 0) Run(__std_find_last_trivial_2_nid_postfix, argv[2]); + else if (std::strcmp(argv[1], "4") == 0) Run(__std_find_last_trivial_4_nid_postfix, argv[2]); + else if (std::strcmp(argv[1], "8") == 0) Run(__std_find_last_trivial_8_nid_postfix, argv[2]); + else Require(false); +} diff --git a/core/libs/tests/GuestFios2.cpp b/core/libs/tests/GuestFios2.cpp new file mode 100644 index 0000000000..96ab8caae3 --- /dev/null +++ b/core/libs/tests/GuestFios2.cpp @@ -0,0 +1,566 @@ +#include "prx/libSceFios2/include/Api.hpp" +#include "prx/libkernel/include/Api.hpp" +#include "prx/libc/include/General.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static void Require(bool condition, int line) { + if (!condition) { + std::fprintf(stderr, "guest_fios2 check failed at line %d\n", line); + std::abort(); + } +} +#define REQUIRE(condition) Require((condition), __LINE__) + +static std::atomic g_completed{0}; +static std::atomic g_deleted{0}; +static std::atomic g_lastError{0}; +static std::atomic g_lastOp{0}; + +static int APS5_VABI CountEvents(void* context, SceFiosOp op, SceFiosOpEvent event, int error) { + REQUIRE(context == reinterpret_cast(0x1234)); + g_lastOp = op; + if (event == SCE_FIOS_OPEVENT_COMPLETE) { + g_lastError = error; + ++g_completed; + } else if (event == SCE_FIOS_OPEVENT_DELETE) { + ++g_deleted; + } + return 0; +} + +static SceFiosOpAttr CountingAttr() { + SceFiosOpAttr attr{}; + attr.pCallback = CountEvents; + attr.pCallbackContext = reinterpret_cast(0x1234); + return attr; +} + +static void CheckInitialization() { + REQUIRE(!sceFiosIsInitialized(nullptr)); + std::uint32_t params[16] = {}; + params[0] = (sizeof(params) << 1) | 0u; + params[3] = 0xfeedbeef; + REQUIRE(sceFiosInitialize(reinterpret_cast(params)) == SCE_FIOS_OK); + REQUIRE(sceFiosIsInitialized(nullptr)); + std::uint32_t copy[16] = {}; + REQUIRE(sceFiosIsInitialized(reinterpret_cast(copy))); + REQUIRE(copy[3] == 0xfeedbeef); + params[3] = 0x1; + sceFiosUpdateParameters(reinterpret_cast(params)); + REQUIRE(sceFiosIsInitialized(reinterpret_cast(copy)) && copy[3] == 0x1); +} + +static void CheckQueries(const std::string& directory, const std::string& file) { + REQUIRE(sceFiosExistsSync(nullptr, file.c_str())); + REQUIRE(sceFiosExistsSync(nullptr, directory.c_str())); + REQUIRE(!sceFiosExistsSync(nullptr, (directory + "/missing").c_str())); + REQUIRE(sceFiosFileExistsSync(nullptr, file.c_str())); + REQUIRE(!sceFiosFileExistsSync(nullptr, directory.c_str())); + REQUIRE(sceFiosDirectoryExistsSync(nullptr, directory.c_str())); + REQUIRE(!sceFiosDirectoryExistsSync(nullptr, file.c_str())); + REQUIRE(!sceFiosExistsSync(nullptr, nullptr)); + REQUIRE(!sceFiosExistsSync(nullptr, "")); + + REQUIRE(sceFiosFileGetSizeSync(nullptr, file.c_str()) == 10); + REQUIRE(sceFiosFileGetSizeSync(nullptr, directory.c_str()) == SCE_FIOS_ERROR_NOT_A_FILE); + REQUIRE(sceFiosFileGetSizeSync(nullptr, (directory + "/missing").c_str()) == SCE_FIOS_ERROR_BAD_PATH); + REQUIRE(sceFiosFileGetSizeSync(nullptr, nullptr) == SCE_FIOS_ERROR_BAD_PTR); + REQUIRE(sceFiosFileGetSizeSync(nullptr, "") == SCE_FIOS_ERROR_BAD_PATH); + const std::string tooLong(2000, 'a'); + REQUIRE(sceFiosFileGetSizeSync(nullptr, tooLong.c_str()) == SCE_FIOS_ERROR_PATH_TOO_LONG); + + SceFiosStat stat{}; + std::memset(&stat, 0xaa, sizeof(stat)); + REQUIRE(sceFiosStatSync(nullptr, file.c_str(), &stat) == SCE_FIOS_OK); + REQUIRE(stat.fileSize == 10 && (stat.statFlags & SCE_FIOS_STAT_DIRECTORY) == 0); + REQUIRE(stat.modificationDate > 1000000000ull * 1000000000ull); + REQUIRE((stat.mode & 0170000) == 0100000); + REQUIRE(sceFiosStatSync(nullptr, directory.c_str(), &stat) == SCE_FIOS_OK); + REQUIRE((stat.statFlags & SCE_FIOS_STAT_DIRECTORY) != 0); + REQUIRE(sceFiosStatSync(nullptr, (directory + "/missing").c_str(), &stat) == SCE_FIOS_ERROR_BAD_PATH); + REQUIRE(sceFiosStatSync(nullptr, file.c_str(), nullptr) == SCE_FIOS_ERROR_BAD_PTR); +} + +static void CheckOperationLifecycle(const std::string& file) { + const SceFiosOp op = sceFiosFileGetSize(nullptr, file.c_str()); + REQUIRE(op > 0); + REQUIRE(sceFiosIsValidHandle(op)); + REQUIRE(sceFiosOpIsDone(op)); + REQUIRE(sceFiosOpGetError(op) == SCE_FIOS_OK); + REQUIRE(sceFiosOpWait(op) == SCE_FIOS_OK); + REQUIRE(sceFiosOpGetActualCount(op) == 10); + REQUIRE(sceFiosOpGetPath(op) != nullptr && file == sceFiosOpGetPath(op)); + REQUIRE(sceFiosOpGetAttr(op) != nullptr && !sceFiosOpIsCancelled(op)); + REQUIRE(sceFiosOpGetOffset(op) == 0); + std::vector listed(8); + REQUIRE(sceFiosGetAllOps(listed.data(), static_cast(listed.size())) >= 1); + REQUIRE(listed[0] == op); + REQUIRE(sceFiosOpSyncWaitForIO(op) == 10); + REQUIRE(!sceFiosIsValidHandle(op)); + REQUIRE(sceFiosOpGetError(op) == SCE_FIOS_ERROR_BAD_OP); + REQUIRE(sceFiosOpWait(op) == SCE_FIOS_ERROR_BAD_OP); + REQUIRE(sceFiosOpSyncWait(op) == SCE_FIOS_ERROR_BAD_OP); + REQUIRE(sceFiosOpGetActualCount(op) == SCE_FIOS_ERROR_BAD_OP); + REQUIRE(sceFiosOpGetPath(op) == nullptr); + sceFiosOpDelete(op); + + bool exists = false; + const SceFiosOp existsOp = sceFiosExists(nullptr, file.c_str(), &exists); + REQUIRE(exists && sceFiosOpGetActualCount(existsOp) == 1); + REQUIRE(sceFiosOpSyncWait(existsOp) == 1); + const SceFiosOp missingOp = sceFiosExists(nullptr, "fios2_probe/none", &exists); + REQUIRE(!exists && sceFiosOpSyncWait(missingOp) == 0); + + const SceFiosOp failed = sceFiosFileGetSize(nullptr, "fios2_probe/none"); + REQUIRE(sceFiosOpGetError(failed) == SCE_FIOS_ERROR_BAD_PATH); + REQUIRE(sceFiosOpSyncWait(failed) == SCE_FIOS_ERROR_BAD_PATH); +} + +static void CheckCallbacks(const std::string& file) { + g_completed = 0; + g_deleted = 0; + const SceFiosOpAttr attr = CountingAttr(); + const SceFiosOp op = sceFiosFileExists(&attr, file.c_str(), nullptr); + REQUIRE(g_completed == 1 && g_lastOp == op && g_lastError == SCE_FIOS_OK); + REQUIRE(g_deleted == 0); + sceFiosOpDelete(op); + REQUIRE(g_deleted == 1); + REQUIRE(sceFiosFileExistsSync(&attr, file.c_str())); + REQUIRE(g_completed == 2 && g_deleted == 2); + + const SceFiosOp failed = sceFiosFileGetSize(&attr, "fios2_probe/none"); + REQUIRE(g_completed == 3 && g_lastError == SCE_FIOS_ERROR_BAD_PATH); + sceFiosOpDelete(failed); + + SceFiosOpAttr stored{}; + REQUIRE(!sceFiosGetGlobalDefaultOpAttr(&stored)); + SceFiosOpAttr global = CountingAttr(); + global.userTag = 77; + sceFiosSetGlobalDefaultOpAttr(&global); + REQUIRE(sceFiosGetGlobalDefaultOpAttr(&stored) && stored.userTag == 77); + SceFiosOpAttr effective{}; + sceFiosGetDefaultOpAttr(&effective); + REQUIRE(effective.userTag == 77); + const SceFiosOp usesGlobal = sceFiosFileExists(nullptr, file.c_str(), nullptr); + REQUIRE(sceFiosOpGetAttr(usesGlobal)->userTag == 77 && g_completed == 4); + sceFiosOpDelete(usesGlobal); + + SceFiosOpAttr thread = CountingAttr(); + thread.userTag = 88; + sceFiosSetThreadDefaultOpAttr(&thread); + REQUIRE(sceFiosGetThreadDefaultOpAttr(&stored) && stored.userTag == 88); + std::thread other([&] { + SceFiosOpAttr seen{}; + REQUIRE(!sceFiosGetThreadDefaultOpAttr(&seen)); + sceFiosGetDefaultOpAttr(&seen); + REQUIRE(seen.userTag == 77); + }); + other.join(); + const SceFiosOp usesThread = sceFiosFileExists(nullptr, file.c_str(), nullptr); + REQUIRE(sceFiosOpGetAttr(usesThread)->userTag == 88); + sceFiosOpDelete(usesThread); + sceFiosSetThreadDefaultOpAttr(nullptr); + REQUIRE(!sceFiosGetThreadDefaultOpAttr(&stored)); + sceFiosSetGlobalDefaultOpAttr(nullptr); + REQUIRE(!sceFiosGetGlobalDefaultOpAttr(&stored)); +} + +static void CheckSuspension(const std::string& file) { + REQUIRE(!sceFiosIsSuspended() && sceFiosGetSuspendCount() == 0 && sceFiosIsIdle()); + g_completed = 0; + const SceFiosOpAttr attr = CountingAttr(); + sceFiosSuspend(); + sceFiosSuspend(); + REQUIRE(sceFiosIsSuspended() && sceFiosGetSuspendCount() == 2); + const SceFiosOp first = sceFiosFileGetSize(&attr, file.c_str()); + const SceFiosOp second = sceFiosFileGetSize(&attr, file.c_str()); + REQUIRE(!sceFiosOpIsDone(first) && !sceFiosOpIsDone(second) && !sceFiosIsIdle()); + REQUIRE(sceFiosOpGetError(first) == 1 && g_completed == 0); + sceFiosResume(); + REQUIRE(!sceFiosOpIsDone(first) && sceFiosGetSuspendCount() == 1); + std::thread resumer([] { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + sceFiosResume(); + }); + REQUIRE(sceFiosOpWait(first) == SCE_FIOS_OK); + resumer.join(); + REQUIRE(sceFiosOpIsDone(second) && sceFiosIsIdle() && g_completed == 2); + REQUIRE(sceFiosOpGetActualCount(first) == 10 && sceFiosOpGetActualCount(second) == 10); + sceFiosOpDelete(first); + sceFiosOpDelete(second); + sceFiosResume(); + REQUIRE(sceFiosGetSuspendCount() == 0); + + sceFiosSuspend(); + const SceFiosOp cancelled = sceFiosFileGetSize(&attr, file.c_str()); + const SceFiosOp alsoCancelled = sceFiosFileGetSize(&attr, file.c_str()); + sceFiosOpCancel(cancelled); + REQUIRE(sceFiosOpIsDone(cancelled) && sceFiosOpIsCancelled(cancelled)); + REQUIRE(sceFiosOpGetError(cancelled) == SCE_FIOS_ERROR_CANCELLED); + REQUIRE(g_lastError == SCE_FIOS_ERROR_CANCELLED); + sceFiosCancelAllOps(); + REQUIRE(sceFiosOpGetError(alsoCancelled) == SCE_FIOS_ERROR_CANCELLED && sceFiosIsIdle()); + sceFiosResume(); + REQUIRE(sceFiosOpSyncWaitForIO(cancelled) == SCE_FIOS_ERROR_CANCELLED); + sceFiosOpDelete(alsoCancelled); + + sceFiosSuspend(); + const SceFiosOp abandoned = sceFiosFileGetSize(nullptr, file.c_str()); + sceFiosOpDelete(abandoned); + REQUIRE(!sceFiosIsValidHandle(abandoned)); + sceFiosResume(); + sceFiosResume(); + REQUIRE(sceFiosGetSuspendCount() == 0); +} + +static void CheckMutations(const std::string& directory) { + const std::string sub = directory + "/sub"; + const std::string target = sub + "/a.bin"; + REQUIRE(sceFiosDirectoryCreateSync(nullptr, sub.c_str()) == SCE_FIOS_OK); + REQUIRE(sceFiosDirectoryCreateSync(nullptr, sub.c_str()) == SCE_FIOS_ERROR_ALREADY_EXISTS); + REQUIRE(sceFiosDirectoryCreateSync(nullptr, (directory + "/none/deeper").c_str()) == SCE_FIOS_ERROR_BAD_PATH); + REQUIRE(sceFiosDirectoryCreateSync(nullptr, nullptr) == SCE_FIOS_ERROR_BAD_PTR); + REQUIRE(sceFiosDirectoryExistsSync(nullptr, sub.c_str())); + + REQUIRE(sceFiosFileWriteSync(nullptr, target.c_str(), "hello world", 11, 0) == 11); + char buffer[32] = {}; + REQUIRE(sceFiosFileReadSync(nullptr, target.c_str(), buffer, 11, 0) == 11 && std::memcmp(buffer, "hello world", 11) == 0); + std::memset(buffer, 0, sizeof(buffer)); + REQUIRE(sceFiosFileReadSync(nullptr, target.c_str(), buffer, 5, 6) == 5 && std::memcmp(buffer, "world", 5) == 0); + REQUIRE(sceFiosFileReadSync(nullptr, target.c_str(), buffer, 100, 6) == 5); + REQUIRE(sceFiosFileReadSync(nullptr, target.c_str(), buffer, 4, 500) == 0); + REQUIRE(sceFiosFileReadSync(nullptr, target.c_str(), buffer, 0, 0) == 0); + REQUIRE(sceFiosFileReadSync(nullptr, (sub + "/none").c_str(), buffer, 4, 0) == SCE_FIOS_ERROR_BAD_PATH); + REQUIRE(sceFiosFileReadSync(nullptr, target.c_str(), buffer, -1, 0) == SCE_FIOS_ERROR_BAD_SIZE); + REQUIRE(sceFiosFileReadSync(nullptr, target.c_str(), buffer, 4, -1) == SCE_FIOS_ERROR_BAD_OFFSET); + REQUIRE(sceFiosFileReadSync(nullptr, target.c_str(), nullptr, 4, 0) == SCE_FIOS_ERROR_BAD_PTR); + REQUIRE(sceFiosFileReadSync(nullptr, sub.c_str(), buffer, 4, 0) < 0); + REQUIRE(sceFiosFileWriteSync(nullptr, target.c_str(), "xyzzy", 5, 20) == 5); + REQUIRE(sceFiosFileGetSizeSync(nullptr, target.c_str()) == 25); + REQUIRE(sceFiosFileWriteSync(nullptr, target.c_str(), nullptr, 5, 0) == SCE_FIOS_ERROR_BAD_PTR); + REQUIRE(sceFiosFileWriteSync(nullptr, (sub + "/none/x").c_str(), "a", 1, 0) == SCE_FIOS_ERROR_BAD_PATH); + REQUIRE(sceFiosFileWriteSync(nullptr, (sub + "/created.bin").c_str(), "a", 1, 0) == 1); + REQUIRE(sceFiosFileDeleteSync(nullptr, (sub + "/created.bin").c_str()) == SCE_FIOS_OK); + + std::memset(buffer, 0, sizeof(buffer)); + const SceFiosOp read = sceFiosFileRead(nullptr, target.c_str(), buffer, 5, 6); + REQUIRE(sceFiosOpGetRequestCount(read) == 5 && sceFiosOpGetOffset(read) == 6 && sceFiosOpGetBuffer(read) == buffer); + REQUIRE(sceFiosOpGetActualCount(read) == 5 && sceFiosOpSyncWaitForIO(read) == 5 && std::memcmp(buffer, "world", 5) == 0); + + REQUIRE(sceFiosFileTruncateSync(nullptr, target.c_str(), 4) == SCE_FIOS_OK); + REQUIRE(sceFiosFileGetSizeSync(nullptr, target.c_str()) == 4); + REQUIRE(sceFiosFileTruncateSync(nullptr, target.c_str(), -1) == SCE_FIOS_ERROR_BAD_SIZE); + REQUIRE(sceFiosFileTruncateSync(nullptr, (sub + "/none").c_str(), 4) == SCE_FIOS_ERROR_BAD_PATH); + + const std::string renamed = sub + "/b.bin"; + REQUIRE(sceFiosRenameSync(nullptr, target.c_str(), renamed.c_str()) == SCE_FIOS_OK); + REQUIRE(!sceFiosExistsSync(nullptr, target.c_str()) && sceFiosFileExistsSync(nullptr, renamed.c_str())); + REQUIRE(sceFiosRenameSync(nullptr, target.c_str(), renamed.c_str()) == SCE_FIOS_ERROR_BAD_PATH); + REQUIRE(sceFiosRenameSync(nullptr, renamed.c_str(), nullptr) == SCE_FIOS_ERROR_BAD_PTR); + REQUIRE(sceFiosRenameSync(nullptr, nullptr, renamed.c_str()) == SCE_FIOS_ERROR_BAD_PTR); + + REQUIRE(sceFiosFileDeleteSync(nullptr, sub.c_str()) == SCE_FIOS_ERROR_NOT_A_FILE); + REQUIRE(sceFiosDirectoryDeleteSync(nullptr, renamed.c_str()) == SCE_FIOS_ERROR_NOT_A_DIRECTORY); + REQUIRE(sceFiosDirectoryDeleteSync(nullptr, sub.c_str()) == SCE_FIOS_ERROR_ACCESS); + REQUIRE(sceFiosFileDeleteSync(nullptr, renamed.c_str()) == SCE_FIOS_OK); + REQUIRE(sceFiosFileDeleteSync(nullptr, renamed.c_str()) == SCE_FIOS_ERROR_BAD_PATH); + REQUIRE(sceFiosDirectoryDeleteSync(nullptr, sub.c_str()) == SCE_FIOS_OK); + REQUIRE(sceFiosDirectoryCreateSync(nullptr, sub.c_str()) == SCE_FIOS_OK); + REQUIRE(sceFiosDeleteSync(nullptr, sub.c_str()) == SCE_FIOS_OK); + REQUIRE(sceFiosFileWriteSync(nullptr, renamed.c_str(), "a", 1, 0) == SCE_FIOS_ERROR_BAD_PATH); + REQUIRE(sceFiosFileWriteSync(nullptr, (directory + "/c.bin").c_str(), "a", 1, 0) == 1); + REQUIRE(sceFiosDeleteSync(nullptr, (directory + "/c.bin").c_str()) == SCE_FIOS_OK); + REQUIRE(sceFiosDeleteSync(nullptr, (directory + "/c.bin").c_str()) == SCE_FIOS_ERROR_BAD_PATH); + + g_completed = 0; + const SceFiosOpAttr attr = CountingAttr(); + const SceFiosOp created = sceFiosDirectoryCreate(&attr, sub.c_str()); + REQUIRE(g_completed == 1 && sceFiosOpGetError(created) == SCE_FIOS_OK); + sceFiosOpDelete(created); + const SceFiosOp removed = sceFiosDelete(&attr, sub.c_str()); + REQUIRE(g_completed == 2 && sceFiosOpSyncWait(removed) == SCE_FIOS_OK); +} + +static void CheckFileHandles(const std::string& directory) { + const std::string path = directory + "/handle.bin"; + SceFiosOpenParams writer{}; + writer.openFlags = (SCE_FIOS_O_READ | SCE_FIOS_O_WRITE | SCE_FIOS_O_CREAT); + SceFiosFH fh = 0; + REQUIRE(sceFiosFHOpenSync(nullptr, &fh, path.c_str(), nullptr) == SCE_FIOS_ERROR_BAD_PATH); + REQUIRE(sceFiosFHOpenSync(nullptr, nullptr, path.c_str(), &writer) == SCE_FIOS_ERROR_BAD_PTR); + REQUIRE(sceFiosFHOpenSync(nullptr, &fh, nullptr, &writer) == SCE_FIOS_ERROR_BAD_PTR); + REQUIRE(sceFiosFHOpenSync(nullptr, &fh, directory.c_str(), nullptr) == SCE_FIOS_ERROR_NOT_A_FILE); + REQUIRE(sceFiosFHOpenSync(nullptr, &fh, path.c_str(), &writer) == SCE_FIOS_OK); + REQUIRE(fh > 0 && sceFiosIsValidHandle(fh)); + REQUIRE(path == sceFiosFHGetPath(fh) && sceFiosFHGetOpenParams(fh)->openFlags == writer.openFlags); + REQUIRE(sceFiosFHGetSize(fh) == 0); + std::uint32_t listed[4] = {}; + REQUIRE(sceFiosGetAllFHs(reinterpret_cast(listed), 4) == 1 && listed[0] == static_cast(fh)); + + REQUIRE(sceFiosFHWriteSync(nullptr, fh, "abcdef", 6) == 6); + REQUIRE(sceFiosFHTell(fh) == 6 && sceFiosFHGetSize(fh) == 6); + REQUIRE(sceFiosFHSeek(fh, 1, SCE_FIOS_SEEK_SET) == 1); + char buffer[16] = {}; + REQUIRE(sceFiosFHReadSync(nullptr, fh, buffer, 3) == 3 && std::memcmp(buffer, "bcd", 3) == 0); + REQUIRE(sceFiosFHSeek(fh, -1, SCE_FIOS_SEEK_END) == 5 && sceFiosFHSeek(fh, 1, SCE_FIOS_SEEK_CUR) == 6); + REQUIRE(sceFiosFHReadSync(nullptr, fh, buffer, 3) == 0); + REQUIRE(sceFiosFHSeek(fh, 0, 7) == SCE_FIOS_ERROR_BAD_OFFSET); + REQUIRE(sceFiosFHSeek(fh, -100, SCE_FIOS_SEEK_SET) == SCE_FIOS_ERROR_BAD_OFFSET); + REQUIRE(sceFiosFHPwriteSync(nullptr, fh, "XY", 2, 1) == 2 && sceFiosFHTell(fh) == 6); + std::memset(buffer, 0, sizeof(buffer)); + REQUIRE(sceFiosFHPreadSync(nullptr, fh, buffer, 6, 0) == 6 && std::memcmp(buffer, "aXYdef", 6) == 0); + REQUIRE(sceFiosFHPreadSync(nullptr, fh, buffer, 4, -1) == SCE_FIOS_ERROR_BAD_OFFSET); + REQUIRE(sceFiosFHReadSync(nullptr, fh, buffer, -1) == SCE_FIOS_ERROR_BAD_SIZE); + REQUIRE(sceFiosFHReadSync(nullptr, fh, nullptr, 4) == SCE_FIOS_ERROR_BAD_PTR); + + char first[2] = {}; + char second[3] = {}; + const SceFiosBuffer reads[2] = {{first, sizeof(first)}, {second, sizeof(second)}}; + REQUIRE(sceFiosFHPreadvSync(nullptr, fh, reads, 2, 1) == 5 && std::memcmp(first, "XY", 2) == 0 && std::memcmp(second, "def", 3) == 0); + REQUIRE(sceFiosFHSeek(fh, 0, SCE_FIOS_SEEK_SET) == 0); + REQUIRE(sceFiosFHReadvSync(nullptr, fh, reads, 2) == 5 && std::memcmp(first, "aX", 2) == 0 && std::memcmp(second, "Yde", 3) == 0); + const SceFiosBuffer writes[2] = {{const_cast("12"), 2}, {const_cast("345"), 3}}; + REQUIRE(sceFiosFHPwritevSync(nullptr, fh, writes, 2, 6) == 5 && sceFiosFHGetSize(fh) == 11); + REQUIRE(sceFiosFHSeek(fh, 0, SCE_FIOS_SEEK_END) == 11); + REQUIRE(sceFiosFHWritevSync(nullptr, fh, writes, 2) == 5 && sceFiosFHGetSize(fh) == 16); + REQUIRE(sceFiosFHReadvSync(nullptr, fh, reads, 0) == SCE_FIOS_ERROR_BAD_IOVCNT); + REQUIRE(sceFiosFHReadvSync(nullptr, fh, reads, 5000) == SCE_FIOS_ERROR_BAD_IOVCNT); + REQUIRE(sceFiosFHReadvSync(nullptr, fh, nullptr, 1) == SCE_FIOS_ERROR_BAD_PTR); + + SceFiosStat stat{}; + REQUIRE(sceFiosFHStatSync(nullptr, fh, &stat) == SCE_FIOS_OK && stat.fileSize == 16); + REQUIRE(sceFiosFHStatSync(nullptr, fh, nullptr) == SCE_FIOS_ERROR_BAD_PTR); + REQUIRE(sceFiosFHTruncateSync(nullptr, fh, 4) == SCE_FIOS_OK && sceFiosFHGetSize(fh) == 4); + REQUIRE(sceFiosFHTruncateSync(nullptr, fh, -1) == SCE_FIOS_ERROR_BAD_SIZE); + REQUIRE(sceFiosFHSyncSync(nullptr, fh) == SCE_FIOS_OK); + + const SceFiosOp op = sceFiosFHPread(nullptr, fh, buffer, 4, 0); + REQUIRE(sceFiosOpGetRequestCount(op) == 4 && sceFiosOpGetOffset(op) == 0 && path == sceFiosOpGetPath(op)); + REQUIRE(sceFiosOpSyncWaitForIO(op) == 4 && std::memcmp(buffer, "aXYd", 4) == 0); + + REQUIRE(sceFiosFHCloseSync(nullptr, fh) == SCE_FIOS_OK); + REQUIRE(!sceFiosIsValidHandle(fh) && sceFiosFHGetPath(fh) == nullptr && sceFiosFHGetOpenParams(fh) == nullptr); + REQUIRE(sceFiosFHCloseSync(nullptr, fh) == SCE_FIOS_ERROR_BAD_FH); + REQUIRE(sceFiosFHReadSync(nullptr, fh, buffer, 1) == SCE_FIOS_ERROR_BAD_FH); + REQUIRE(sceFiosFHSeek(fh, 0, 0) == SCE_FIOS_ERROR_BAD_FH && sceFiosFHTell(fh) == SCE_FIOS_ERROR_BAD_FH); + REQUIRE(sceFiosFHGetSize(fh) == SCE_FIOS_ERROR_BAD_FH); + REQUIRE(sceFiosFHStatSync(nullptr, fh, &stat) == SCE_FIOS_ERROR_BAD_FH); + REQUIRE(sceFiosFHSyncSync(nullptr, fh) == SCE_FIOS_ERROR_BAD_FH); + + SceFiosOpenParams reader{}; + reader.openFlags = SCE_FIOS_O_READ; + REQUIRE(sceFiosFHOpenSync(nullptr, &fh, path.c_str(), &reader) == SCE_FIOS_OK); + REQUIRE(sceFiosFHWriteSync(nullptr, fh, "z", 1) == SCE_FIOS_ERROR_READ_ONLY); + REQUIRE(sceFiosFHTruncateSync(nullptr, fh, 1) == SCE_FIOS_ERROR_READ_ONLY); + REQUIRE(sceFiosFHPreadSync(nullptr, fh, buffer, 4, 0) == 4); + sceFiosCloseAllFiles(); + REQUIRE(!sceFiosIsValidHandle(fh) && sceFiosGetAllFHs(nullptr, 0) == 0); + + SceFiosOpenParams writeOnly{}; + writeOnly.openFlags = SCE_FIOS_O_WRITE | SCE_FIOS_O_TRUNC; + REQUIRE(sceFiosFHOpenSync(nullptr, &fh, path.c_str(), &writeOnly) == SCE_FIOS_OK); + REQUIRE(sceFiosFHGetSize(fh) == 0 && sceFiosFHReadSync(nullptr, fh, buffer, 1) == SCE_FIOS_ERROR_WRITE_ONLY); + REQUIRE(sceFiosFHCloseSync(nullptr, fh) == SCE_FIOS_OK); + + SceFiosOpenParams appender{}; + appender.openFlags = SCE_FIOS_O_WRITE | SCE_FIOS_O_APPEND | SCE_FIOS_O_CREAT; + const std::string created = directory + "/appended.bin"; + REQUIRE(sceFiosFHOpenWithModeSync(nullptr, &fh, created.c_str(), &appender, 0644) == SCE_FIOS_OK); + REQUIRE(sceFiosFHWriteSync(nullptr, fh, "ab", 2) == 2 && sceFiosFHWriteSync(nullptr, fh, "cd", 2) == 2); + REQUIRE(sceFiosFHCloseSync(nullptr, fh) == SCE_FIOS_OK); + REQUIRE(sceFiosFileGetSizeSync(nullptr, created.c_str()) == 4); + + g_completed = 0; + const SceFiosOpAttr attr = CountingAttr(); + const SceFiosOp opened = sceFiosFHOpen(&attr, &fh, created.c_str(), &reader); + REQUIRE(g_completed == 1 && sceFiosOpGetError(opened) == SCE_FIOS_OK && sceFiosIsValidHandle(fh)); + sceFiosOpDelete(opened); + const SceFiosOp closed = sceFiosFHClose(&attr, fh); + REQUIRE(g_completed == 2 && sceFiosOpSyncWait(closed) == SCE_FIOS_OK); + + sceFiosSuspend(); + const SceFiosOp deferred = sceFiosFHOpen(nullptr, &fh, created.c_str(), &reader); + REQUIRE(!sceFiosOpIsDone(deferred)); + sceFiosResume(); + REQUIRE(sceFiosOpSyncWait(deferred) == SCE_FIOS_OK && sceFiosIsValidHandle(fh)); + sceFiosShutdownAndCancelOps(); + REQUIRE(!sceFiosIsValidHandle(fh)); + REQUIRE(sceFiosDeleteSync(nullptr, created.c_str()) == SCE_FIOS_OK); + REQUIRE(sceFiosDeleteSync(nullptr, path.c_str()) == SCE_FIOS_OK); +} + +static void CheckDirectories(const std::string& directory) { + const std::string root = directory + "/listing"; + REQUIRE(sceFiosDirectoryCreateSync(nullptr, root.c_str()) == SCE_FIOS_OK); + REQUIRE(sceFiosFileWriteSync(nullptr, (root + "/one.bin").c_str(), "12345", 5, 0) == 5); + REQUIRE(sceFiosFileWriteSync(nullptr, (root + "/two.bin").c_str(), "1", 1, 0) == 1); + REQUIRE(sceFiosDirectoryCreateSync(nullptr, (root + "/inner").c_str()) == SCE_FIOS_OK); + + SceFiosDH dh = 0; + char storage[256] = {}; + SceFiosBuffer small{storage, 1}; + SceFiosBuffer none{nullptr, 0}; + SceFiosBuffer big{storage, sizeof(storage)}; + REQUIRE(sceFiosDHOpenSync(nullptr, &dh, root.c_str(), none) == SCE_FIOS_ERROR_BAD_SIZE); + const SceFiosOp probe = sceFiosDHOpen(nullptr, &dh, root.c_str(), small); + REQUIRE(sceFiosOpWait(probe) == SCE_FIOS_ERROR_BAD_SIZE); + const std::int64_t required = sceFiosOpGetActualCount(probe); + REQUIRE(required > 0 && required <= static_cast(sizeof(storage))); + sceFiosOpDelete(probe); + REQUIRE(dh == 0 && sceFiosGetAllDHs(nullptr, 0) == 0); + REQUIRE(sceFiosDHOpenSync(nullptr, &dh, (root + "/missing").c_str(), big) == SCE_FIOS_ERROR_BAD_PATH); + REQUIRE(sceFiosDHOpenSync(nullptr, &dh, (root + "/one.bin").c_str(), big) == SCE_FIOS_ERROR_NOT_A_DIRECTORY); + REQUIRE(sceFiosDHOpenSync(nullptr, nullptr, root.c_str(), big) == SCE_FIOS_ERROR_BAD_PTR); + REQUIRE(sceFiosDHOpenSync(nullptr, &dh, nullptr, big) == SCE_FIOS_ERROR_BAD_PTR); + REQUIRE(sceFiosDHOpenSync(nullptr, &dh, root.c_str(), big) == SCE_FIOS_OK); + REQUIRE(sceFiosIsValidHandle(dh) && root == sceFiosDHGetPath(dh) && sceFiosGetAllDHs(nullptr, 0) == 1); + + int files = 0, directories = 0; + SceFiosDirEntry entry{}; + for (;;) { + std::memset(&entry, 0x7f, sizeof(entry)); + const int result = sceFiosDHReadSync(nullptr, dh, &entry); + if (result == SCE_FIOS_ERROR_EOF) break; + REQUIRE(result == SCE_FIOS_OK); + const std::string name(entry.fullPath + entry.offsetToName, entry.nameLength); + REQUIRE(std::string(entry.fullPath, entry.fullPathLength) == root + "/" + name && entry.fullPath[entry.fullPathLength] == 0); + if (name == "inner") { + ++directories; + REQUIRE((entry.statFlags & SCE_FIOS_STAT_DIRECTORY) != 0); + } else { + ++files; + REQUIRE(entry.statFlags == 0 && entry.fileSize == (name == "one.bin" ? 5 : 1)); + REQUIRE(name == "one.bin" || name == "two.bin"); + } + } + REQUIRE(files == 2 && directories == 1); + REQUIRE(sceFiosDHReadSync(nullptr, dh, &entry) == SCE_FIOS_ERROR_EOF); + REQUIRE(sceFiosDHReadSync(nullptr, dh, nullptr) == SCE_FIOS_ERROR_BAD_PTR); + REQUIRE(sceFiosDHCloseSync(nullptr, dh) == SCE_FIOS_OK); + REQUIRE(!sceFiosIsValidHandle(dh) && sceFiosDHGetPath(dh) == nullptr); + REQUIRE(sceFiosDHCloseSync(nullptr, dh) == SCE_FIOS_ERROR_BAD_DH); + REQUIRE(sceFiosDHReadSync(nullptr, dh, &entry) == SCE_FIOS_ERROR_BAD_DH); + + g_completed = 0; + const SceFiosOpAttr attr = CountingAttr(); + const SceFiosOp opened = sceFiosDHOpen(&attr, &dh, (root + "/").c_str(), big); + REQUIRE(g_completed == 1 && sceFiosOpGetError(opened) == SCE_FIOS_OK); + sceFiosOpDelete(opened); + const SceFiosOp read = sceFiosDHRead(nullptr, dh, &entry); + REQUIRE(sceFiosOpGetError(read) == SCE_FIOS_OK && entry.fullPathLength > root.size() + 1); + REQUIRE(entry.fullPath[root.size()] == '/' && entry.fullPath[root.size() + 1] != '/'); + sceFiosOpDelete(read); + REQUIRE(sceFiosDHCloseSync(nullptr, dh) == SCE_FIOS_OK); + REQUIRE(sceFiosDHOpenSync(nullptr, &dh, root.c_str(), big) == SCE_FIOS_OK); + sceFiosTerminate(); + REQUIRE(!sceFiosIsValidHandle(dh)); + REQUIRE(sceFiosDeleteSync(nullptr, (root + "/inner").c_str()) == SCE_FIOS_OK); + REQUIRE(sceFiosDeleteSync(nullptr, (root + "/one.bin").c_str()) == SCE_FIOS_OK); + REQUIRE(sceFiosDeleteSync(nullptr, (root + "/two.bin").c_str()) == SCE_FIOS_OK); + REQUIRE(sceFiosDeleteSync(nullptr, root.c_str()) == SCE_FIOS_OK); +} + +static void CheckDates() { + SceFiosTm tm{}; + tm.tm_year = 124; + tm.tm_mon = 1; + tm.tm_mday = 29; + tm.tm_hour = 13; + tm.tm_min = 14; + tm.tm_sec = 15; + const SceFiosDate date = sceFiosDateFromComponents(&tm); + REQUIRE(date == 1709212455ull * 1000000000ull); + SceFiosTm back{}; + REQUIRE(sceFiosDateToComponents(date + 999999999, &back) == &back); + REQUIRE(back.tm_year == 124 && back.tm_mon == 1 && back.tm_mday == 29 && back.tm_hour == 13 && back.tm_min == 14 && back.tm_sec == 15); + REQUIRE(back.tm_wday == 4 && back.tm_yday == 59 && back.tm_isdst == 0); + REQUIRE(sceFiosDateToComponents(0, &back) == &back && back.tm_year == 70 && back.tm_mon == 0 && back.tm_mday == 1 && back.tm_wday == 4 && back.tm_yday == 0); + REQUIRE(sceFiosDateToComponents(date, nullptr) == nullptr && sceFiosDateFromComponents(nullptr) == 0); + SceFiosTm overflow{}; + overflow.tm_year = 99; + overflow.tm_mon = 12; + overflow.tm_mday = 32; + overflow.tm_hour = 24; + overflow.tm_min = 60; + overflow.tm_sec = 60; + SceFiosTm normalized{}; + sceFiosDateToComponents(sceFiosDateFromComponents(&overflow), &normalized); + REQUIRE(normalized.tm_year == 100 && normalized.tm_mon == 1 && normalized.tm_mday == 2 && normalized.tm_hour == 1 && normalized.tm_min == 1 && normalized.tm_sec == 0); + SceFiosTm negative{}; + negative.tm_year = 70; + negative.tm_mon = -1; + negative.tm_mday = 1; + REQUIRE(sceFiosDateFromComponents(&negative) == 0); + const SceFiosDate now = sceFiosDateGetCurrent(); + REQUIRE(now > 1700000000ull * 1000000000ull && sceFiosDateGetCurrent() >= now); + REQUIRE(sceFiosTimeIntervalFromNanoseconds(-5) == -5 && sceFiosTimeIntervalToNanoseconds(123456789012) == 123456789012); +} + +static void CheckBadPointers(const std::string& file) { + const auto badAttr = reinterpret_cast(0x10); + REQUIRE(sceFiosFileGetSizeSync(badAttr, file.c_str()) == SCE_FIOS_ERROR_BAD_PTR); + REQUIRE(sceFiosFileGetSize(badAttr, file.c_str()) == SCE_FIOS_ERROR_BAD_PTR); + REQUIRE(!sceFiosFileExistsSync(badAttr, file.c_str())); + REQUIRE(sceFiosStatSync(badAttr, file.c_str(), nullptr) == SCE_FIOS_ERROR_BAD_PTR); + sceFiosSetGlobalDefaultOpAttr(badAttr); + SceFiosOpAttr stored{}; + REQUIRE(!sceFiosGetGlobalDefaultOpAttr(&stored)); + sceFiosSetThreadDefaultOpAttr(badAttr); + REQUIRE(!sceFiosGetThreadDefaultOpAttr(&stored)); + + const auto badParams = reinterpret_cast(0x10); + SceFiosFH fh = 0; + REQUIRE(sceFiosFHOpenSync(nullptr, &fh, file.c_str(), badParams) == SCE_FIOS_ERROR_BAD_PTR); + REQUIRE(sceFiosFHOpenWithModeSync(nullptr, &fh, file.c_str(), badParams, -1) == SCE_FIOS_ERROR_BAD_PTR); + REQUIRE(fh == 0 && sceFiosGetAllFHs(nullptr, 0) == 0); + + SceFiosOpenParams reader{}; + reader.openFlags = SCE_FIOS_O_READ; + REQUIRE(sceFiosFHOpenSync(nullptr, &fh, file.c_str(), &reader) == SCE_FIOS_OK); + const auto badVectors = reinterpret_cast(0x10); + REQUIRE(sceFiosFHReadvSync(nullptr, fh, badVectors, 2) == SCE_FIOS_ERROR_BAD_PTR); + REQUIRE(sceFiosFHPreadvSync(nullptr, fh, badVectors, 2, 0) == SCE_FIOS_ERROR_BAD_PTR); + REQUIRE(sceFiosFHWritevSync(nullptr, fh, badVectors, 2) == SCE_FIOS_ERROR_BAD_PTR); + REQUIRE(sceFiosFHCloseSync(nullptr, fh) == SCE_FIOS_OK); + + REQUIRE(sceFiosDateFromComponents(reinterpret_cast(0x10)) == 0); + REQUIRE(sceFiosInitialize(reinterpret_cast(0x10)) == SCE_FIOS_ERROR_BAD_PTR); + REQUIRE(!sceFiosIsInitialized(nullptr)); + sceFiosUpdateParameters(reinterpret_cast(0x10)); +} + +int main() { + const std::filesystem::path directory = "fios2_probe"; + std::filesystem::remove_all(directory); + std::filesystem::create_directories(directory); + const std::string file = (directory / "data.bin").generic_string(); + { + std::ofstream out(file, std::ios::binary); + out << "0123456789"; + } + CheckInitialization(); + CheckQueries(directory.generic_string(), file); + CheckOperationLifecycle(file); + CheckCallbacks(file); + CheckSuspension(file); + CheckMutations(directory.generic_string()); + CheckFileHandles(directory.generic_string()); + CheckDirectories(directory.generic_string()); + CheckDates(); + CheckBadPointers(file); + sceFiosTerminate(); + REQUIRE(!sceFiosIsInitialized(nullptr)); + std::filesystem::remove_all(directory); + std::puts("guest_fios2 ok"); + return 0; +} diff --git a/core/libs/tests/GuestFirmwareConstantReturns.cpp b/core/libs/tests/GuestFirmwareConstantReturns.cpp index d63502eb3f..961bd458f3 100644 --- a/core/libs/tests/GuestFirmwareConstantReturns.cpp +++ b/core/libs/tests/GuestFirmwareConstantReturns.cpp @@ -12,6 +12,7 @@ #include #include #include +#include namespace { @@ -52,6 +53,10 @@ void CheckComposite() { Require(sceCompositorSetIndirectCanvasCommand() == 0); Require(sceCompositorSetIndirectCompositionCommand() == 0); Require(sceCompositorSetIndirectRenderTargetConfigCommand() == 0); + const std::uint32_t virtualHandles[] = {0xff00, 0xff01, 0xffff, 0xffffff00, 0x1234ff78, 0xffffffff, 0x8000ff00}; + const std::uint32_t otherHandles[] = {0, 0xfe00, 0x7f00, 0x12345678, 0x100, 0xff}; + for (const std::uint32_t handle : virtualHandles) Require(sceCompositorIsVirtualCanvasHandle(handle)); + for (const std::uint32_t handle : otherHandles) Require(!sceCompositorIsVirtualCanvasHandle(handle)); } void CheckFsInternal() { @@ -77,6 +82,35 @@ void CheckSdma() { void CheckSsl() { Require(CRYPTO_uninitAsymmetricKey() == 0); Require(VLONG_freeVlongQueue() == 0); + + for (const int context : {0, 1, -1, 0x7fffffff, static_cast(0x80000000u)}) { + int moduleId = 0x5a5a5a5a; + Require(sceSslGetNanoSSLModuleId(context, &moduleId) == 0); + Require(moduleId == context); + Require(sceSslGetNanoSSLModuleId(context, nullptr) == 0); + } + + constexpr int InvalidArgument = static_cast(0x8095177a); + unsigned char key[24]; + const unsigned char blob[4] = {1, 2, 3, 4}; + std::memset(key, 0xa5, sizeof(key)); + Require(CA_MGMT_extractKeyBlobEx(0, nullptr, 4, key) == InvalidArgument); + Require(CA_MGMT_extractKeyBlobEx(0, blob, 4, nullptr) == InvalidArgument); + Require(CA_MGMT_extractKeyBlobEx(0, nullptr, 0, nullptr) == InvalidArgument); + for (unsigned char byte : key) Require(byte == 0xa5); + Require(CA_MGMT_extractKeyBlobEx(-1, blob, 0, key) == 0); + const void* stored = nullptr; + std::memcpy(&stored, key + 8, sizeof(stored)); + Require(stored == blob); + for (int index = 0; index < 8; ++index) Require(key[index] == 0xa5); + for (int index = 16; index < 24; ++index) Require(key[index] == 0xa5); + + std::memset(key, 0xa5, sizeof(key)); + Require(CRYPTO_initAsymmetricKey(key) == 0); + std::uint32_t initialized = 0; + std::memcpy(&initialized, key, sizeof(initialized)); + Require(initialized == 1); + for (int index = 4; index < 24; ++index) Require(key[index] == 0xa5); } void CheckVisionManager() { diff --git a/core/libs/tests/GuestFont.cpp b/core/libs/tests/GuestFont.cpp index 8ddd747ee1..537afc95b5 100644 --- a/core/libs/tests/GuestFont.cpp +++ b/core/libs/tests/GuestFont.cpp @@ -193,6 +193,16 @@ static void CheckWritingDoesNotOwnCharacters(const FontMemory* memory, FontHandl FontString string = create(); FontWriting first{}; Require(sceFontWritingInit(&first, string, characters) == SCE_FONT_OK); + FontWritingMetrics metrics{1.0f, 2.0f, {3.0f, 4.0f, 5.0f, 6.0f}}; + const auto metricsAreZero = [&] { + return metrics.advanceX == 0.0f && metrics.advanceY == 0.0f && metrics.Extent.top == 0.0f && metrics.Extent.bottom == 0.0f && metrics.Extent.left == 0.0f && metrics.Extent.right == 0.0f; + }; + Require(sceFontWritingGetRenderMetrics(nullptr, &metrics) == SCE_FONT_ERROR_INVALID_PARAMETER && metricsAreZero()); + Require(sceFontWritingGetRenderMetrics(&first, nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER); + Require(sceFontWritingGetRenderMetrics(&first, &metrics) == SCE_FONT_OK); + Require(sceFontWritingSetMaskInvisible(&first, 1) == SCE_FONT_OK); + Require(sceFontWritingSetMaskInvisible(&first, 0) == SCE_FONT_OK); + Require(sceFontWritingSetMaskInvisible(&first, 2) == SCE_FONT_ERROR_INVALID_PARAMETER); FontWriting copy; std::memcpy(©, &first, sizeof(copy)); Require(steps(&first) == text.size() && steps(©) == text.size()); @@ -286,6 +296,17 @@ static void CheckTextCodes(const FontMemory* memory, FontHandle font) { std::uint32_t count = 0; FontTextCharacter* characters = sceFontStringRefersTextCharacters(string, &count); Require(characters != nullptr && count == 3); + FontHandle characterFont = font; + std::uint32_t characterCode = 'A'; + Require(sceFontCharacterGetTextFontCode(nullptr, &characterFont, &characterCode) == SCE_FONT_ERROR_INVALID_PARAMETER && characterFont == nullptr && characterCode == 0); + Require(sceFontCharacterGetTextFontCode(nullptr, nullptr, nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER); + Require(sceFontCharacterGetTextFontCode(characters, nullptr, &characterCode) == SCE_FONT_ERROR_INVALID_PARAMETER); + Require(sceFontCharacterGetTextFontCode(characters, &characterFont, nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER); + Require(sceFontCharacterGetTextFontCode(characters, &characterFont, &characterCode) == SCE_FONT_OK && characterFont == font && characterCode == 'A'); + void* textOrder = characters; + Require(sceFontCharacterGetTextOrder(characters, nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER); + Require(sceFontCharacterGetTextOrder(nullptr, &textOrder) == SCE_FONT_ERROR_INVALID_PARAMETER && textOrder == nullptr); + Require(sceFontCharacterGetTextOrder(characters, &textOrder) == SCE_FONT_OK && textOrder == characters->textOrder); Require(sceFontStringGetWritingForm(string) == 0x12); Require(sceFontStringGetTerminateCode(string) == 0 && sceFontStringGetTerminateOrder(string) == parsed.text + 3); ParsedText line{"AV\nA", 0, font}; @@ -633,6 +654,8 @@ int main() { Require(sceFontOpenFontSet(nullptr, SystemFontSet, 1, nullptr, &font) == SCE_FONT_ERROR_INVALID_LIBRARY); const unsigned char notAFont[64] = {1, 2, 3, 4}; Require(sceFontOpenFontMemory(library, notAFont, sizeof(notAFont), nullptr, &font) == SCE_FONT_ERROR_NO_SUPPORT_FUNCTION && font == nullptr); + Require(sceFontSupportExternalFonts(library, 0x04000000u, 0x52) == SCE_FONT_ERROR_INVALID_PARAMETER); + Require(sceFontSupportExternalFonts(library, 0xffffffffu, 0x52) == SCE_FONT_ERROR_INVALID_PARAMETER); Require(sceFontSupportExternalFonts(library, 4, 0x52) == SCE_FONT_OK); Require(sceFontSupportExternalFonts(library, 4, 0x52) == SCE_FONT_ERROR_ALREADY_SPECIFIED); font = reinterpret_cast(&memory); @@ -656,6 +679,14 @@ int main() { 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); + Require(sceFontGetLibrary(font, nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER); + FontLibrary fontLibrary = nullptr; + Require(sceFontGetLibrary(font, &fontLibrary) == SCE_FONT_OK && fontLibrary == library); + Font::FontLibNative invalidLibrary{}; + Require(sceFontSetFontsOpenMode(nullptr, 0) == SCE_FONT_ERROR_INVALID_LIBRARY); + Require(sceFontSetFontsOpenMode(&invalidLibrary, 0) == SCE_FONT_ERROR_INVALID_LIBRARY); + Require(sceFontSetFontsOpenMode(library, 3) == SCE_FONT_ERROR_INVALID_PARAMETER); + for (const auto mode : {0, 1, 2}) Require(sceFontSetFontsOpenMode(library, mode) == SCE_FONT_OK); int setting = -1; Require(sceFontGetScriptLanguage(font, 3, &setting) == SCE_FONT_ERROR_INVALID_PARAMETER && setting == 0); Require(sceFontSetScriptLanguage(font, 3, 7) == SCE_FONT_ERROR_INVALID_PARAMETER); diff --git a/core/libs/tests/GuestFontGsAgc.cpp b/core/libs/tests/GuestFontGsAgc.cpp new file mode 100644 index 0000000000..1e74c58485 --- /dev/null +++ b/core/libs/tests/GuestFontGsAgc.cpp @@ -0,0 +1,331 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/Pthread/include/Mutex.hpp" +#include "prx/libSceFont/libSceFont.hpp" +#include "prx/libSceFontGs.native/libSceFontGs.native.hpp" +#include +#include +#include + +namespace { + +constexpr int Fatal = static_cast(0x80460001); +constexpr int InvalidParameter = static_cast(0x80460002); +constexpr int InvalidSequence = static_cast(0x8046009a); +constexpr int SequenceLocked = static_cast(0x804600b8); +constexpr int InvalidObject = static_cast(0x80460084); + +int g_errorCount = 0; +int g_lastError = 0; + +void APS5_VABI RecordError(int error) { + g_lastError = error; + ++g_errorCount; +} + +void Require(bool value) { if (!value) std::abort(); } + +struct TestDrawupRecord { + std::uint8_t type; + std::uint8_t sizeTag; + std::uint16_t flags; + std::uint32_t sequence; + TestDrawupRecord* next; + std::uint32_t state; + std::uint32_t mode; + void* payload; + std::uint32_t params[8]; + std::uint8_t reserved[0x40]; +}; + +struct TestDrawupEntry { + std::uint8_t* memory; + std::uint32_t reserved8; + std::uint32_t slotSize; + std::uint32_t total; + std::uint32_t used; + std::uint16_t* nodes; + std::uint16_t* scan; + std::uint64_t reserved28; +}; + +struct TestDrawupPool { + PthreadMutex* mutex; + std::uint32_t reserved8; + std::uint32_t entryCount; + std::uint32_t used; + std::uint32_t maximum; + TestDrawupEntry entries[1]; +}; + +struct TestDrawupOwner { + std::uint8_t reserved[0x160]; + TestDrawupPool* pool; +}; + +struct TestDrawupObject { + std::uint64_t reserved0; + TestDrawupOwner* owner; + void* resource; + std::uint64_t reserved18[2]; + TestDrawupRecord* head; + TestDrawupRecord* tail; +}; + +struct TestDrawupFrame { + std::uint16_t tag; + std::uint16_t reserved2; + int error; + TestDrawupObject* object; + std::uint32_t unitSize; + std::uint32_t unitUsed; + void* unitHead; + std::uint8_t* unit; + std::uint32_t sequence; +}; + +static_assert(sizeof(TestDrawupRecord) == 0x80); +static_assert(offsetof(TestDrawupRecord, state) == 0x10); +static_assert(offsetof(TestDrawupOwner, pool) == 0x160); +static_assert(offsetof(TestDrawupFrame, object) == 0x8 && offsetof(TestDrawupFrame, sequence) == 0x28); + +struct TestAgcSurface { + void* head; + std::uint8_t reserved8[5]; + std::uint8_t tag; + std::uint8_t reserved0e; + std::uint8_t mode; + std::uint8_t reserved10[0xc]; + std::uint32_t busy; + std::uint8_t reserved20[8]; + const void* fillObject; + std::uint8_t reserved30[8]; + std::uint32_t initialized; +}; + +static_assert(offsetof(TestAgcSurface, tag) == 0x0d); +static_assert(offsetof(TestAgcSurface, mode) == 0x0f); +static_assert(offsetof(TestAgcSurface, busy) == 0x1c); +static_assert(offsetof(TestAgcSurface, fillObject) == 0x28); +static_assert(offsetof(TestAgcSurface, initialized) == 0x38); + +struct TestAgcCommands { + std::uint8_t reserved[0x28]; + const void* first; + std::uint32_t firstSize; + std::uint32_t secondCount; + const void* second; +}; + +static_assert(offsetof(TestAgcCommands, first) == 0x28); +static_assert(offsetof(TestAgcCommands, firstSize) == 0x30); +static_assert(offsetof(TestAgcCommands, secondCount) == 0x34); +static_assert(offsetof(TestAgcCommands, second) == 0x38); + +struct TestAgcSequence { + std::uint16_t tag; + std::uint8_t reserved2[0x19]; + std::uint8_t lock; + std::uint8_t reserved1c[0xc]; + TestAgcCommands* commands; +}; + +static_assert(offsetof(TestAgcSequence, lock) == 0x1b); +static_assert(offsetof(TestAgcSequence, commands) == 0x28); + +constexpr std::uint32_t TotalSlots = 8; + +struct DrawupFixture { + PthreadMutex mutex{}; + std::uint8_t memory[TotalSlots * 0x80]{}; + std::uint16_t nodes[TotalSlots]{}; + TestDrawupPool pool{}; + TestDrawupOwner owner{}; + TestDrawupObject object{}; + TestDrawupFrame frame{}; + std::uint8_t descriptor[0x200]{}; + TestAgcSurface surface{}; + + DrawupFixture() { + scePthreadMutexInit(&mutex, nullptr, "agc-drawup"); + nodes[0] = TotalSlots << 1; + pool.mutex = &mutex; + pool.entryCount = 1; + pool.entries[0] = {memory, 0, 0x80, TotalSlots, 0, nodes, nodes, 0}; + owner.pool = &pool; + object.owner = &owner; + frame.tag = 0xf53; + frame.object = &object; + surface.head = &object; + surface.tag = 0x58; + surface.mode = 0x22; + surface.fillObject = descriptor; + surface.initialized = 1; + for (std::size_t i = 0; i < sizeof(descriptor); ++i) descriptor[i] = static_cast(i); + } + + ~DrawupFixture() { scePthreadMutexDestroy(&mutex); } +}; + +void AgcSelect() { + Require(sceFontSelectGraphicsAgc(1) == nullptr); + Require(sceFontSelectGraphicsAgc(0xffffffff) == nullptr); +} + +void AgcDrawupImage() { + { + void* out = reinterpret_cast(0xdead); + const int before = g_errorCount; + Require(sceFontGraphicsAgcDrawupFillTextureImage(nullptr, nullptr, &out, 0.0f, 0.0f, 8.0f, 8.0f) == InvalidParameter); + Require(out == nullptr); + Require(g_errorCount == before); + } + { + DrawupFixture f; + auto* frame = reinterpret_cast(&f.frame); + const int before = g_errorCount; + Require(sceFontGraphicsAgcDrawupFillTextureImage(frame, &f.surface, nullptr, 0.0f, 0.0f, 8.0f, 8.0f) == InvalidParameter); + Require(g_errorCount == before); + } + { + DrawupFixture f; + auto* frame = reinterpret_cast(&f.frame); + void* out = reinterpret_cast(0xdead); + const int before = g_errorCount; + f.surface.tag = 0x57; + Require(sceFontGraphicsAgcDrawupFillTextureImage(frame, &f.surface, &out, 0.0f, 0.0f, 8.0f, 8.0f) == InvalidParameter); + Require(out == nullptr); + f.surface.tag = 0x58; + f.surface.head = nullptr; + Require(sceFontGraphicsAgcDrawupFillTextureImage(frame, &f.surface, &out, 0.0f, 0.0f, 8.0f, 8.0f) == InvalidParameter); + f.surface.head = &f.object; + f.surface.initialized = 0; + Require(sceFontGraphicsAgcDrawupFillTextureImage(frame, &f.surface, &out, 0.0f, 0.0f, 8.0f, 8.0f) == InvalidParameter); + f.surface.initialized = 1; + f.surface.busy = 1; + Require(sceFontGraphicsAgcDrawupFillTextureImage(frame, &f.surface, &out, 0.0f, 0.0f, 8.0f, 8.0f) == InvalidParameter); + f.surface.busy = 0; + Require(sceFontGraphicsAgcDrawupFillTextureImage(frame, &f.surface, &out, -1.0f, 0.0f, 8.0f, 8.0f) == InvalidParameter); + Require(sceFontGraphicsAgcDrawupFillTextureImage(frame, &f.surface, &out, 0.0f, -1.0f, 8.0f, 8.0f) == InvalidParameter); + const float nan = std::strtof("nan", nullptr); + Require(!(nan >= 0.0f)); + Require(sceFontGraphicsAgcDrawupFillTextureImage(frame, &f.surface, &out, nan, 0.0f, 8.0f, 8.0f) == InvalidParameter); + Require(sceFontGraphicsAgcDrawupFillTextureImage(frame, &f.surface, &out, 0.0f, nan, 8.0f, 8.0f) == InvalidParameter); + Require(sceFontGraphicsAgcDrawupFillTextureImage(frame, &f.surface, &out, 0.0f, 0.0f, nan, 8.0f) == InvalidParameter); + Require(sceFontGraphicsAgcDrawupFillTextureImage(frame, &f.surface, &out, 0.0f, 0.0f, 8.0f, nan) == InvalidParameter); + Require(sceFontGraphicsAgcDrawupFillTextureImage(frame, &f.surface, &out, 16830.0f, 0.0f, 8.0f, 8.0f) == InvalidParameter); + Require(sceFontGraphicsAgcDrawupFillTextureImage(frame, &f.surface, &out, 0.0f, 0.0f, -4.0f, 8.0f) == InvalidParameter); + Require(sceFontGraphicsAgcDrawupFillTextureImage(frame, &f.surface, &out, 0.0f, 0.0f, 8.0f, -4.0f) == InvalidParameter); + Require(sceFontGraphicsAgcDrawupFillTextureImage(frame, &f.surface, &out, 0.0f, 0.0f, 16834.0f, 16834.0f) == 0); + Require(out != nullptr); + const auto* firstRecord = static_cast(out); + Require(f.object.head == firstRecord && f.object.tail == firstRecord); + out = reinterpret_cast(0xdead); + Require(sceFontGraphicsAgcDrawupFillTextureImage(frame, &f.surface, &out, 0.0f, 0.0f, 8.0f, 8.0f) == 0); + Require(out != nullptr); + const auto* record = static_cast(out); + Require(record->state == 1); + Require(record->mode == 0x22); + Require(f.object.head == firstRecord && f.object.tail == record); + Require(g_errorCount == before); + } + { + DrawupFixture f; + auto* frame = reinterpret_cast(&f.frame); + void* out = reinterpret_cast(0xdead); + f.frame.tag = 0; + Require(sceFontGraphicsAgcDrawupFillTextureImage(frame, &f.surface, &out, 0.0f, 0.0f, 8.0f, 8.0f) == InvalidObject); + Require(out == nullptr); + Require(f.frame.error == InvalidObject); + Require(g_lastError == InvalidObject); + g_lastError = 0; + } +} + +void AgcDrawupPattern() { + { + DrawupFixture f; + auto* frame = reinterpret_cast(&f.frame); + const int before = g_errorCount; + Require(sceFontGraphicsAgcDrawupFillTexturePattern(frame, &f.surface, 1, 2, 3, 4, 5, 6, nullptr) == InvalidParameter); + f.surface.initialized = 0; + void* out = reinterpret_cast(0xdead); + Require(sceFontGraphicsAgcDrawupFillTexturePattern(frame, &f.surface, 1, 2, 3, 4, 5, 6, &out) == InvalidParameter); + Require(out == nullptr); + Require(g_errorCount == before); + f.surface.initialized = 1; + Require(sceFontGraphicsAgcDrawupFillTexturePattern(frame, &f.surface, 1, 2, 3, 4, 5, 6, &out) == 0); + Require(out != nullptr); + const auto* record = static_cast(out); + Require(record->state == 1); + Require(record->mode == 0x22); + } +} + +void AgcSequences() { + std::uint32_t firstData[8] = {1, 2, 3, 4, 5, 6, 7, 8}; + std::uint32_t secondData[3] = {9, 10, 11}; + TestAgcCommands commands{}; + commands.first = firstData; + commands.firstSize = 0x20; + commands.secondCount = 3; + commands.second = secondData; + TestAgcSequence sequence{}; + sequence.tag = 0xf80; + sequence.commands = &commands; + auto* seq = reinterpret_cast(&sequence); + { + void* out = reinterpret_cast(0xdead); + void* out2 = reinterpret_cast(0xbeef); + std::uint32_t count = 0xcccc; + std::uint32_t count2 = 0xdddd; + const int before = g_errorCount; + Require(sceFontGraphicsSequenceGetAgcCommandsForJump(nullptr, &out, &count, &out2, &count2) == InvalidSequence); + Require(out == nullptr && count == 0 && out2 == nullptr && count2 == 0); + Require(sceFontGraphicsSequenceGetAgcCommandsForJump(seq, &out, nullptr, &out2, &count2) == InvalidParameter); + Require(out == nullptr && out2 == nullptr && count2 == 0); + Require(g_errorCount == before); + sequence.tag = 0xf81; + Require(sceFontGraphicsSequenceGetAgcCommandsForJump(seq, &out, &count, &out2, &count2) == InvalidSequence); + Require(sceFontGraphicsSequenceGetAgcCommandsForJump(seq, nullptr, nullptr, nullptr, nullptr) == InvalidSequence); + sequence.tag = 0xf80; + sequence.lock = 1; + Require(sceFontGraphicsSequenceGetAgcCommandsForJump(seq, &out, &count, &out2, &count2) == SequenceLocked); + sequence.lock = 0; + auto* saved = sequence.commands; + sequence.commands = nullptr; + Require(sceFontGraphicsSequenceGetAgcCommandsForJump(seq, &out, &count, &out2, &count2) == Fatal); + sequence.commands = saved; + Require(sceFontGraphicsSequenceGetAgcCommandsForJump(seq, &out, &count, &out2, &count2) == 0); + Require(out == firstData && count == 8 && out2 == secondData && count2 == 3); + Require(g_errorCount == before); + } + { + void* out = reinterpret_cast(0xdead); + std::uint32_t count = 0xcccc; + const int before = g_errorCount; + Require(sceFontGraphicsSequenceGetAgcCommandsForSubmit(nullptr, &out, &count) == InvalidSequence); + Require(out == nullptr && count == 0); + Require(sceFontGraphicsSequenceGetAgcCommandsForSubmit(seq, nullptr, &count) == InvalidParameter); + Require(count == 0); + sequence.tag = 0xf81; + Require(sceFontGraphicsSequenceGetAgcCommandsForSubmit(seq, nullptr, nullptr) == InvalidSequence); + sequence.tag = 0xf80; + sequence.lock = 1; + Require(sceFontGraphicsSequenceGetAgcCommandsForSubmit(seq, &out, &count) == SequenceLocked); + sequence.lock = 0; + Require(sceFontGraphicsSequenceGetAgcCommandsForSubmit(seq, &out, &count) == 0); + Require(out == firstData && count == 0x20); + Require(g_errorCount == before); + } +} + +} + +int main() { + sceFontSetGraphicsErrorCallback(RecordError); + AgcSelect(); + AgcDrawupImage(); + AgcDrawupPattern(); + AgcSequences(); + sceFontSetGraphicsErrorCallback(nullptr); +} diff --git a/core/libs/tests/GuestFontVerticalLayout.cpp b/core/libs/tests/GuestFontVerticalLayout.cpp new file mode 100644 index 0000000000..da7bf3a68b --- /dev/null +++ b/core/libs/tests/GuestFontVerticalLayout.cpp @@ -0,0 +1,122 @@ +#include "prx/libSceFontFt/include/Api.hpp" +#include "prx/libSceFont/include/Api.hpp" +#include "prx/libSceFont/include/FontTypes.hpp" +#include "tests/FontFixture.hpp" +#include +#include +#include +#include +#include + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "Font vertical layout check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +static int allocations = 0; +static void* APS5_VABI Allocate(void*, std::uint32_t size) { + ++allocations; + return std::malloc(size); +} +static void APS5_VABI Release(void*, void* pointer) { + if (pointer) --allocations; + std::free(pointer); +} + +static void Put16(std::vector& out, int value) { + out.push_back(static_cast((value >> 8) & 0xFF)); + out.push_back(static_cast(value & 0xFF)); +} + +static std::vector Words(std::initializer_list values) { + std::vector out; + for (const int value : values) Put16(out, value); + return out; +} + +static std::uint32_t ReadU32(const std::vector& font, std::size_t offset) { + return (static_cast(font[offset]) << 24) | (static_cast(font[offset + 1]) << 16) | + (static_cast(font[offset + 2]) << 8) | static_cast(font[offset + 3]); +} + +static void WriteI16(std::vector& font, std::size_t offset, int value) { + font[offset] = static_cast((value >> 8) & 0xFF); + font[offset + 1] = static_cast(value & 0xFF); +} + +static void SetHeadBounds(std::vector& font, int xMin, int yMin, int xMax, int yMax) { + const int tableCount = static_cast(font[4] << 8 | font[5]); + for (int table = 0; table < tableCount; ++table) { + const std::size_t record = 12 + static_cast(table) * 16; + if (std::memcmp(font.data() + record, "head", 4) != 0) continue; + const std::size_t head = ReadU32(font, record + 8); + WriteI16(font, head + 36, xMin); + WriteI16(font, head + 38, yMin); + WriteI16(font, head + 40, xMax); + WriteI16(font, head + 42, yMax); + return; + } + std::abort(); +} + +static std::vector VerticalFont() { + auto font = FontFixture::BuildFont(1, { + {"vhea", Words({1, 0, 800, -200, 0, 500, 1000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1})}, + {"vmtx", Words({500, 0})}, + }); + SetHeadBounds(font, -100, -200, 900, 800); + return font; +} + +static void CheckLayout(FontHandle font, FontVerticalLayout& layout) { + Require(sceFontGetVerticalLayout(font, &layout) == SCE_FONT_OK); + Require(layout.columnAdvance > 0.0f); + Require(layout.baselineOffsetX < 0.0f); + Require(layout.decorationSpan == 0.0f); +} + +int main() { + const FontMemoryInterface iface{Allocate, Release, nullptr, nullptr, nullptr, nullptr}; + FontMemory memory{}; + Require(sceFontMemoryInit(&memory, nullptr, 0, &iface, nullptr, nullptr, nullptr) == SCE_FONT_OK); + FontLibrary library = nullptr; + Require(sceFontCreateLibrary(&memory, sceFontSelectLibraryFt(0), &library) == SCE_FONT_OK); + Require(sceFontSupportExternalFonts(library, 1, 0x52) == SCE_FONT_OK); + const std::vector fontData = VerticalFont(); + FontHandle font = nullptr; + Require(sceFontOpenFontMemory(library, fontData.data(), static_cast(fontData.size()), nullptr, &font) == SCE_FONT_OK); + + FontVerticalLayout layout{}; + Require(sceFontGetVerticalLayout(font, nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER); + Require(sceFontGetVerticalLayout(nullptr, &layout) == SCE_FONT_ERROR_INVALID_FONT_HANDLE); + Require(layout.baselineOffsetX == 0.0f && layout.columnAdvance == 0.0f && layout.decorationSpan == 0.0f); + layout = {1.0f, 1.0f, 1.0f}; + FontHandleOpaque unopened{}; + Require(sceFontGetVerticalLayout(&unopened, &layout) == SCE_FONT_ERROR_INVALID_FONT_HANDLE); + Require(layout.baselineOffsetX == 0.0f && layout.columnAdvance == 0.0f && layout.decorationSpan == 0.0f); + + Require(sceFontSetScalePixel(font, 100.0f, 100.0f) == SCE_FONT_OK); + CheckLayout(font, layout); + const FontVerticalLayout pixelLayout = layout; + Require(sceFontSetResolutionDpi(font, 144, 144) == SCE_FONT_OK); + CheckLayout(font, layout); + Require(std::memcmp(&layout, &pixelLayout, sizeof(pixelLayout)) == 0); + + Require(sceFontSetScalePoint(font, 20.0f, 30.0f) == SCE_FONT_OK); + CheckLayout(font, layout); + const FontVerticalLayout pointLayout = layout; + FontHandle instance = nullptr; + Require(sceFontOpenFontInstance(font, nullptr, &instance) == SCE_FONT_OK); + FontVerticalLayout instanceLayout{}; + Require(sceFontGetVerticalLayout(instance, &instanceLayout) == SCE_FONT_OK); + Require(std::memcmp(&instanceLayout, &pointLayout, sizeof(pointLayout)) == 0); + Require(sceFontCloseFont(instance) == SCE_FONT_OK); + + Require(sceFontCloseFont(font) == SCE_FONT_OK); + Require(sceFontDestroyLibrary(&library) == SCE_FONT_OK); + Require(allocations == 0); + Require(sceFontMemoryTerm(&memory) == SCE_FONT_OK); +} diff --git a/core/libs/tests/GuestFreeTypeFaceCompat.cpp b/core/libs/tests/GuestFreeTypeFaceCompat.cpp new file mode 100644 index 0000000000..7f6a27eab6 --- /dev/null +++ b/core/libs/tests/GuestFreeTypeFaceCompat.cpp @@ -0,0 +1,74 @@ +#include "prx/libSceFreeType/include/Api.hpp" +#include "FontFixture.hpp" +#include "prx/libSceFreeType/include/GuestObjects.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" + +#include FT_FONT_FORMATS_H + +#include +#include +#include +#include +#include +#include + +using namespace FreeType; + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "FreeType face compat check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +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 Query(FacePair& face, const char* format) { + const char* actual = FT_Get_Font_Format_nid_postfix(&face.guest.rec); + const char* expected = FT_Get_Font_Format(face.oracle); + Require(actual && expected && std::strcmp(actual, expected) == 0 && std::strcmp(actual, format) == 0); + Require(actual == FT_Get_Font_Format(face.guest.host)); + Require(FT_Get_Font_Format_nid_postfix(&face.guest.rec) == FT_Get_X11_Font_Format(face.guest.host)); + Require(FT_Face_CheckTrueTypePatents_nid_postfix(&face.guest.rec) == FT_Face_CheckTrueTypePatents(face.oracle)); + Require(FT_Face_CheckTrueTypePatents_nid_postfix(&face.guest.rec) == 0); + for (const std::uint8_t value : {0, 1, 0xFF}) { + Require(FT_Face_SetUnpatentedHinting_nid_postfix(&face.guest.rec, value) == FT_Face_SetUnpatentedHinting(face.oracle, value)); + Require(FT_Face_SetUnpatentedHinting_nid_postfix(&face.guest.rec, value) == 0); + } +} + +int main() { + GuestLibrary library{}; + FT_Library nativeLibrary = nullptr; + Require(FT_Init_FreeType(&library.host) == 0 && FT_Init_FreeType(&nativeLibrary) == 0); + { + FacePair bitmap(library, nativeLibrary, FontFixture::BitmapFont()); + Query(bitmap, "BDF"); + FacePair outline(library, nativeLibrary, FontFixture::BuildFont(3, {})); + Query(outline, "TrueType"); + GuestFaceRec empty{}; + for (GuestFaceRec* face : {static_cast(nullptr), &empty}) { + Require(FT_Get_Font_Format_nid_postfix(face) == FT_Get_Font_Format(nullptr)); + Require(FT_Face_CheckTrueTypePatents_nid_postfix(face) == 0); + Require(FT_Face_SetUnpatentedHinting_nid_postfix(face, 1) == 0); + } + } + Require(FT_Done_FreeType(library.host) == 0 && FT_Done_FreeType(nativeLibrary) == 0); +} diff --git a/core/libs/tests/GuestFreeTypeMemoryRecord.cpp b/core/libs/tests/GuestFreeTypeMemoryRecord.cpp new file mode 100644 index 0000000000..d862f2fe3a --- /dev/null +++ b/core/libs/tests/GuestFreeTypeMemoryRecord.cpp @@ -0,0 +1,78 @@ +#include "prx/libSceFreeType/include/MemoryRecord.hpp" +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#else +#include +#endif + +static void Require(bool condition, int line) { + if (!condition) { + std::fprintf(stderr, "FreeType memory record check failed at line %d\n", line); + std::exit(EXIT_FAILURE); + } +} +#define Check(value) Require((value), __LINE__) + +int main(int count, char** arguments) { + Check(count == 3); + const bool patched = std::strcmp(arguments[2], "patched") == 0; + Check(patched || std::strcmp(arguments[2], "unpatched") == 0); + const char* const newSymbol = patched ? "tCMhTh7h8D4" : "FT_New_Memory_nid_postfix"; + const char* const doneSymbol = patched ? "UTumVjeIQtg" : "FT_Done_Memory_nid_postfix"; +#ifdef _WIN32 + const auto module = LoadLibraryW(std::filesystem::path(arguments[1]).c_str()); + Check(module != nullptr); + const auto newAddress = GetProcAddress(module, newSymbol); + const auto doneAddress = GetProcAddress(module, doneSymbol); +#else + const auto module = dlopen(arguments[1], RTLD_NOW | RTLD_LOCAL); + if (module == nullptr) std::fprintf(stderr, "%s\n", dlerror()); + Check(module != nullptr); + const auto newAddress = dlsym(module, newSymbol); + const auto doneAddress = dlsym(module, doneSymbol); +#endif + if (newAddress == nullptr) std::fprintf(stderr, "Missing FreeType entry: %s\n", newSymbol); + if (doneAddress == nullptr) std::fprintf(stderr, "Missing FreeType entry: %s\n", doneSymbol); + Check(newAddress != nullptr); + Check(doneAddress != nullptr); + const auto create = std::bit_cast(newAddress); + const auto destroy = std::bit_cast(doneAddress); + + FreeType::GuestMemoryRec* memory = create(); + Check(memory != nullptr); + Check(memory->user == nullptr); + Check(memory->alloc != nullptr); + Check(memory->free != nullptr); + Check(memory->realloc != nullptr); + + unsigned char* block = static_cast(memory->alloc(memory, 64)); + Check(block != nullptr); + for (int index = 0; index < 64; ++index) block[index] = static_cast(index); + block = static_cast(memory->realloc(memory, 64, 128, block)); + Check(block != nullptr); + for (int index = 0; index < 64; ++index) Check(block[index] == static_cast(index)); + memory->free(memory, block); + + destroy(memory); + destroy(nullptr); + + for (int index = 0; index < 256; ++index) { + FreeType::GuestMemoryRec* record = create(); + Check(record != nullptr); + void* probe = record->alloc(record, 1024); + Check(probe != nullptr); + record->free(record, probe); + destroy(record); + } + +#ifdef _WIN32 + Check(FreeLibrary(module) != 0); +#else + Check(dlclose(module) == 0); +#endif +} diff --git a/core/libs/tests/GuestFreebsdClocks.cpp b/core/libs/tests/GuestFreebsdClocks.cpp index da14b7118e..2331ab57b2 100644 --- a/core/libs/tests/GuestFreebsdClocks.cpp +++ b/core/libs/tests/GuestFreebsdClocks.cpp @@ -91,6 +91,18 @@ static void OmittedTimeIsFaultAfterClockValidation() { Require(rejected); } +static void InvalidClockIdFailsGettime() { + KernelTimespec time{-1, -1}; + for (const int clockId : {-1, 999}) { + *__error_nid_postfix() = 0; + Require(clock_gettime_nid_postfix(clockId, &time) == -1 && *__error_nid_postfix() == 22); + Require(time.tv_sec == -1 && time.tv_nsec == -1); + *__error_nid_postfix() = 0; + Require(sceKernelClockGettime(clockId, &time) == static_cast(0x80020016u)); + Require(time.tv_sec == -1 && time.tv_nsec == -1); + } +} + static KernelTimespec Read(int clockId) { KernelTimespec time{-1, -1}; Require(clock_gettime_nid_postfix(clockId, &time) == SCE_OK); @@ -342,6 +354,7 @@ int main() { KernelTimezoneIsUtc(); UnsupportedResolutionStillThrows(); OmittedTimeIsFaultAfterClockValidation(); + InvalidClockIdFailsGettime(); SettimeValidatesLikeFreebsd(); CalendarConversionsInitializeTheirState(); CalendarConversionsFollowTheHostZone(); diff --git a/core/libs/tests/GuestFstatvfs.cpp b/core/libs/tests/GuestFstatvfs.cpp index 9137793fbb..6f53575826 100644 --- a/core/libs/tests/GuestFstatvfs.cpp +++ b/core/libs/tests/GuestFstatvfs.cpp @@ -31,6 +31,9 @@ static void RequireFailure(int descriptor, int error) { Require(fstatvfs_nid_postfix(descriptor, reinterpret_cast(output.data())) == -1); Require(*__error_nid_postfix() == error); Require(output == saved); + *__error_nid_postfix() = 0; + Require(fstatvfs_nid_postfix(descriptor, nullptr) == -1); + Require(*__error_nid_postfix() == error); } static void RequireVolume(int descriptor, const std::filesystem::path& path) { @@ -50,6 +53,18 @@ static void RequireVolume(int descriptor, const std::filesystem::path& path) { Require(fields[9] == 0 && fields[10] >= 255); } +static void RequireDevice(int descriptor) { + std::array guarded; + guarded.fill(0xA5A5A5A5A5A5A5A5ull); + *__error_nid_postfix() = 12345; + Require(fstatvfs_nid_postfix(descriptor, reinterpret_cast(guarded.data() + 1)) == 0); + Require(*__error_nid_postfix() == 12345); + Require(guarded.front() == 0xA5A5A5A5A5A5A5A5ull && guarded.back() == guarded.front()); + const auto* fields = guarded.data() + 1; + const std::array expected{0, 0, 2, 0, 0, 0, 512, 0, 512, 0, 255}; + Require(std::memcmp(fields, expected.data(), sizeof(expected)) == 0); +} + int main() { const auto root = std::filesystem::temp_directory_path() / ("anyps5-fstatvfs-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); @@ -71,20 +86,46 @@ int main() { const int directory = sceKernelOpen("/anyps5-fstatvfs-test", SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); Require(directory >= 0); RequireVolume(directory, root); + Require(fstatvfs_nid_postfix(directory, nullptr) == -1 && *__error_nid_postfix() == 14); Require(close_nid_postfix(directory) == 0); + RequireFailure(directory, 9); + + const int random = sceKernelOpen("/dev/urandom", SCE_KERNEL_O_RDONLY, 0); + Require(random >= 0); + RequireDevice(random); + Require(fstatvfs_nid_postfix(random, nullptr) == -1 && *__error_nid_postfix() == 14); + Require(close_nid_postfix(random) == 0); + RequireFailure(random, 9); + + const int queue = kqueue_nid_postfix(); + Require(queue >= 0); + RequireFailure(queue, 22); + Require(close_nid_postfix(queue) == 0); + RequireFailure(queue, 9); int pipes[2]; Require(pipe_nid_postfix(pipes) == 0); RequireFailure(pipes[0], 22); RequireFailure(pipes[1], 22); for (int fd : pipes) Require(close_nid_postfix(fd) == 0); + RequireFailure(pipes[0], 9); + RequireFailure(pipes[1], 9); + int sockets[2]; Require(socketpair_nid_postfix(1, 1, 0, sockets) == 0); RequireFailure(sockets[0], 22); + RequireFailure(sockets[1], 22); for (int fd : sockets) Require(close_nid_postfix(fd) == 0); RequireFailure(sockets[0], 9); + RequireFailure(sockets[1], 9); + RequireFailure(-1, 9); + RequireFailure(-100, 9); + RequireFailure(1024, 9); + Require(close_nid_postfix(file) == 0); + RequireFailure(file, 9); + RemovePathAlias_nid_no_patch("/anyps5-fstatvfs-test"); Require(std::filesystem::remove(filePath)); Require(std::filesystem::remove(root)); diff --git a/core/libs/tests/GuestGnmNeoContextStateInternal.cpp b/core/libs/tests/GuestGnmNeoContextStateInternal.cpp index efe6173f22..d297dbcb72 100644 --- a/core/libs/tests/GuestGnmNeoContextStateInternal.cpp +++ b/core/libs/tests/GuestGnmNeoContextStateInternal.cpp @@ -50,10 +50,12 @@ int main() { static std::array process{}; Require(ProcessParametersFixture::Install(process.data(), process.size()), "the process parameters could not be installed"); - for (const std::uint32_t sdkVersion : {0x03500000u, 0x03ffffffu}) { + for (const std::uint32_t sdkVersion : {0x00000000u, 0x03500000u, 0x03ffffffu}) { std::memcpy(process.data() + 0x10, &sdkVersion, sizeof(sdkVersion)); + const int internalSize = sceGnmDrawInitToDefaultContextStateInternalSize(); + Require(internalSize == 0x20, "internal context state size before SDK 0x04000000 is not 0x20"); Buffer words = Fresh(); - Require(sceGnmDrawInitToDefaultContextStateInternalCommand(words.data(), 0x20) == 0x20, "size 0x20 was not taken before SDK 0x04000000"); + Require(sceGnmDrawInitToDefaultContextStateInternalCommand(words.data(), static_cast(internalSize)) == 0x20, "size 0x20 was not taken before SDK 0x04000000"); for (std::size_t index = 0; index < BufferWords; ++index) Require(words[index] == (index < ContextStateBefore400.size() ? ContextStateBefore400[index] : Untouched), "the words before SDK 0x04000000 differ from firmware 13.42's"); for (const std::uint32_t size : {0u, 0x1fu, 0x21u, 0xffu, 0x100u, 0x101u}) { @@ -62,16 +64,18 @@ int main() { } } - for (const std::uint32_t sdkVersion : {0x04000000u, 0x09000000u}) { + for (const std::uint32_t sdkVersion : {0x04000000u, 0x05500000u, 0x09000000u}) { std::memcpy(process.data() + 0x10, &sdkVersion, sizeof(sdkVersion)); + const int internalSize = sceGnmDrawInitToDefaultContextStateInternalSize(); + Require(internalSize == 0x100, "internal context state size from SDK 0x04000000 is not 0x100"); Buffer words = Fresh(); - Require(sceGnmDrawInitToDefaultContextStateInternalCommand(words.data(), 0x100) == 0x100, "size 0x100 was not taken from SDK 0x04000000"); + Require(sceGnmDrawInitToDefaultContextStateInternalCommand(words.data(), static_cast(internalSize)) == 0x100, "size 0x100 was not taken from SDK 0x04000000"); std::size_t written = 0; while (written < BufferWords && words[written] != Untouched) ++written; Require(written == 102, "size 0x100 did not write 102 words from SDK 0x04000000"); for (const std::uint32_t size : {0u, 0x20u, 0xffu, 0x100u, 0x101u}) CheckSize(sdkVersion, sceGnmDrawInitToDefaultContextState400, size); } - std::puts("GnmNeo InternalCommand tests passed"); + std::puts("GnmNeo ContextStateInternal tests passed"); return 0; } diff --git a/core/libs/tests/GuestHmd2.cpp b/core/libs/tests/GuestHmd2.cpp index e6eee4ba71..7ad8a6a0de 100644 --- a/core/libs/tests/GuestHmd2.cpp +++ b/core/libs/tests/GuestHmd2.cpp @@ -9,6 +9,7 @@ #include #include #include +#include namespace Hmd2 { @@ -275,6 +276,33 @@ void SizeQueriesReturnTheConsoleSizes() { Require(work.size == 0x4000 && work.align == 0x100, "mirroring work memory size"); } +template +bool AnswersNoSession(Call call) { + try { + return call() == Hmd2::ReprojectionNotInitialized; + } catch (const std::exception&) { + return false; + } +} + +void TestGatedCallsInsideReprojectionSession() { + std::array memory; + memory.fill(0xaa); + const Hmd2::MirroringParameter mirroring{memory.data(), memory.data() + 32}; + std::uint64_t time = 0xaaaaaaaaaaaaaaaa; + Require(!AnswersNoSession([&] { return sceHmd2ReprojectionEnableMirroring(&mirroring); }), "mirroring inside a reprojection session answered as without one"); + Require(!AnswersNoSession([&] { return sceHmd2ReprojectionGetPredictedDisplayTime(1, &time); }), "predicted display time inside a reprojection session answered as without one"); + Require(!AnswersNoSession([&] { return sceHmd2ReprojectionSetRenderConfig(1, 0, 0); }), "render config inside a reprojection session answered as without one"); + Require(!AnswersNoSession([&] { return sceHmd2ReprojectionSetRenderConfig(1ull << 32, 0, 0); }), "render config with the second accepted bit inside a reprojection session answered as without one"); + for (const std::uint64_t frameId : {0ull, 1ull, 0x123456789ull}) { + Require(sceHmd2ReprojectionGetPredictedDisplayTime(frameId, nullptr) == Hmd2::ParameterNull, "predicted display time into a null output inside a reprojection session"); + } + Require(sceHmd2ReprojectionSetRenderConfig(0, 1, 1) == Hmd2::ParameterInvalid, "render config with nonzero reserved arguments inside a reprojection session"); + Require(sceHmd2ReprojectionSetRenderConfig(2, 0, 0) == Hmd2::ParameterInvalid, "render config with a bit outside the accepted ones inside a reprojection session"); + Require(sceHmd2ReprojectionSetRenderConfig(1, 1, 0) == Hmd2::ParameterInvalid && sceHmd2ReprojectionSetRenderConfig(1, 0, 1) == Hmd2::ParameterInvalid, "render config with one nonzero reserved argument inside a reprojection session"); + Require(sceHmd2ReprojectionSetRenderConfig(~0ull, 0, 0) == Hmd2::ParameterInvalid && sceHmd2ReprojectionSetRenderConfig(3, 0, 0) == Hmd2::ParameterInvalid, "render config with several bits inside a reprojection session"); +} + void TestReprojection() { Require(sceHmd2ReprojectionSetAllowPositionalReprojection(2) == 0, "the positional setting without a session did not succeed"); @@ -356,7 +384,9 @@ void TestReprojection() { const Hmd2::SizeAlign cleared = sceHmd2ReprojectionQueryBufferSizeAlign(); Require(cleared.size == Hmd2::ReprojectionBufferSize && cleared.align == Hmd2::ReprojectionBufferAlign, "clearing the positional setting did not restore the work-size answer"); + TestGatedCallsInsideReprojectionSession(); Require(sceHmd2ReprojectionTerminate() == 0, "terminate did not succeed"); + TestCallsWithoutReprojectionSession(); Require(sceHmd2ReprojectionSetTiming(3000) == Hmd2::ReprojectionNotInitialized, "setting the timing after terminate"); Require(sceHmd2ReprojectionEnableVrMode(Hmd2::OutputMode89_91Hz) == Hmd2::ReprojectionNotInitialized, diff --git a/core/libs/tests/GuestHmdSetupDialog.cpp b/core/libs/tests/GuestHmdSetupDialog.cpp index af83eb7cc9..adab84d2f0 100644 --- a/core/libs/tests/GuestHmdSetupDialog.cpp +++ b/core/libs/tests/GuestHmdSetupDialog.cpp @@ -1,7 +1,9 @@ #include "prx/libSceHmdSetupDialog/include/Api.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include +#include namespace { @@ -15,6 +17,28 @@ constexpr int kErrAlreadyInitialized = static_cast(0x80B80004); constexpr int kErrNotFinished = static_cast(0x80B80005); constexpr int kErrArgNull = static_cast(0x80B8000D); constexpr int kResultUserCanceled = 1; +constexpr int kRaceRounds = 200000; + +template +void RequireTerminateWins(Finish finish) { + std::atomic round{0}; + std::atomic done{0}; + std::thread terminator([&] { + for (int i = 1; i <= kRaceRounds; ++i) { + while (round.load() != i) std::this_thread::yield(); + sceHmdSetupDialogTerminate(); + done.fetch_add(1); + } + }); + for (int i = 1; i <= kRaceRounds; ++i) { + Require(sceHmdSetupDialogInitialize() == 0); + round.store(i); + finish(); + while (done.load() != i) std::this_thread::yield(); + Require(sceHmdSetupDialogGetStatus() == kStatusNone); + } + terminator.join(); +} } @@ -72,4 +96,7 @@ int main() { Require(sceHmdSetupDialogGetResult(result) == kErrNotInitialized); Require(sceHmdSetupDialogGetStatus() == kStatusNone); Require(sceHmdSetupDialogUpdateStatus() == kStatusNone); + + RequireTerminateWins([&] { sceHmdSetupDialogOpen(¶m); }); + RequireTerminateWins([] { sceHmdSetupDialogClose(); }); } diff --git a/core/libs/tests/GuestHttp2Transport.cpp b/core/libs/tests/GuestHttp2Transport.cpp index 39ebff7827..ac1ffb12c6 100644 --- a/core/libs/tests/GuestHttp2Transport.cpp +++ b/core/libs/tests/GuestHttp2Transport.cpp @@ -918,6 +918,22 @@ void Deadline(int ssl, Mode mode, bool overall) { } +void HostAuthority(int ssl) { + std::fprintf(stderr, "HTTP2 host header scenario\n"); + Server server(Mode::Normal); + Client client(ssl); + Require(sceHttp2AddRequestHeader(client.tmpl, "Host", "template.example", 0) == 0, "template host"); + client.Create(server.Url()); + Require(sceHttp2AddRequestHeader(client.request, "host", "request.example", 0) == 0, "request host"); + Require(sceHttp2SendRequest(client.request, nullptr, 0) == 0, "send host request"); + std::array buffer; + while (sceHttp2ReadData(client.request, buffer.data(), buffer.size()) > 0) {} + server.Join(); + const auto& headers = server.Headers(); + Require(headers.count(":authority") == 1 && headers.find(":authority")->second == "request.example", "host header becomes the authority"); + Require(headers.count("host") == 0, "host header is not sent"); +} + int main(int argc, char** argv) { #ifndef _WIN32 std::signal(SIGPIPE, SIG_IGN); @@ -978,6 +994,7 @@ int main(int argc, char** argv) { Failure(untrusted, Mode::Normal); Success(untrusted, Mode::Normal, true); Require(sceSslTerm_nid_postfix(untrusted) == 0, "untrusted SSL term"); + HostAuthority(ssl); } Require(sceSslTerm_nid_postfix(ssl) == 0, "SSL term"); std::puts("HTTP2 loopback transport passed"); diff --git a/core/libs/tests/GuestIcuConstrainedFieldPosition.cpp b/core/libs/tests/GuestIcuConstrainedFieldPosition.cpp new file mode 100644 index 0000000000..55ab4380c3 --- /dev/null +++ b/core/libs/tests/GuestIcuConstrainedFieldPosition.cpp @@ -0,0 +1,103 @@ +#include "prx/libSceIcu/libSceIcu.hpp" +#include +#include +#include +#include +#include + +namespace { + +constexpr std::int32_t Success = 0; +constexpr std::int32_t IllegalArgument = 1; +constexpr std::int32_t InvalidFormat = 3; +constexpr std::int32_t BufferOverflow = 15; + +void Require(bool condition, const char* message) { + if (condition) return; + std::fprintf(stderr, "ICU constrained field position: %s\n", message); + std::abort(); +} + +template +bool Rejects(F call) { + try { + call(); + } catch (const std::invalid_argument&) { + return true; + } + return false; +} + +void Defaults() { + std::int32_t error = Success; + auto* position = ucfpos_open_nid_postfix(&error); + Require(position && error == Success, "open"); + Require(ucfpos_getCategory_nid_postfix(position, &error) == 0 && ucfpos_getField_nid_postfix(position, &error) == 0, "undefined category and field"); + std::int32_t start = -1, limit = -1; + ucfpos_getIndexes_nid_postfix(position, &start, &limit, &error); + Require(start == 0 && limit == 0 && error == Success, "empty indexes"); + ucfpos_close_nid_postfix(position); + error = BufferOverflow; + position = ucfpos_open_nid_postfix(&error); + Require(position && error == BufferOverflow, "open ignores an incoming error"); + ucfpos_close_nid_postfix(position); + position = ucfpos_open_nid_postfix(nullptr); + Require(position != nullptr, "open accepts a null error pointer"); + ucfpos_close_nid_postfix(position); +} + +void Constraints() { + std::int32_t error = Success; + auto* position = ucfpos_open_nid_postfix(&error); + ucfpos_constrainCategory_nid_postfix(position, 5, &error); + Require(ucfpos_getCategory_nid_postfix(position, &error) == 5 && ucfpos_getField_nid_postfix(position, &error) == 0, "category constraint"); + ucfpos_constrainField_nid_postfix(position, 7, 3, &error); + Require(ucfpos_getCategory_nid_postfix(position, &error) == 7 && ucfpos_getField_nid_postfix(position, &error) == 3, "field constraint"); + ucfpos_constrainCategory_nid_postfix(position, -9, &error); + Require(ucfpos_getCategory_nid_postfix(position, &error) == -9 && ucfpos_getField_nid_postfix(position, &error) == 3, "a category constraint keeps the field"); + Require(error == Success, "constraints succeed"); + error = IllegalArgument; + ucfpos_constrainField_nid_postfix(position, 1, 1, &error); + ucfpos_constrainCategory_nid_postfix(position, 1, &error); + Require(ucfpos_getCategory_nid_postfix(position, &error) == 0 && ucfpos_getField_nid_postfix(position, &error) == 0, "a set error returns defaults"); + std::int32_t start = 11, limit = 12; + ucfpos_getIndexes_nid_postfix(position, &start, &limit, &error); + Require(start == 11 && limit == 12 && error == IllegalArgument, "a set error leaves indexes untouched"); + error = Success; + Require(ucfpos_getCategory_nid_postfix(position, &error) == -9 && ucfpos_getField_nid_postfix(position, &error) == 3, "a set error changes nothing"); + ucfpos_close_nid_postfix(position); +} + +void Handles() { + std::int32_t error = Success; + Require(ucfpos_getCategory_nid_postfix(nullptr, &error) == 0 && error == IllegalArgument, "null handle"); + error = Success; + ucfpos_constrainCategory_nid_postfix(nullptr, 4, &error); + Require(error == IllegalArgument, "null handle constraint"); + alignas(8) std::array foreign{}; + auto* forged = reinterpret_cast(foreign.data()); + error = Success; + Require(ucfpos_getField_nid_postfix(forged, &error) == 0 && error == InvalidFormat, "foreign memory is rejected"); + error = Success; + std::int32_t start = 21, limit = 22; + ucfpos_getIndexes_nid_postfix(forged, &start, &limit, &error); + Require(start == 21 && limit == 22 && error == InvalidFormat, "foreign memory indexes"); + ucfpos_close_nid_postfix(nullptr); + ucfpos_close_nid_postfix(forged); + auto* position = ucfpos_open_nid_postfix(&error); + Require(Rejects([&] { ucfpos_getCategory_nid_postfix(position, nullptr); }), "null error pointer"); + Require(Rejects([&] { ucfpos_constrainField_nid_postfix(position, 1, 2, nullptr); }), "null error pointer in a constraint"); + error = Success; + Require(Rejects([&] { ucfpos_getIndexes_nid_postfix(position, nullptr, &start, &error); }), "null start output"); + Require(Rejects([&] { ucfpos_getIndexes_nid_postfix(position, &start, nullptr, &error); }), "null limit output"); + ucfpos_close_nid_postfix(position); +} + +} + +int main() { + Defaults(); + Constraints(); + Handles(); + return 0; +} diff --git a/core/libs/tests/GuestIcuErrorName.cpp b/core/libs/tests/GuestIcuErrorName.cpp new file mode 100644 index 0000000000..2b1a7a558c --- /dev/null +++ b/core/libs/tests/GuestIcuErrorName.cpp @@ -0,0 +1,60 @@ +#include "prx/libSceIcu/libSceIcu.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr const char* Bogus = "[BOGUS UErrorCode]"; + +void Require(bool condition, const char* message) { + if (condition) return; + std::fprintf(stderr, "ICU error name: %s\n", message); + std::abort(); +} + +bool Named(std::int32_t code, const char* expected) { + const char* name = u_errorName_nid_postfix(code); + return name && std::strcmp(name, expected) == 0; +} + +void Boundaries() { + Require(Named(-129, Bogus) && Named(-128, "U_USING_FALLBACK_WARNING") && Named(-124, "U_STRING_NOT_TERMINATED_WARNING"), "warning start"); + Require(Named(-120, "U_PLUGIN_CHANGED_LEVEL_WARNING") && Named(-119, Bogus) && Named(-1, Bogus), "warning limit"); + Require(Named(0, "U_ZERO_ERROR") && Named(1, "U_ILLEGAL_ARGUMENT_ERROR") && Named(15, "U_BUFFER_OVERFLOW_ERROR"), "standard errors"); + Require(Named(31, "U_INPUT_TOO_LONG_ERROR") && Named(32, Bogus) && Named(0xffff, Bogus), "standard limit"); + Require(Named(0x10000, "U_BAD_VARIABLE_DEFINITION") && Named(0x10022, "U_INVALID_FUNCTION") && Named(0x10023, Bogus), "transliterator errors"); + Require(Named(0x10100, "U_UNEXPECTED_TOKEN") && Named(0x1010d, "U_DUPLICATE_KEYWORD") && Named(0x10120, "U_MF_BAD_OPTION") && Named(0x10121, Bogus), "format errors"); + Require(Named(0x10200, "U_BRK_INTERNAL_ERROR") && Named(0x10206, "U_BRK_VARIABLE_REDFINITION") && Named(0x1020d, "U_BRK_MALFORMED_RULE_TAG") && Named(0x1020e, Bogus), "break iterator errors"); + Require(Named(0x10300, "U_REGEX_INTERNAL_ERROR") && Named(0x10315, "U_REGEX_INVALID_CAPTURE_GROUP_NAME") && Named(0x10316, Bogus), "regular expression errors"); + Require(Named(0x10400, "U_STRINGPREP_PROHIBITED_ERROR") && Named(0x10408, "U_IDNA_DOMAIN_NAME_TOO_LONG_ERROR") && Named(0x10409, Bogus), "IDNA errors"); + Require(Named(0x10500, "U_PLUGIN_TOO_HIGH") && Named(0x10501, "U_PLUGIN_DIDNT_SET_LEVEL") && Named(0x10502, Bogus), "plugin errors"); + Require(Named(std::numeric_limits::min(), Bogus) && Named(std::numeric_limits::max(), Bogus), "extreme codes"); +} + +void Coverage() { + std::set names; + std::int32_t named = 0; + for (std::int32_t code = -0x200; code < 0x10600; ++code) { + const char* name = u_errorName_nid_postfix(code); + Require(name != nullptr, "every code has a name"); + Require(name == u_errorName_nid_postfix(code), "names are static strings"); + if (std::strcmp(name, Bogus) == 0) continue; + ++named; + Require(std::strncmp(name, "U_", 2) == 0, "names start with U_"); + names.insert(name); + } + Require(named == 156 && names.size() == 156, "156 distinct named codes"); +} + +} + +int main() { + Boundaries(); + Coverage(); + return 0; +} diff --git a/core/libs/tests/GuestIcuNumeric.cpp b/core/libs/tests/GuestIcuNumeric.cpp new file mode 100644 index 0000000000..4658595f1c --- /dev/null +++ b/core/libs/tests/GuestIcuNumeric.cpp @@ -0,0 +1,141 @@ +#include "prx/libSceIcu/libSceIcu.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr std::uint64_t NaNBits = 0x7ff8000000000000; +constexpr std::uint64_t InfinityBits = 0x7ff0000000000000; +constexpr std::uint64_t NegativeZeroBits = 0x8000000000000000; + +void Require(bool condition, const char* message) { + if (condition) return; + std::fprintf(stderr, "ICU numeric: %s\n", message); + std::abort(); +} + +double FromBits(std::uint64_t bits) { + return std::bit_cast(bits); +} + +std::uint64_t Bits(double value) { + return std::bit_cast(value); +} + +template +bool Rejects(F call) { + try { + call(); + } catch (const std::invalid_argument&) { + return true; + } + return false; +} + +void Classification() { + const double infinity = std::numeric_limits::infinity(); + for (const std::uint64_t bits : std::array{NaNBits, 0x7ff0000000000001ull, 0x7fffffffffffffffull, 0xfff8000000000000ull, 0xfff0000000000001ull}) { + Require(uprv_isNaN_nid_postfix(FromBits(bits)) == 1, "NaN patterns are NaN"); + Require(uprv_isInfinite_nid_postfix(FromBits(bits)) == 0, "NaN patterns are not infinite"); + Require(uprv_isPositiveInfinity_nid_postfix(FromBits(bits)) == 0 && uprv_isNegativeInfinity_nid_postfix(FromBits(bits)) == 0, "NaN has no infinite sign"); + } + for (const double value : {0.0, -0.0, 1.0, -1.0, std::numeric_limits::max(), std::numeric_limits::denorm_min(), -std::numeric_limits::max()}) { + Require(uprv_isNaN_nid_postfix(value) == 0, "finite values are not NaN"); + Require(uprv_isInfinite_nid_postfix(value) == 0, "finite values are not infinite"); + Require(uprv_isPositiveInfinity_nid_postfix(value) == 0 && uprv_isNegativeInfinity_nid_postfix(value) == 0, "finite values have no infinite sign"); + } + Require(uprv_isInfinite_nid_postfix(infinity) == 1 && uprv_isInfinite_nid_postfix(-infinity) == 1, "infinities are infinite"); + Require(uprv_isNaN_nid_postfix(infinity) == 0 && uprv_isNaN_nid_postfix(-infinity) == 0, "infinities are not NaN"); + Require(uprv_isPositiveInfinity_nid_postfix(infinity) == 1 && uprv_isPositiveInfinity_nid_postfix(-infinity) == 0, "positive infinity"); + Require(uprv_isNegativeInfinity_nid_postfix(-infinity) == 1 && uprv_isNegativeInfinity_nid_postfix(infinity) == 0, "negative infinity"); + Require(Bits(uprv_getNaN_nid_postfix()) == NaNBits, "NaN bit pattern"); + Require(Bits(uprv_getInfinity_nid_postfix()) == InfinityBits, "infinity bit pattern"); +} + +void LibraryMath() { + Require(uprv_floor_nid_postfix(-2.5) == -3.0 && uprv_floor_nid_postfix(2.5) == 2.0, "floor"); + Require(uprv_ceil_nid_postfix(-2.5) == -2.0 && uprv_ceil_nid_postfix(2.5) == 3.0, "ceil"); + Require(Bits(uprv_fabs_nid_postfix(-0.0)) == 0 && uprv_fabs_nid_postfix(-7.25) == 7.25, "fabs"); + double integral = 0; + Require(uprv_modf_nid_postfix(-3.25, &integral) == -0.25 && integral == -3.0, "modf"); + Require(Rejects([] { uprv_modf_nid_postfix(1.5, nullptr); }), "modf rejects a null integral pointer"); + Require(uprv_fmod_nid_postfix(7.5, 2.0) == 1.5 && uprv_fmod_nid_postfix(-7.5, 2.0) == -1.5, "fmod"); + Require(uprv_pow_nid_postfix(2.0, 10.0) == 1024.0 && uprv_pow_nid_postfix(-2.0, 3.0) == -8.0, "pow"); + Require(uprv_pow10_nid_postfix(3) == 1000.0 && uprv_pow10_nid_postfix(0) == 1.0, "pow10"); + Require(uprv_pow10_nid_postfix(-2) == std::pow(10.0, -2.0) && uprv_pow10_nid_postfix(400) == std::numeric_limits::infinity(), "pow10 of negative and large exponents"); + Require(uprv_log_nid_postfix(1.0) == 0.0 && uprv_log_nid_postfix(0.0) == -std::numeric_limits::infinity(), "log"); + Require(uprv_isNaN_nid_postfix(uprv_log_nid_postfix(-1.0)) == 1, "log of a negative number"); + Require(uprv_maxMantissa_nid_postfix() == 18014398509481983.0, "maximum mantissa is 2^54 - 1"); +} + +void Rounding() { + Require(uprv_round_nid_postfix(2.5) == 3.0 && uprv_round_nid_postfix(-2.5) == -2.0, "round half up"); + Require(uprv_round_nid_postfix(-2.6) == -3.0 && uprv_round_nid_postfix(2.4) == 2.0, "round to nearest"); + Require(uprv_round_nid_postfix(0.49999999999999994) == 1.0, "round adds one half before flooring"); + Require(Bits(uprv_round_nid_postfix(-0.5)) == 0, "round of minus one half is positive zero"); + Require(uprv_trunc_nid_postfix(3.7) == 3.0 && uprv_trunc_nid_postfix(-3.7) == -3.0, "trunc"); + Require(Bits(uprv_trunc_nid_postfix(-0.5)) == NegativeZeroBits && Bits(uprv_trunc_nid_postfix(-0.0)) == NegativeZeroBits, "trunc keeps the sign of zero"); + Require(Bits(uprv_trunc_nid_postfix(0.5)) == 0, "trunc of a positive fraction"); + Require(Bits(uprv_trunc_nid_postfix(std::numeric_limits::infinity())) == InfinityBits, "trunc of positive infinity"); + Require(Bits(uprv_trunc_nid_postfix(-std::numeric_limits::infinity())) == InfinityBits, "trunc of negative infinity returns positive infinity"); + Require(Bits(uprv_trunc_nid_postfix(FromBits(0xfff8000000000001ull))) == NaNBits, "trunc returns the canonical NaN"); +} + +void Extremes() { + const double nan = FromBits(0xfff8000000000001ull); + Require(Bits(uprv_fmax_nid_postfix(nan, 1.0)) == NaNBits && Bits(uprv_fmax_nid_postfix(1.0, nan)) == NaNBits, "fmax with NaN"); + Require(Bits(uprv_fmin_nid_postfix(nan, 1.0)) == NaNBits && Bits(uprv_fmin_nid_postfix(1.0, nan)) == NaNBits, "fmin with NaN"); + Require(uprv_fmax_nid_postfix(1.0, 2.0) == 2.0 && uprv_fmax_nid_postfix(2.0, 1.0) == 2.0, "fmax"); + Require(uprv_fmin_nid_postfix(1.0, 2.0) == 1.0 && uprv_fmin_nid_postfix(2.0, 1.0) == 1.0, "fmin"); + Require(uprv_fmax_nid_postfix(-std::numeric_limits::infinity(), 3.0) == 3.0, "fmax with negative infinity"); + Require(Bits(uprv_fmax_nid_postfix(-0.0, 0.0)) == 0 && Bits(uprv_fmax_nid_postfix(0.0, 0.0)) == 0, "fmax prefers positive zero after negative zero"); + Require(Bits(uprv_fmax_nid_postfix(0.0, -0.0)) == NegativeZeroBits && Bits(uprv_fmax_nid_postfix(-0.0, -0.0)) == NegativeZeroBits, "fmax returns its second zero"); + Require(Bits(uprv_fmin_nid_postfix(0.0, -0.0)) == NegativeZeroBits && Bits(uprv_fmin_nid_postfix(-0.0, 0.0)) == NegativeZeroBits, "fmin prefers negative zero"); + Require(Bits(uprv_fmin_nid_postfix(0.0, 0.0)) == 0, "fmin of positive zeros"); + constexpr std::int32_t low = std::numeric_limits::min(); + constexpr std::int32_t high = std::numeric_limits::max(); + Require(uprv_max_nid_postfix(low, high) == high && uprv_max_nid_postfix(high, low) == high && uprv_max_nid_postfix(-4, -4) == -4, "max"); + Require(uprv_min_nid_postfix(low, high) == low && uprv_min_nid_postfix(high, low) == low && uprv_min_nid_postfix(7, 7) == 7, "min"); +} + +void Overflow() { + constexpr std::int32_t low = std::numeric_limits::min(); + constexpr std::int32_t high = std::numeric_limits::max(); + std::int32_t result = 0; + Require(uprv_add32_overflow_nid_postfix(2, 3, &result) == 0 && result == 5, "addition"); + Require(uprv_add32_overflow_nid_postfix(high, 1, &result) == 1 && result == low, "addition overflow wraps"); + Require(uprv_add32_overflow_nid_postfix(low, -1, &result) == 1 && result == high, "addition underflow wraps"); + Require(uprv_add32_overflow_nid_postfix(high, low, &result) == 0 && result == -1, "addition of the extremes"); + Require(uprv_mul32_overflow_nid_postfix(-46340, 46340, &result) == 0 && result == -2147395600, "multiplication"); + Require(uprv_mul32_overflow_nid_postfix(65536, 65536, &result) == 1 && result == 0, "multiplication overflow wraps"); + Require(uprv_mul32_overflow_nid_postfix(-46341, 46341, &result) == 1 && result == 2147479015, "negative multiplication overflow wraps"); + Require(uprv_mul32_overflow_nid_postfix(low, -1, &result) == 1 && result == low, "negating the minimum overflows"); + Require(Rejects([] { uprv_add32_overflow_nid_postfix(1, 2, nullptr); }), "addition rejects a null result"); + Require(Rejects([] { uprv_mul32_overflow_nid_postfix(1, 2, nullptr); }), "multiplication rejects a null result"); +} + +void MaximumPointer() { + constexpr std::uintptr_t top = std::numeric_limits::max(); + const auto limit = [](std::uintptr_t base) { return reinterpret_cast(uprv_maximumPtr_nid_postfix(reinterpret_cast(base))); }; + Require(limit(0) == 0x7fffffff && limit(0x1000) == 0x80000fff, "maximum pointer adds 2^31 - 1"); + Require(limit(top - 0x7fffffff) == top, "maximum pointer reaching the top"); + Require(limit(top - 0x7ffffffe) == top && limit(top) == top, "maximum pointer saturates instead of wrapping"); +} + +} + +int main() { + Classification(); + LibraryMath(); + Rounding(); + Extremes(); + Overflow(); + MaximumPointer(); + return 0; +} diff --git a/core/libs/tests/GuestIcuVersion.cpp b/core/libs/tests/GuestIcuVersion.cpp index 31c85b68c5..26caf49bbc 100644 --- a/core/libs/tests/GuestIcuVersion.cpp +++ b/core/libs/tests/GuestIcuVersion.cpp @@ -207,6 +207,14 @@ void VersionToString() { u_versionToString_nid_postfix(version.data(), nullptr); } +void GetVersion() { + Version version{9, 9, 9, 9}; + u_getVersion_nid_postfix(version.data()); + Require(version == Version{78, 1, 0, 0}, "library version is 78.1.0.0"); + Require(ToString(version) == "78.1", "library version string"); + u_getVersion_nid_postfix(nullptr); +} + void RoundTrip() { const auto written = [](std::uint32_t part) { return part < 100 || part % 100 >= 10; }; for (std::uint32_t part = 0; part < 256; ++part) { @@ -226,6 +234,7 @@ int main() { VersionFromString(); VersionFromUString(); VersionToString(); + GetVersion(); RoundTrip(); return 0; } diff --git a/core/libs/tests/GuestImeComposition.cpp b/core/libs/tests/GuestImeComposition.cpp new file mode 100644 index 0000000000..d1377020bb --- /dev/null +++ b/core/libs/tests/GuestImeComposition.cpp @@ -0,0 +1,213 @@ +#include "prx/libSceIme/include/Api.hpp" +#include "SceTypes.hpp" +#include "prx/libSceIme/include/ImeInput.hpp" + +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr uint32_t EventUpdateText = 1; +constexpr uint32_t EventUpdateCaret = 2; +constexpr uint32_t EventPressClose = 4; +constexpr uint32_t EventPressEnter = 5; + +struct Recorded { + uint32_t id; + std::u16string text; +}; + +std::vector g_events; +int g_filterCalls = 0; + +void APS5_VABI Record(void*, const ImeEvent* event) { + Recorded recorded{event->id, {}}; + if (event->id == EventUpdateText || event->id == EventPressEnter || event->id == EventPressClose) recorded.text = event->param.text.str; + g_events.push_back(recorded); +} + +int32_t APS5_VABI CountingFilter(char16_t* outText, uint32_t* outTextLength, const char16_t* sourceText, uint32_t sourceTextLength) { + ++g_filterCalls; + for (uint32_t i = 0; i < sourceTextLength; ++i) outText[i] = sourceText[i]; + *outTextLength = sourceTextLength; + return 0; +} + +void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "%s\n", message); + std::abort(); +} + +struct Buffers { + char16_t text[2049] = {}; + alignas(16) unsigned char work[0x5000] = {}; + int arg = 0; +}; + +Param MakeParam(Buffers& buffers, uint32_t maxLength) { + Param param; + sceImeParamInit(¶m); + param.user_id = 1; + param.max_text_length = maxLength; + param.input_text_buffer = buffers.text; + param.posx = 100.0f; + param.posy = 200.0f; + param.work = buffers.work; + param.arg = &buffers.arg; + param.handler = Record; + return param; +} + +std::vector Drain() { + g_events.clear(); + Require(sceImeUpdate(Record) == 0, "update failed"); + return g_events; +} + +void Publish(ImeHostInputKind kind, const char* text = "", std::int32_t start = 0, std::int32_t length = 0) { + ImeHostInput input{}; + input.kind = kind; + std::strncpy(input.text, text, sizeof(input.text) - 1); + input.start = start; + input.length = length; + ImePublishInput_nid_postfix(input); +} + +const char16_t* Work(const Buffers& buffers) { + return reinterpret_cast(buffers.work); +} + +void CheckLifecycle() { + Buffers buffers; + std::memcpy(buffers.text, u"ab", sizeof(u"ab")); + Param param = MakeParam(buffers, 16); + param.filter = reinterpret_cast(&CountingFilter); + Require(sceImeOpen_nid_postfix(¶m, nullptr) == 0, "open"); + Drain(); + + Publish(ImeHostInputKind::Preedit, "\xe3\x81\x93", 1, 0); + Require(Drain().empty(), "a preedit queued an event"); + Publish(ImeHostInputKind::Preedit, "\xe3\x81\x93\xe3\x82\x93", 2, 0); + Require(Drain().empty(), "a second preedit queued an event"); + Require(std::u16string(buffers.text) == u"ab" && std::u16string(Work(buffers)) == u"ab", "a preedit reached the guest buffers"); + Require(g_filterCalls == 0, "a preedit reached the text filter"); + + Publish(ImeHostInputKind::Preedit, ""); + Publish(ImeHostInputKind::Text, "\xe3\x81\x93\xe3\x82\x93"); + const auto committed = Drain(); + Require(committed.size() == 1 && committed[0].id == EventUpdateText && committed[0].text == u"ab\u3053\u3093", "the commit is one text update"); + Require(std::u16string(buffers.text) == u"ab\u3053\u3093", "the commit reached the input buffer"); + Require(g_filterCalls == 1, "the commit reached the text filter once"); + + Publish(ImeHostInputKind::Preedit, "ni", 2, 0); + Publish(ImeHostInputKind::Text, "\xe4\xbb\x8a"); + Publish(ImeHostInputKind::Backspace); + const auto afterCommit = Drain(); + Require(afterCommit.size() == 2 && afterCommit[1].text == u"ab\u3053\u3093", "a commit ends the composition so Backspace edits again"); + Require(sceImeClose_nid_postfix() == 0, "close"); +} + +void CheckCancel() { + Buffers buffers; + std::memcpy(buffers.text, u"ab", sizeof(u"ab")); + Param param = MakeParam(buffers, 16); + Require(sceImeOpen_nid_postfix(¶m, nullptr) == 0, "open for cancel"); + Drain(); + + Publish(ImeHostInputKind::Preedit, "ni", 2, 0); + Publish(ImeHostInputKind::Escape); + Require(Drain().empty(), "Escape while composing closed the panel"); + Publish(ImeHostInputKind::Escape); + const auto closed = Drain(); + Require(closed.size() == 1 && closed[0].id == EventPressClose, "the next Escape reaches the title"); + + Publish(ImeHostInputKind::Preedit, "ni", 2, 0); + Publish(ImeHostInputKind::Enter); + Require(Drain().empty(), "Enter while composing accepted the text"); + Publish(ImeHostInputKind::Enter); + const auto entered = Drain(); + Require(entered.size() == 1 && entered[0].id == EventPressEnter, "the next Enter reaches the title"); + + Publish(ImeHostInputKind::Preedit, "ni", 2, 0); + Publish(ImeHostInputKind::Preedit, ""); + Publish(ImeHostInputKind::Escape); + const auto emptied = Drain(); + Require(emptied.size() == 1 && emptied[0].id == EventPressClose, "an emptied composition no longer swallows Escape"); + Require(std::u16string(buffers.text) == u"ab", "a discarded composition changed the buffer"); + Require(sceImeClose_nid_postfix() == 0, "close after cancel"); +} + +void CheckEditKeysBelongToTheInputMethod() { + Buffers buffers; + std::memcpy(buffers.text, u"abc", sizeof(u"abc")); + Param param = MakeParam(buffers, 16); + Require(sceImeOpen_nid_postfix(¶m, nullptr) == 0, "open for edit keys"); + Drain(); + + Publish(ImeHostInputKind::Preedit, "ni", 2, 0); + for (const auto kind : {ImeHostInputKind::Backspace, ImeHostInputKind::Delete, ImeHostInputKind::Left, ImeHostInputKind::Right, ImeHostInputKind::Home, ImeHostInputKind::End}) Publish(kind); + Require(Drain().empty() && std::u16string(buffers.text) == u"abc", "an editing key acted on the text while composing"); + + Publish(ImeHostInputKind::Preedit, ""); + Publish(ImeHostInputKind::Left); + const auto moved = Drain(); + Require(moved.size() == 1 && moved[0].id == EventUpdateCaret, "Left works once the composition is gone"); + Publish(ImeHostInputKind::Backspace); + const auto erased = Drain(); + Require(erased.size() == 1 && erased[0].text == u"ac", "Backspace works once the composition is gone"); + Require(sceImeClose_nid_postfix() == 0, "close after edit keys"); +} + +void CheckCloseDiscardsTheComposition() { + Buffers buffers; + std::memcpy(buffers.text, u"ab", sizeof(u"ab")); + Param param = MakeParam(buffers, 16); + Require(sceImeOpen_nid_postfix(¶m, nullptr) == 0, "first open"); + Drain(); + Publish(ImeHostInputKind::Preedit, "ni", 2, 0); + Drain(); + Require(sceImeClose_nid_postfix() == 0, "close with a composition"); + Require(sceImeOpen_nid_postfix(¶m, nullptr) == 0, "second open"); + Drain(); + Publish(ImeHostInputKind::Backspace); + const auto erased = Drain(); + Require(erased.size() == 1 && erased[0].text == u"a", "a composition survived the close"); + Require(sceImeClose_nid_postfix() == 0, "close the second session"); +} + +void CheckMalformedPreedit() { + Buffers buffers; + Param param = MakeParam(buffers, 16); + Require(sceImeOpen_nid_postfix(¶m, nullptr) == 0, "open for malformed"); + Drain(); + char cut[sizeof(ImeHostInput::text)] = {}; + std::memset(cut, 'a', sizeof(cut)); + cut[30] = '\xe3'; + cut[31] = '\x81'; + ImeHostInput input{}; + input.kind = ImeHostInputKind::Preedit; + std::memcpy(input.text, cut, sizeof(cut)); + ImePublishInput_nid_postfix(input); + Publish(ImeHostInputKind::Preedit, "\xff\xfe\x80", 0, 0); + Require(Drain().empty() && buffers.text[0] == u'\0', "a malformed preedit changed the session"); + Publish(ImeHostInputKind::Preedit, ""); + Publish(ImeHostInputKind::Text, "z"); + const auto typed = Drain(); + Require(typed.size() == 1 && typed[0].text == u"z", "typing still works after a malformed preedit"); + Require(sceImeClose_nid_postfix() == 0, "close after malformed"); +} + +} + +int main() { + CheckLifecycle(); + CheckCancel(); + CheckEditKeysBelongToTheInputMethod(); + CheckCloseDiscardsTheComposition(); + CheckMalformedPreedit(); +} diff --git a/core/libs/tests/GuestImeDialog.cpp b/core/libs/tests/GuestImeDialog.cpp index 0dcba47a4a..229ca4cfe5 100644 --- a/core/libs/tests/GuestImeDialog.cpp +++ b/core/libs/tests/GuestImeDialog.cpp @@ -80,6 +80,8 @@ Param DialogParam(char16_t* buffer, std::uint32_t maxLength) { } std::uint32_t EndStatus() { + Require(sceImeDialogGetResult(nullptr) == ErrorNotOpened); + Require(sceImeDialogGetResult(reinterpret_cast(0x10)) == ErrorNotOpened); Result result{}; Require(sceImeDialogGetResult(&result) == 0); return result.endstatus; diff --git a/core/libs/tests/GuestImeDialogComposition.cpp b/core/libs/tests/GuestImeDialogComposition.cpp new file mode 100644 index 0000000000..e9eea007e9 --- /dev/null +++ b/core/libs/tests/GuestImeDialogComposition.cpp @@ -0,0 +1,191 @@ +#include "prx/libSceImeDialog/include/Api.hpp" +#include "SceTypes.hpp" +#include "prx/libSceIme/include/ImeInput.hpp" +#include "prx/libSceIme/include/ImeKeyboardView.hpp" + +#include +#include +#include +#include +#include + +namespace { + +constexpr int StatusRunning = 1; +constexpr int StatusFinished = 2; +constexpr std::uint32_t EndStatusOk = 0; +constexpr std::uint32_t EndStatusUserCanceled = 1; + +void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "%s\n", message); + std::abort(); +} + +void Publish(ImeHostInputKind kind, const char* text = "", std::int32_t start = 0, std::int32_t length = 0) { + ImeHostInput input{}; + input.kind = kind; + std::strncpy(input.text, text, sizeof(input.text) - 1); + input.start = start; + input.length = length; + ImePublishInput_nid_postfix(input); +} + +Param DialogParam(char16_t* buffer, std::uint32_t maxLength) { + Param param{}; + param.max_text_length = maxLength; + param.input_text_buffer = buffer; + return param; +} + +ImeKeyboardView View() { + ImeKeyboardView view; + Require(ImeReadKeyboardView_nid_postfix(view), "the dialog shows no keyboard"); + return view; +} + +std::uint32_t EndStatus() { + Result result{}; + Require(sceImeDialogGetResult(&result) == 0, "no result"); + return result.endstatus; +} + +void Open(const Param& param) { + Require(sceImeDialogInit(¶m, nullptr) == 0, "init"); + Require(sceImeDialogGetStatus() == StatusRunning, "not running"); +} + +void Close() { + Require(sceImeDialogTerm() == 0, "term"); +} + +void CheckLifecycle() { + char16_t buffer[16] = u"ab"; + const Param param = DialogParam(buffer, 8); + Open(param); + + Require(View().preedit.empty(), "a fresh dialog shows a composition"); + Publish(ImeHostInputKind::Preedit, "\xe3\x81\x93", 1, 0); + auto view = View(); + Require(view.preedit == u"\u3053" && view.preeditCursor == 1 && view.preeditSelection == 0, "the first edit is shown"); + Require(view.text == u"ab" && view.caret == 2, "the preedit entered the committed text"); + Publish(ImeHostInputKind::Preedit, "\xe3\x81\x93\xe3\x82\x93\xe3\x81\xab", 1, 2); + view = View(); + Require(view.preedit == u"\u3053\u3093\u306B" && view.preeditCursor == 1 && view.preeditSelection == 2, "the second edit replaces the first and keeps the selection"); + Require(view.text == u"ab", "a second edit entered the committed text"); + Require(std::u16string(buffer) == u"ab", "a preedit reached the guest buffer"); + + Publish(ImeHostInputKind::Text, "\xe3\x81\x93\xe3\x82\x93\xe3\x81\xab"); + view = View(); + Require(view.preedit.empty() && view.preeditCursor == 0 && view.preeditSelection == 0, "a commit left the preedit shown"); + Require(view.text == u"ab\u3053\u3093\u306B" && view.caret == 5, "a commit is inserted at the caret"); + Require(std::u16string(buffer) == u"ab", "the title sees text before the dialog is accepted"); + + Publish(ImeHostInputKind::Preedit, "ni", 2, 0); + Publish(ImeHostInputKind::Enter); + Require(sceImeDialogGetStatus() == StatusRunning, "Enter while composing accepted the dialog"); + Require(View().preedit.empty(), "Enter did not discard the composition"); + Publish(ImeHostInputKind::Enter); + Require(sceImeDialogGetStatus() == StatusFinished && EndStatus() == EndStatusOk, "the next Enter does not accept the dialog"); + Require(std::u16string(buffer) == u"ab\u3053\u3093\u306B", "a discarded preedit leaked into the accepted text"); + Close(); +} + +void CheckCancel() { + char16_t buffer[16] = u"keep"; + const Param param = DialogParam(buffer, 8); + Open(param); + Publish(ImeHostInputKind::Preedit, "ni", 2, 0); + Publish(ImeHostInputKind::Escape); + Require(sceImeDialogGetStatus() == StatusRunning && View().preedit.empty(), "Escape while composing cancelled the dialog or kept the composition"); + Publish(ImeHostInputKind::Escape); + Require(sceImeDialogGetStatus() == StatusFinished && EndStatus() == EndStatusUserCanceled, "the next Escape does not cancel the dialog"); + Require(std::u16string(buffer) == u"keep", "a cancelled dialog changed the buffer"); + Close(); +} + +void CheckEditKeysAndPad() { + char16_t buffer[16] = u"abc"; + const Param param = DialogParam(buffer, 8); + Open(param); + Publish(ImeHostInputKind::Preedit, "ni", 2, 0); + for (const auto kind : {ImeHostInputKind::Backspace, ImeHostInputKind::Delete, ImeHostInputKind::Left, ImeHostInputKind::Right, ImeHostInputKind::Home, ImeHostInputKind::End}) Publish(kind); + auto view = View(); + Require(view.text == u"abc" && view.caret == 3 && view.preedit == u"ni", "an editing key acted while composing"); + Publish(ImeHostInputKind::Preedit, ""); + Publish(ImeHostInputKind::Left); + Publish(ImeHostInputKind::Backspace); + view = View(); + Require(view.text == u"ac" && view.caret == 1, "editing keys do not work after the composition ends"); + Close(); +} + +void CheckClosing() { + char16_t buffer[16] = u"ab"; + const Param param = DialogParam(buffer, 8); + Open(param); + Publish(ImeHostInputKind::Preedit, "ni", 2, 0); + Require(sceImeDialogAbort() == 0, "abort"); + ImeKeyboardView view; + Require(!ImeReadKeyboardView_nid_postfix(view), "a finished dialog still shows a keyboard"); + Close(); + + Open(param); + Require(View().preedit.empty(), "a composition survived into the next dialog"); + Publish(ImeHostInputKind::Preedit, "ni", 2, 0); + Require(sceImeDialogForceClose() == 0, "force close"); + Publish(ImeHostInputKind::Preedit, "late", 0, 0); + Open(param); + Require(View().preedit.empty(), "a preedit after the close reached the next dialog"); + Publish(ImeHostInputKind::Backspace); + Require(View().text == u"a", "a composition after the close froze the next dialog"); + Close(); + + Publish(ImeHostInputKind::Preedit, "late", 0, 0); + Open(param); + Require(View().preedit.empty(), "a preedit before init reached the dialog"); + Close(); +} + +void CheckSelectionAndCaretOffsets() { + char16_t buffer[16] = u""; + const Param param = DialogParam(buffer, 8); + Open(param); + Publish(ImeHostInputKind::Preedit, "a\xf0\x9f\x98\x80z", 2, 1); + auto view = View(); + Require(view.preedit == u"a\U0001F600z" && view.preeditCursor == 3 && view.preeditSelection == 1, "offsets count a surrogate pair as one character"); + Publish(ImeHostInputKind::Preedit, "abc", 40, 40); + view = View(); + Require(view.preeditCursor == 3 && view.preeditSelection == 0, "offsets past the end are clamped"); + Publish(ImeHostInputKind::Preedit, "abc", -2, -2); + view = View(); + Require(view.preeditCursor == 0 && view.preeditSelection == 0, "negative offsets are zero"); + Close(); +} + +void CheckMalformedPreedit() { + char16_t buffer[16] = u""; + const Param param = DialogParam(buffer, 8); + Open(param); + ImeHostInput input{}; + input.kind = ImeHostInputKind::Preedit; + std::memset(input.text, 'a', sizeof(input.text)); + input.text[30] = '\xe3'; + input.text[31] = '\x81'; + ImePublishInput_nid_postfix(input); + Require(View().preedit == std::u16string(30, u'a'), "a preedit cut inside a character shows half of it"); + Publish(ImeHostInputKind::Preedit, "\xff\x01" "b", 0, 0); + Require(View().preedit == u"\uFFFDb", "an invalid byte is not replaced and a control character is not removed"); + Close(); +} + +} + +int main() { + CheckLifecycle(); + CheckCancel(); + CheckEditKeysAndPad(); + CheckClosing(); + CheckSelectionAndCaretOffsets(); + CheckMalformedPreedit(); +} diff --git a/core/libs/tests/GuestImeDialogPaste.cpp b/core/libs/tests/GuestImeDialogPaste.cpp new file mode 100644 index 0000000000..7c62866e04 --- /dev/null +++ b/core/libs/tests/GuestImeDialogPaste.cpp @@ -0,0 +1,158 @@ +#include "prx/libSceImeDialog/include/Api.hpp" +#include "SceTypes.hpp" +#include "prx/libSceIme/include/ImeInput.hpp" +#include "prx/libSceIme/include/ImeKeyboardView.hpp" + +#include +#include +#include +#include +#include + +namespace { + +constexpr int StatusRunning = 1; + +void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "%s\n", message); + std::abort(); +} + +void Paste(const std::string& utf8) { + ImePublishPaste_nid_postfix(utf8.data(), utf8.size()); +} + +void Publish(ImeHostInputKind kind, const char* text = "", std::int32_t start = 0, std::int32_t length = 0) { + ImeHostInput input{}; + input.kind = kind; + std::strncpy(input.text, text, sizeof(input.text) - 1); + input.start = start; + input.length = length; + ImePublishInput_nid_postfix(input); +} + +Param DialogParam(char16_t* buffer, std::uint32_t maxLength) { + Param param{}; + param.max_text_length = maxLength; + param.input_text_buffer = buffer; + return param; +} + +ImeKeyboardView View() { + ImeKeyboardView view; + Require(ImeReadKeyboardView_nid_postfix(view), "the dialog shows no keyboard"); + return view; +} + +void Open(const Param& param) { + Require(sceImeDialogInit(¶m, nullptr) == 0, "init"); + Require(sceImeDialogGetStatus() == StatusRunning, "not running"); +} + +void Close() { + Require(sceImeDialogTerm() == 0, "term"); +} + +void CheckInsertAtCaret() { + char16_t buffer[16] = u"ab"; + const Param param = DialogParam(buffer, 8); + Open(param); + Publish(ImeHostInputKind::Left); + Paste("XY"); + auto view = View(); + Require(view.text == u"aXYb" && view.caret == 3, "a paste lands at the caret and moves it"); + Require(std::u16string(buffer) == u"ab", "a paste reached the buffer before the dialog was accepted"); + Close(); +} + +void CheckLineBreaksAndControls() { + char16_t buffer[16] = u""; + const Param param = DialogParam(buffer, 16); + Open(param); + Paste(std::string("a\r\nb\nc\td\x01" "e\x7f" "f", 13)); + Require(View().text == u"abcdef", "line breaks and controls are not removed from a paste"); + Close(); +} + +void CheckMaxLength() { + char16_t buffer[16] = u"abc"; + const Param param = DialogParam(buffer, 5); + Open(param); + Paste("d\xf0\x9f\x98\x80"); + Require(View().text == u"abcd", "a paste split a surrogate pair at the limit"); + Paste("ef"); + Require(View().text == u"abcde", "a paste went past the maximum length"); + Paste("g"); + Require(View().text == u"abcde", "a paste into a full dialog changed it"); + Close(); + + char16_t wide[16] = u""; + const Param roomForPair = DialogParam(wide, 4); + Open(roomForPair); + Paste("x\xf0\x9f\x98\x80\xf0\x9f\x98\x80"); + Require(View().text == u"x\U0001F600", "a pair that fits is not kept whole"); + Close(); +} + +void CheckHugeClipboard() { + char16_t buffer[2049] = u""; + const Param param = DialogParam(buffer, 2048); + Open(param); + const std::string huge(8u * 1024u * 1024u, 'x'); + Paste(huge); + Require(View().text.size() == 2048, "a huge clipboard did not fill the dialog to its limit exactly"); + Close(); +} + +void CheckInvalidUtf8() { + char16_t buffer[16] = u""; + const Param param = DialogParam(buffer, 16); + Open(param); + Paste("a\xff" "b\xe3\x81"); + Require(View().text == u"a\uFFFDb\uFFFD", "an invalid clipboard is not replaced"); + Close(); +} + +void CheckNothingCaptured() { + Require(!ImeCapturesKeyboard_nid_postfix(), "something is capturing"); + Paste("ignored"); + char16_t buffer[16] = u""; + const Param param = DialogParam(buffer, 16); + Open(param); + Require(View().text.empty(), "a paste before init was kept"); + Close(); + Paste("ignored"); + Open(param); + Require(View().text.empty(), "a paste after term was kept"); + Require(sceImeDialogAbort() == 0, "abort"); + Paste("ignored"); + ImeKeyboardView view; + Require(!ImeReadKeyboardView_nid_postfix(view), "a finished dialog shows a keyboard"); + Close(); +} + +void CheckPasteWhileComposing() { + char16_t buffer[16] = u"ab"; + const Param param = DialogParam(buffer, 16); + Open(param); + Publish(ImeHostInputKind::Preedit, "ni", 2, 0); + Paste("XY"); + Require(View().text == u"ab", "a paste went in while a composition was shown"); + Publish(ImeHostInputKind::Preedit, ""); + Paste("XY"); + Require(View().text == u"abXY", "a paste does not work once the composition ends"); + Close(); +} + +} + +int main() { + CheckNothingCaptured(); + CheckInsertAtCaret(); + CheckLineBreaksAndControls(); + CheckMaxLength(); + CheckHugeClipboard(); + CheckInvalidUtf8(); + CheckPasteWhileComposing(); +} diff --git a/core/libs/tests/GuestImeForTestFunction.cpp b/core/libs/tests/GuestImeForTestFunction.cpp new file mode 100644 index 0000000000..8601812367 --- /dev/null +++ b/core/libs/tests/GuestImeForTestFunction.cpp @@ -0,0 +1,22 @@ +#include "prx/libSceIme/include/Api.hpp" + +#include +#include + +namespace { + +void Require(bool value, const char* what) { + if (value) return; + std::fprintf(stderr, "%s\n", what); + std::abort(); +} + +} // namespace + +int main() { + const int first = sceImeForTestFunction(); + const int second = sceImeForTestFunction(); + Require(first < 0, "sceImeForTestFunction did not answer an error"); + Require(first == second, "sceImeForTestFunction did not answer the same error on a repeated call"); + return 0; +} diff --git a/core/libs/tests/GuestImePaste.cpp b/core/libs/tests/GuestImePaste.cpp new file mode 100644 index 0000000000..a03d1385e9 --- /dev/null +++ b/core/libs/tests/GuestImePaste.cpp @@ -0,0 +1,231 @@ +#include "prx/libSceIme/include/Api.hpp" +#include "SceTypes.hpp" +#include "prx/libSceIme/include/ImeInput.hpp" + +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr uint32_t EventUpdateText = 1; + +struct Recorded { + uint32_t id; + std::u16string text; + uint32_t index; + int32_t length; +}; + +std::vector g_events; +int g_filterCalls = 0; + +void APS5_VABI Record(void*, const ImeEvent* event) { + Recorded recorded{event->id, {}, 0, 0}; + if (event->id == EventUpdateText) { + recorded.text = event->param.text.str; + recorded.index = event->param.text.text_area[0].index; + recorded.length = event->param.text.text_area[0].length; + } + g_events.push_back(recorded); +} + +int32_t APS5_VABI DigitsOnlyFilter(char16_t* outText, uint32_t* outTextLength, const char16_t* sourceText, uint32_t sourceTextLength) { + ++g_filterCalls; + uint32_t length = 0; + for (uint32_t i = 0; i < sourceTextLength; ++i) { + if (sourceText[i] >= u'0' && sourceText[i] <= u'9') outText[length++] = sourceText[i]; + } + *outTextLength = length; + return 0; +} + +void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "%s\n", message); + std::abort(); +} + +struct Buffers { + char16_t text[2049] = {}; + alignas(16) unsigned char work[0x5000] = {}; + int arg = 0; +}; + +Param MakeParam(Buffers& buffers, uint32_t maxLength, uint32_t option = 0) { + Param param; + sceImeParamInit(¶m); + param.user_id = 1; + param.max_text_length = maxLength; + param.input_text_buffer = buffers.text; + param.option = option; + param.posx = 100.0f; + param.posy = 200.0f; + param.work = buffers.work; + param.arg = &buffers.arg; + param.handler = Record; + return param; +} + +std::vector Drain() { + g_events.clear(); + Require(sceImeUpdate(Record) == 0, "update failed"); + return g_events; +} + +void Paste(const std::string& utf8) { + ImePublishPaste_nid_postfix(utf8.data(), utf8.size()); +} + +void Publish(ImeHostInputKind kind, const char* text = "", std::int32_t start = 0, std::int32_t length = 0) { + ImeHostInput input{}; + input.kind = kind; + std::strncpy(input.text, text, sizeof(input.text) - 1); + input.start = start; + input.length = length; + ImePublishInput_nid_postfix(input); +} + +struct Session { + Buffers buffers; + explicit Session(const char16_t* initial, uint32_t maxLength, uint32_t option = 0, ImeTextFilter filter = nullptr) { + std::memcpy(buffers.text, initial, (std::char_traits::length(initial) + 1) * sizeof(char16_t)); + Param param = MakeParam(buffers, maxLength, option); + param.filter = filter; + Require(sceImeOpen_nid_postfix(¶m, nullptr) == 0, "open"); + Drain(); + } + ~Session() { sceImeClose_nid_postfix(); } + std::u16string Text() const { return buffers.text; } +}; + +void CheckInsertAtCaret() { + Session session(u"ab", 16); + Publish(ImeHostInputKind::Left); + Drain(); + Paste("XY"); + const auto events = Drain(); + Require(events.size() == 1 && events[0].id == EventUpdateText, "a paste is one text update"); + Require(events[0].text == u"aXYb" && events[0].index == 1 && events[0].length == 2, "the paste lands at the caret"); + Require(session.Text() == u"aXYb", "the paste reached the input buffer"); + Publish(ImeHostInputKind::Text, "z"); + Require(Drain().at(0).text == u"aXYzb", "the caret sits after the pasted text"); +} + +void CheckSingleLineAndMultiline() { + { + Session session(u"", 32); + Paste("a\r\nb\nc\rd"); + Drain(); + Require(session.Text() == u"abcd", "a single-line session drops line breaks"); + } + { + Session session(u"", 32, 0x00000001); + Paste("a\r\nb\nc\rd"); + Drain(); + Require(session.Text() == u"a\nb\nc\nd", "a multi-line session keeps line breaks as LF"); + } +} + +void CheckControlCharacters() { + Session session(u"", 32); + Paste(std::string("a\tb\x01" "c\x1b" "d\x7f" "e\0f", 11)); + Drain(); + Require(session.Text() == u"abcdef", "controls are stripped from a paste"); +} + +void CheckMaxLength() { + { + Session session(u"abc", 5); + Paste("defgh"); + const auto events = Drain(); + Require(session.Text() == u"abcde" && events.size() == 1 && events[0].length == 2, "a paste stops at the maximum length"); + Paste("z"); + Require(Drain().empty() && session.Text() == u"abcde", "a paste into a full field does nothing"); + } + { + Session session(u"abc", 5); + Paste("d\xf0\x9f\x98\x80"); + Drain(); + Require(session.Text() == u"abcd", "a cut that would split a surrogate pair drops the whole character"); + } + { + Session session(u"abc", 6); + Paste("d\xf0\x9f\x98\x80"); + Drain(); + Require(session.Text() == u"abcd\U0001F600", "a pair that fits is kept whole"); + } + { + Session session(u"", 1); + Paste("\xf0\x9f\x98\x80"); + Require(Drain().empty() && session.Text().empty(), "a pair that cannot fit is not half inserted"); + } +} + +void CheckFilter() { + g_filterCalls = 0; + Session session(u"", 16, 0, reinterpret_cast(&DigitsOnlyFilter)); + Paste("1a2b3\xe3\x81\x82" "4"); + const auto events = Drain(); + Require(g_filterCalls == 1, "a paste runs the text filter once"); + Require(session.Text() == u"1234" && events.size() == 1 && events[0].text == u"1234", "the filter shapes the pasted text"); +} + +void CheckHugeClipboard() { + Session session(u"", 2048); + const std::string huge(8u * 1024u * 1024u, 'x'); + Paste(huge); + Drain(); + Require(session.Text().size() == 2048, "a huge clipboard fills the field and no more"); +} + +void CheckInvalidUtf8() { + Session session(u"", 16); + Paste("a\xff" "b\xe3\x81"); + Drain(); + Require(session.Text() == u"a\uFFFDb\uFFFD", "an invalid clipboard is replaced and never throws"); +} + +void CheckNothingCaptured() { + Require(!ImeCapturesKeyboard_nid_postfix(), "nothing should be capturing"); + Paste("ignored"); + ImePublishPaste_nid_postfix(nullptr, 5); + Buffers buffers; + Param param = MakeParam(buffers, 16); + Require(sceImeOpen_nid_postfix(¶m, nullptr) == 0, "open after an ignored paste"); + const auto opened = Drain(); + Require(opened.size() == 1 && opened[0].id == 0 && buffers.text[0] == u'\0', "a paste with no session was kept for later"); + Paste(""); + Require(Drain().empty(), "an empty paste queued an update"); + ImePublishPaste_nid_postfix(nullptr, 5); + Require(Drain().empty(), "a null clipboard queued an update"); + Require(sceImeClose_nid_postfix() == 0, "close"); +} + +void CheckPasteWhileComposing() { + Session session(u"ab", 16); + Publish(ImeHostInputKind::Preedit, "ni", 2, 0); + Paste("XY"); + Require(Drain().empty() && session.Text() == u"ab", "a paste went in while the input method held a composition"); + Publish(ImeHostInputKind::Preedit, ""); + Paste("XY"); + Drain(); + Require(session.Text() == u"abXY", "a paste works once the composition ends"); +} + +} + +int main() { + CheckNothingCaptured(); + CheckInsertAtCaret(); + CheckSingleLineAndMultiline(); + CheckControlCharacters(); + CheckMaxLength(); + CheckFilter(); + CheckHugeClipboard(); + CheckInvalidUtf8(); + CheckPasteWhileComposing(); +} diff --git a/core/libs/tests/GuestImeTextInput.cpp b/core/libs/tests/GuestImeTextInput.cpp new file mode 100644 index 0000000000..c0d5cfebdf --- /dev/null +++ b/core/libs/tests/GuestImeTextInput.cpp @@ -0,0 +1,251 @@ +#include "prx/libSceImeDialog/include/Api.hpp" +#include "prx/libSceIme/include/Api.hpp" +#include "SceTypes.hpp" +#include "prx/libSceIme/include/ImeInput.hpp" +#include "prx/libSceIme/include/ImeKeyboardView.hpp" +#include "prx/libSceVideoOut/include/ImeTextInput.hpp" + +#include +#include +#include +#include +#include + +namespace { + +constexpr unsigned WindowId = 7; +constexpr int StatusRunning = 1; +#ifdef __APPLE__ +constexpr unsigned PasteModifier = KMOD_LGUI; +constexpr unsigned WrongModifier = KMOD_LCTRL; +#else +constexpr unsigned PasteModifier = KMOD_LCTRL; +constexpr unsigned WrongModifier = KMOD_LGUI; +#endif + +std::string g_clipboard; +bool g_clipboardAvailable = true; +int g_reads = 0; +ImeTextInput g_window; + +bool ReadClipboard(std::string& text) { + ++g_reads; + if (!g_clipboardAvailable) return false; + text = g_clipboard; + return true; +} + +void APS5_VABI Ignore(void*, const ImeEvent*) {} + +void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "%s\n", message); + std::abort(); +} + +void Edit(const char* utf8, std::int32_t start, std::int32_t length, unsigned windowId = WindowId) { + SDL_Event event{}; + event.type = SDL_TEXTEDITING; + event.edit.windowID = windowId; + std::strncpy(event.edit.text, utf8, sizeof(event.edit.text) - 1); + event.edit.start = start; + event.edit.length = length; + g_window.HandleEvent(event, WindowId); +} + +void Type(const char* utf8) { + SDL_Event event{}; + event.type = SDL_TEXTINPUT; + event.text.windowID = WindowId; + std::strncpy(event.text.text, utf8, sizeof(event.text.text) - 1); + g_window.HandleEvent(event, WindowId); +} + +void Key(SDL_Scancode scancode, SDL_Keycode sym, unsigned modifiers, bool repeat = false, unsigned type = SDL_KEYDOWN, unsigned windowId = WindowId) { + SDL_Event event{}; + event.type = type; + event.key.windowID = windowId; + event.key.keysym.scancode = scancode; + event.key.keysym.sym = sym; + event.key.keysym.mod = static_cast(modifiers); + event.key.repeat = repeat ? 1 : 0; + g_window.HandleEvent(event, WindowId); +} + +Param DialogParam(char16_t* buffer, std::uint32_t maxLength) { + Param param{}; + param.max_text_length = maxLength; + param.input_text_buffer = buffer; + return param; +} + +ImeKeyboardView View() { + ImeKeyboardView view; + Require(ImeReadKeyboardView_nid_postfix(view), "the dialog shows no keyboard"); + return view; +} + +void Open(const Param& param) { + Require(sceImeDialogInit(¶m, nullptr) == 0, "init"); + Require(sceImeDialogGetStatus() == StatusRunning, "not running"); +} + +void Close() { + Require(sceImeDialogTerm() == 0, "term"); +} + +void CheckEditingEvents() { + char16_t buffer[16] = u"x"; + const Param param = DialogParam(buffer, 8); + Open(param); + Edit("k", 1, 0); + Require(View().preedit == u"k" && View().preeditCursor == 1, "an SDL text editing event is not shown as a preedit"); + Edit("ka", 2, 0); + Require(View().preedit == u"ka" && View().preeditCursor == 2, "a second edit does not replace the first"); + Edit("\xe3\x81\x8b", 1, 1, WindowId + 1); + Require(View().preedit == u"ka", "an edit for another window was shown"); + Type("\xe3\x81\x8b"); + Edit("", 0, 0); + Require(View().preedit.empty() && View().text == u"x\u304B" && std::u16string(buffer) == u"x", "the commit is not the only text and the preedit is not gone"); + Edit("", 0, 0); + Require(View().text == u"x\u304B", "an empty edit changed the text"); + Edit("ni", 2, 0); + Edit("", 0, 0); + Require(View().preedit.empty(), "an empty edit does not clear the composition"); + Key(SDL_SCANCODE_ESCAPE, SDLK_ESCAPE, 0); + Require(sceImeDialogGetStatus() == 2, "Escape with no composition does not cancel"); + Close(); + + Open(param); + Edit("ni", 2, 0); + Key(SDL_SCANCODE_ESCAPE, SDLK_ESCAPE, 0); + Require(sceImeDialogGetStatus() == StatusRunning && View().preedit.empty(), "Escape from the keyboard with a composition cancels the dialog"); + Close(); + + Edit("late", 0, 0); + Open(param); + Require(View().preedit.empty(), "an edit with no dialog was kept"); + Close(); +} + +void CheckPasteShortcuts() { + char16_t buffer[64] = u""; + const Param param = DialogParam(buffer, 32); + g_window.SetClipboardProvider(&ReadClipboard); + g_clipboard = "ab"; + Open(param); + + g_reads = 0; + Key(SDL_SCANCODE_V, SDLK_v, PasteModifier); + Require(g_reads == 1 && View().text == u"ab", "Ctrl+V does not paste"); + Key(SDL_SCANCODE_INSERT, SDLK_INSERT, KMOD_LSHIFT); + Require(g_reads == 2 && View().text == u"abab", "Shift+Insert does not paste"); + Key(SDL_SCANCODE_V, SDLK_v, PasteModifier | KMOD_LSHIFT); + Require(g_reads == 3 && View().text == u"ababab", "Ctrl+Shift+V does not paste"); + + Key(SDL_SCANCODE_V, SDLK_v, PasteModifier, true); + Key(SDL_SCANCODE_INSERT, SDLK_INSERT, KMOD_LSHIFT, true); + Key(SDL_SCANCODE_V, SDLK_v, PasteModifier | KMOD_LALT); + Key(SDL_SCANCODE_V, SDLK_v, 0); + Key(SDL_SCANCODE_V, SDLK_v, WrongModifier); + Key(SDL_SCANCODE_INSERT, SDLK_INSERT, 0); + Key(SDL_SCANCODE_V, SDLK_v, PasteModifier, false, SDL_KEYUP); + Key(SDL_SCANCODE_V, SDLK_v, PasteModifier, false, SDL_KEYDOWN, WindowId + 1); + Key(SDL_SCANCODE_C, SDLK_c, PasteModifier); + Key(SDL_SCANCODE_X, SDLK_x, PasteModifier); + Key(SDL_SCANCODE_A, SDLK_a, PasteModifier); + Require(g_reads == 3 && View().text == u"ababab", "a key that is not a paste read the clipboard or changed the text"); + + g_clipboardAvailable = false; + Key(SDL_SCANCODE_V, SDLK_v, PasteModifier); + Require(g_reads == 4 && View().text == u"ababab", "an unavailable clipboard changed the text"); + g_clipboardAvailable = true; + g_clipboard.clear(); + Key(SDL_SCANCODE_V, SDLK_v, PasteModifier); + Require(g_reads == 5 && View().text == u"ababab", "an empty clipboard changed the text"); + g_clipboard = "cd"; + + Edit("ni", 2, 0); + Key(SDL_SCANCODE_V, SDLK_v, PasteModifier); + Require(View().text == u"ababab", "Ctrl+V pasted while a composition was shown"); + Edit("", 0, 0); + Key(SDL_SCANCODE_V, SDLK_v, PasteModifier); + Require(View().text == u"abababcd", "Ctrl+V does not paste once the composition ends"); + Require(std::u16string(buffer).empty(), "a paste reached the buffer before accept"); + Close(); +} + +void CheckNothingCaptured() { + g_window.SetClipboardProvider(&ReadClipboard); + g_clipboard = "secret"; + g_reads = 0; + Require(!ImeCapturesKeyboard_nid_postfix(), "something is capturing"); + Key(SDL_SCANCODE_V, SDLK_v, PasteModifier); + Key(SDL_SCANCODE_INSERT, SDLK_INSERT, KMOD_LSHIFT); + Require(g_reads == 0, "the clipboard was read while no dialog captures the keyboard"); + + char16_t buffer[16] = u""; + const Param param = DialogParam(buffer, 16); + Open(param); + Require(sceImeDialogAbort() == 0, "abort"); + Key(SDL_SCANCODE_V, SDLK_v, PasteModifier); + Require(g_reads == 0, "the clipboard was read after the dialog finished"); + Close(); + + ImeTextInput bare; + Open(param); + SDL_Event event{}; + event.type = SDL_KEYDOWN; + event.key.windowID = WindowId; + event.key.keysym.scancode = SDL_SCANCODE_V; + event.key.keysym.sym = SDLK_v; + event.key.keysym.mod = static_cast(PasteModifier); + bare.HandleEvent(event, WindowId); + Require(View().text.empty() && g_reads == 0, "a window with no clipboard provider pasted"); + Close(); +} + +void CheckHugeClipboard() { + char16_t buffer[2049] = u""; + const Param param = DialogParam(buffer, 2048); + g_window.SetClipboardProvider(&ReadClipboard); + g_clipboard.assign(4u * 1024u * 1024u, 'x'); + Open(param); + Key(SDL_SCANCODE_V, SDLK_v, PasteModifier); + Require(View().text.size() == 2048, "a huge clipboard is not cut at the dialog limit"); + Close(); +} + +void CheckSession() { + struct Buffers { + char16_t text[2049] = {}; + alignas(16) unsigned char work[0x5000] = {}; + } buffers; + Param param; + sceImeParamInit(¶m); + param.user_id = 1; + param.max_text_length = 16; + param.input_text_buffer = buffers.text; + param.option = 0x00000001; + param.work = buffers.work; + param.handler = Ignore; + g_window.SetClipboardProvider(&ReadClipboard); + g_clipboard = "a\r\nb"; + Require(sceImeOpen_nid_postfix(¶m, nullptr) == 0, "open"); + Key(SDL_SCANCODE_V, SDLK_v, PasteModifier); + Edit("ni", 2, 0); + Key(SDL_SCANCODE_V, SDLK_v, PasteModifier); + Require(sceImeUpdate(param.handler) == 0, "update"); + Require(std::u16string(buffers.text) == u"a\nb", "the session text is not the single paste with a normalised break"); + Require(sceImeClose_nid_postfix() == 0, "close"); +} + +} + +int main() { + CheckEditingEvents(); + CheckPasteShortcuts(); + CheckNothingCaptured(); + CheckHugeClipboard(); + CheckSession(); +} diff --git a/core/libs/tests/GuestIndexedDraw.cpp b/core/libs/tests/GuestIndexedDraw.cpp index 14333e04f6..60957670e3 100644 --- a/core/libs/tests/GuestIndexedDraw.cpp +++ b/core/libs/tests/GuestIndexedDraw.cpp @@ -49,6 +49,8 @@ int main() { draw[1] = 3; draw[4] = 3; draw[4]=4; Reject([&]{AgcDriver::Pm4::Validate({draw,6},0);}); + draw[4]=0xffffffffu; + Reject([&]{AgcDriver::Pm4::Validate({draw,6},0);}); draw[4]=3; draw[5]=2; Reject([&]{AgcDriver::Pm4::Validate({draw,6},0);}); @@ -59,6 +61,17 @@ int main() { Reject([&]{AgcDriver::Pm4::Validate({draw,6},0);}); draw[2]=1; draw[3]=0; queue.indexType=1; Reject([&]{AgcDriver::Pm4::ResolveDraw({draw,6},queue);}); + draw[2]=0; draw[3]=0; + Reject([&]{AgcDriver::Pm4::ResolveDraw({draw,6},queue);}); + draw[2]=static_cast(reinterpret_cast(indices.data())); + draw[3]=static_cast(reinterpret_cast(indices.data())>>32u); + queue.indexBase=0; + Reject([&]{AgcDriver::Pm4::ResolveDraw(offsetDraw,queue);}); + queue.indexBase=reinterpret_cast(indices.data()); + const std::array invalidOffsetCount{0xc0033500u,2,1,3,0}; + Reject([&]{AgcDriver::Pm4::Validate(invalidOffsetCount,0);}); + const std::array maxOffsetCount{0xc0033500u,2,1,0xffffffffu,0}; + Reject([&]{AgcDriver::Pm4::Validate(maxOffsetCount,0);}); auto* saved=buffer.cursor_up; Reject([&]{sceAgcDcbDrawIndex(&buffer,3,nullptr,0);}); Reject([&]{sceAgcDcbDrawIndex(&buffer,3,indices.data(),1ull<<40);}); @@ -66,5 +79,15 @@ int main() { buffer.cursor_down=buffer.cursor_up+5; Reject([&]{sceAgcDcbDrawIndex(&buffer,3,indices.data(),0);}); Require(buffer.cursor_up==saved); + buffer.cursor_down=words.data()+words.size(); + Reject([&]{sceAgcDcbDrawIndex(nullptr,3,indices.data(),0);}); + Reject([&]{sceAgcDcbSetIndexCount(nullptr,3);}); + Reject([&]{sceAgcDcbSetIndexBuffer(nullptr,reinterpret_cast(indices.data()));}); + Reject([&]{sceAgcDcbDrawIndexAuto(nullptr,3,0);}); + Reject([&]{sceAgcDcbDrawIndexOffset(nullptr,0,3,0);}); + Reject([&]{sceAgcDcbDrawIndexIndirect(nullptr,0,0);}); + Reject([&]{sceAgcDcbDrawIndexIndirectMulti(nullptr,0,0,1,nullptr,20,0);}); + Reject([&]{sceAgcDcbDrawIndexMultiInstanced(nullptr,3,indices.data(),nullptr,1,0);}); + Require(buffer.cursor_up==saved); return 0; } diff --git a/core/libs/tests/GuestInterfaces.cpp b/core/libs/tests/GuestInterfaces.cpp index 94e0d0ffd8..913b414b08 100644 --- a/core/libs/tests/GuestInterfaces.cpp +++ b/core/libs/tests/GuestInterfaces.cpp @@ -20,7 +20,6 @@ #include #include #include -#include #endif #include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libc/include/general/VabiMacros.hpp" @@ -29,7 +28,9 @@ #include #include #include +#include #include +#include #include static_assert(sizeof(GuestInterfaceAddress) == 56); @@ -128,6 +129,146 @@ static void SendLoopbackDatagram() { } #endif +#ifdef _WIN32 +static void RequireWindowsLink(bool value, const char* message) { + if (!value) { + std::fprintf(stderr, "guest_interfaces: %s\n", message); + std::abort(); + } +} + +static void CheckWindowsLinks() { + GuestInterfaceAddress* first = nullptr; + GuestInterfaceAddress* second = nullptr; + const ULONG options = GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_DNS_SERVER; + ULONG size = 0; + std::vector storage; + ULONG result = GetAdaptersAddresses(AF_UNSPEC, options, nullptr, nullptr, &size); + for (unsigned calls = 1; result == ERROR_BUFFER_OVERFLOW && calls < 4; ++calls) { + storage.resize(size); + result = GetAdaptersAddresses(AF_UNSPEC, options, nullptr, reinterpret_cast(storage.data()), &size); + } + Require(result == NO_ERROR); + std::vector baseline; + for (auto* adapter = reinterpret_cast(storage.data()); adapter; adapter = adapter->Next) { + MIB_IF_ROW2 row{}; + row.InterfaceLuid = adapter->Luid; + Require(GetIfEntry2(&row) == NO_ERROR); + baseline.push_back(row); + } + Require(getifaddrs_nid_postfix(&first) == 0); + Require(getifaddrs_nid_postfix(&second) == 0); + bool loopback = false; + bool hardware = false; + std::size_t adapters = 0; + for (auto* adapter = reinterpret_cast(storage.data()); adapter; adapter = adapter->Next) { + ++adapters; + char nativeName[NDIS_IF_MAX_STRING_SIZE + 1] = {}; + Require(ConvertInterfaceLuidToNameA(&adapter->Luid, nativeName, sizeof(nativeName)) == NO_ERROR); + const auto index = adapter->IfIndex ? adapter->IfIndex : adapter->Ipv6IfIndex; + const std::string name = std::strlen(nativeName) < 16 ? nativeName : "if" + std::to_string(index); + const GuestInterfaceAddress* links[2] = {}; + const GuestInterfaceAddress* lists[] = {first, second}; + for (std::size_t snapshot = 0; snapshot < 2; ++snapshot) { + unsigned count = 0; + for (auto* item = lists[snapshot]; item; item = item->next) { + if (name != item->name) continue; + const auto* address = static_cast(item->address); + Require(address != nullptr); + if (address[1] == 18) { + RequireWindowsLink(count == 0, "duplicate AF_LINK entry"); + links[snapshot] = item; + ++count; + } else { + RequireWindowsLink(count == 1, "address entry has no preceding AF_LINK entry"); + RequireWindowsLink(item->flags == links[snapshot]->flags, "link and address flags disagree"); + bool nativeAddress = false; + for (auto* unicast = adapter->FirstUnicastAddress; unicast; unicast = unicast->Next) { + const auto* hostAddress = unicast->Address.lpSockaddr; + if (!hostAddress || (hostAddress->sa_family != AF_INET && hostAddress->sa_family != AF_INET6)) continue; + const auto expected = HostAddress(hostAddress); + const bool ipv4 = address[1] == 2; + nativeAddress = nativeAddress || (expected.family == (ipv4 ? AF_INET : AF_INET6) && + std::memcmp(address + (ipv4 ? 4 : 8), expected.address, ipv4 ? 4 : 16) == 0); + } + RequireWindowsLink(nativeAddress, "address belongs to a different native interface index"); + } + } + RequireWindowsLink(count == 1, "native adapter has no AF_LINK entry"); + const auto* link = static_cast(links[snapshot]->address); + const auto* data = static_cast(links[snapshot]->data); + RequireWindowsLink(data != nullptr, "AF_LINK entry has no if_data"); + std::uint16_t guestIndex = 0; + std::uint16_t dataLength = 0; + std::uint32_t mtu = 0; + std::memcpy(&guestIndex, link + 2, sizeof(guestIndex)); + std::memcpy(&dataLength, data + 6, sizeof(dataLength)); + std::memcpy(&mtu, data + 8, sizeof(mtu)); + RequireWindowsLink(guestIndex == index, "AF_LINK index differs from native adapter"); + const std::uint8_t type = adapter->IfType == IF_TYPE_ETHERNET_CSMACD ? 0x6 + : adapter->IfType == IF_TYPE_SOFTWARE_LOOPBACK ? 0x18 : adapter->IfType == IF_TYPE_PPP ? 0x17 : 0x1; + RequireWindowsLink(link[4] == type, "AF_LINK type differs from native adapter"); + Require(link[0] >= 54 && link[0] % 8 == 0 && link[5] == name.size() && link[7] == 0); + Require(std::memcmp(link + 8, name.data(), name.size()) == 0); + Require(links[snapshot]->netmask == nullptr && links[snapshot]->destination == nullptr); + Require(dataLength == 152 && data[0] == link[4] && data[2] == link[6]); + RequireWindowsLink(mtu == adapter->Mtu, "AF_LINK MTU differs from native adapter"); + if (adapter->IfType == IF_TYPE_SOFTWARE_LOOPBACK) { + RequireWindowsLink(link[4] == 0x18 && link[6] == 0 && data[3] == 0, "loopback link type or hardware address is incorrect"); + loopback = true; + } else { + RequireWindowsLink(link[6] == adapter->PhysicalAddressLength && + std::memcmp(link + 8 + name.size(), adapter->PhysicalAddress, link[6]) == 0, "AF_LINK MAC differs from native adapter"); + hardware = hardware || adapter->PhysicalAddressLength != 0; + } + } + const auto* before = static_cast(links[0]->data); + const auto* after = static_cast(links[1]->data); + const auto& initial = baseline[adapters - 1]; + MIB_IF_ROW2 final{}; + final.InterfaceLuid = adapter->Luid; + Require(GetIfEntry2(&final) == NO_ERROR); + RequireWindowsLink(initial.InterfaceIndex == index && final.InterfaceIndex == index, "adapter and counter indices disagree"); + const auto counters = [](const MIB_IF_ROW2& row) { + return std::array{row.InUcastPkts + row.InNUcastPkts, row.InErrors, + row.OutUcastPkts + row.OutNUcastPkts, row.OutErrors, 0, row.InOctets, row.OutOctets, + row.InNUcastPkts, row.OutNUcastPkts, row.InDiscards, row.OutDiscards, row.InUnknownProtos}; + }; + const auto lower = counters(initial); + const auto upper = counters(final); + for (std::size_t offset = 24; offset <= 112; offset += 8) { + std::uint64_t oldValue = 0; + std::uint64_t newValue = 0; + std::memcpy(&oldValue, before + offset, sizeof(oldValue)); + std::memcpy(&newValue, after + offset, sizeof(newValue)); + RequireWindowsLink(newValue >= oldValue, "interface counter decreased between snapshots"); + RequireWindowsLink(oldValue >= lower[(offset - 24) / 8] && newValue <= upper[(offset - 24) / 8], + "interface counter differs from native bounds"); + } + } + for (const auto* list : {first, second}) { + std::size_t links = 0; + for (auto* item = list; item; item = item->next) { + const auto* address = static_cast(item->address); + if (address[1] == 18) ++links; + else { + unsigned owners = 0; + for (auto* candidate = list; candidate; candidate = candidate->next) { + const auto* link = static_cast(candidate->address); + if (link[1] == 18 && std::strcmp(candidate->name, item->name) == 0) ++owners; + } + RequireWindowsLink(owners == 1, "address entry does not have exactly one same-name AF_LINK entry"); + } + } + RequireWindowsLink(links == adapters, "AF_LINK count differs from native adapter count"); + } + RequireWindowsLink(loopback, "no loopback AF_LINK checked"); + RequireWindowsLink(hardware, "no non-loopback MAC and MTU checked"); + freeifaddrs_nid_postfix(first); + freeifaddrs_nid_postfix(second); +} +#endif + static std::vector HostSnapshot() { std::vector entries; #ifdef _WIN32 @@ -220,6 +361,9 @@ static std::vector HostSnapshot() { int main() { std::array api{}; ApplicationHeapRegister_nid_no_patch(api.data()); +#ifdef _WIN32 + CheckWindowsLinks(); +#endif constexpr std::uint32_t knownFlags = 0x1 | 0x2 | 0x8 | 0x10 | 0x40 | 0x80 | 0x100 | 0x200 | 0x8000; const std::uint8_t loopbackAddress[] = {127, 0, 0, 1}; const std::uint8_t loopbackMask[] = {255, 0, 0, 0}; @@ -236,6 +380,10 @@ int main() { bool equal = true; bool loopback = false; for (auto* item = list; item; item = item->next) { +#ifdef _WIN32 + const auto* family = static_cast(item->address); + if (family && family[1] == 18) continue; +#endif Require(++entries < 100000); Require(item->name != nullptr); const std::size_t nameLength = std::strlen(item->name); diff --git a/core/libs/tests/GuestItimer.cpp b/core/libs/tests/GuestItimer.cpp index 7a688d67d0..2e263792b7 100644 --- a/core/libs/tests/GuestItimer.cpp +++ b/core/libs/tests/GuestItimer.cpp @@ -2,6 +2,8 @@ #include "prx/libc/include/Api.hpp" #include "SceTypes.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include +#include #include #include #include @@ -93,8 +95,29 @@ static int UnblockPendingDefault() { return 1; } +static int ShortExpiries() { + const GuestSignalSet alarmSet{{1u << 13, 0, 0, 0}}; + const KernelTimespec patience{2, 0}; + const GuestItimerval shortOnce{{0, 0}, {0, 2000}}; + Require(_sigprocmask_nid_postfix(1, &alarmSet, nullptr) == 0, "blocking SIGALRM failed"); + std::array samples{}; + for (auto& sample : samples) { + const auto before = std::chrono::steady_clock::now(); + Require(setitimer_nid_postfix(0, &shortOnce, nullptr) == 0, "arming the 2 ms timer failed"); + Require(sigtimedwait_nid_postfix(&alarmSet, nullptr, &patience) == 14, "the 2 ms timer did not expire"); + sample = std::chrono::steady_clock::now() - before; + Require(sample >= std::chrono::milliseconds(2), "the 2 ms timer expired early"); + } + std::sort(samples.begin(), samples.end()); + const auto median = samples[samples.size() / 2]; + std::fprintf(stderr, "setitimer(2 ms) expiry median: %lld ns\n", static_cast(median.count())); + Require(median < std::chrono::milliseconds(12), "the 2 ms timer expired on a coarse tick"); + return 0; +} + int main(int argc, char** argv) { if (argc > 1 && std::strcmp(argv[1], "pending-default") == 0) return UnblockPendingDefault(); + if (argc > 1 && std::strcmp(argv[1], "short-expiry") == 0) return ShortExpiries(); const auto ignore = reinterpret_cast(std::uintptr_t{1}); Require(signal_nid_postfix(14, ignore) == nullptr, "SIGALRM was not initially defaulted"); const GuestItimerval valid{{0, 0}, {30, 0}}; diff --git a/core/libs/tests/GuestKernelErrors.cpp b/core/libs/tests/GuestKernelErrors.cpp index 598af1167c..5096b9e3e2 100644 --- a/core/libs/tests/GuestKernelErrors.cpp +++ b/core/libs/tests/GuestKernelErrors.cpp @@ -50,7 +50,43 @@ static void RequireTimesOut(TLock lock) { Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(15)); } +struct ReaderHold { + PthreadRwlock* lock; + std::atomic held{false}; + std::atomic release{false}; +}; + +static void* APS5_VABI HoldRead(void* argument) { + auto& hold = *static_cast(argument); + Require(scePthreadRwlockRdlock(hold.lock) == SCE_OK); + hold.held.store(true); + while (!hold.release.load()) std::this_thread::yield(); + Require(scePthreadRwlockUnlock(hold.lock) == SCE_OK); + return nullptr; +} + +static void CheckTimedRwlockWithReader() { + PthreadRwlock rwlock = nullptr; + Require(scePthreadRwlockInit(&rwlock, nullptr, nullptr) == SCE_OK); + ReaderHold hold{&rwlock}; + Pthread reader = nullptr; + Require(scePthreadCreate(&reader, nullptr, HoldRead, &hold, "reader-hold") == SCE_OK); + while (!hold.held.load()) std::this_thread::yield(); + Require(scePthreadRwlockTimedrdlock(&rwlock, 20000) == SCE_OK); + Require(scePthreadRwlockUnlock(&rwlock) == SCE_OK); + RequireTimesOut([&](KernelUseconds usec) { return scePthreadRwlockTimedwrlock(&rwlock, usec); }); + Require(scePthreadRwlockTrywrlock(&rwlock) == SCE_KERNEL_ERROR_EBUSY); + hold.release.store(true); + Require(scePthreadJoin(reader, nullptr) == SCE_OK); + Require(scePthreadRwlockTimedwrlock(&rwlock, 20000) == SCE_OK); + Require(scePthreadRwlockUnlock(&rwlock) == SCE_OK); + Require(scePthreadRwlockTimedrdlock(&rwlock, 20000) == SCE_OK); + Require(scePthreadRwlockUnlock(&rwlock) == SCE_OK); + Require(scePthreadRwlockDestroy(&rwlock) == SCE_OK); +} + int main() { + CheckTimedRwlockWithReader(); constexpr int maximum = std::numeric_limits::max(); KernelSema semaphore = nullptr; Require(sceKernelCreateSema(&semaphore, "overflow", 1, maximum - 1, maximum, nullptr) == SCE_OK); @@ -84,6 +120,7 @@ int main() { Require(sceKernelDeleteEqueue(eq) == SCE_KERNEL_ERROR_EBADF); Require(sceKernelWaitEqueue(eq, &event, 1, &count, &timeout) == SCE_KERNEL_ERROR_EBADF); Require(sceKernelCreateEqueue(nullptr, "errors") == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelCreateEqueue(&eq, nullptr) == SCE_KERNEL_ERROR_EINVAL); PthreadMutexattr attr = nullptr; Require(scePthreadMutexattrInit(&attr) == SCE_OK); diff --git a/core/libs/tests/GuestKernelFcntl.cpp b/core/libs/tests/GuestKernelFcntl.cpp index 62fbfa2a3f..54f0a6d4e6 100644 --- a/core/libs/tests/GuestKernelFcntl.cpp +++ b/core/libs/tests/GuestKernelFcntl.cpp @@ -213,6 +213,10 @@ static void SocketsKeepTheirPath() { Require(sceKernelFcntl(socket, DarwinReadAhead, 1) == SceEbadf); int queued = -1; Require(ioctl_nid_postfix(socket, ReadableRequest, &queued) == 0 && queued == 0); + Require(ioctl_nid_postfix(socket, CloseOnExecRequest, nullptr) == 0 && fcntl_nid_postfix(socket, GetDescriptorFlags) == 1); + Require(ioctl_nid_postfix(socket, CloseOnExecRequest, &queued) == 0 && fcntl_nid_postfix(socket, GetDescriptorFlags) == 1); + Require(ioctl_nid_postfix(socket, KeepOnExecRequest, nullptr) == 0 && fcntl_nid_postfix(socket, GetDescriptorFlags) == 0); + Require(ioctl_nid_postfix(socket, KeepOnExecRequest, &queued) == 0 && fcntl_nid_postfix(socket, GetDescriptorFlags) == 0); RequireMalformedRequestsFail(socket); RequireFailure(ioctl_nid_postfix(socket, IntegerArgumentRequest, nullptr), GuestEfault); RequireFailure(ioctl_nid_postfix(socket, EmptyInputRequest, nullptr), GuestEfault); diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index 281d35ea46..337c82205a 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -5,6 +5,7 @@ #include "prx/libSceAmpr/include/Api.hpp" #include "prx/libkernel/Pthread/include/Cond.hpp" #include "prx/libkernel/Time/include/Time.hpp" +#include "prx/libkernel/Pthread/include/Mutex.hpp" #include "prx/libkernel/File/include/File.hpp" #include "prx/libc/include/Api.hpp" #include "SceTypes.hpp" @@ -57,6 +58,39 @@ int Error() { return *__error_nid_postfix(); } +void ThreadAndSessionTests(int descriptor) { + PthreadMutex mutex = nullptr; + Require(scePthreadMutexIsowned(&mutex) == 0); + Require(scePthreadMutexInit(&mutex, nullptr, "isowned") == 0); + Require(scePthreadMutexIsowned(&mutex) == 0); + Require(scePthreadMutexLock(&mutex) == 0); + Require(scePthreadMutexIsowned(&mutex) == SceKernelError(1)); + int fromOther = -1; + std::thread([&] { fromOther = scePthreadMutexIsowned(&mutex); }).join(); + Require(fromOther == 0); + Require(scePthreadMutexUnlock(&mutex) == 0); + Require(scePthreadMutexIsowned(&mutex) == 0); + Require(scePthreadMutexDestroy(&mutex) == 0); + Require(scePthreadMutexIsowned(&mutex) == 0); + + const char record[] = "trace"; + *__error_nid_postfix() = 0; + Require(utrace_nid_postfix(record, sizeof(record)) == 0 && Error() == 0); + Require(utrace_nid_postfix(nullptr, 0) == 0); + Require(utrace_nid_postfix(nullptr, 4096) == 0 && Error() == 0); + + const int session = getsid_nid_postfix(0); + Require(session > 0); + Require(tcsetsid_nid_postfix(descriptor, session + 1) == -1 && Error() == 22); + Require(tcsetsid_nid_postfix(-1, session + 1) == -1 && Error() == 22); + Require(tcsetsid_nid_postfix(descriptor, session) == -1 && Error() == 25); + Require(tcsetsid_nid_postfix(-1, session) == -1 && Error() == 9); + const int socketDescriptor = socket_nid_postfix(2, 2, 0); + Require(socketDescriptor >= 0); + Require(tcsetsid_nid_postfix(socketDescriptor, session) == -1 && Error() == 25); + Require(close_nid_postfix(socketDescriptor) == 0); +} + bool ChrootThrows(const std::string& path) { try { chroot_nid_postfix(path.c_str()); @@ -407,6 +441,63 @@ void CheckSymlinkTargetRestrictions(const std::filesystem::path& root) { } } +void CheckLinkSymbolicLinkLimit(const std::filesystem::path& root) { + const auto chain = root / "link-chain"; + const auto folder = chain / "folder"; + Require(std::filesystem::create_directories(folder)); + std::ofstream((chain / "file.txt").string()).put('x'); + std::ofstream((folder / "file.txt").string()).put('x'); + const auto at = [&](const std::string& name) { return (chain / name).string(); }; + const auto fileLink = [](int index) { return "file" + std::to_string(index); }; + const auto folderLink = [](int index) { return "folder" + std::to_string(index); }; + const int first = symlink_nid_postfix("file.txt", at(fileLink(1)).c_str()); + if (first != 0) { + Require(first == -1 && Error() == 1); + std::fprintf(stderr, "symbolic links unavailable: the link chain limit not tested\n"); + Require(std::filesystem::remove(folder / "file.txt") && std::filesystem::remove(folder)); + Require(std::filesystem::remove(chain / "file.txt") && std::filesystem::remove(chain)); + return; + } + Require(symlink_nid_postfix("folder", at(folderLink(1)).c_str()) == 0); + for (int index = 2; index <= 33; ++index) { + Require(symlink_nid_postfix(fileLink(index - 1).c_str(), at(fileLink(index)).c_str()) == 0); + Require(symlink_nid_postfix(folderLink(index - 1).c_str(), at(folderLink(index)).c_str()) == 0); + } + const auto copy = at("copy.txt"); + const auto linksTo = [&](const std::string& source, const std::string& created, const std::filesystem::path& file) { + const bool linked = link_nid_postfix(source.c_str(), created.c_str()) == 0 && std::filesystem::hard_link_count(file) == 2; + return linked && std::filesystem::remove(created); + }; + const auto refused = [](int result) { return result == -1 && Error() == 62; }; + Require(linksTo(at(fileLink(32)), copy, chain / "file.txt")); + Require(refused(link_nid_postfix(at(fileLink(33)).c_str(), copy.c_str()))); + Require(refused(linkat_nid_postfix(-100, at(fileLink(33)).c_str(), -100, copy.c_str(), 0x400))); + Require(linkat_nid_postfix(-100, at(fileLink(33)).c_str(), -100, copy.c_str(), 0) == 0 && RemoveSymlink(copy)); + Require(linksTo(at(folderLink(32) + "/file.txt"), copy, folder / "file.txt")); + Require(refused(link_nid_postfix(at(folderLink(33) + "/file.txt").c_str(), copy.c_str()))); + Require(refused(linkat_nid_postfix(-100, at(folderLink(33) + "/file.txt").c_str(), -100, copy.c_str(), 0))); + Require(symlink_nid_postfix("file.txt", (folder / "inner").string().c_str()) == 0); + Require(linksTo(at(folderLink(31) + "/inner"), copy, folder / "file.txt")); + Require(refused(link_nid_postfix(at(folderLink(32) + "/inner").c_str(), copy.c_str()))); + Require(RemoveSymlink((folder / "inner").string())); + Require(linksTo(at("file.txt"), at(folderLink(32) + "/created.txt"), chain / "file.txt")); + Require(refused(link_nid_postfix(at("file.txt").c_str(), at(folderLink(33) + "/created.txt").c_str()))); + Require(refused(linkat_nid_postfix(-100, at("file.txt").c_str(), -100, at(folderLink(33) + "/created.txt").c_str(), 0x400))); +#ifndef _WIN32 + const int chainDirectory = sceKernelOpen(chain.string().c_str(), SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); + Require(chainDirectory >= 0); + const auto belowMissing = "missing/../" + fileLink(33); + Require(linkat_nid_postfix(chainDirectory, belowMissing.c_str(), -100, copy.c_str(), 0x400) == -1 && Error() == 2); + const auto belowFile = "file.txt/../" + fileLink(33); + Require(linkat_nid_postfix(chainDirectory, belowFile.c_str(), -100, copy.c_str(), 0x400) == -1 && Error() == 20); + Require(sceKernelClose(chainDirectory) == 0); +#endif + Require(!std::filesystem::exists(copy) && !std::filesystem::exists(folder / "created.txt")); + for (int index = 1; index <= 33; ++index) Require(RemoveSymlink(at(fileLink(index))) && RemoveSymlink(at(folderLink(index)))); + Require(std::filesystem::remove(folder / "file.txt") && std::filesystem::remove(folder)); + Require(std::filesystem::remove(chain / "file.txt") && std::filesystem::remove(chain)); +} + UserUcontext sigreturnContext{}; UserUcontext sigreturnResume{}; volatile int sigreturnPasses = 0; @@ -1083,89 +1174,34 @@ void CheckKernelEnvironment() { constexpr int get = 0, set = 1, unset = 2, dump = 3; char buffer[300]; std::memset(buffer, 0x5a, sizeof(buffer)); + const std::string longName(129, 'n'); + const std::string longestName(128, 'n'); + const std::string longValue(1000, 'v'); Require(Kenv(dump, nullptr, nullptr, 0) == 0); + Require(Kenv(dump, nullptr, buffer, sizeof(buffer)) == 0 && buffer[0] == 0x5a); + Require(Kenv(dump, nullptr, reinterpret_cast(0x10), sizeof(buffer)) == 0); + Require(Kenv(dump, reinterpret_cast(0x10), buffer, -1) == 0); Require(Kenv(get, "anyps5.alpha", buffer, sizeof(buffer)) == -1 && Error() == 2); - Require(Kenv(unset, "anyps5.alpha", nullptr, 0) == -1 && Error() == 2); - Require(Kenv(4, "anyps5.alpha", buffer, sizeof(buffer)) == -1 && Error() == 22); - Require(Kenv(-1, "anyps5.alpha", buffer, sizeof(buffer)) == -1 && Error() == 22); - Require(Kenv(set, "anyps5.alpha", "one", 0) == -1 && Error() == 22); - Require(Kenv(set, "anyps5.alpha", "one", 3) == -1 && Error() == 63); - Require(Kenv(get, "anyps5.alpha", buffer, sizeof(buffer)) == -1 && Error() == 2); - Require(Kenv(set, "anyps5.negative", "one", -1) == 0); - Require(Kenv(get, "anyps5.negative", buffer, sizeof(buffer)) == 4 && std::strcmp(buffer, "one") == 0); - Require(Kenv(set, "anyps5.negative", std::string(128, 'v').c_str(), -1) == 0); - Require(Kenv(set, "anyps5.negative", std::string(129, 'v').c_str(), -1) == -1 && Error() == 63); - Require(Kenv(unset, "anyps5.negative", nullptr, 0) == 0); - - Require(Kenv(set, "anyps5.alpha", "one", 4) == 0); - Require(Kenv(get, "anyps5.alpha", buffer, sizeof(buffer)) == 4 && std::strcmp(buffer, "one") == 0); - Require(Kenv(get, "anyps5", buffer, sizeof(buffer)) == -1 && Error() == 2); - Require(Kenv(get, "anyps5.alph", buffer, sizeof(buffer)) == -1 && Error() == 2); - Require(Kenv(get, "anyps5.alphaa", buffer, sizeof(buffer)) == -1 && Error() == 2); - std::memset(buffer, 0x5a, sizeof(buffer)); - Require(Kenv(get, "anyps5.alpha", buffer, 2) == 2 && buffer[0] == 'o' && buffer[1] == 'n' && buffer[2] == 0x5a); - Require(Kenv(get, "anyps5.alpha", buffer, 0) == 0 && buffer[2] == 0x5a); - Require(Kenv(get, "anyps5.alpha", nullptr, 0) == 0); - Require(Kenv(get, "anyps5.alpha", nullptr, 4) == -1 && Error() == 14); - Require(Kenv(get, "anyps5.alpha", reinterpret_cast(0x10), 4) == -1 && Error() == 14); - Require(Kenv(set, "anyps5.alpha", "", 1) == 0); - Require(Kenv(get, "anyps5.alpha", buffer, sizeof(buffer)) == 1 && buffer[0] == '\0'); - Require(Kenv(set, "anyps5.alpha", "one", 129) == 0); - Require(Kenv(set, "anyps5.beta", "two", 1000) == 0); - Require(Kenv(set, "anyps5.alpha", "uno", 4) == 0); - Require(Kenv(get, "anyps5.alpha", buffer, sizeof(buffer)) == 4 && std::strcmp(buffer, "uno") == 0); - - constexpr char expected[] = "anyps5.alpha=uno\0anyps5.beta=two"; - constexpr int needed = sizeof(expected); - Require(Kenv(dump, nullptr, nullptr, 0) == needed); - Require(Kenv(dump, nullptr, buffer, 0) == needed); - Require(Kenv(dump, nullptr, nullptr, sizeof(buffer)) == needed); - std::memset(buffer, 0x5a, sizeof(buffer)); - Require(Kenv(dump, nullptr, buffer, sizeof(buffer)) == 0 && std::memcmp(buffer, expected, needed) == 0 && buffer[needed] == 0x5a); - Require(Kenv(dump, nullptr, buffer, needed) == 0); - std::memset(buffer, 0x5a, sizeof(buffer)); - Require(Kenv(dump, nullptr, buffer, 20) == needed && std::memcmp(buffer, expected, 20) == 0 && buffer[20] == 0x5a); - Require(Kenv(dump, nullptr, reinterpret_cast(0x10), sizeof(buffer)) == -1 && Error() == 14); - Require(Kenv(dump, reinterpret_cast(0x10), buffer, sizeof(buffer)) == 0); - + Require(Kenv(get, longestName.c_str(), buffer, sizeof(buffer)) == -1 && Error() == 2); + Require(Kenv(get, longName.c_str(), buffer, sizeof(buffer)) == -1 && Error() == 63); Require(Kenv(get, nullptr, buffer, sizeof(buffer)) == -1 && Error() == 14); Require(Kenv(get, reinterpret_cast(0x10), buffer, sizeof(buffer)) == -1 && Error() == 14); + Require(Kenv(4, "anyps5.alpha", buffer, sizeof(buffer)) == -1 && Error() == 22); + Require(Kenv(-1, "anyps5.alpha", buffer, sizeof(buffer)) == -1 && Error() == 22); Require(Kenv(4, nullptr, buffer, sizeof(buffer)) == -1 && Error() == 14); - Require(Kenv(set, "anyps5.gamma", nullptr, 4) == -1 && Error() == 14); - Require(Kenv(set, "anyps5.gamma", reinterpret_cast(0x10), 4) == -1 && Error() == 14); - Require(Kenv(get, "anyps5.gamma", buffer, sizeof(buffer)) == -1 && Error() == 2); - - const std::string longest(128, 'n'); - const std::string longestValue(128, 'v'); - Require(Kenv(set, (longest + "x").c_str(), "v", 2) == -1 && Error() == 63); - Require(Kenv(get, (longest + "x").c_str(), buffer, sizeof(buffer)) == -1 && Error() == 63); - Require(Kenv(set, longest.c_str(), longestValue.c_str(), 129) == 0); - Require(Kenv(get, longest.c_str(), buffer, sizeof(buffer)) == 129 && std::strcmp(buffer, longestValue.c_str()) == 0); - Require(Kenv(set, "anyps5.gamma", (longestValue + "v").c_str(), 1000) == -1 && Error() == 63); - Require(Kenv(set, "anyps5.gamma", longestValue.c_str(), 128) == -1 && Error() == 63); - Require(Kenv(unset, longest.c_str(), nullptr, 0) == 0); - Require(Kenv(get, longest.c_str(), buffer, sizeof(buffer)) == -1 && Error() == 2); - - Require(Kenv(set, "anyps5.eq=name", "tail", 5) == 0); - Require(Kenv(get, "anyps5.eq", buffer, sizeof(buffer)) == 10 && std::strcmp(buffer, "name=tail") == 0); - Require(Kenv(unset, "anyps5.eq", nullptr, 0) == 0); - Require(Kenv(unset, "anyps5.eq", nullptr, 0) == -1 && Error() == 2); - Require(Kenv(unset, "anyps5.alpha", nullptr, 0) == 0); + for (const int what : {set, unset}) { + Require(Kenv(what, "anyps5.alpha", "one", 4) == -1 && Error() == 1); + Require(Kenv(what, "anyps5.alpha", "one", 0) == -1 && Error() == 1); + Require(Kenv(what, "anyps5.alpha", "one", -1) == -1 && Error() == 1); + Require(Kenv(what, "anyps5.alpha", longValue.c_str(), 1000) == -1 && Error() == 1); + Require(Kenv(what, "anyps5.alpha", nullptr, 4) == -1 && Error() == 1); + Require(Kenv(what, "anyps5.alpha", reinterpret_cast(0x10), 4) == -1 && Error() == 1); + Require(Kenv(what, nullptr, "one", 4) == -1 && Error() == 1); + Require(Kenv(what, reinterpret_cast(0x10), "one", 4) == -1 && Error() == 1); + Require(Kenv(what, longName.c_str(), "one", 4) == -1 && Error() == 1); + Require(Kenv(what, "", "one", 4) == -1 && Error() == 1); + } Require(Kenv(get, "anyps5.alpha", buffer, sizeof(buffer)) == -1 && Error() == 2); - Require(Kenv(dump, nullptr, nullptr, 0) == static_cast(sizeof("anyps5.beta=two"))); - Require(Kenv(unset, "anyps5.beta", nullptr, 0) == 0); - Require(Kenv(dump, nullptr, nullptr, 0) == 0); - - for (int i = 0; i < 512; ++i) Require(Kenv(set, ("anyps5.fill" + std::to_string(i)).c_str(), "x", 2) == 0); - Require(Kenv(set, "anyps5.overflow", "x", 2) == 0); - Require(Kenv(get, "anyps5.overflow", buffer, sizeof(buffer)) == -1 && Error() == 2); - Require(Kenv(set, "anyps5.fill0", "y", 2) == 0); - Require(Kenv(get, "anyps5.fill0", buffer, sizeof(buffer)) == 2 && buffer[0] == 'y'); - Require(Kenv(unset, "anyps5.fill511", nullptr, 0) == 0); - Require(Kenv(set, "anyps5.overflow", "x", 2) == 0); - Require(Kenv(get, "anyps5.overflow", buffer, sizeof(buffer)) == 2); - for (int i = 0; i < 511; ++i) Require(Kenv(unset, ("anyps5.fill" + std::to_string(i)).c_str(), nullptr, 0) == 0); - Require(Kenv(unset, "anyps5.overflow", nullptr, 0) == 0); Require(Kenv(dump, nullptr, nullptr, 0) == 0); } @@ -1188,6 +1224,8 @@ int main() { Require(profil_nid_postfix(nullptr, 0, 0, 0xFFFFFFFFu) == -1 && Error() == 22); Require(profil_nid_postfix(reinterpret_cast(0x10), 4096, 0, 0) == 0); Require(profil_nid_postfix(nullptr, 0, 0, 0) == 0); + Require(is_in_sandbox_nid_postfix() == 1); + Require(sceKernelIsInSandbox() == 1); Require(umask_nid_postfix(0) == 022); Require(umask_nid_postfix(07777) == 0); Require(umask_nid_postfix(022) == 07777); @@ -1206,11 +1244,13 @@ int main() { Require(std::filesystem::create_directory(root)); DirectoryRenameTests(root); OwnershipAndLinkTests(root); + CheckLinkSymbolicLinkLimit(root); const auto file = (root / "file.txt").string(); std::ofstream(file).put('x'); CheckPathconf(root, file); const int descriptor = open_nid_postfix(file.c_str(), 0, 0); Require(descriptor >= 0); + ThreadAndSessionTests(descriptor); KernelStatfs publicStats{}; auto* viaPublic = reinterpret_cast(&publicStats); KernelStatfs syscallStats{}; diff --git a/core/libs/tests/GuestKernelSysBarrier.cpp b/core/libs/tests/GuestKernelSysBarrier.cpp index 084228c772..eec8e7bc91 100644 --- a/core/libs/tests/GuestKernelSysBarrier.cpp +++ b/core/libs/tests/GuestKernelSysBarrier.cpp @@ -34,20 +34,41 @@ static void* APS5_VABI Waiter(void* arg) { int main() { PthreadBarrierattrPrivate* attr = nullptr; + Require(scePthreadBarrierattrDestroy(nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadBarrierattrDestroy(&attr) == SCE_KERNEL_ERROR_EINVAL); + Require(pthread_barrierattr_destroy_nid_postfix(nullptr) == 22); + Require(pthread_barrierattr_destroy_nid_postfix(&attr) == 22); Require(scePthreadBarrierattrInit(nullptr) == SCE_KERNEL_ERROR_EINVAL); Require(scePthreadBarrierattrInit(&attr) == 0 && attr != nullptr); int pshared = -1; Require(scePthreadBarrierattrGetpshared(&attr, &pshared) == 0 && pshared == PROCESS_PRIVATE); + Require(pthread_barrierattr_setpshared_nid_postfix(&attr, -1) == 22); + Require(pthread_barrierattr_setpshared_nid_postfix(&attr, 2) == 22); + Require(pthread_barrierattr_setpshared_nid_postfix(&attr, 9999) == 22); + Require(pthread_barrierattr_setpshared_nid_postfix(&attr, PROCESS_SHARED) == 22); + Require(scePthreadBarrierattrSetpshared(&attr, -1) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadBarrierattrSetpshared(&attr, 2) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadBarrierattrSetpshared(&attr, 9999) == SCE_KERNEL_ERROR_EINVAL); Require(scePthreadBarrierattrSetpshared(&attr, PROCESS_SHARED) == SCE_KERNEL_ERROR_EINVAL); Require(scePthreadBarrierattrSetpshared(&attr, PROCESS_PRIVATE) == 0); Context context; - Require(scePthreadBarrierInit(&context.barrier, &attr, 0, "zero") == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadBarrierInit(&context.barrier, &attr, 0, "zero") == SCE_KERNEL_ERROR_EINVAL && context.barrier == nullptr); + Require(scePthreadBarrierInit(&context.barrier, nullptr, 0, "zero") == SCE_KERNEL_ERROR_EINVAL && context.barrier == nullptr); + Require(scePthreadBarrierInit(nullptr, &attr, 0, "null") == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadBarrierInit(nullptr, nullptr, 0, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(pthread_barrier_init_nid_postfix(&context.barrier, &attr, 0) == 22 && context.barrier == nullptr); + Require(pthread_barrier_init_nid_postfix(&context.barrier, nullptr, 0) == 22 && context.barrier == nullptr); + Require(pthread_barrier_init_nid_postfix(nullptr, &attr, 0) == 22); + Require(pthread_barrier_init_nid_postfix(nullptr, nullptr, 0) == 22); Require(scePthreadBarrierInit(&context.barrier, &attr, 0x80000000u, "over") == SCE_KERNEL_ERROR_EINVAL && context.barrier == nullptr); Require(scePthreadBarrierInit(nullptr, &attr, WAITERS, "null") == SCE_KERNEL_ERROR_EINVAL); Require(scePthreadBarrierInit(&context.barrier, &attr, WAITERS, "barrier") == 0 && context.barrier != nullptr); Require(scePthreadBarrierattrDestroy(&attr) == 0 && attr == nullptr); Require(scePthreadBarrierattrDestroy(&attr) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadBarrierattrDestroy(nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(pthread_barrierattr_destroy_nid_postfix(&attr) == 22); + Require(pthread_barrierattr_destroy_nid_postfix(nullptr) == 22); for (int round = 0; round < 2; ++round) { context.serial = 0; @@ -62,6 +83,8 @@ int main() { Require(scePthreadBarrierWait(nullptr) == SCE_KERNEL_ERROR_EINVAL); PthreadBarrierPrivate* single = nullptr; + Require(scePthreadBarrierInit(&single, nullptr, 0, nullptr) == SCE_KERNEL_ERROR_EINVAL && single == nullptr); + Require(pthread_barrier_init_nid_postfix(&single, nullptr, 0) == 22 && single == nullptr); Require(scePthreadBarrierInit(&single, nullptr, 1, nullptr) == 0); Require(scePthreadBarrierWait(&single) == BARRIER_SERIAL_THREAD); Require(scePthreadBarrierDestroy(&single) == 0); diff --git a/core/libs/tests/GuestKernelSysConstantUnknowns.cpp b/core/libs/tests/GuestKernelSysConstantUnknowns.cpp new file mode 100644 index 0000000000..edd2d3811c --- /dev/null +++ b/core/libs/tests/GuestKernelSysConstantUnknowns.cpp @@ -0,0 +1,26 @@ +#include "prx/libkernel_sys/include/AdditionalExports.hpp" + +#include +#include +#include + +static void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "%s\n", message); + std::abort(); +} + +int main() { + Require(libkernel_sysUnknown_pCbBddNl66Y_nid_no_patch() == 0, "pCbBddNl66Y returns 0"); + Require(libkernel_sysUnknown_zAloppPJxy0_nid_no_patch() == 0, "zAloppPJxy0 returns 0"); + Require(libkernel_sysUnknown_vq2_plus_HyP5U5I_nid_no_patch() == 0, "vq2+HyP5U5I returns 0"); + Require(libkernel_sysUnknown_z_plus_e0vV_minus_tnH4_nid_no_patch() == 0, "z+e0vV-tnH4 returns 0"); + Require(libkernel_sysUnknown_gJ5gpW45mU0_nid_no_patch() == 0, "gJ5gpW45mU0 returns 0"); + Require(libkernel_sysUnknown_N94a0T4F4_plus_U_nid_no_patch() == 0x8002004eu, "N94a0T4F4+U returns 0x8002004e zero-extended"); + Require(libkernel_sysUnknown_C49jelxiaVE_nid_no_patch() == 0x20018u, "C49jelxiaVE returns 0x20018"); + Require(libkernel_sysUnknown_FR0odl35yQY_nid_no_patch() == 0, "FR0odl35yQY reads 0 before the flag is set"); + Require(libkernel_sysUnknown_1WXQlxMpnNA_nid_no_patch() == 0, "1WXQlxMpnNA returns 0"); + Require(libkernel_sysUnknown_FR0odl35yQY_nid_no_patch() == 1, "FR0odl35yQY reads 1 after 1WXQlxMpnNA"); + Require(libkernel_sysUnknown_1WXQlxMpnNA_nid_no_patch() == 0, "a second 1WXQlxMpnNA returns 0"); + Require(libkernel_sysUnknown_FR0odl35yQY_nid_no_patch() == 1, "the flag stays set"); +} diff --git a/core/libs/tests/GuestKernelSysCpuset.cpp b/core/libs/tests/GuestKernelSysCpuset.cpp index 93f6e5c4ff..1c95832857 100644 --- a/core/libs/tests/GuestKernelSysCpuset.cpp +++ b/core/libs/tests/GuestKernelSysCpuset.cpp @@ -107,5 +107,30 @@ int main() { Require(cpuset_getid_nid_postfix(LevelCpuset, WhichProcess, -1, &setid) == 0); Require(setid == 0); + Require(cpuset_getid_nid_postfix(LevelRoot, WhichCpuset, -1, &setid) == 0 && setid == 0); + Require(cpuset_getid_nid_postfix(LevelCpuset, WhichCpuset, -1, &setid) == 0 && setid == 0); + Require(cpuset_getid_nid_postfix(LevelWhich, WhichCpuset, -1, &setid) == 0 && setid == 0); + Require(cpuset_getid_nid_postfix(LevelCpuset, WhichCpuset, 999999, &setid) == -1 && Error() == Esrch); + Require(cpuset_getid_nid_postfix(LevelCpuset, WhichCpuset, 4294967296LL, &setid) == 0 && setid == 0); + Require(cpuset_getid_nid_postfix(LevelCpuset, WhichCpuset, -4294967296LL, &setid) == 0 && setid == 0); + Require(cpuset_getid_nid_postfix(LevelCpuset, WhichCpuset, 4294967296LL + 999999, &setid) == -1 && Error() == Esrch); + Require(cpuset_getid_nid_postfix(LevelCpuset, WhichCpuset, 0, nullptr) == -1 && Error() == Efault); + Require(cpuset_getid_nid_postfix(LevelCpuset, WhichCpuset, -1, nullptr) == -1 && Error() == Efault); + Require(cpuset_getid_nid_postfix(LevelRoot, WhichCpuset, 0, &setid) == 0 && setid == 0); + Require(cpuset_getid_nid_postfix(LevelCpuset, WhichCpuset, 0, &setid) == 0 && setid == 0); + Require(cpuset_getid_nid_postfix(LevelWhich, WhichCpuset, 0, &setid) == 0 && setid == 0); + Require(cpuset_getid_nid_postfix(LevelRoot, WhichCpuset, 1, &setid) == 0 && setid == 0); + Require(cpuset_getid_nid_postfix(LevelCpuset, WhichCpuset, 1, &setid) == 0 && setid == 1); + Require(cpuset_getid_nid_postfix(LevelWhich, WhichCpuset, 1, &setid) == 0 && setid == 1); + Require(cpuset_getid_nid_postfix(LevelCpuset, WhichCpuset, set2, &setid) == -1 && Error() == Esrch); + + int set3 = -1; + Require(cpuset_nid_postfix(&set3) == 0 && set3 >= 2); + Require(cpuset_getid_nid_postfix(LevelRoot, WhichCpuset, set3, &setid) == 0 && setid == 0); + Require(cpuset_getid_nid_postfix(LevelCpuset, WhichCpuset, set3, &setid) == 0 && setid == set3); + Require(cpuset_getid_nid_postfix(LevelWhich, WhichCpuset, set3, &setid) == 0 && setid == set3); + Require(cpuset_setid_nid_postfix(WhichProcess, -1, 0) == 0); + Require(cpuset_getid_nid_postfix(LevelCpuset, WhichCpuset, set3, &setid) == -1 && Error() == Esrch); + return 0; } diff --git a/core/libs/tests/GuestKernelSysKtimer.cpp b/core/libs/tests/GuestKernelSysKtimer.cpp index bb189576b4..304bf49fa0 100644 --- a/core/libs/tests/GuestKernelSysKtimer.cpp +++ b/core/libs/tests/GuestKernelSysKtimer.cpp @@ -90,9 +90,13 @@ void InvalidIds() { Require(ktimer_delete_nid_postfix(bad) == -1 && Error() == Einval); Require(ktimer_settime_nid_postfix(bad, 0, &value, nullptr) == -1 && Error() == Einval); Require(ktimer_gettime_nid_postfix(bad, &value) == -1 && Error() == Einval); + Require(ktimer_gettime_nid_postfix(bad, nullptr) == -1 && Error() == Einval); Require(ktimer_getoverrun_nid_postfix(bad) == -1 && Error() == Einval); } Require(ktimer_settime_nid_postfix(id, 0, nullptr, nullptr) == -1 && Error() == Efault); + const int valid = Create(); + Require(ktimer_gettime_nid_postfix(valid, nullptr) == -1 && Error() == Efault); + Require(ktimer_delete_nid_postfix(valid) == 0); } void Values() { @@ -102,6 +106,9 @@ void Values() { for (const GuestItimerspec bad : {GuestItimerspec{{0, 0}, {-1, 0}}, GuestItimerspec{{0, 0}, {0, -1}}, GuestItimerspec{{0, 0}, {0, Second}}, GuestItimerspec{{-1, 0}, {1, 0}}, GuestItimerspec{{0, Second}, {1, 0}}}) Require(ktimer_settime_nid_postfix(id, 0, &bad, nullptr) == -1 && Error() == Einval); + const GuestItimerspec valid{{0, 0}, {1, 0}}; + for (const int badFlags : {-1, 2, 4, ~TimerAbstime}) + Require(ktimer_settime_nid_postfix(id, badFlags, &valid, nullptr) == -1 && Error() == Einval); const GuestItimerspec clear{{-1, 0}, {0, 0}}; Require(ktimer_settime_nid_postfix(id, 0, &clear, nullptr) == 0); diff --git a/core/libs/tests/GuestKernelSysThreadDefaults.cpp b/core/libs/tests/GuestKernelSysThreadDefaults.cpp index f38f272d56..72b76c7593 100644 --- a/core/libs/tests/GuestKernelSysThreadDefaults.cpp +++ b/core/libs/tests/GuestKernelSysThreadDefaults.cpp @@ -3,6 +3,7 @@ #include "prx/libkernel/include/Api.hpp" #include "SceTypes.hpp" #include +#include #include #include @@ -67,8 +68,30 @@ static void CheckSetName() { Require(name[0] == '\0'); } +static void* APS5_VABI ReportFpuControl(void* out) { + asm volatile("fnstcw %0" : "=m"(*static_cast(out))); + return nullptr; +} + +static void CheckFpuControl() { + std::uint16_t creator = 0; + asm volatile("fnstcw %0" : "=m"(creator)); + const std::uint16_t doublePrecision = 0x027f; + asm volatile("fldcw %0" : : "m"(doublePrecision)); + std::uint16_t started = 0; + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, nullptr, ReportFpuControl, &started, "fpu") == 0); + Require(scePthreadJoin(thread, nullptr) == 0); + std::uint16_t creatorAfter = 0; + asm volatile("fnstcw %0" : "=m"(creatorAfter)); + asm volatile("fldcw %0" : : "m"(creator)); + Require(started == 0x037f); + Require(creatorAfter == doublePrecision); +} + int main() { CheckDefaultStackSize(); CheckSetName(); + CheckFpuControl(); return 0; } diff --git a/core/libs/tests/GuestKqueue.cpp b/core/libs/tests/GuestKqueue.cpp index 2b72072cdf..ce01f4b2c6 100644 --- a/core/libs/tests/GuestKqueue.cpp +++ b/core/libs/tests/GuestKqueue.cpp @@ -31,6 +31,8 @@ static constexpr int GuestEperm = 1; static constexpr int GuestEnoent = 2; static constexpr int GuestEbadf = 9; static constexpr int GuestEinval = 22; +static constexpr int GuestEspipe = 29; +static constexpr int GuestEopnotsupp = 45; static constexpr int GuestEpipe = 32; static constexpr int GuestEagain = 35; static constexpr int SetStatusFlags = 4; @@ -1641,6 +1643,21 @@ static void StandardInput() { Require(close_nid_postfix(kq) == 0); } +static void KqueueDescriptorOperations() { + const int kq = kqueue_nid_postfix(); + Require(kq >= 0); + char byte = 0; + RequireFailure(static_cast(read_nid_postfix(kq, &byte, 1)), GuestEopnotsupp); + RequireFailure(static_cast(write_nid_postfix(kq, "x", 1)), GuestEopnotsupp); + Require(read_nid_postfix(kq, &byte, 0) == 0); + RequireFailure(static_cast(write_nid_postfix(kq, "x", 0)), GuestEopnotsupp); + RequireFailure(static_cast(lseek_nid_postfix(kq, 0, 0)), GuestEspipe); + RequireFailure(fsync_nid_postfix(kq), GuestEinval); + RequireFailure(fdatasync_nid_postfix(kq), GuestEinval); + RequireFailure(flock_nid_postfix(kq, 1), GuestEopnotsupp); + Require(close_nid_postfix(kq) == 0); +} + int main(int argc, char** argv) { if (argc == 2 && std::string(argv[1]) == "cleared-race") { ClearedKqueueKeepsConcurrentActivations(); @@ -1650,6 +1667,7 @@ int main(int argc, char** argv) { StandardInput(); CreateAndClose(); SocketReplacesDescriptor(); + KqueueDescriptorOperations(); Timers(); TimerReadd(); UnsupportedFilters(); diff --git a/core/libs/tests/GuestLibcInternalExceptionDestructors.cpp b/core/libs/tests/GuestLibcInternalExceptionDestructors.cpp new file mode 100644 index 0000000000..c8ddb4aadd --- /dev/null +++ b/core/libs/tests/GuestLibcInternalExceptionDestructors.cpp @@ -0,0 +1,94 @@ +#include "prx/libSceLibcInternal/include/Api.hpp" +#include "prx/libc/include/Api.hpp" +#include "prx/libc/include/ApplicationHeap.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include + +namespace { + +unsigned releases = 0; +void* lastReleased = nullptr; + +void Require(bool condition, const char* message) { + if (condition) return; + std::fprintf(stderr, "libc internal exception destructors: %s\n", message); + std::abort(); +} + +void* APS5_VABI TestAllocate(std::size_t size) { return std::malloc(size); } +void APS5_VABI TestFree(void* pointer) { + ++releases; + lastReleased = pointer; + std::free(pointer); +} +void* APS5_VABI TestCalloc(std::size_t count, std::size_t size) { return std::calloc(count, size); } +void* APS5_VABI TestReallocate(void* pointer, std::size_t size) { return std::realloc(pointer, size); } +void* APS5_VABI TestAlign(std::size_t, std::size_t size) { return std::malloc(size); } +void* APS5_VABI TestRealign(void* pointer, std::size_t size, std::size_t) { return std::realloc(pointer, size); } +int APS5_VABI TestPosixAlign(void** pointer, std::size_t, std::size_t size) { + *pointer = std::malloc(size); + return *pointer ? 0 : 12; +} + +using Destructor = void (APS5_VABI*)(void*); + +void CheckMessageDestructor(Destructor destructor, const char* text) { + LibcException::ExceptionObject object{}; + _ZNSt13runtime_errorC1EPKc_nid_postfix(&object, text); + Require(object.message != nullptr && std::strcmp(object.message, text) == 0, "runtime_error keeps its message"); + destructor(&object); + Require(object.message == nullptr, "the base-object destructor releases the message"); + destructor(&object); + Require(object.message == nullptr, "a second destruction leaves the message released"); +} + +void CheckFutureError() { + LibcException::FutureErrorObject inline_{}; + inline_.message.capacity = 15; + inline_.message.size = 3; + std::memcpy(inline_.message.buffer, "abc", 4); + releases = 0; + sceLibcInternalFutureErrorBaseDestructor_nid_no_patch(&inline_); + Require(inline_.message.reserved == 0 && inline_.message.size == 0 && inline_.message.capacity == 15, "an inline message is reset"); + Require(inline_.message.buffer[0] == 0 && releases == 0, "an inline message is not released"); + + LibcException::FutureErrorObject heap{}; + heap.message.capacity = 24; + heap.message.size = 24; + heap.message.pointer = static_cast(_Znwm_nid_postfix(25)); + std::memcpy(heap.message.pointer, "future already retrieved", 25); + char* storage = heap.message.pointer; + releases = 0; + sceLibcInternalFutureErrorBaseDestructor_nid_no_patch(&heap); + Require(releases == 1 && lastReleased == storage, "a heap message is released"); + Require(heap.message.size == 0 && heap.message.capacity == 15 && heap.message.buffer[0] == 0, "a heap message is reset to the inline buffer"); + + LibcException::FutureErrorObject expected{}; + expected.message.capacity = 15; + LibcException::FutureErrorObject actual = expected; + _ZNSt12future_errorD1Ev_nid_postfix(&expected); + sceLibcInternalFutureErrorBaseDestructor_nid_no_patch(&actual); + Require(std::memcmp(&expected, &actual, sizeof(expected)) == 0, "D2 leaves the object as D1 does"); +} + +} + +int main() { + const std::array api{ + reinterpret_cast(&TestAllocate), reinterpret_cast(&TestFree), + reinterpret_cast(&TestCalloc), reinterpret_cast(&TestReallocate), + reinterpret_cast(&TestAlign), reinterpret_cast(&TestRealign), + reinterpret_cast(&TestPosixAlign), nullptr, nullptr, nullptr}; + ApplicationHeapRegister_nid_no_patch(api.data()); + + CheckMessageDestructor(&sceLibcInternalOverflowErrorBaseDestructor_nid_no_patch, "value too large"); + CheckMessageDestructor(&sceLibcInternalUnderflowErrorBaseDestructor_nid_no_patch, "value too small"); + CheckFutureError(); + + std::puts("libc internal exception base-object destructor checks passed"); + return 0; +} diff --git a/core/libs/tests/GuestLibcInternalPad.cpp b/core/libs/tests/GuestLibcInternalPad.cpp new file mode 100644 index 0000000000..d27aa7b0cc --- /dev/null +++ b/core/libs/tests/GuestLibcInternalPad.cpp @@ -0,0 +1,48 @@ +#include "prx/libSceLibcInternal/include/Api.hpp" +#include "prx/libc/include/Api.hpp" +#include +#include +#include + +namespace { + +void Require(bool condition, const char* message) { + if (condition) return; + std::fprintf(stderr, "libc internal _Pad destructor: %s\n", message); + std::abort(); +} + +template +bool Throws(Function function) { + try { + function(); + } catch (const std::exception&) { + return true; + } + return false; +} + +} + +int main() { + GuestPad pad{}; + _ZNSt4_PadC2Ev_nid_postfix(&pad); + Require(pad.condition != nullptr && pad.mutex != nullptr && !pad.started, "the base-object constructor creates the mutex and condition"); + + sceLibcInternalPadCompleteDestructor_nid_no_patch(&pad); + Require(pad.condition == nullptr && pad.mutex == nullptr, "the complete-object destructor releases the mutex and condition"); + Require(!pad.started && pad.vtable == nullptr, "the destructor leaves the other members alone"); + + GuestPad again{}; + _ZNSt4_PadC2Ev_nid_postfix(&again); + sceLibcInternalPadCompleteDestructor_nid_no_patch(&again); + _ZNSt4_PadC2Ev_nid_postfix(&again); + Require(again.condition != nullptr && again.mutex != nullptr, "a destroyed pad can be constructed again"); + _ZNSt4_PadD2Ev_nid_postfix(&again); + + Require(Throws([&] { sceLibcInternalPadCompleteDestructor_nid_no_patch(&pad); }), "destroying a pad twice is rejected"); + Require(Throws([] { sceLibcInternalPadCompleteDestructor_nid_no_patch(nullptr); }), "a null pad is rejected"); + + std::puts("libc internal _Pad complete-object destructor checks passed"); + return 0; +} diff --git a/core/libs/tests/GuestLibreSslBio.cpp b/core/libs/tests/GuestLibreSslBio.cpp new file mode 100644 index 0000000000..650b44b984 --- /dev/null +++ b/core/libs/tests/GuestLibreSslBio.cpp @@ -0,0 +1,564 @@ +#include "prx/libSceLibreSsl/libSceLibreSsl.hpp" +#include "prx/libc/include/Api.hpp" +#include "prx/libSceLibreSsl/ErrorQueue.hpp" +#include "prx/libc/include/ApplicationHeap.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef ANYPS5_BIO_DYNAMIC +#ifdef _WIN32 +#include +#else +#include +#endif +#endif + +using LibreSsl::Bio; +using LibreSsl::BioBuffer; +using LibreSsl::BioMethod; + + +#ifdef ANYPS5_BIO_DYNAMIC +namespace { +decltype(&BIO_s_mem_nid_postfix) bioMemoryMethod; +decltype(&BIO_new_nid_postfix) bioNew; +decltype(&BIO_new_mem_buf_nid_postfix) bioNewBuffer; +decltype(&BIO_free_nid_postfix) bioFree; +decltype(&BIO_ctrl_nid_postfix) bioControl; +decltype(&BIO_write_nid_postfix) bioWrite; +decltype(&BIO_puts_nid_postfix) bioPuts; +decltype(&calloc_nid_postfix) guestCalloc; +decltype(&free_nid_postfix) guestFree; +decltype(&ApplicationHeapRegister_nid_no_patch) registerHeap; +decltype(&ERR_clear_error_nid_postfix) clearError; +decltype(&ERR_peek_error_nid_postfix) peekError; +decltype(&ERR_get_error_nid_postfix) getError; + +void* Load(const std::filesystem::path& path) { +#ifdef _WIN32 + auto* module = LoadLibraryExW(path.c_str(), nullptr, LOAD_WITH_ALTERED_SEARCH_PATH); +#else + auto* module = dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL); +#endif + if (!module) throw std::runtime_error("failed to load patched BIO dependency: " + path.string()); + return module; +} + +template +void Resolve(TFunction& function, void* module, const char* name) { +#ifdef _WIN32 + function = reinterpret_cast(GetProcAddress(static_cast(module), name)); +#else + function = reinterpret_cast(dlsym(module, name)); +#endif + if (!function) throw std::runtime_error(std::string("missing patched BIO dependency: ") + name); +} + +void InitializePatched(const std::filesystem::path& directory) { + void* libc = Load(directory / "libc.prx"); + void* bio = Load(directory / "libSceLibreSsl.prx"); + Resolve(bioMemoryMethod, bio, "R86sXhW699g"); + Resolve(bioNew, bio, "ARlY6zhSacE"); + Resolve(bioNewBuffer, bio, "GMzjS9jvyDQ"); + Resolve(bioFree, bio, "lpJ3zhgftCg"); + Resolve(bioControl, bio, "RHpq9hQfTj8"); + Resolve(bioWrite, bio, "nH7XM+HhXWc"); + Resolve(bioPuts, bio, "eVcWmVlimPA"); + Resolve(guestCalloc, libc, "2X5agFjKxMc"); + Resolve(guestFree, libc, "tIhsqj0qsFE"); + Resolve(registerHeap, libc, "ApplicationHeapRegister_nid_no_patch"); + Resolve(clearError, bio, "sieQ--PINWg"); + Resolve(peekError, bio, "Aytjey6FPTU"); + Resolve(getError, bio, "DJg+qWX1rwY"); +} +} // namespace + +#define BIO_s_mem_nid_postfix bioMemoryMethod +#define BIO_new_nid_postfix bioNew +#define BIO_new_mem_buf_nid_postfix bioNewBuffer +#define BIO_free_nid_postfix bioFree +#define BIO_ctrl_nid_postfix bioControl +#define BIO_write_nid_postfix bioWrite +#define BIO_puts_nid_postfix bioPuts +#define calloc_nid_postfix guestCalloc +#define free_nid_postfix guestFree +#define ApplicationHeapRegister_nid_no_patch registerHeap +#define ERR_clear_error_nid_postfix clearError +#define ERR_peek_error_nid_postfix peekError +#define ERR_get_error_nid_postfix getError +#endif + +namespace { + +void Require(bool condition, const char* message) { + if (!condition) { + std::fprintf(stderr, "%s\n", message); + std::abort(); + } +} + +int allocationsUntilFailure = -1; +std::atomic allocations{0}; +void* wipePointer = nullptr; +std::size_t wipeSize = 0; + +void* APS5_VABI Allocate(std::size_t) { throw std::logic_error("unexpected nonzeroed BIO allocation"); } +void* APS5_VABI Reallocate(void*, std::size_t) { throw std::logic_error("unexpected BIO reallocation"); } +void* APS5_VABI Align(std::size_t, std::size_t) { throw std::logic_error("unexpected BIO aligned allocation"); } +void* APS5_VABI Realign(void*, std::size_t, std::size_t) { throw std::logic_error("unexpected BIO realignment"); } +int APS5_VABI PosixAlign(void**, std::size_t, std::size_t) { throw std::logic_error("unexpected BIO aligned allocation"); } + +void* APS5_VABI Calloc(std::size_t count, std::size_t size) { + if (allocationsUntilFailure == 0) return nullptr; + if (allocationsUntilFailure > 0) --allocationsUntilFailure; + void* result = std::calloc(count, size); + if (result) ++allocations; + return result; +} + +void APS5_VABI Free(void* pointer) { + if (pointer && pointer == wipePointer) { + for (std::size_t i = 0; i < wipeSize; ++i) Require(static_cast(pointer)[i] == 0, "freed buffer was not wiped"); + wipePointer = nullptr; + } + if (pointer) --allocations; + std::free(pointer); +} + +void InitializeHeap() { + const std::array api{ + reinterpret_cast(Allocate), reinterpret_cast(Free), reinterpret_cast(Calloc), + reinterpret_cast(Reallocate), reinterpret_cast(Align), reinterpret_cast(Realign), + reinterpret_cast(PosixAlign)}; + ApplicationHeapRegister_nid_no_patch(api.data()); +} + +template +void RequireUnsupported(TFunction function) { + bool threw = false; + try { + function(); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw, "unsupported BIO path did not throw"); +} + +void RequireError(std::uint64_t expected) { + Require(ERR_peek_error_nid_postfix() == expected, "BIO error code or ordering"); + Require(ERR_get_error_nid_postfix() == expected, "consume BIO error"); + Require(ERR_get_error_nid_postfix() == 0, "unexpected extra BIO error"); +} + +BioBuffer* Buffer(Bio* bio) { + BioBuffer* buffer = nullptr; + Require(BIO_ctrl_nid_postfix(bio, 115, 0, &buffer) == 1 && buffer, "get buffer"); + return buffer; +} + +void RequireData(Bio* bio, const std::string& expected) { + char* bytes = nullptr; + Require(BIO_ctrl_nid_postfix(bio, 3, 0, &bytes) == static_cast(expected.size()), "buffer data length"); + Require(expected.empty() || (bytes && std::memcmp(bytes, expected.data(), expected.size()) == 0), "buffer data bytes"); + Require(BIO_ctrl_nid_postfix(bio, 10, 0, nullptr) == static_cast(expected.size()), "pending bytes"); + Require(BIO_ctrl_nid_postfix(bio, 2, 0, nullptr) == expected.empty(), "EOF flag"); +} + +void TestWritable() { + const auto* method = BIO_s_mem_nid_postfix(); + Require(method && method == BIO_s_mem_nid_postfix(), "stable method pointer"); + Require(method->type == 0x401 && std::strcmp(method->name, "memory buffer") == 0, "method identity"); + Require(method->write && method->read && method->puts && method->gets && method->control && method->create && method->destroy, "method callbacks"); + Require(method->callbackControl == nullptr, "no callback control method"); + Bio* bio = BIO_new_nid_postfix(method); + Require(bio && bio->method == method && bio->initialized == 1 && bio->references == 1, "new BIO state"); + Require(!bio->callback && !bio->callbackArgument && !bio->flags && !bio->retryReason && !bio->next && !bio->previous && !bio->extraData, "new BIO zero fields"); + Require(bio->number == -1 && bio->bytesRead == 0 && bio->bytesWritten == 0, "new BIO counters"); + Require(BIO_write_nid_postfix(bio, nullptr, 7) == -1, "null write reaches memory method"); + RequireError(0x20fff073); + Require(BIO_write_nid_postfix(bio, "x", 0) == 0, "zero-length owned write"); + RequireError(0); + RequireData(bio, ""); + auto* buffer = Buffer(bio); + Require(buffer->capacity == 0 && buffer->data == nullptr, "new buffer storage"); + char output[16]{}; + Require(method->read(bio, output, sizeof(output)) == -1 && (bio->flags & 0xf) == 9, "empty writable BIO retry"); + Require(BIO_write_nid_postfix(bio, "abc", 3) == 3 && (bio->flags & 0xf) == 0, "write clears retry flags"); + RequireData(bio, "abc"); + Require(buffer->capacity == 8 && buffer->data[3] == 0, "first buffer expansion"); + std::memset(buffer->data + buffer->length, 0xa5, buffer->capacity - buffer->length); + wipePointer = buffer->data; + wipeSize = buffer->capacity; + Require(BIO_write_nid_postfix(bio, "0123456789", 10) == 10, "second write"); + Require(wipePointer == nullptr, "growth frees the wiped allocation"); + Require(buffer->capacity == 20 && buffer->data[13] == 0, "second buffer expansion"); + RequireData(bio, "abc0123456789"); + Require(method->read(bio, output, 4) == 4 && std::memcmp(output, "abc0", 4) == 0, "partial memory read"); + RequireData(bio, "123456789"); + Require(BIO_puts_nid_postfix(bio, "xy") == 2 && bio->bytesWritten == 15, "puts and byte accounting"); + RequireData(bio, "123456789xy"); + Require(method->read(bio, nullptr, 3) == 3, "null raw read reports available count"); + RequireData(bio, "123456789xy"); + Require(method->read(bio, output, -3) == -3, "negative raw read"); + Require(BIO_write_nid_postfix(bio, nullptr, 5) == -1, "reject null public source"); + RequireError(0x20fff073); + RequireData(bio, "123456789xy"); + Require(BIO_ctrl_nid_postfix(bio, 11, 0, nullptr) == 1 && BIO_ctrl_nid_postfix(bio, 12, 0, nullptr) == 1, "flush and duplicate controls"); + Require(BIO_ctrl_nid_postfix(bio, 13, 0, nullptr) == 0, "no pending output"); + Require(BIO_ctrl_nid_postfix(bio, 6, 0, nullptr) == 0 && BIO_ctrl_nid_postfix(bio, 7, 0, nullptr) == 0, "push and pop controls"); + Require(BIO_ctrl_nid_postfix(bio, 0x7fffffff, 0, nullptr) == 0, "unknown memory control"); + Require(BIO_ctrl_nid_postfix(bio, 1, 0, nullptr) == 1, "reset writable BIO"); + RequireData(bio, ""); + for (std::size_t i = 0; i < buffer->capacity; ++i) Require(buffer->data[i] == 0, "reset wipes full allocation"); + Require(BIO_ctrl_nid_postfix(bio, 130, 7, nullptr) == 1, "set EOF return"); + Require(method->read(bio, output, 1) == 7 && (bio->flags & 0xf) == 9, "configured EOF return and retry"); + Require(BIO_ctrl_nid_postfix(bio, 130, 0, nullptr) == 1, "set terminal EOF"); + Require(method->read(bio, output, 1) == 0 && (bio->flags & 0xf) == 0, "terminal EOF clears retry"); + Require(BIO_puts_nid_postfix(bio, "secret") == 6, "write before destruction"); + wipePointer = buffer->data; + wipeSize = buffer->capacity; + Require(BIO_free_nid_postfix(bio) == 1, "free writable BIO"); + Require(wipePointer == nullptr, "destruction frees wiped storage"); +} + +void TestReadOnlyAndLines() { + const std::array source{'a', '\0', 'b', '\n', 'c', 'd', '\n', 'e'}; + Bio* bio = BIO_new_mem_buf_nid_postfix(source.data(), source.size()); + Require(bio && (bio->flags & 0x200) && bio->number == 0, "read-only BIO state"); + auto* buffer = Buffer(bio); + Require(buffer->data == source.data() && buffer->capacity == source.size(), "read-only buffer is borrowed"); + char output[16]; + std::memset(output, 0x55, sizeof(output)); + Require(bio->method->gets(bio, output, sizeof(output)) == 4 && std::memcmp(output, source.data(), 4) == 0 && output[4] == 0, "line includes embedded NUL and newline"); + Require(buffer->data == source.data() + 4 && buffer->length == 4, "borrowed buffer advances"); + Require(bio->method->gets(bio, output, 3) == 2 && std::strcmp(output, "cd") == 0, "line length bound"); + Require(bio->method->gets(bio, output, 1) == 0 && output[0] == 0 && buffer->length == 2, "one-byte line buffer"); + Require(bio->method->gets(bio, output, sizeof(output)) == 1 && std::strcmp(output, "\n") == 0, "newline on next read"); + Require(bio->method->read(bio, output, sizeof(output)) == 1 && output[0] == 'e', "last read-only byte"); + Require(bio->method->read(bio, output, 1) == 0 && (bio->flags & 0xf) == 0, "read-only EOF"); + Require(BIO_ctrl_nid_postfix(bio, 1, 0, nullptr) == 1, "read-only reset"); + Require(buffer->data == source.data() && buffer->length == source.size(), "read-only reset restores original range"); + Require(BIO_write_nid_postfix(bio, "x", 1) == -1, "reject read-only write"); + RequireError(0x20fff07e); + Require(buffer->data == source.data() && buffer->length == source.size(), "rejected write preserves input"); + Require(BIO_free_nid_postfix(bio) == 1 && source[0] == 'a' && source[7] == 'e', "borrowed bytes survive free"); + for (const int size : {-1, -99}) { + bio = BIO_new_mem_buf_nid_postfix("ab\0cd", size); + RequireData(bio, "ab"); + Require(BIO_free_nid_postfix(bio) == 1, "free string BIO"); + } + bio = BIO_new_mem_buf_nid_postfix("", 0); + RequireData(bio, ""); + output[0] = 'x'; + Require(bio->method->gets(bio, output, 8) == 0 && output[0] == 0, "empty gets"); + Require(BIO_free_nid_postfix(bio) == 1, "free empty BIO"); +} + +void TestBufferOwnership() { + Bio* bio = BIO_new_nid_postfix(BIO_s_mem_nid_postfix()); + Require(BIO_puts_nid_postfix(bio, "retained") == 8, "write retained buffer"); + auto* retained = Buffer(bio); + Require(BIO_ctrl_nid_postfix(bio, 8, 0, nullptr) == 1, "default close flag"); + Require(BIO_ctrl_nid_postfix(bio, 9, 0, nullptr) == 1 && BIO_ctrl_nid_postfix(bio, 8, 0, nullptr) == 0, "disable close"); + Require(BIO_free_nid_postfix(bio) == 1, "free BIO without its buffer"); + Require(retained->length == 8 && std::memcmp(retained->data, "retained", 8) == 0, "detached storage remains valid"); + free_nid_postfix(retained->data); + free_nid_postfix(retained); + + bio = BIO_new_nid_postfix(BIO_s_mem_nid_postfix()); + Require(BIO_puts_nid_postfix(bio, "old") == 3, "write old buffer"); + auto* replacement = static_cast(calloc_nid_postfix(1, sizeof(BioBuffer))); + replacement->data = static_cast(calloc_nid_postfix(8, 1)); + replacement->length = 3; + replacement->capacity = 8; + std::memcpy(replacement->data, "new", 3); + Require(BIO_ctrl_nid_postfix(bio, 114, 0, replacement) == 1 && Buffer(bio) == replacement, "replace buffer"); + RequireData(bio, "new"); + Require(BIO_write_nid_postfix(bio, "!", 1) == 1, "write replacement buffer"); + RequireData(bio, "new!"); + Require(BIO_free_nid_postfix(bio) == 1, "free BIO retaining replacement"); + Require(replacement->length == 4 && std::memcmp(replacement->data, "new!", 4) == 0, "caller owns replacement"); + free_nid_postfix(replacement->data); + free_nid_postfix(replacement); + + bio = BIO_new_nid_postfix(BIO_s_mem_nid_postfix()); + Require(BIO_ctrl_nid_postfix(bio, 114, 1, nullptr) == 1, "detach without replacement"); + Require(BIO_ctrl_nid_postfix(bio, 8, 0, nullptr) == 1, "close control without storage"); + Require(BIO_ctrl_nid_postfix(bio, 11, 0, nullptr) == 1, "flush without storage"); + void* missing = bio; + Require(BIO_ctrl_nid_postfix(bio, 115, 0, &missing) == 1 && missing == nullptr, "query detached storage"); + Require(BIO_free_nid_postfix(bio) == 1, "free detached BIO"); +} + +void TestInterleavedTransfers() { + Bio* bio = BIO_new_nid_postfix(BIO_s_mem_nid_postfix()); + std::string pending; + std::uint32_t sequence = 0x346de821; + std::uint64_t written = 0; + for (int step = 0; step < 2000; ++step) { + sequence = sequence * 1664525 + 1013904223; + const int count = static_cast((sequence >> 16) % 65); + if ((sequence & 3) != 0) { + std::array input{}; + for (int i = 0; i < count; ++i) input[i] = static_cast(sequence + i * 37); + Require(BIO_write_nid_postfix(bio, input.data(), count) == count, "interleaved write count"); + pending.append(input.data(), count); + written += count; + } else { + std::array output; + output.fill(static_cast(0xa5)); + const int expected = static_cast(std::min(count, pending.size())); + const int result = bio->method->read(bio, output.data(), count); + Require(result == (pending.empty() ? -1 : expected), "interleaved read count"); + if (expected != 0) Require(std::memcmp(output.data(), pending.data(), expected) == 0, "interleaved read bytes"); + Require(output[expected] == static_cast(0xa5), "read preserves destination tail"); + pending.erase(0, expected); + } + RequireData(bio, pending); + Require(bio->bytesWritten == written && bio->bytesRead == 0, "interleaved transfer counters"); + if (step % 127 == 0) { + Require(BIO_ctrl_nid_postfix(bio, 1, 0, nullptr) == 1, "interleaved reset"); + pending.clear(); + } + } + Require(BIO_free_nid_postfix(bio) == 1, "free interleaved BIO"); +} + +struct WriteEvent { + int operation; + const char* source; + int size; + std::int64_t result; + int flags; + std::uint64_t bytesWritten; +}; + +struct WriteTrace { + std::vector events; + int veto = 0; + int postAdjustment = 0; +}; + +std::int64_t APS5_VABI TraceWrite(Bio* bio, int operation, const char* source, int size, std::int64_t, std::int64_t result) { + auto& trace = *reinterpret_cast(bio->callbackArgument); + trace.events.push_back({operation, source, size, result, bio->flags, bio->bytesWritten}); + if (operation == 3 && trace.veto) return trace.veto; + return (operation & 0x80) ? result + trace.postAdjustment : result; +} + +void TestWriteBoundaries() { + const char source[] = "abc"; + for (const bool readOnly : {false, true}) { + Bio* bio = readOnly ? BIO_new_mem_buf_nid_postfix(source, 3) : BIO_new_nid_postfix(BIO_s_mem_nid_postfix()); + WriteTrace trace; + bio->callback = TraceWrite; + bio->callbackArgument = reinterpret_cast(&trace); + for (const int size : {0, 1}) { + bio->flags |= 0x4009; + const int flags = bio->flags; + trace.events.clear(); + Require(BIO_write_nid_postfix(bio, nullptr, size) == -1, "null source dispatch result"); + RequireError(0x20fff073); + Require(trace.events.size() == 2, "null source has pre/post callbacks"); + Require(trace.events[0].operation == 3 && trace.events[1].operation == 0x83, "null source callback operations"); + Require(trace.events[0].source == nullptr && trace.events[1].source == nullptr && + trace.events[0].size == size && trace.events[1].size == size, "null source callback arguments"); + Require(trace.events[0].result == 1 && trace.events[1].result == -1, "null source callback results"); + Require(trace.events[0].flags == flags && trace.events[1].flags == flags && bio->flags == flags, "null source preserves retry flags"); + Require(trace.events[0].bytesWritten == 0 && trace.events[1].bytesWritten == 0 && bio->bytesWritten == 0, "failed write does not count bytes"); + } + bio->flags |= 0x4009; + const int flags = bio->flags; + trace.events.clear(); + Require(BIO_write_nid_postfix(bio, source, 0) == (readOnly ? -1 : 0), "zero-length method result"); + RequireError(readOnly ? 0x20fff07e : 0); + Require(trace.events.size() == 2 && trace.events[0].operation == 3 && trace.events[1].operation == 0x83, "zero-length write callback operations"); + Require(trace.events[0].source == source && trace.events[1].source == source && + trace.events[0].size == 0 && trace.events[1].size == 0, "zero-length callback arguments"); + Require(trace.events[0].result == 1 && trace.events[1].result == (readOnly ? -1 : 0), "zero-length callback results"); + Require(trace.events[0].flags == flags && trace.events[1].flags == (readOnly ? flags : flags & ~0xf), "retry flags change during the method"); + Require(bio->flags == (readOnly ? flags : flags & ~0xf) && bio->bytesWritten == 0, "zero-length retry flags and byte count"); + RequireData(bio, readOnly ? "abc" : ""); + trace.events.clear(); + trace.veto = -7; + Require(BIO_write_nid_postfix(bio, nullptr, 0) == -7 && trace.events.size() == 1, "null write callback veto prevents method"); + RequireError(0); + trace.veto = 0; + trace.postAdjustment = 5; + trace.events.clear(); + Require(BIO_write_nid_postfix(bio, nullptr, 0) == 4 && bio->bytesWritten == 0, "post callback can override failure without counting bytes"); + RequireError(0x20fff073); + bio->callback = nullptr; + Require(BIO_free_nid_postfix(bio) == 1, "free boundary BIO"); + } + Bio* bio = BIO_new_nid_postfix(BIO_s_mem_nid_postfix()); + WriteTrace trace; + bio->callback = TraceWrite; + bio->callbackArgument = reinterpret_cast(&trace); + const auto* method = bio->method; + bio->method = nullptr; + Require(BIO_write_nid_postfix(bio, nullptr, 0) == -2 && trace.events.empty(), "method validation precedes callback"); + RequireError(0x20fff079); + bio->method = method; + bio->initialized = 0; + Require(BIO_write_nid_postfix(bio, nullptr, 0) == -2 && trace.events.size() == 1 && trace.events[0].operation == 3, "initialization validation follows pre-callback"); + RequireError(0x20fff078); + bio->initialized = 1; + bio->callback = nullptr; + Require(BIO_free_nid_postfix(bio) == 1, "free dispatch-order BIO"); +} + +struct CallbackState { + std::vector events; + std::int64_t argument = 0; + int veto = 0; +}; + +std::int64_t APS5_VABI Callback(Bio* bio, int operation, const char*, int, std::int64_t argument, std::int64_t result) { + auto& state = *reinterpret_cast(bio->callbackArgument); + state.events.push_back(operation); + state.argument = argument; + if (state.veto && !(operation & 0x80)) return state.veto; + return (operation & 0x80) ? result + 2 : 1; +} + +int APS5_VABI CreateCustom(Bio* bio) { + bio->initialized = 1; + return 1; +} + +int APS5_VABI WriteCustom(Bio*, const char*, int count) { + return count - 1; +} + +std::int64_t APS5_VABI ControlCustom(Bio*, int command, std::int64_t argument, void*) { + return argument + command; +} + +int APS5_VABI RejectCreate(Bio*) { + return 0; +} + +void TestDispatchAndReferences() { + Bio* bio = BIO_new_nid_postfix(BIO_s_mem_nid_postfix()); + CallbackState state; + bio->callback = Callback; + bio->callbackArgument = reinterpret_cast(&state); + Require(BIO_write_nid_postfix(bio, "abc", 3) == 5 && bio->bytesWritten == 3, "callback changes result after accounting"); + Require(state.events == std::vector{3, 0x83}, "write callback ordering"); + state.events.clear(); + Require(BIO_puts_nid_postfix(bio, "d") == 3 && state.events == std::vector{4, 0x84}, "puts callbacks"); + state.events.clear(); + Require(BIO_ctrl_nid_postfix(bio, 10, 0x1234567887654321LL, nullptr) == 6, "control callback result"); + Require(state.argument == 0x1234567887654321LL && state.events == std::vector{6, 0x86}, "64-bit control argument"); + state.events.clear(); + state.veto = -7; + Require(BIO_write_nid_postfix(bio, "x", 1) == -7 && state.events == std::vector{3}, "write callback veto"); + Require(bio->bytesWritten == 4 && static_cast(bio->data)->length == 4, "veto prevents transfer"); + state.events.clear(); + Require(BIO_free_nid_postfix(bio) == -7 && state.events == std::vector{1} && bio->references == 0, "free callback veto"); + bio->callback = nullptr; + Require(BIO_free_nid_postfix(bio) == 1, "free after veto"); + + BioMethod custom{0x480, "test", WriteCustom, nullptr, nullptr, nullptr, ControlCustom, CreateCustom, nullptr, nullptr}; + bio = BIO_new_nid_postfix(&custom); + Require(bio && bio->method == &custom, "title-defined method"); + Require(BIO_write_nid_postfix(bio, "abc", 3) == 2 && bio->bytesWritten == 2, "title-defined writer"); + Require(BIO_write_nid_postfix(bio, nullptr, 3) == 2 && bio->bytesWritten == 4, "custom method receives null source"); + Require(BIO_write_nid_postfix(bio, "", 0) == -1 && bio->bytesWritten == 4, "custom method receives zero length"); + RequireError(0); + Require(BIO_ctrl_nid_postfix(bio, 17, 0x1234567800000000LL, nullptr) == 0x1234567800000011LL, "64-bit method result"); + constexpr int referenceCount = 8; + std::atomic_ref(bio->references).store(referenceCount + 1); + std::array threads; + for (auto& thread : threads) thread = std::thread([bio] { Require(BIO_free_nid_postfix(bio) == 1, "release shared reference"); }); + for (auto& thread : threads) thread.join(); + Require(bio->references == 1 && BIO_free_nid_postfix(bio) == 1, "last reference release"); + custom.create = RejectCreate; + Require(BIO_new_nid_postfix(&custom) == nullptr, "custom create can reject allocation"); +} + +void TestUnsupported() { + Require(BIO_free_nid_postfix(nullptr) == 0, "free null"); + Require(BIO_ctrl_nid_postfix(nullptr, 1, 0, nullptr) == 0, "control null"); + Require(BIO_write_nid_postfix(nullptr, "a", 1) == 0, "write null BIO"); + Require(BIO_new_mem_buf_nid_postfix(nullptr, 0) == nullptr, "reject null borrowed input"); + RequireError(0x20fff073); + Require(BIO_puts_nid_postfix(nullptr, "a") == -2, "reject puts without a method"); + RequireError(0x20fff079); + Bio* bio = BIO_new_nid_postfix(BIO_s_mem_nid_postfix()); + bio->initialized = 0; + Require(BIO_write_nid_postfix(bio, "a", 1) == -2, "reject uninitialized write"); + RequireError(0x20fff078); + bio->initialized = 1; + const BioMethod* method = bio->method; + bio->method = nullptr; + Require(BIO_ctrl_nid_postfix(bio, 1, 0, nullptr) == -2, "reject control without a method"); + RequireError(0x20fff079); + Require(BIO_write_nid_postfix(bio, "a", 1) == -2, "reject write without a method"); + RequireError(0x20fff079); + bio->method = method; + auto* buffer = Buffer(bio); + buffer->length = 0x5ffffffd; + Require(BIO_write_nid_postfix(bio, "a", 1) == -1, "reject oversized growth"); + RequireError(0x07fff041); + buffer->length = 0; + Require(BIO_free_nid_postfix(bio) == 1, "free after unsupported cases"); + for (const int failAfter : {0, 1}) { + allocationsUntilFailure = failAfter; + Require(BIO_new_nid_postfix(BIO_s_mem_nid_postfix()) == nullptr, "failed BIO allocation"); + RequireError(failAfter == 0 ? 0x20fff041 : 0x07fff041); + Require(allocations == 0, "failed creation leaked guest heap storage"); + } + allocationsUntilFailure = -1; + bio = BIO_new_nid_postfix(BIO_s_mem_nid_postfix()); + Require(BIO_puts_nid_postfix(bio, "abc") == 3, "write before growth failure"); + allocationsUntilFailure = 0; + Require(BIO_puts_nid_postfix(bio, "0123456789") == -1, "failed buffer expansion"); + RequireError(0x07fff041); + RequireData(bio, "abc"); + allocationsUntilFailure = -1; + Require(BIO_free_nid_postfix(bio) == 1, "free after growth failure"); + + bio = BIO_new_nid_postfix(BIO_s_mem_nid_postfix()); + bio->initialized = 0; + Require(BIO_puts_nid_postfix(bio, "a") == -2, "reject uninitialized puts"); + RequireError(0x20fff078); + bio->initialized = 1; + Require(bio->method->write(bio, nullptr, 1) == -1, "reject null raw write source"); + RequireError(0x20fff073); + Require(BIO_free_nid_postfix(bio) == 1, "free after method errors"); +} + +} // namespace + +int main(int argc, char** argv) { +#ifdef ANYPS5_BIO_DYNAMIC + Require(argc == 2, "patched BIO test needs the libraries directory"); + InitializePatched(std::filesystem::absolute(argv[1])); +#else + static_cast(argc); + static_cast(argv); +#endif + InitializeHeap(); + ERR_clear_error_nid_postfix(); + TestWritable(); + TestReadOnlyAndLines(); + TestBufferOwnership(); + TestInterleavedTransfers(); + TestWriteBoundaries(); + TestDispatchAndReferences(); + TestUnsupported(); + Require(allocations == 0, "BIO tests leaked guest heap storage"); + Require(ERR_get_error_nid_postfix() == 0, "BIO tests left an unexpected error"); +} diff --git a/core/libs/tests/GuestLibreSslBn.cpp b/core/libs/tests/GuestLibreSslBn.cpp new file mode 100644 index 0000000000..09e6c4e641 --- /dev/null +++ b/core/libs/tests/GuestLibreSslBn.cpp @@ -0,0 +1,204 @@ +#include "prx/libSceLibreSsl/libSceLibreSsl.hpp" +#include "prx/libc/include/ApplicationHeap.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include + +using Bn = Bignum; +using BnCtx = BignumContext; + +static_assert(sizeof(Bn) == 24); +static_assert(offsetof(Bn, top) == 8); +static_assert(offsetof(Bn, neg) == 16); + +static void Require(bool value, int line = __builtin_LINE()) { + if (value) return; + std::fprintf(stderr, "check failed at line %d\n", line); + std::abort(); +} + +static Bn* FromHex(const char* text) { + Bn* value = nullptr; + Require(BN_hex2bn_nid_postfix(&value, text) > 0 && value); + return value; +} + +static std::string Hex(const Bn* value) { + unsigned char bytes[64]; + const int length = BN_bn2bin_nid_postfix(value, bytes); + Require(length >= 0 && length <= 64); + static constexpr char digits[] = "0123456789ABCDEF"; + std::string text; + for (int i = 0; i < length; i++) { + text += digits[bytes[i] >> 4]; + text += digits[bytes[i] & 15]; + } + while (text.size() > 1 && text[0] == '0') text.erase(0, 1); + if (text.empty()) text = "0"; + return value->neg ? "-" + text : text; +} + +static void InitializeHeap() { + void* const api[10] {}; + ApplicationHeapRegister_nid_no_patch(api); +} + +int main() { + InitializeHeap(); + Bn* fresh = BN_new_nid_postfix(); + Require(fresh && fresh->top == 0 && fresh->neg == 0 && BN_num_bits_nid_postfix(fresh) == 0); + BN_free_nid_postfix(fresh); + BN_free_nid_postfix(nullptr); + + BnCtx* ctx = BN_CTX_new_nid_postfix(); + Require(ctx); + BN_CTX_start_nid_postfix(ctx); + BN_CTX_end_nid_postfix(ctx); + BN_CTX_free_nid_postfix(ctx); + BN_CTX_free_nid_postfix(nullptr); + ctx = BN_CTX_new_nid_postfix(); + Require(ctx); + + { + Bn* a = FromHex("FFFFFFFFFFFFFFFF"); + Bn* b = FromHex("FFFFFFFFFFFFFFFF"); + Bn* r = BN_new_nid_postfix(); + Require(BN_mul_nid_postfix(r, a, b, ctx) == 1); + Require(Hex(r) == "FFFFFFFFFFFFFFFE0000000000000001"); + Require(r->top == 2 && r->neg == 0); + BN_free_nid_postfix(a); + BN_free_nid_postfix(b); + BN_free_nid_postfix(r); + } + { + Bn* a = FromHex("0123456789ABCDEFFEDCBA98765432100F1E2D3C4B5A6978"); + Bn* b = FromHex("00FF00FF00FF00FF00FF00FF00FF00FF00FF"); + Bn* r = BN_new_nid_postfix(); + Require(BN_mul_nid_postfix(r, a, b, ctx) == 1); + Require(Hex(r) == "12223444566678876665444322210000F0F1CFC09E8F6D55CB34DD56FF79219B43BB52CA61D970E88"); + Require(BN_mul_nid_postfix(a, a, a, ctx) == 1); + Require(Hex(a) == "14B66DC33F6ACDEE9D609EBE253A6424AD884E9A04269A9706601BB096443363E2CE44634913DF9C3B9FAA3D3A840"); + Bn* c = FromHex("-3"); + Bn* d = FromHex("5"); + Require(BN_mul_nid_postfix(d, c, d, ctx) == 1 && Hex(d) == "-F" && d->neg == 1); + Require(BN_mul_nid_postfix(c, c, c, ctx) == 1 && Hex(c) == "9" && c->neg == 0); + Bn* zero = FromHex("0"); + Require(BN_mul_nid_postfix(d, d, zero, ctx) == 1 && d->top == 0 && d->neg == 0); + BN_free_nid_postfix(a); + BN_free_nid_postfix(b); + BN_free_nid_postfix(c); + BN_free_nid_postfix(d); + BN_free_nid_postfix(r); + BN_free_nid_postfix(zero); + } + { + const unsigned char bytes[] = {0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09}; + Bn* value = BN_bin2bn_nid_postfix(bytes, sizeof(bytes), nullptr); + Require(value && value->neg == 0 && value->top == 2); + Require(Hex(value) == "10203040506070809"); + unsigned char out[16]; + std::memset(out, 0xEE, sizeof(out)); + Require(BN_bn2bin_nid_postfix(value, out) == 9); + Require(std::memcmp(out, bytes + 2, 9) == 0 && out[9] == 0xEE); + Require(BN_bin2bn_nid_postfix(bytes, 4, value) == value && Hex(value) == "102" && value->top == 1); + const unsigned char zeros[] = {0, 0, 0}; + Require(BN_bin2bn_nid_postfix(zeros, 3, value) == value && value->top == 0); + std::memset(out, 0xEE, sizeof(out)); + Require(BN_bn2bin_nid_postfix(value, out) == 0 && out[0] == 0xEE); + Require(BN_bin2bn_nid_postfix(bytes, 0, value) == value && value->top == 0); + Require(BN_bin2bn_nid_postfix(bytes, -1, value) == nullptr); + BN_free_nid_postfix(value); + } + { + Bn* value = BN_new_nid_postfix(); + Require(BN_num_bits_nid_postfix(value) == 0); + Require(BN_set_word_nid_postfix(value, 1) == 1 && BN_num_bits_nid_postfix(value) == 1); + Require(BN_set_word_nid_postfix(value, 0x100) == 1 && BN_num_bits_nid_postfix(value) == 9); + Bn* big = FromHex("10000000000000000"); + Require(BN_num_bits_nid_postfix(big) == 65 && big->top == 2); + Require(BN_set_word_nid_postfix(big, 0) == 1 && BN_num_bits_nid_postfix(big) == 0 && big->top == 0 && big->neg == 0); + Require(BN_set_word_nid_postfix(value, 0xFFFFFFFFFFFFFFFFull) == 1 && BN_num_bits_nid_postfix(value) == 64); + BN_free_nid_postfix(value); + BN_free_nid_postfix(big); + } + { + Bn* value = nullptr; + Require(BN_hex2bn_nid_postfix(&value, "-1F") == 3 && value); + Require(value->neg == 1 && value->top == 1 && value->d[0] == 0x1F); + Bn* same = value; + Require(BN_hex2bn_nid_postfix(&value, "ABC") == 3 && value == same); + Require(value->neg == 0 && value->d[0] == 0xABC); + Require(BN_hex2bn_nid_postfix(&value, "FFzz") == 2 && value->d[0] == 0xFF); + Require(BN_hex2bn_nid_postfix(&value, "") == 0 && value->d[0] == 0xFF); + Require(BN_hex2bn_nid_postfix(&value, "zz") == 0 && BN_hex2bn_nid_postfix(&value, "-") == 0); + Require(BN_hex2bn_nid_postfix(&value, "-0") == 2 && value->top == 0 && value->neg == 0); + Require(BN_hex2bn_nid_postfix(&value, "deadBEEF0123456789abcdef") == 24); + Require(Hex(value) == "DEADBEEF0123456789ABCDEF" && value->top == 2); + Require(BN_hex2bn_nid_postfix(nullptr, "12G") == 2); + BN_free_nid_postfix(value); + } + { + Bn* value = FromHex("FFFFFFFFFFFFFFFF"); + Require(BN_add_word_nid_postfix(value, 1) == 1); + Require(value->top == 2 && value->d[0] == 0 && value->d[1] == 1 && Hex(value) == "10000000000000000"); + Require(BN_sub_word_nid_postfix(value, 1) == 1); + Require(value->top == 1 && value->d[0] == 0xFFFFFFFFFFFFFFFFull && value->neg == 0); + Require(BN_set_word_nid_postfix(value, 3) == 1); + Require(BN_sub_word_nid_postfix(value, 5) == 1 && value->neg == 1 && value->top == 1 && value->d[0] == 2); + Require(BN_add_word_nid_postfix(value, 2) == 1 && value->top == 0 && value->neg == 0); + Require(BN_sub_word_nid_postfix(value, 7) == 1 && value->neg == 1 && Hex(value) == "-7"); + Require(BN_add_word_nid_postfix(value, 10) == 1 && value->neg == 0 && Hex(value) == "3"); + Require(BN_sub_word_nid_postfix(value, 0) == 1 && Hex(value) == "3"); + Bn* wide = FromHex("-10000000000000000"); + Require(BN_sub_word_nid_postfix(wide, 1) == 1 && wide->neg == 1 && Hex(wide) == "-10000000000000001"); + Require(BN_add_word_nid_postfix(wide, 2) == 1 && wide->neg == 1 && Hex(wide) == "-FFFFFFFFFFFFFFFF" && wide->top == 1); + Bn* chain = FromHex("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"); + Require(BN_add_word_nid_postfix(chain, 1) == 1 && chain->top == 3 && Hex(chain) == "100000000000000000000000000000000"); + Require(BN_sub_word_nid_postfix(chain, 1) == 1 && chain->top == 2 && Hex(chain) == "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"); + BN_free_nid_postfix(value); + BN_free_nid_postfix(wide); + BN_free_nid_postfix(chain); + } + { + Bn* r = BN_new_nid_postfix(); + Bn* three = FromHex("3"); + Bn* eleven = FromHex("B"); + Require(BN_mod_inverse_nid_postfix(r, three, eleven, ctx) == r && Hex(r) == "4"); + Bn* seven = FromHex("7"); + Bn* forty = FromHex("28"); + Require(BN_mod_inverse_nid_postfix(r, seven, forty, ctx) == r && Hex(r) == "17"); + Bn* four = FromHex("4"); + Bn* eight = FromHex("8"); + Require(BN_mod_inverse_nid_postfix(r, four, eight, ctx) == nullptr); + Bn* allocated = BN_mod_inverse_nid_postfix(nullptr, three, eleven, ctx); + Require(allocated && Hex(allocated) == "4"); + Bn* minusThree = FromHex("-3"); + Require(BN_mod_inverse_nid_postfix(r, minusThree, eleven, ctx) == r && Hex(r) == "7"); + Bn* fifteen = FromHex("F"); + Require(BN_mod_inverse_nid_postfix(r, fifteen, eleven, ctx) == r && Hex(r) == "3"); + Bn* inPlace = FromHex("3"); + Require(BN_mod_inverse_nid_postfix(inPlace, inPlace, eleven, ctx) == inPlace && Hex(inPlace) == "4"); + + Bn* prime = FromHex("7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"); + Bn* a = FromHex("123456789ABCDEF0FEDCBA9876543210"); + Bn* inverse = BN_mod_inverse_nid_postfix(nullptr, a, prime, ctx); + Require(inverse && Hex(inverse) == "62CFEFE6EB41053969967BD711C5169F"); + Bn* product = BN_new_nid_postfix(); + Require(BN_mul_nid_postfix(product, a, inverse, ctx) == 1); + Bn* quotient = FromHex("E0DA4F6BE78BFD74B8C9F95AF178811"); + Bn* check = BN_new_nid_postfix(); + Require(BN_mul_nid_postfix(check, quotient, prime, ctx) == 1); + Require(BN_add_word_nid_postfix(check, 1) == 1); + Require(Hex(check) == Hex(product)); + + Bn* all[] = {r, three, eleven, seven, forty, four, eight, allocated, minusThree, fifteen, inPlace, prime, a, inverse, product, quotient, check}; + for (Bn* value : all) BN_free_nid_postfix(value); + } + BN_CTX_free_nid_postfix(ctx); + return 0; +} diff --git a/core/libs/tests/GuestLibreSslCmac.cpp b/core/libs/tests/GuestLibreSslCmac.cpp new file mode 100644 index 0000000000..aa4890d4bf --- /dev/null +++ b/core/libs/tests/GuestLibreSslCmac.cpp @@ -0,0 +1,444 @@ +#include "prx/libSceLibreSsl/libSceLibreSsl.hpp" +#include "prx/libSceLibreSsl/include/Cmac.hpp" +#include "prx/libSceLibreSsl/ErrorQueue.hpp" +#include "prx/libc/include/ApplicationHeap.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#else +#include +#endif + +#ifdef ANYPS5_CMAC_DYNAMIC +namespace { +decltype(&CMAC_CTX_new_nid_postfix) cmacNew; +decltype(&CMAC_CTX_free_nid_postfix) cmacFree; +decltype(&CMAC_Init_nid_postfix) cmacInit; +decltype(&CMAC_Update_nid_postfix) cmacUpdate; +decltype(&CMAC_Final_nid_postfix) cmacFinal; +decltype(&EVP_aes_128_cbc_nid_postfix) aes128Method; +decltype(&EVP_aes_192_cbc_nid_postfix) aes192Method; +decltype(&EVP_aes_256_cbc_nid_postfix) aes256Method; +decltype(&ERR_clear_error_nid_postfix) clearError; +decltype(&ERR_get_error_nid_postfix) getError; +decltype(&ApplicationHeapRegister_nid_no_patch) registerHeap; + +void* Load(const std::string& path) { +#ifdef _WIN32 + auto* library = LoadLibraryExA(path.c_str(), nullptr, LOAD_WITH_ALTERED_SEARCH_PATH); +#else + auto* library = dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL); +#endif + if (library == nullptr) throw std::runtime_error("failed to load patched CMAC dependency: " + path); + return library; +} + +template void Resolve(TFunction& destination, void* library, const char* name) { +#ifdef _WIN32 + destination = reinterpret_cast(GetProcAddress(static_cast(library), name)); +#else + destination = reinterpret_cast(dlsym(library, name)); +#endif + if (destination == nullptr) throw std::runtime_error(std::string("missing patched CMAC dependency: ") + name); +} + +void InitializePatched(const char* directory) { + const auto root = std::string(directory) + "/"; + auto* libc = Load(root + "libc.prx"); + auto* library = Load(root + "libSceLibreSsl.prx"); + Resolve(cmacNew, library, "mwJYST4Bb3U"); + Resolve(cmacFree, library, "cBNGAG1DkXc"); + Resolve(cmacInit, library, "LATrF+dIZYI"); + Resolve(cmacUpdate, library, "XpAJ1Ckg-q4"); + Resolve(cmacFinal, library, "6MM7ng9P+18"); + Resolve(aes128Method, library, "rV07jlO-+6Q"); + Resolve(aes192Method, library, "0X5DnA3olsk"); + Resolve(aes256Method, library, "J44dsZUcK4I"); + Resolve(clearError, library, "sieQ--PINWg"); + Resolve(getError, library, "DJg+qWX1rwY"); + Resolve(registerHeap, libc, "ApplicationHeapRegister_nid_no_patch"); +} +} + +#define CMAC_CTX_new_nid_postfix cmacNew +#define CMAC_CTX_free_nid_postfix cmacFree +#define CMAC_Init_nid_postfix cmacInit +#define CMAC_Update_nid_postfix cmacUpdate +#define CMAC_Final_nid_postfix cmacFinal +#define EVP_aes_128_cbc_nid_postfix aes128Method +#define EVP_aes_192_cbc_nid_postfix aes192Method +#define EVP_aes_256_cbc_nid_postfix aes256Method +#define ERR_clear_error_nid_postfix clearError +#define ERR_get_error_nid_postfix getError +#define ApplicationHeapRegister_nid_no_patch registerHeap +#endif + +namespace { + +void Require(bool condition, const char* message) { + if (condition) return; + std::fprintf(stderr, "failed: %s\n", message); + std::abort(); +} + +int allocationsUntilFailure = -1; +std::vector> allocations; + +void* APS5_VABI Allocate(std::size_t size) { + if (allocationsUntilFailure == 0) return nullptr; + if (allocationsUntilFailure > 0) --allocationsUntilFailure; + auto* pointer = std::malloc(size); + if (pointer != nullptr) allocations.emplace_back(pointer, size); + return pointer; +} +void* APS5_VABI Reallocate(void*, std::size_t) { throw std::logic_error("unexpected CMAC reallocation"); } +void* APS5_VABI Align(std::size_t, std::size_t) { throw std::logic_error("unexpected CMAC aligned allocation"); } +void* APS5_VABI Realign(void*, std::size_t, std::size_t) { throw std::logic_error("unexpected CMAC realignment"); } +int APS5_VABI PosixAlign(void**, std::size_t, std::size_t) { throw std::logic_error("unexpected CMAC aligned allocation"); } + +void* APS5_VABI Calloc(std::size_t count, std::size_t size) { + if (allocationsUntilFailure == 0) return nullptr; + if (allocationsUntilFailure > 0) --allocationsUntilFailure; + auto* pointer = std::calloc(count, size); + if (pointer != nullptr) allocations.emplace_back(pointer, count * size); + return pointer; +} + +void APS5_VABI Free(void* pointer) { + if (pointer == nullptr) return; + const auto found = std::find_if(allocations.begin(), allocations.end(), [&](const auto& entry) { return entry.first == pointer; }); + Require(found != allocations.end(), "free a guest-owned allocation"); + const auto* bytes = static_cast(pointer); + Require(std::all_of(bytes, bytes + found->second, [](auto value) { return value == 0; }), "wipe contexts and key storage before freeing"); + allocations.erase(found); + std::free(pointer); +} + +void InitializeHeap() { + const std::array api{ + reinterpret_cast(Allocate), reinterpret_cast(Free), reinterpret_cast(Calloc), + reinterpret_cast(Reallocate), reinterpret_cast(Align), reinterpret_cast(Realign), + reinterpret_cast(PosixAlign)}; + ApplicationHeapRegister_nid_no_patch(api.data()); +} + +template void RequireThrows(TAction action) { + try { action(); } catch (const std::exception&) { return; } + Require(false, "unsupported input must throw"); +} + +std::vector Hex(std::string_view text) { + Require(text.size() % 2 == 0, "hex fixture has complete bytes"); + std::vector bytes; + for (std::size_t index = 0; index < text.size(); index += 2) { + const auto nibble = [](char value) { return value <= '9' ? value - '0' : value - 'a' + 10; }; + bytes.push_back(static_cast((nibble(text[index]) << 4) | nibble(text[index + 1]))); + } + return bytes; +} + +using CmacFree = void (APS5_VABI*)(LibreSslCmac*); +using Context = std::unique_ptr; + +Context NewContext() { + Context context(CMAC_CTX_new_nid_postfix(), CMAC_CTX_free_nid_postfix); + Require(context != nullptr, "allocate CMAC context"); + return context; +} + +std::array Final(LibreSslCmac* context) { + std::array output; + output.fill(0xa5); + std::size_t length = 99; + Require(CMAC_Final_nid_postfix(context, output.data() + 1, &length) == 1, "finalize CMAC"); + Require(length == 16 && output.front() == 0xa5 && output.back() == 0xa5, "tag size and output bounds"); + std::array result; + std::copy_n(output.data() + 1, 16, result.data()); + return result; +} + +const LibreSslCipher* Methods[3]; + +void RfcVectors() { + const auto key = Hex("2b7e151628aed2a6abf7158809cf4f3c"); + const auto message = Hex("6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e5130c81c46a35ce411e5fbc1191a0a52eff69f2445df4f9b17ad2b417be66c3710"); + const std::size_t lengths[] = {0, 16, 40, 64}; + const char* tags[] = {"bb1d6929e95937287fa37d129b756746", "070a16b46b4d4144f79bdd9dd04a287c", "dfa66747de9ae63030ca32611497c827", "51f0bebf7e3b9d92fc49741779363cfe"}; + auto context = NewContext(); + for (std::size_t vector = 0; vector < 4; ++vector) { + const auto expected = Hex(tags[vector]); + for (std::size_t split = 0; split <= lengths[vector]; ++split) { + Require(CMAC_Init_nid_postfix(context.get(), key.data(), key.size(), Methods[0], nullptr) == 1, "RFC key initialization"); + Require(CMAC_Update_nid_postfix(context.get(), message.data(), split) == 1, "RFC first update"); + Require(CMAC_Update_nid_postfix(context.get(), nullptr, 0) == 1, "empty update"); + Require(CMAC_Update_nid_postfix(context.get(), message.data() + split, lengths[vector] - split) == 1, "RFC second update"); + std::size_t length = 99; + Require(CMAC_Final_nid_postfix(context.get(), nullptr, &length) == 1 && length == 16, "length-only final"); + Require(CMAC_Final_nid_postfix(context.get(), nullptr, &length) == 1 && length == 16, "repeat length query"); + const auto actual = Final(context.get()); + if (!std::equal(actual.begin(), actual.end(), expected.begin())) { + std::fprintf(stderr, "RFC vector %zu split %zu: ", vector, split); + for (auto byte : actual) std::fprintf(stderr, "%02x", byte); + std::fputc('\n', stderr); + } + Require(std::equal(actual.begin(), actual.end(), expected.begin()), "RFC 4493 tag"); + } + } +} + +void Differential() { + const mbedtls_cipher_type_t types[] = {MBEDTLS_CIPHER_AES_128_ECB, MBEDTLS_CIPHER_AES_192_ECB, MBEDTLS_CIPHER_AES_256_ECB}; + std::array key; + std::array message; + for (std::size_t index = 0; index < key.size(); ++index) key[index] = static_cast(index * 73 + 41); + for (std::size_t index = 0; index < message.size(); ++index) message[index] = static_cast(index * 197 + 17); + auto context = NewContext(); + for (std::size_t method = 0; method < 3; ++method) { + const auto* cipher = Methods[method]; + Require(cipher->nid == 419 + static_cast(method) * 4 && cipher->blockSize == 16 && cipher->keyLength == 16 + static_cast(method) * 8 && cipher->ivLength == 16 && cipher->flags == 0x5002, "AES descriptor metadata"); + const auto* reference = mbedtls_cipher_info_from_type(types[method]); + Require(reference != nullptr, "reference cipher available"); + for (std::size_t length = 0; length <= message.size(); ++length) { + std::array expected; + Require(mbedtls_cipher_cmac(reference, key.data(), cipher->keyLength * 8, message.data(), length, expected.data()) == 0, "reference CMAC"); + for (const auto chunk : {1u, 7u, 15u, 16u, 17u, 64u, 257u}) { + Require(CMAC_Init_nid_postfix(context.get(), key.data(), cipher->keyLength, cipher, nullptr) == 1, "differential initialization"); + for (std::size_t offset = 0; offset < length;) { + const auto take = std::min(static_cast(chunk), length - offset); + Require(CMAC_Update_nid_postfix(context.get(), message.data() + offset, take) == 1, "differential update"); + offset += take; + } + Require(Final(context.get()) == expected, "CMAC agrees with independent backend"); + } + } + } +} + +void Lifecycle() { + auto context = NewContext(); + const std::array key{}; + const std::array message{}; + std::array output; + output.fill(0xa5); + std::size_t length = 99; + Require(CMAC_Update_nid_postfix(context.get(), nullptr, 1) == 0, "uninitialized update"); + Require(CMAC_Final_nid_postfix(context.get(), output.data(), &length) == 0 && length == 99 && output[0] == 0xa5, "uninitialized final preserves outputs"); + Require(CMAC_Final_nid_postfix(context.get(), nullptr, nullptr) == 0, "uninitialized final precedes null output length"); + Require(CMAC_Init_nid_postfix(context.get(), nullptr, 0, nullptr, nullptr) == 0, "uninitialized restart"); + Require(CMAC_Init_nid_postfix(context.get(), key.data(), 16, nullptr, nullptr) == 0, "key without cipher"); + Require(CMAC_Init_nid_postfix(context.get(), nullptr, 1, nullptr, nullptr) == 1, "nonzero key length without a key is not restart"); + Require(CMAC_Init_nid_postfix(context.get(), nullptr, 0, Methods[0], nullptr) == 1, "stage cipher selection"); + Require(CMAC_Update_nid_postfix(context.get(), message.data(), 1) == 0, "cipher-only context has no key"); + ERR_clear_error_nid_postfix(); + Require(CMAC_Init_nid_postfix(context.get(), key.data(), 15, nullptr, nullptr) == 0, "reject invalid key length"); + Require(ERR_get_error_nid_postfix() == 0x06fff082, "invalid key length publishes EVP error"); + Require(CMAC_Init_nid_postfix(context.get(), key.data(), 16, nullptr, nullptr) == 1, "finish staged initialization"); + Require(CMAC_Update_nid_postfix(context.get(), message.data(), message.size()) == 1, "first message"); + const auto expected = Final(context.get()); + const auto repeated = Final(context.get()); + Require(repeated != expected, "final advances the CBC state"); + Require(CMAC_Init_nid_postfix(context.get(), nullptr, 0, nullptr, nullptr) == 1, "restart reuses the key"); + Require(CMAC_Update_nid_postfix(context.get(), message.data(), message.size()) == 1 && Final(context.get()) == expected, "restart resets state"); + Require(CMAC_Init_nid_postfix(context.get(), key.data(), (std::size_t{1} << 32) + 16, nullptr, nullptr) == 1, "upstream key length narrows to int"); + Require(CMAC_Update_nid_postfix(context.get(), message.data(), message.size()) == 1 && Final(context.get()) == expected, "narrowed key length tag"); + Require(CMAC_Init_nid_postfix(context.get(), key.data(), 24, Methods[1], nullptr) == 1, "switch AES key size"); + Require(CMAC_Init_nid_postfix(context.get(), key.data(), 32, Methods[2], nullptr) == 1, "switch AES key size again"); + RequireThrows([&] { CMAC_Init_nid_postfix(nullptr, key.data(), 16, Methods[0], nullptr); }); + RequireThrows([&] { CMAC_Update_nid_postfix(nullptr, nullptr, 0); }); + RequireThrows([&] { CMAC_Final_nid_postfix(nullptr, output.data(), &length); }); + RequireThrows([&] { CMAC_Update_nid_postfix(context.get(), nullptr, 1); }); + RequireThrows([&] { CMAC_Final_nid_postfix(context.get(), output.data(), nullptr); }); + CMAC_CTX_free_nid_postfix(nullptr); +} + +void AllocationFailures() { + allocationsUntilFailure = 0; + Require(CMAC_CTX_new_nid_postfix() == nullptr, "context allocation failure"); + allocationsUntilFailure = 1; + auto context = NewContext(); + const std::array key{}; + ERR_clear_error_nid_postfix(); + Require(CMAC_Init_nid_postfix(context.get(), key.data(), key.size(), Methods[0], nullptr) == 0, "cipher allocation failure"); + Require(ERR_get_error_nid_postfix() == 0x06fff041, "cipher allocation failure publishes EVP error"); + allocationsUntilFailure = -1; + Require(CMAC_Init_nid_postfix(context.get(), key.data(), key.size(), Methods[0], nullptr) == 1, "recover from failed allocation"); +} + +void CipherCallbacks() { + std::array key{}; + std::array iv; + for (std::size_t index = 0; index < iv.size(); ++index) iv[index] = static_cast(index + 1); + for (const auto* method : Methods) { + for (std::size_t length = 0; length <= 48; ++length) { + const auto padded = (length + 15) / 16 * 16; + std::vector input(padded); + for (std::size_t index = 0; index < length; ++index) input[index] = static_cast(index * 31 + 17); + std::vector expected(padded); + auto referenceIv = iv; + mbedtls_aes_context reference; + mbedtls_aes_init(&reference); + Require(mbedtls_aes_setkey_enc(&reference, key.data(), method->keyLength * 8) == 0, "reference AES key"); + Require(mbedtls_aes_crypt_cbc(&reference, MBEDTLS_AES_ENCRYPT, padded, referenceIv.data(), input.data(), expected.data()) == 0, "reference CBC"); + mbedtls_aes_free(&reference); + std::vector storage(method->contextSize); + LibreSslCipherContext context{}; + context.cipher = method; + context.cipherData = storage.data(); + context.keyLength = method->keyLength; + for (const bool inPlace : {false, true}) { + context.encrypt = 1; + std::copy(iv.begin(), iv.end(), context.iv); + Require(method->init(&context, key.data(), nullptr, 1) == 1, "AES descriptor encryption setup"); + auto output = input; + output.push_back(0xa5); + Require(method->cipher(&context, output.data(), inPlace ? output.data() : input.data(), length) == 1, "AES descriptor partial encryption"); + Require(output.back() == 0xa5 && std::equal(expected.begin(), expected.end(), output.begin()), "CBC partial encryption and bounds"); + Require(std::equal(referenceIv.begin(), referenceIv.end(), context.iv), "CBC encryption IV"); + context.encrypt = 0; + std::copy(iv.begin(), iv.end(), context.iv); + Require(method->init(&context, key.data(), nullptr, 0) == 1, "AES descriptor decryption setup"); + if (!inPlace) output.assign(padded + 1, 0xa5); + const auto before = output; + Require(method->cipher(&context, output.data(), inPlace ? output.data() : expected.data(), length) == 1, "AES descriptor partial decryption"); + Require(std::equal(input.begin(), input.begin() + length, output.begin()), "CBC decrypted prefix"); + Require(std::equal(before.begin() + length, before.end(), output.begin() + length), "partial decryption preserves trailing bytes"); + Require(std::equal(referenceIv.begin(), referenceIv.end(), context.iv), "CBC decryption IV"); + } + } + } +} + +int APS5_VABI DesInit(LibreSslCipherContext* context, const unsigned char* key, const unsigned char*, int) { + auto* des = static_cast(context->cipherData); + mbedtls_des3_init(des); + return mbedtls_des3_set3key_enc(des, key) == 0; +} + +int APS5_VABI DesCipher(LibreSslCipherContext* context, unsigned char* output, const unsigned char* input, std::size_t length) { + return mbedtls_des3_crypt_cbc(static_cast(context->cipherData), MBEDTLS_DES_ENCRYPT, length, context->iv, input, output) == 0; +} + +void GuestDes() { + const LibreSslCipher method{44, 8, 24, 8, 2, DesInit, DesCipher, nullptr, sizeof(mbedtls_des3_context), nullptr, nullptr, nullptr, nullptr}; + std::array key; + std::array message; + for (std::size_t index = 0; index < key.size(); ++index) key[index] = static_cast(index * 31 + 7); + for (std::size_t index = 0; index < message.size(); ++index) message[index] = static_cast(index * 71 + 3); + const auto* reference = mbedtls_cipher_info_from_type(MBEDTLS_CIPHER_DES_EDE3_ECB); + Require(reference != nullptr, "reference 64-bit cipher"); + auto context = NewContext(); + for (std::size_t length = 0; length <= message.size(); ++length) { + std::array expected; + Require(mbedtls_cipher_cmac(reference, key.data(), 192, message.data(), length, expected.data()) == 0, "reference 64-bit CMAC"); + Require(CMAC_Init_nid_postfix(context.get(), key.data(), key.size(), &method, nullptr) == 1, "guest 64-bit cipher initialization"); + for (std::size_t index = 0; index < length; ++index) Require(CMAC_Update_nid_postfix(context.get(), message.data() + index, 1) == 1, "guest 64-bit cipher update"); + std::array actual; + actual.fill(0xa5); + std::size_t outputLength = 99; + Require(CMAC_Final_nid_postfix(context.get(), actual.data() + 1, &outputLength) == 1, "guest 64-bit cipher final"); + Require(outputLength == 8 && actual.front() == 0xa5 && actual.back() == 0xa5 && std::equal(expected.begin(), expected.end(), actual.begin() + 1), "64-bit CMAC tag and output bounds"); + } +} + +const LibreSslCipher* delegated; +int initCalls; +int cipherCalls; +int cleanupCalls; +int failAt; + +int APS5_VABI GuestInit(LibreSslCipherContext* context, const unsigned char* key, const unsigned char* iv, int encrypt) { + ++initCalls; + Require(context->encrypt == 1 && context->keyLength == 16 && context->cipherData != nullptr, "guest callback context"); + return delegated->init(context, key, iv, encrypt); +} + +int APS5_VABI GuestCipher(LibreSslCipherContext* context, unsigned char* output, const unsigned char* input, std::size_t length) { + ++cipherCalls; + if (failAt == cipherCalls) { + std::memset(output, 0x5a, length); + return 0; + } + return delegated->cipher(context, output, input, length); +} + +int APS5_VABI GuestCleanup(LibreSslCipherContext*) { + ++cleanupCalls; + return 1; +} + +int APS5_VABI GuestControl(LibreSslCipherContext* context, int command, int argument, void*) { + if (command == 0) return 1; + if (command == 1 && argument == 16) { + context->keyLength = argument; + return 1; + } + return -1; +} + +void Callbacks() { + delegated = Methods[0]; + auto method = *delegated; + method.init = GuestInit; + method.cipher = GuestCipher; + method.cleanup = GuestCleanup; + method.control = GuestControl; + method.flags |= 0xc0; + std::array key{}; + std::array message{}; + { + auto context = NewContext(); + Require(CMAC_Init_nid_postfix(context.get(), key.data(), key.size(), &method, nullptr) == 1 && initCalls == 1, "guest initialization callback"); + Require(CMAC_Update_nid_postfix(context.get(), message.data(), message.size()) == 1 && cipherCalls == 3, "guest cipher callbacks"); + const auto tag = Final(context.get()); + Require(CMAC_Init_nid_postfix(context.get(), nullptr, 0, nullptr, nullptr) == 1, "guest callback restart"); + Require(CMAC_Update_nid_postfix(context.get(), message.data(), message.size()) == 1 && Final(context.get()) == tag, "guest-defined cipher matches builtin"); + failAt = cipherCalls + 1; + std::array output; + output.fill(0xa5); + std::size_t length = 99; + Require(CMAC_Final_nid_postfix(context.get(), output.data(), &length) == 0 && length == 16, "cipher failure propagates"); + Require(std::all_of(output.begin(), output.end(), [](auto value) { return value == 0; }), "failed final clears output"); + failAt = 0; + } + Require(cleanupCalls == 1, "guest cleanup callback"); + auto context = NewContext(); + method.blockSize = 32; + ERR_clear_error_nid_postfix(); + Require(CMAC_Init_nid_postfix(context.get(), key.data(), key.size(), &method, nullptr) == 0, "reject unsupported block size"); + Require(ERR_get_error_nid_postfix() == 0x06fff088, "bad block length publishes EVP error"); +} +} + +int main(int argc, char** argv) { +#ifdef ANYPS5_CMAC_DYNAMIC + Require(argc == 2, "patched test needs the library directory"); + InitializePatched(argv[1]); +#endif + InitializeHeap(); + Methods[0] = EVP_aes_128_cbc_nid_postfix(); + Methods[1] = EVP_aes_192_cbc_nid_postfix(); + Methods[2] = EVP_aes_256_cbc_nid_postfix(); + RfcVectors(); + Differential(); + Lifecycle(); + Callbacks(); + AllocationFailures(); + CipherCallbacks(); + GuestDes(); + Require(allocations.empty(), "all guest allocations released"); + std::puts("CMAC vectors, streaming, lifecycle and callback checks passed"); +} diff --git a/core/libs/tests/GuestLibreSslEvp.cpp b/core/libs/tests/GuestLibreSslEvp.cpp new file mode 100644 index 0000000000..c96b5e0002 --- /dev/null +++ b/core/libs/tests/GuestLibreSslEvp.cpp @@ -0,0 +1,410 @@ +#include "prx/libSceLibreSsl/include/Api.hpp" +#include "prx/libc/include/ApplicationHeap.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct Algorithm { + const char* name; + const EvpMd* md; + int type; + int pkeyType; + int size; + int block; + int contextSize; + std::uint64_t flags; + const char* digests[3]; + const char* macs[6]; +}; + +static void Require(bool condition, std::source_location location = std::source_location::current()) { + if (condition) return; + std::fprintf(stderr, "LibreSSL EVP 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 int allocationsBeforeFailure = -1; +static int liveBlocks = 0; + +static bool FailAllocation() { + if (allocationsBeforeFailure < 0) return false; + return allocationsBeforeFailure-- == 0; +} + +static void* APS5_VABI Allocate(std::size_t bytes) { + if (FailAllocation()) return nullptr; + void* const block = std::malloc(bytes ? bytes : 1); + if (block) ++liveBlocks; + return block; +} + +static void APS5_VABI Release(void* block) { + if (block) --liveBlocks; + std::free(block); +} + +static void* APS5_VABI AllocateZeroed(std::size_t count, std::size_t bytes) { + if (FailAllocation()) return nullptr; + void* const block = std::calloc(count ? count : 1, bytes ? bytes : 1); + if (block) ++liveBlocks; + return block; +} + +static void* APS5_VABI Reallocate(void*, std::size_t) { return nullptr; } +static void* APS5_VABI Align(std::size_t, std::size_t) { return nullptr; } +static void* APS5_VABI Realign(void*, std::size_t, std::size_t) { return nullptr; } +static int APS5_VABI PosixAlign(void**, std::size_t, std::size_t) { return 12; } + +static void InitializeHeap() { + void* const api[10] { + reinterpret_cast(&Allocate), reinterpret_cast(&Release), reinterpret_cast(&AllocateZeroed), + reinterpret_cast(&Reallocate), reinterpret_cast(&Align), reinterpret_cast(&Realign), + reinterpret_cast(&PosixAlign), + }; + ApplicationHeapRegister_nid_no_patch(api); +} + +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::vector Bytes(const char* text) { + return std::vector(text, text + std::strlen(text)); +} + +static bool AllZero(const void* data, std::size_t size) { + const auto* bytes = static_cast(data); + return std::all_of(bytes, bytes + size, [](unsigned char value) { return value == 0; }); +} + +template static T Field(const void* base, std::size_t offset) { + T value; + std::memcpy(&value, static_cast(base) + offset, sizeof(value)); + return value; +} + +static std::string DigestInChunks(EvpMdContext& context, const EvpMd* md, const std::vector& message, std::size_t chunk) { + unsigned char digest[64] {}; + unsigned int size = 0; + Require(EVP_DigestInit_ex_nid_postfix(&context, md, nullptr) == 1); + for (std::size_t offset = 0; offset < message.size(); offset += chunk) + Require(EVP_DigestUpdate_nid_postfix(&context, message.data() + offset, std::min(chunk, message.size() - offset)) == 1); + Require(EVP_DigestFinal_ex_nid_postfix(&context, digest, &size) == 1); + return Hex(digest, size); +} + +static std::string MacInChunks(HmacContext* context, const std::vector& key, const EvpMd* md, const std::vector& message, std::size_t chunk) { + unsigned char mac[64] {}; + unsigned int size = 0; + Require(HMAC_Init_ex_nid_postfix(context, key.data(), static_cast(key.size()), md, nullptr) == 1); + for (std::size_t offset = 0; offset < message.size(); offset += chunk) + Require(HMAC_Update_nid_postfix(context, message.data() + offset, std::min(chunk, message.size() - offset)) == 1); + Require(HMAC_Final_nid_postfix(context, mac, &size) == 1); + return Hex(mac, size); +} + +static int customInits = 0; +static int customCleanups = 0; + +static int APS5_VABI CustomInit(EvpMdContext* context) { + ++customInits; + std::memset(context->mdData, 0, 4); + return 1; +} + +static int APS5_VABI CustomUpdate(EvpMdContext* context, const void* data, std::size_t length) { + auto* sum = static_cast(context->mdData); + for (std::size_t i = 0; i < length; ++i) sum[i % 4] = static_cast(sum[i % 4] + static_cast(data)[i]); + return 1; +} + +static int APS5_VABI CustomFinal(EvpMdContext* context, unsigned char* digest) { + std::memcpy(digest, context->mdData, 4); + return 1; +} + +static int APS5_VABI CustomCleanup(EvpMdContext*) { + ++customCleanups; + return 1; +} + +int main() { + InitializeHeap(); + const std::vector empty; + const auto abc = Bytes("abc"); + std::vector longMessage(1000); + for (std::size_t i = 0; i < longMessage.size(); ++i) longMessage[i] = static_cast(i * 7 + 3); + const std::vector> messages {empty, abc, longMessage}; + + std::vector, std::vector>> macCases; + macCases.emplace_back(std::vector(20, 0x0b), Bytes("Hi There")); + macCases.emplace_back(Bytes("Jefe"), Bytes("what do ya want for nothing?")); + macCases.emplace_back(std::vector(20, 0xaa), std::vector(50, 0xdd)); + std::vector counting(25); + for (std::size_t i = 0; i < counting.size(); ++i) counting[i] = static_cast(i + 1); + macCases.emplace_back(counting, std::vector(50, 0xcd)); + macCases.emplace_back(std::vector(131, 0xaa), Bytes("Test Using Larger Than Block-Size Key - Hash Key First")); + macCases.emplace_back(std::vector(131, 0xaa), Bytes("This is a test using a larger than block-size key and a larger than block-size data. The key needs to be hashed before being used by the HMAC algorithm.")); + + const Algorithm algorithms[] { + {"MD5", EVP_md5_nid_postfix(), 4, 8, 16, 64, 100, 0, + {"d41d8cd98f00b204e9800998ecf8427e", "900150983cd24fb0d6963f7d28e17f72", "10046f077f2082ac19676b8079f1cb1a"}, + {"5ccec34ea9656392457fa1ac27f08fbc", "750c783e6ab0b503eaa86e310a5db738", "2ab8b9a9f7d3894d15ad8383b97044b2", + "697eaf0aca3a3aea3a75164746ffaa79", "bfecaf4efff90a3a668f3922fec3762d", "09b8ae7b15adbbb243aca3491b51512b"}}, + {"SHA256", EVP_sha256_nid_postfix(), 672, 668, 32, 64, 120, 0xc, + {"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad", + "1e9bc38cbf860b9ec31918b065f9b52476c549a782e0e7990bed8ce3868d2371"}, + {"b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7", "5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843", + "773ea91e36800e46854db8ebd09181a72959098b3ef8c122d9635514ced565fe", "82558a389a443c0ea4cc819899f2083a85f0faa3e578f8077a2e3ff46729665b", + "60e431591ee0b67f0d8a26aacbf5b77f8e0bc6213728c5140546040f0ee37f54", "9b09ffa71b942fcb27635fbcd5b0e944bfdc63644f0713938a7f51535c3a35e2"}}, + {"SHA384", EVP_sha384_nid_postfix(), 673, 669, 48, 128, 224, 0xc, + {"38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b", + "cb00753f45a35e8bb5a03d699ac65007272c32ab0eded1631a8b605a43ff5bed8086072ba1e7cc2358baeca134c825a7", + "94c38db521ca733b8904c2d14b6e82d33dcfcc26e1318c579dbee1fc2f472019034e792263b9d46e90e700de2f6e7e91"}, + {"afd03944d84895626b0825f4ab46907f15f9dadbe4101ec682aa034c7cebc59cfaea9ea9076ede7f4af152e8b2fa9cb6", + "af45d2e376484031617f78d2b58a6b1b9c7ef464f5a01b47e42ec3736322445e8e2240ca5e69e2c78b3239ecfab21649", + "88062608d3e6ad8a0aa2ace014c8a86f0aa635d947ac9febe83ef4e55966144b2a5ab39dc13814b94e3ab6e101a34f27", + "3e8a69b7783c25851933ab6290af6ca77a9981480850009cc5577c6e1f573b4e6801dd23c4a7d679ccf8a386c674cffb", + "4ece084485813e9088d2c63a041bc5b44f9ef1012a2b588f3cd11f05033ac4c60c2ef6ab4030fe8296248df163f44952", + "6617178e941f020d351e2f254e8fd32c602420feb0b8fb9adccebb82461e99c5a678cc31e799176d3860e6110c46523e"}}, + {"SHA512", EVP_sha512_nid_postfix(), 674, 670, 64, 128, 224, 0xc, + {"cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e", + "ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f", + "00e36fccf193e59697a92b5ab24666ce6326d7fa16bf10832d0991ddc591112e9dfa6a636950ed9c4d67344a760654c2ff7785e1d60094d651038735b5dccabd"}, + {"87aa7cdea5ef619d4ff0b4241a1d6cb02379f4e2ce4ec2787ad0b30545e17cdedaa833b7d6b8a702038b274eaea3f4e4be9d914eeb61f1702e696c203a126854", + "164b7a7bfcf819e2e395fbe73b56e0a387bd64222e831fd610270cd7ea2505549758bf75c05a994a6d034f65f8f0e6fdcaeab1a34d4a6b4b636e070a38bce737", + "fa73b0089d56a284efb0f0756c890be9b1b5dbdd8ee81a3655f83e33b2279d39bf3e848279a722c806b485a47e67c807b946a337bee8942674278859e13292fb", + "b0ba465637458c6990e5a8c5f61d4af7e576d97ff94b872de76f8050361ee3dba91ca5c11aa25eb4d679275cc5788063a5f19741120c4f2de2adebeb10a298dd", + "80b24263c7c1a3ebb71493c1dd7be8b49b46d1f41b4aeec1121b013783f8f3526b56d037e05f2598bd0fd2215d6a1e5295e64f73f63f0aec8b915a985d786598", + "e37b6a775dc87dbaa4dfa9f96e5e3ffddebd71f8867289865df5a32d20cdc944b6022cac3c4982b10d5eeb55c3e4de15134676fb6de0446065c97440fa8c6a58"}}, + }; + + Require(EVP_MD_size_nid_postfix(nullptr) == -1); + Require(EVP_md5_nid_postfix() == algorithms[0].md && EVP_sha512_nid_postfix() == algorithms[3].md); + for (std::size_t i = 0; i < 4; ++i) + for (std::size_t j = i + 1; j < 4; ++j) Require(algorithms[i].md != algorithms[j].md); + for (const auto& algorithm : algorithms) { + Require(algorithm.md != nullptr); + Require(EVP_MD_size_nid_postfix(algorithm.md) == algorithm.size); + Require(Field(algorithm.md, 0) == algorithm.type); + Require(Field(algorithm.md, 4) == algorithm.pkeyType); + Require(Field(algorithm.md, 8) == algorithm.size); + Require(Field(algorithm.md, 16) == algorithm.flags); + Require(Field(algorithm.md, 80) == 6 && Field(algorithm.md, 84) == 19); + Require(Field(algorithm.md, 100) == algorithm.block); + Require(Field(algorithm.md, 104) == algorithm.contextSize); + Require(Field(algorithm.md, 24) && Field(algorithm.md, 32) && Field(algorithm.md, 40)); + Require(Field(algorithm.md, 48) == nullptr && Field(algorithm.md, 56) == nullptr); + + auto* created = EVP_MD_CTX_create_nid_postfix(); + Require(created && AllZero(created, sizeof(EvpMdContext))); + for (std::size_t m = 0; m < messages.size(); ++m) { + for (const std::size_t chunk : {std::size_t(1), std::size_t(algorithm.block - 1), std::size_t(algorithm.block), std::size_t(algorithm.block + 1), std::size_t(1000)}) + Require(DigestInChunks(*created, algorithm.md, messages[m], chunk) == algorithm.digests[m]); + unsigned char digest[64] {}; + unsigned int size = 0; + Require(EVP_Digest_nid_postfix(messages[m].data(), messages[m].size(), digest, &size, algorithm.md, nullptr) == 1); + Require(size == static_cast(algorithm.size) && Hex(digest, size) == algorithm.digests[m]); + Require(EVP_Digest_nid_postfix(messages[m].data(), messages[m].size(), digest, nullptr, algorithm.md, nullptr) == 1); + } + Require(created->digest == algorithm.md && Field(created, 40) == Field(algorithm.md, 32)); + Require(created->mdData && AllZero(created->mdData, static_cast(algorithm.contextSize))); + void* const data = created->mdData; + Require(EVP_DigestInit_ex_nid_postfix(created, algorithm.md, nullptr) == 1 && created->mdData == data); + Require(EVP_MD_CTX_cleanup_nid_postfix(created) == 1 && AllZero(created, sizeof(EvpMdContext))); + EVP_MD_CTX_destroy_nid_postfix(created); + + HmacContext hmac; + const auto* hmacBytes = reinterpret_cast(&hmac); + std::memset(&hmac, 0x5a, sizeof(hmac)); + HMAC_CTX_init_nid_postfix(&hmac); + Require(Field(&hmac, 0) == nullptr && AllZero(hmacBytes + 8, 3 * sizeof(EvpMdContext))); + for (std::size_t c = 0; c < macCases.size(); ++c) { + const auto& [key, message] = macCases[c]; + for (const std::size_t chunk : {std::size_t(1), std::size_t(algorithm.block), std::size_t(1000)}) + Require(MacInChunks(&hmac, key, algorithm.md, message, chunk) == algorithm.macs[c]); + Require(Field(&hmac, 0) == algorithm.md); + const bool hashed = key.size() > static_cast(algorithm.block); + const auto keyLength = Field(&hmac, 152); + Require(keyLength == (hashed ? static_cast(algorithm.size) : key.size())); + Require(hashed || std::memcmp(hmacBytes + 156, key.data(), key.size()) == 0); + Require(AllZero(hmacBytes + 156 + keyLength, 128 - keyLength)); + + unsigned char mac[64] {}; + unsigned int size = 0; + Require(HMAC_Init_ex_nid_postfix(&hmac, nullptr, 0, nullptr, nullptr) == 1); + Require(HMAC_Update_nid_postfix(&hmac, message.data(), message.size()) == 1); + Require(HMAC_Final_nid_postfix(&hmac, mac, &size) == 1 && Hex(mac, size) == algorithm.macs[c]); + Require(HMAC_Init_ex_nid_postfix(&hmac, nullptr, 0, algorithm.md, nullptr) == 1); + Require(HMAC_Update_nid_postfix(&hmac, message.data(), message.size()) == 1); + Require(HMAC_Final_nid_postfix(&hmac, mac, nullptr) == 1 && Hex(mac, static_cast(algorithm.size)) == algorithm.macs[c]); + + std::memset(mac, 0, sizeof(mac)); + size = 0; + Require(HMAC_nid_postfix(algorithm.md, key.data(), static_cast(key.size()), message.data(), message.size(), mac, &size) == mac); + Require(size == static_cast(algorithm.size) && Hex(mac, size) == algorithm.macs[c]); + const unsigned char* shared = HMAC_nid_postfix(algorithm.md, key.data(), static_cast(key.size()), message.data(), message.size(), nullptr, nullptr); + Require(shared && Hex(shared, static_cast(algorithm.size)) == algorithm.macs[c]); + } + HMAC_CTX_cleanup_nid_postfix(&hmac); + Require(AllZero(&hmac, sizeof(hmac))); + } + + const auto* sha256 = EVP_sha256_nid_postfix(); + const auto* sha512 = EVP_sha512_nid_postfix(); + const auto* md5 = EVP_md5_nid_postfix(); + + EvpMdContext context; + std::memset(&context, 0x77, sizeof(context)); + EVP_MD_CTX_init_nid_postfix(&context); + Require(AllZero(&context, sizeof(context))); + Require(EVP_DigestInit_ex_nid_postfix(&context, nullptr, nullptr) == 0); + RequireThrows([&] { EVP_DigestUpdate_nid_postfix(&context, abc.data(), abc.size()); }); + RequireThrows([&] { EVP_DigestFinal_ex_nid_postfix(&context, nullptr, nullptr); }); + Require(EVP_DigestInit_ex_nid_postfix(&context, sha256, nullptr) == 1); + Require(EVP_DigestInit_ex_nid_postfix(&context, sha512, nullptr) == 1 && context.digest == sha512); + Require(DigestInChunks(context, sha512, abc, 2) == algorithms[3].digests[1]); + RequireThrows([&] { EVP_DigestInit_ex_nid_postfix(&context, nullptr, nullptr); }); + RequireThrows([&] { EVP_DigestUpdate_nid_postfix(&context, nullptr, 1); }); + RequireThrows([&] { EVP_DigestUpdate_nid_postfix(nullptr, abc.data(), abc.size()); }); + RequireThrows([&] { EVP_MD_CTX_init_nid_postfix(nullptr); }); + RequireThrows([&] { EVP_MD_CTX_cleanup_nid_postfix(nullptr); }); + EVP_MD_CTX_destroy_nid_postfix(nullptr); + Require(EVP_MD_CTX_cleanup_nid_postfix(&context) == 1); + + EVP_MD_CTX_init_nid_postfix(&context); + context.flags = 0x100; + Require(EVP_DigestInit_ex_nid_postfix(&context, sha256, nullptr) == 1); + Require(context.digest == sha256 && context.mdData == nullptr && context.update == nullptr); + Require(EVP_MD_CTX_cleanup_nid_postfix(&context) == 1 && AllZero(&context, sizeof(context))); + + EvpMd custom {}; + custom.type = 12345; + custom.mdSize = 4; + custom.init = CustomInit; + custom.update = CustomUpdate; + custom.finalize = CustomFinal; + custom.cleanup = CustomCleanup; + custom.blockSize = 64; + custom.contextSize = 16; + unsigned char sum[4] {}; + unsigned int sumSize = 0; + const unsigned char bytes[] {1, 2, 3, 4, 5, 6, 7, 8, 9}; + Require(EVP_Digest_nid_postfix(bytes, sizeof(bytes), sum, &sumSize, &custom, nullptr) == 1); + Require(sumSize == 4 && sum[0] == 15 && sum[1] == 8 && sum[2] == 10 && sum[3] == 12); + Require(customInits == 1 && customCleanups == 1); + Require(EVP_MD_size_nid_postfix(&custom) == 4); + custom.mdSize = 65; + Require(EVP_Digest_nid_postfix(bytes, sizeof(bytes), sum, &sumSize, &custom, nullptr) == 0); + + HmacContext hmac {}; + const auto* hmacBytes = reinterpret_cast(&hmac); + HMAC_CTX_init_nid_postfix(&hmac); + unsigned char mac[64] {}; + unsigned int macSize = 0; + Require(HMAC_Update_nid_postfix(&hmac, abc.data(), abc.size()) == 0); + Require(HMAC_Final_nid_postfix(&hmac, mac, &macSize) == 0); + Require(HMAC_Init_ex_nid_postfix(&hmac, nullptr, 0, nullptr, nullptr) == 0); + Require(HMAC_Init_ex_nid_postfix(&hmac, nullptr, 0, sha256, nullptr) == 0); + Require(HMAC_Init_ex_nid_postfix(&hmac, "k", -1, sha256, nullptr) == 0); + Require(HMAC_Init_ex_nid_postfix(&hmac, "Jefe", 4, sha256, nullptr) == 1); + Require(HMAC_Init_ex_nid_postfix(&hmac, nullptr, 0, md5, nullptr) == 0); + Require(HMAC_Init_ex_nid_postfix(&hmac, "Jefe", -1, nullptr, nullptr) == 0); + Require(HMAC_Init_ex_nid_postfix(&hmac, "Jefe", 4, nullptr, nullptr) == 1); + const auto question = Bytes("what do ya want for nothing?"); + Require(HMAC_Update_nid_postfix(&hmac, question.data(), question.size()) == 1); + Require(HMAC_Final_nid_postfix(&hmac, mac, &macSize) == 1 && Hex(mac, macSize) == algorithms[1].macs[1]); + HMAC_CTX_cleanup_nid_postfix(&hmac); + + std::memset(&hmac, 0x33, sizeof(hmac)); + Require(HMAC_Init_nid_postfix(&hmac, "Jefe", 4, md5) == 1); + Require(HMAC_Update_nid_postfix(&hmac, question.data(), question.size()) == 1); + Require(HMAC_Final_nid_postfix(&hmac, mac, &macSize) == 1 && Hex(mac, macSize) == algorithms[0].macs[1]); + Require(HMAC_Init_nid_postfix(&hmac, nullptr, 0, nullptr) == 1); + Require(HMAC_Update_nid_postfix(&hmac, question.data(), question.size()) == 1); + Require(HMAC_Final_nid_postfix(&hmac, mac, &macSize) == 1 && Hex(mac, macSize) == algorithms[0].macs[1]); + HMAC_CTX_cleanup_nid_postfix(&hmac); + + std::vector blockKey(128); + for (std::size_t i = 0; i < blockKey.size(); ++i) blockKey[i] = static_cast(i * 3 + 1); + const struct { const EvpMd* md; std::size_t keySize; const char* mac; } blockKeyCases[] { + {sha256, 64, "631102ef12fb140c75c4a32ec732a95ce65da9a6603779445246ba02f9df465e"}, + {sha512, 128, "4638cabcab823870028a23978c0291c3c51861040154631b99929a35e84aa6f5eebf2d3b0081ae6a45dd8241f0175302562bdf3f83b299af324bb54095207b03"}, + }; + for (const auto& [md, keySize, expected] : blockKeyCases) { + const std::vector key(blockKey.begin(), blockKey.begin() + static_cast(keySize)); + HMAC_CTX_init_nid_postfix(&hmac); + for (const std::size_t chunk : {std::size_t(1), keySize, std::size_t(1000)}) { + Require(MacInChunks(&hmac, key, md, longMessage, chunk) == expected); + Require(Field(&hmac, 152) == keySize && std::memcmp(hmacBytes + 156, key.data(), keySize) == 0); + } + HMAC_CTX_cleanup_nid_postfix(&hmac); + Require(HMAC_nid_postfix(md, key.data(), static_cast(keySize), longMessage.data(), longMessage.size(), mac, &macSize) == mac); + Require(Hex(mac, macSize) == expected); + } + + Require(HMAC_nid_postfix(sha256, nullptr, 0, abc.data(), abc.size(), mac, &macSize) == nullptr); + Require(HMAC_nid_postfix(sha256, "", 0, nullptr, 0, mac, &macSize) == mac && macSize == 32); + Require(Hex(mac, macSize) == "b613679a0814d9ec772f95d778c35fc5ff1697c493715653c6c712144292c5ad"); + RequireThrows([&] { HMAC_CTX_init_nid_postfix(nullptr); }); + RequireThrows([&] { HMAC_Update_nid_postfix(nullptr, abc.data(), abc.size()); }); + Require(liveBlocks == 0); + + EVP_MD_CTX_init_nid_postfix(&context); + allocationsBeforeFailure = 0; + Require(EVP_DigestInit_ex_nid_postfix(&context, sha256, nullptr) == 0); + Require(AllZero(&context, sizeof(context)) && liveBlocks == 0); + RequireThrows([&] { EVP_DigestUpdate_nid_postfix(&context, abc.data(), abc.size()); }); + RequireThrows([&] { EVP_DigestFinal_ex_nid_postfix(&context, mac, &macSize); }); + Require(DigestInChunks(context, sha256, abc, 1) == algorithms[1].digests[1] && liveBlocks == 1); + allocationsBeforeFailure = 0; + Require(EVP_DigestInit_ex_nid_postfix(&context, sha512, nullptr) == 0); + Require(AllZero(&context, sizeof(context)) && liveBlocks == 0); + Require(DigestInChunks(context, sha512, abc, 1) == algorithms[3].digests[1] && liveBlocks == 1); + Require(EVP_MD_CTX_cleanup_nid_postfix(&context) == 1 && liveBlocks == 0); + + allocationsBeforeFailure = 0; + Require(EVP_MD_CTX_create_nid_postfix() == nullptr && liveBlocks == 0); + allocationsBeforeFailure = 0; + Require(EVP_Digest_nid_postfix(abc.data(), abc.size(), mac, &macSize, sha256, nullptr) == 0 && liveBlocks == 0); + Require(EVP_Digest_nid_postfix(abc.data(), abc.size(), mac, &macSize, sha256, nullptr) == 1 && Hex(mac, macSize) == algorithms[1].digests[1]); + + for (const std::size_t c : {std::size_t(1), std::size_t(4)}) { + const auto& [key, message] = macCases[c]; + for (int failure = 0; failure < 3; ++failure) { + HMAC_CTX_init_nid_postfix(&hmac); + allocationsBeforeFailure = failure; + Require(HMAC_Init_ex_nid_postfix(&hmac, key.data(), static_cast(key.size()), sha256, nullptr) == 0); + Require(allocationsBeforeFailure == -1 && liveBlocks == failure); + for (const std::size_t offset : {std::size_t(8), std::size_t(56), std::size_t(104)}) + Require(Field(&hmac, offset + 24) || (!Field(&hmac, offset) && !Field(&hmac, offset + 40))); + if (key.size() <= 64) RequireThrows([&] { HMAC_Update_nid_postfix(&hmac, message.data(), message.size()); }); + Require(MacInChunks(&hmac, key, sha256, message, 1000) == algorithms[1].macs[c] && liveBlocks == 3); + HMAC_CTX_cleanup_nid_postfix(&hmac); + Require(liveBlocks == 0); + } + allocationsBeforeFailure = 2; + Require(HMAC_nid_postfix(sha256, key.data(), static_cast(key.size()), message.data(), message.size(), mac, &macSize) == nullptr); + Require(allocationsBeforeFailure == -1 && liveBlocks == 0); + } + return 0; +} diff --git a/core/libs/tests/GuestLocale.cpp b/core/libs/tests/GuestLocale.cpp index 35c2a6ece2..d334314329 100644 --- a/core/libs/tests/GuestLocale.cpp +++ b/core/libs/tests/GuestLocale.cpp @@ -484,6 +484,25 @@ static void CheckFacetGetcat() { Require(punctVtable->falsename(&name, punctFacet)->size == 5 && std::memcmp(contents, "false", 5) == 0); contents = name.size <= 15 ? name.buffer : name.pointer; Require(punctVtable->truename(&name, punctFacet)->size == 4 && std::memcmp(contents, "true", 4) == 0); + Require(sizeof(_ZNSt8numpunctIwE2idE_nid_postfix) == 8); + _ZNSt8numpunctIwE2idE_nid_postfix = 11; + Require(_ZNSt8numpunctIwE2idE_nid_postfix == 11); + _ZNSt8numpunctIwE2idE_nid_postfix = 0; + GuestLocale::Facet* widePunct = nullptr; + Require(_ZNSt8numpunctIwE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(&widePunct, &locale) == 4 && widePunct != nullptr); + auto* widePunctVtable = reinterpret_cast(widePunct->vtable); + auto* widePunctFacet = reinterpret_cast(widePunct); + Require(widePunctVtable->decimalPoint(widePunctFacet) == u'.' && widePunctVtable->thousandsSep(widePunctFacet) == u','); + GuestLocale::String wideName{}; + Require(widePunctVtable->grouping(&wideName, widePunctFacet) == &wideName && wideName.size == 0); + Require(wideName.capacity == 15 && wideName.buffer[0] == '\0'); + const auto* wideContents = reinterpret_cast(wideName.size <= 7 ? wideName.buffer : wideName.pointer); + Require(widePunctVtable->falsename(&wideName, widePunctFacet)->size == 5); + wideContents = reinterpret_cast(wideName.size <= 7 ? wideName.buffer : wideName.pointer); + Require(std::memcmp(wideContents, u"false", 10) == 0); + Require(widePunctVtable->truename(&wideName, widePunctFacet)->size == 4); + wideContents = reinterpret_cast(wideName.size <= 7 ? wideName.buffer : wideName.pointer); + Require(std::memcmp(wideContents, u"true", 8) == 0); GuestLocale::Facet* numPut = nullptr; Require(_ZNSt7num_putIcSt19ostreambuf_iteratorIcSt11char_traitsIcEEE7_GetcatEPPKNSt6locale5facetEPKS5__nid_postfix(&numPut, &locale) == 4 && numPut != nullptr); GuestLocale::Facet* timePut = nullptr; @@ -491,7 +510,7 @@ static void CheckFacetGetcat() { _ZNSt8time_putIcSt19ostreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix = 11; Require(_ZNSt8time_putIcSt19ostreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix == 11); _ZNSt8time_putIcSt19ostreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix = 0; - for (GuestLocale::Facet* facet : {codecvt, punct, numPut, timePut}) { + for (GuestLocale::Facet* facet : {codecvt, punct, widePunct, numPut, timePut}) { facet->vtable->retain(facet); facet->vtable->retain(facet); Require(facet->references == 2); diff --git a/core/libs/tests/GuestLoginService.cpp b/core/libs/tests/GuestLoginService.cpp index dfee6d6f6c..a2f3fde5ec 100644 --- a/core/libs/tests/GuestLoginService.cpp +++ b/core/libs/tests/GuestLoginService.cpp @@ -1,27 +1,31 @@ #include "prx/libSceLoginService/include/Api.hpp" #include "prx/libc/include/general/VabiMacros.hpp" -#include #include -#include -#include -static void Require(bool value) { +namespace { + +void Require(bool value) { if (!value) std::abort(); } -int main() { - Require(sceLoginServiceInitialize() == 0); - Require(sceLoginServiceTerminate() == 0); - unsigned char buffer[0x80]{}; - try { - sceLoginServiceRequestDevices(reinterpret_cast(buffer)); +constexpr int kErrNotInitialized = static_cast(0x813a0001u); +constexpr int kErrInvalidArg = static_cast(0x813a0003u); - } catch (const std::runtime_error& error) { - if (std::string(error.what()) == "sceLoginServiceRequestDevices not implemented") return 0; - std::fprintf(stderr, "sceLoginServiceRequestDevices threw: %s\n", error.what()); - return 1; - } - std::fprintf(stderr, "sceLoginServiceRequestDevices returned instead of reporting that it is not implemented\n"); - return 1; +} + +int main() { + unsigned char buffer[0x80]{}; + const auto* request = reinterpret_cast(buffer); + + Require(sceLoginServiceRequestDevices(nullptr) == kErrInvalidArg); + Require(sceLoginServiceRequestDevices(request) == kErrNotInitialized); + + Require(sceLoginServiceInitialize() == 0); + Require(sceLoginServiceRequestDevices(nullptr) == kErrInvalidArg); + + Require(sceLoginServiceTerminate() == 0); + Require(sceLoginServiceRequestDevices(nullptr) == kErrInvalidArg); + Require(sceLoginServiceRequestDevices(request) == kErrNotInitialized); + return 0; } diff --git a/core/libs/tests/GuestMainStack.cpp b/core/libs/tests/GuestMainStack.cpp new file mode 100644 index 0000000000..0e2241682e --- /dev/null +++ b/core/libs/tests/GuestMainStack.cpp @@ -0,0 +1,107 @@ +#include "prx/libkernel/include/Api.hpp" +#include "prx/libkernel/Pthread/include/Pthread.hpp" +#include "SceTypes.hpp" +#include +#include +#include +#include +#include +#include + +#ifdef ANYPS5_ENABLE_ASAN +static constexpr std::uintptr_t GuestArenaOffset = 0x0000100000000000ull; +#else +static constexpr std::uintptr_t GuestArenaOffset = 0; +#endif +static constexpr std::uintptr_t ConsoleMainStackTop = 0x00000007EEFFC000ull + GuestArenaOffset; +static constexpr std::uintptr_t SmallestStack = 0x100000; +static int marker = 0; +static bool expectRefusal = false; + +#define Require(value) do { if (!(value)) { std::fprintf(stderr, "GuestMainStack failed at line %d\n", __LINE__); std::_Exit(1); } } while (false) + +static char Permissions(std::uintptr_t address) { + std::FILE* maps = std::fopen("/proc/self/maps", "r"); + Require(maps != nullptr); + char result = 0; + char line[512]; + while (std::fgets(line, sizeof(line), maps) != nullptr) { + unsigned long long low = 0; + unsigned long long high = 0; + char permissions[5] = {}; + if (std::sscanf(line, "%llx-%llx %4s", &low, &high, permissions) != 3 || address < low || address >= high) continue; + result = permissions[0] == 'r' && permissions[1] == 'w' ? 'w' : permissions[0] == 'r' ? 'r' : '-'; + break; + } + std::fclose(maps); + return result; +} + +struct WorkerContext { + std::atomic queried{false}; + void* local = nullptr; + void* start = nullptr; + void* end = nullptr; + int result = -1; +}; + +static void* APS5_VABI Worker(void* arg) { + auto& context = *static_cast(arg); + int local = 0; + context.local = &local; + context.result = sceKernelIsStack(&local, &context.start, &context.end); + context.queried.store(true); + return nullptr; +} + +static void APS5_VABI Entry(void* arguments) { + Require(!expectRefusal); + Require(arguments == &marker); + int local = 0; + const auto address = reinterpret_cast(&local); + void* start = nullptr; + void* end = nullptr; + Require(sceKernelIsStack(&local, &start, &end) == 0); + const auto low = reinterpret_cast(start); + const auto high = reinterpret_cast(end); + Require(high == ConsoleMainStackTop); + Require(low <= address && address < high); + Require(high - low >= SmallestStack); + Require(Permissions(low) == 'w' && Permissions(high - 1) == 'w'); + Require(Permissions(low - 1) == '-'); + + WorkerContext context; + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, nullptr, Worker, &context, nullptr) == 0); + Require(scePthreadJoin(thread, nullptr) == 0); + Require(context.queried.load() && context.result == 0); + Require(reinterpret_cast(context.end) <= low || reinterpret_cast(context.start) >= high); + std::exit(0); +} + +static void RefusesAnOverlap() { + constexpr std::size_t Bytes = 0x4000; + constexpr std::uint32_t Pattern = 0xa55a1234u; + void* sentinel = reinterpret_cast(ConsoleMainStackTop - 0x10000); + Require(sceKernelMapFlexibleMemory(&sentinel, Bytes, 3, 0x10) == 0); + Require(reinterpret_cast(sentinel) == ConsoleMainStackTop - 0x10000); + auto* words = static_cast(sentinel); + for (std::size_t i = 0; i < Bytes / 4; ++i) words[i] = Pattern; + bool refused = false; + expectRefusal = true; + try { + EnterGuestMainStack_nid_no_patch(&marker, Entry); + } catch (const std::runtime_error& error) { + refused = std::string(error.what()).find("overlaps a guest mapping") != std::string::npos; + } + expectRefusal = false; + Require(refused); + Require(Permissions(reinterpret_cast(sentinel)) == 'w'); + for (std::size_t i = 0; i < Bytes / 4; ++i) Require(words[i] == Pattern); + Require(sceKernelMunmap(reinterpret_cast(sentinel), Bytes) == 0); +} + +int main() { + RefusesAnOverlap(); + EnterGuestMainStack_nid_no_patch(&marker, Entry); +} diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index 1016d47504..e8afd12c3e 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -488,6 +488,13 @@ static void CheckNanConversions() { } static void CheckFloatUnderflowErrno() { + struct WideUnderflow { const char16_t* text; std::uint64_t bits; int error; }; + for (const WideUnderflow& test : {WideUnderflow{u"1e-310", 0x000012688b70e62bull, ERANGE}, WideUnderflow{u"-4.9406564584124654e-324", 0x8000000000000001ull, ERANGE}, + WideUnderflow{u"1e-330", 0, ERANGE}, WideUnderflow{u"2.2250738585072014e-308", 0x0010000000000000ull, 0}, WideUnderflow{u"0x1p-1074", 1, 0}, + WideUnderflow{u"0x1.8p-1075", 1, ERANGE}, WideUnderflow{u"-0x1.000001p-1075", 0x8000000000000001ull, ERANGE}}) { + errno = 0; + Require(std::bit_cast(wcstod_nid_postfix(test.text, nullptr)) == test.bits && errno == test.error); + } char* end = nullptr; errno = 7; Require(strtod_nid_postfix("1e-310", &end) == 1e-310 && errno == ERANGE); @@ -914,6 +921,35 @@ static long double MakeExtended(std::uint64_t mantissa, std::uint16_t signExpone return value; } +static void CheckExpConsoleBits() { + constexpr struct { + std::uint64_t input; + std::uint64_t expected; + int error; + } cases[] = { + {0x0000000000000000ull, 0x3ff0000000000000ull, 0}, {0x8000000000000000ull, 0x3ff0000000000000ull, 0}, + {0x3e2fffffffffffffull, 0x3ff0000001000000ull, 0}, {0x3e30000000000000ull, 0x3ff0000001000000ull, 0}, + {0x3fd62e42fefa39efull, 0x3ff6a09e667f3bccull, 0}, {0x3fd62e4300000000ull, 0x3ff6a09e66dbc8daull, 0}, + {0x3ff0a2b1ffffffffull, 0x4006a09e0d126a08ull, 0}, {0x3ff0a2b200000000ull, 0x4006a09e0d126a0aull, 0}, + {0x3ff0000000000000ull, 0x4005bf0a8b145769ull, 0}, {0xbff0000000000000ull, 0x3fd78b56362cef38ull, 0}, + {0x4024000000000000ull, 0x40d5829dcf950560ull, 0}, {0xc024000000000000ull, 0x3f07cd79b5647c9aull, 0}, + {0x40862e42fefa39efull, 0x7fefffffffffff2aull, 0}, {0x40862e42fefa39f0ull, 0x7ff0000000000000ull, ERANGE}, + {0xc086232bdd7abcd2ull, 0x001000000000007cull, 0}, {0xc086232bdd7abcd3ull, 0x000ffffffffffe7cull, 0}, + {0xc0874910d52d3051ull, 0x0000000000000001ull, 0}, {0xc0874910d52d3052ull, 0x0000000000000000ull, ERANGE}, + {0x7fefffffffffffffull, 0x7ff0000000000000ull, ERANGE}, {0xffefffffffffffffull, 0x0000000000000000ull, ERANGE}, + {0x7ff0000000000000ull, 0x7ff0000000000000ull, 0}, {0xfff0000000000000ull, 0x0000000000000000ull, 0}, + {0x7ff8000000000001ull, 0x7ff8000000000001ull, 0}, {0x4033ddceb4331e0aull, 0x41b94d703f21e868ull, 0}, + {0x401ce991ff636adcull, 0x4095866363930ec4ull, 0}, {0xc05c538238a89dfaull, 0x35b731b89f8aa7a0ull, 0}, + {0xbfcc83b6d471607dull, 0x3fe99c0d1b777558ull, 0}, {0x3fb6d36f6d11ceb0ull, 0x3ff17dfdd6f97518ull, 0}, + {0x4046b271a77d0918ull, 0x440677f14023ea1cull, 0}, + }; + for (const auto& item : cases) { + errno = 1234; + Require(std::bit_cast(exp_nid_postfix(std::bit_cast(item.input))) == item.expected); + Require(errno == (item.error != 0 ? item.error : 1234)); + } +} + static void CheckLog10EdgeFlags() { for (const double input : {-1.0, -std::numeric_limits::infinity(), -std::numeric_limits::quiet_NaN()}) { errno = 0; @@ -2892,6 +2928,7 @@ int main() { CheckNextafterEnvironment(nextafterf_nid_postfix, 0x80000000u, 0x3f800000u, 0x00800000u, 0x7f7fffffu, 0x7f800000u); CheckNextafterEnvironment(nextafter_nid_postfix, std::uint64_t{0x8000000000000000ull}, std::uint64_t{0x3ff0000000000000ull}, std::uint64_t{0x0010000000000000ull}, std::uint64_t{0x7fefffffffffffffull}, std::uint64_t{0x7ff0000000000000ull}); CheckExpm1ScalingRounding(); + CheckExpConsoleBits(); CheckLog10ConsoleBits(); CheckLog10EdgeFlags(); CheckLog2ConsoleBits(); diff --git a/core/libs/tests/GuestMathExact.cpp b/core/libs/tests/GuestMathExact.cpp index c1075d499e..3144de5fec 100644 --- a/core/libs/tests/GuestMathExact.cpp +++ b/core/libs/tests/GuestMathExact.cpp @@ -9,6 +9,7 @@ #include #include #include +#include static void Require(bool value, std::source_location location = std::source_location::current()) { if (value) return; @@ -153,6 +154,14 @@ static void CheckRoundingModes() { Require(rintf_nid_postfix(-1.5f) == static_cast(test.negative) && rintf_nid_postfix(1.5f) == static_cast(test.positive)); Require(rintl_nid_postfix(-1.5L) == test.negative && rintl_nid_postfix(1.5L) == test.positive); Require(lrint_nid_postfix(-1.5) == static_cast(test.negative) && llrint_nid_postfix(1.5) == static_cast(test.positive)); + for (const double small : {0.0, 0.1, 0.5, 0x1.8p-1030}) { + Require(floor_nid_postfix(small) == 0.0 && !std::signbit(floor_nid_postfix(small))); + Require(floorf_nid_postfix(static_cast(small)) == 0.0f && !std::signbit(floorf_nid_postfix(static_cast(small)))); + } + for (const float small : {std::numeric_limits::denorm_min(), 0x1p-130f, std::numeric_limits::min()}) + Require(floorf_nid_postfix(small) == 0.0f && !std::signbit(floorf_nid_postfix(small))); + Require(floor_nid_postfix(-0.5) == -1.0 && floorf_nid_postfix(-0.5f) == -1.0f); + Require(std::signbit(floor_nid_postfix(-0.0)) && std::signbit(floorf_nid_postfix(-0.0f))); Require(std::fegetround() == test.mode); } Require(std::fesetround(saved) == 0); @@ -190,6 +199,26 @@ static void CheckClassification() { Require(_LDsign_nid_postfix(-2.0L) == -32768 && _LDsign_nid_postfix(2.0L) == 0); } +static void CheckRoundHalfwayAwayFromZero() { + const double doubleCases[][2] = {{0.5, 1.0}, {-0.5, -1.0}, {1.5, 2.0}, {2.5, 3.0}, {-1.5, -2.0}, {-2.5, -3.0}, {0.4, 0.0}, {-0.6, -1.0}, {4503599627370495.5, 4503599627370496.0}, {-4503599627370495.5, -4503599627370496.0}}; + for (const auto& item : doubleCases) Require(round_nid_postfix(item[0]) == item[1]); + Require(round_nid_postfix(0.49999999999999994) == 0.0 && !std::signbit(round_nid_postfix(0.49999999999999994))); + Require(std::signbit(round_nid_postfix(-0.4)) && round_nid_postfix(-0.4) == 0.0); + Require(!std::signbit(round_nid_postfix(0.0)) && std::signbit(round_nid_postfix(-0.0))); + Require(round_nid_postfix(INFINITY) == INFINITY && round_nid_postfix(-INFINITY) == -INFINITY); + Require(std::isnan(round_nid_postfix(NAN))); + Require(round_nid_postfix(4503599627370497.0) == 4503599627370497.0 && round_nid_postfix(1e300) == 1e300); + + const float floatCases[][2] = {{0.5f, 1.0f}, {-0.5f, -1.0f}, {1.5f, 2.0f}, {2.5f, 3.0f}, {-1.5f, -2.0f}, {-2.5f, -3.0f}, {0.4f, 0.0f}, {-0.6f, -1.0f}, {8388607.5f, 8388608.0f}, {-8388607.5f, -8388608.0f}}; + for (const auto& item : floatCases) Require(roundf_nid_postfix(item[0]) == item[1]); + Require(roundf_nid_postfix(0.49999997f) == 0.0f && !std::signbit(roundf_nid_postfix(0.49999997f))); + Require(std::signbit(roundf_nid_postfix(-0.4f)) && roundf_nid_postfix(-0.4f) == 0.0f); + Require(!std::signbit(roundf_nid_postfix(0.0f)) && std::signbit(roundf_nid_postfix(-0.0f))); + Require(roundf_nid_postfix(INFINITY) == INFINITY && roundf_nid_postfix(-INFINITY) == -INFINITY); + Require(std::isnan(roundf_nid_postfix(NAN))); + Require(roundf_nid_postfix(8388609.0f) == 8388609.0f && roundf_nid_postfix(1e38f) == 1e38f); +} + int main() { CheckRoundingAndScaling(); CheckLogblLroundl(); @@ -200,5 +229,6 @@ int main() { CheckRoundingModes(); CheckRintTiesToEven(); CheckClassification(); + CheckRoundHalfwayAwayFromZero(); return 0; } diff --git a/core/libs/tests/GuestMbus.cpp b/core/libs/tests/GuestMbus.cpp index 1324294c49..8288fd5dc9 100644 --- a/core/libs/tests/GuestMbus.cpp +++ b/core/libs/tests/GuestMbus.cpp @@ -20,6 +20,7 @@ void Require(bool condition, const char* message) { } void CheckDeviceIdConversion() { + constexpr auto unconvertibleClass = static_cast(0x809B0008u); std::uint32_t local = 7; std::uint32_t deviceClass = 7; Require(sceMbusConvertToLocalDeviceId(0x0020123456789ABCull, &local, &deviceClass) == 0, "local conversion failed"); @@ -40,7 +41,6 @@ void CheckDeviceIdConversion() { Require(sceMbusConvertToMbusDeviceId(0x123456789ABCull, 0x20, &id) == 0 && id == 0x0020123456789ABCull, "class 0x20 is placed in the top 16 bits"); Require(sceMbusConvertToMbusDeviceId(0xFFFF000000000001ull, 0x10, &id) == 0 && id == 0xFFFF000000000001ull, "the local id is not masked"); Require(sceMbusConvertToMbusDeviceId(1, 0x10, nullptr) == 0, "conversion without an output"); - constexpr auto unconvertibleClass = static_cast(0x809B0008u); for (const std::uint32_t other : {0u, 1u, 0x11u, 0x21u, 0x30u, 0x1000u, 0xFFFFFFFFu}) { id = 7; Require(sceMbusConvertToMbusDeviceId(1, other, &id) == unconvertibleClass && id == 0, "other classes are refused and clear the output"); @@ -52,6 +52,7 @@ void CheckDeviceIdConversion() { int main() { static_assert(sizeof(MbusEventCallbacks) == 16); + sceMbusEventCallbackFuncsInit_(nullptr, sizeof(MbusEventCallbacks)); alignas(MbusEventCallbacks) std::array callbackBytes{}; callbackBytes.fill(0xa5); sceMbusEventCallbackFuncsInit_(reinterpret_cast(callbackBytes.data()), 8); diff --git a/core/libs/tests/GuestMemcmpProtected.cpp b/core/libs/tests/GuestMemcmpProtected.cpp new file mode 100644 index 0000000000..e1046d85d6 --- /dev/null +++ b/core/libs/tests/GuestMemcmpProtected.cpp @@ -0,0 +1,96 @@ +#include "prx/libc/include/Api.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static void Require(bool condition) { + if (!condition) { + std::fputs("GuestMemcmpProtected check failed\n", stderr); + std::abort(); + } +} + +#ifdef _WIN32 +#include +#else +#include +#include +#endif + +class Buffer { + unsigned char* allocation; + std::size_t page; +public: + Buffer() { +#ifdef _WIN32 + SYSTEM_INFO info{}; + GetSystemInfo(&info); + page = info.dwPageSize; + allocation = static_cast(VirtualAlloc(nullptr, page * 3, MEM_RESERVE | MEM_COMMIT, PAGE_NOACCESS)); + Require(allocation != nullptr); + DWORD previous; + Require(VirtualProtect(allocation + page, page, PAGE_READWRITE, &previous) != 0); +#else + const long size = sysconf(_SC_PAGESIZE); + Require(size > 0); + page = static_cast(size); + allocation = static_cast(mmap(nullptr, page * 3, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)); + Require(allocation != MAP_FAILED); + Require(mprotect(allocation + page, page, PROT_READ | PROT_WRITE) == 0); +#endif + std::memset(allocation + page, 0xa5, page); + } + ~Buffer() { +#ifdef _WIN32 + Require(VirtualFree(allocation, 0, MEM_RELEASE) != 0); +#else + Require(munmap(allocation, page * 3) == 0); +#endif + } + Buffer(const Buffer&) = delete; + Buffer& operator=(const Buffer&) = delete; + unsigned char* At(std::size_t count) { + Require(count <= page - 32); + return allocation + page * 2 - (std::max)(count, std::size_t{1}); + } +}; + +static int Sign(int value) { return (value > 0) - (value < 0); } + +int main() { + Buffer left, right; + auto* a = left.At(1); + auto* b = right.At(1); + for (unsigned x = 0; x < 256; ++x) { + for (unsigned y = 0; y < 256; ++y) { + *a = static_cast(x); + *b = static_cast(y); + Require(Sign(memcmp_nid_postfix(a, b, 1)) == Sign(static_cast(x) - static_cast(y))); + Require(*a == x && *b == y); + } + } + for (std::size_t length = 0; length <= 128; ++length) { + a = left.At(length); + b = right.At(length); + const auto extent = (std::max)(length, std::size_t{1}); + std::memset(a, 0x80, extent); + std::memset(b, 0x80, extent); + Require(memcmp_nid_postfix(a, b, length) == 0); + for (std::size_t position = 0; position < length; ++position) { + a[position] = 0xff; + b[position] = 0x7f; + if (position + 1 < length) { a[position + 1] = 0; b[position + 1] = 0xff; } + Require(memcmp_nid_postfix(a, b, length) > 0); + Require(memcmp_nid_postfix(b, a, length) < 0); + Require(memcmp_nid_postfix(a, b, position) == 0); + std::memset(a, 0x80, extent); + std::memset(b, 0x80, extent); + } + } +} diff --git a/core/libs/tests/GuestMemcpyProtected.cpp b/core/libs/tests/GuestMemcpyProtected.cpp new file mode 100644 index 0000000000..15789cc3cc --- /dev/null +++ b/core/libs/tests/GuestMemcpyProtected.cpp @@ -0,0 +1,79 @@ +#include "prx/libc/include/Api.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static void Require(bool condition) { + if (!condition) { + std::fputs("GuestMemcpyProtected check failed\n", stderr); + std::abort(); + } +} + +#ifdef _WIN32 +#include +#else +#include +#include +#endif + +class Buffer { + unsigned char* allocation; + std::size_t page; +public: + Buffer() { +#ifdef _WIN32 + SYSTEM_INFO info{}; + GetSystemInfo(&info); + page = info.dwPageSize; + allocation = static_cast(VirtualAlloc(nullptr, page * 3, MEM_RESERVE | MEM_COMMIT, PAGE_NOACCESS)); + Require(allocation != nullptr); + DWORD previous; + Require(VirtualProtect(allocation + page, page, PAGE_READWRITE, &previous) != 0); +#else + const long size = sysconf(_SC_PAGESIZE); + Require(size > 0); + page = static_cast(size); + allocation = static_cast(mmap(nullptr, page * 3, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)); + Require(allocation != MAP_FAILED); + Require(mprotect(allocation + page, page, PROT_READ | PROT_WRITE) == 0); +#endif + std::memset(allocation + page, 0xa5, page); + } + ~Buffer() { +#ifdef _WIN32 + Require(VirtualFree(allocation, 0, MEM_RELEASE) != 0); +#else + Require(munmap(allocation, page * 3) == 0); +#endif + } + Buffer(const Buffer&) = delete; + Buffer& operator=(const Buffer&) = delete; + unsigned char* At(std::size_t count) { + Require(count <= page - 32); + return allocation + page * 2 - (std::max)(count, std::size_t{1}); + } +}; + +int main() { + Buffer input, output; + for (std::size_t length = 0; length <= 256; ++length) { + auto* source = input.At(length); + auto* destination = output.At(length); + const auto extent = (std::max)(length, std::size_t{1}); + for (std::size_t i = 0; i < extent; ++i) source[i] = static_cast(i * 131 + length); + std::vector before(source, source + extent); + std::memset(destination - 16, 0xa5, extent + 16); + Require(memcpy_nid_postfix(destination, source, length) == destination); + for (std::size_t i = 0; i < length; ++i) Require(destination[i] == before[i]); + if (length == 0) Require(destination[0] == 0xa5); + Require(std::equal(before.begin(), before.end(), source)); + for (int i = 1; i <= 16; ++i) Require(destination[-i] == 0xa5); + } +} diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index 3a53eee433..c399ffd493 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -38,6 +38,8 @@ #include #include #include +#elif defined(__APPLE__) +#include #endif #ifdef _WIN32 #define WIN32_LEAN_AND_MEAN @@ -416,6 +418,7 @@ static void CheckMunmapScopedFlexibleRelease() { Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before); } +#ifndef __APPLE__ static void CheckGuestModuleUsesFlexibleMemory() { constexpr std::size_t moduleData = 0x10000; const auto path = std::filesystem::relative(GUEST_MEMORY_MODULE).generic_string(); @@ -430,6 +433,7 @@ static void CheckGuestModuleUsesFlexibleMemory() { Require(dlclose_nid_postfix(module) == 0); Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before); } +#endif static void CheckBatchMapStopsAtInvalidEntry() { constexpr std::size_t page = 0x4000; @@ -617,6 +621,41 @@ static void CheckDirectMemoryFollowsPhysicalPages() { Require(sceKernelReleaseDirectMemory(again, page * 2) == 0); } +static void CheckDirectMemoryBecomesExecutable() { + constexpr std::size_t page = 0x4000; + std::int64_t phys = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page, 0, 0, &phys) == 0); + void* mapped = nullptr; + Require(sceKernelMapDirectMemory(&mapped, page, 3, 0, phys, 0) == 0); + constexpr std::array code{0xb8, 0x2a, 0x00, 0x00, 0x00, 0xc3}; + std::memcpy(mapped, code.data(), code.size()); + Require(sceKernelMprotect(mapped, page, 5) == 0); + Require(reinterpret_cast(mapped)() == 42); + Require(sceKernelMprotect(mapped, page, 7) == 0); + static_cast(mapped)[1] = 0x2b; + Require(reinterpret_cast(mapped)() == 43); + Require(sceKernelMunmap(reinterpret_cast(mapped), page) == 0); + Require(sceKernelReleaseDirectMemory(phys, page) == 0); +} + +#ifndef _WIN32 +static void CheckDirectMemoryReleasesHostDescriptors() { + constexpr std::size_t page = 0x4000; + const int before = dup(0); + Require(before >= 0 && close(before) == 0); + for (int round = 0; round < 64; ++round) { + std::int64_t phys = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page, 0, 0, &phys) == 0); + void* mapped = nullptr; + Require(sceKernelMapDirectMemory(&mapped, page, 3, 0, phys, 0) == 0); + Require(sceKernelMunmap(reinterpret_cast(mapped), page) == 0); + Require(sceKernelReleaseDirectMemory(phys, page) == 0); + } + const int after = dup(0); + Require(after == before && close(after) == 0); +} +#endif + static void CheckSparseDirectMemoryBacking() { constexpr std::size_t page = 0x4000; constexpr std::size_t size = 256u * 1024u * 1024u; @@ -2415,6 +2454,41 @@ static void CheckSecondDirectMappingWaitsForLeases() { #endif } +static GuestAllocations::Lease pinHolder; + +static bool PinWaiterTakesANewLease(std::uintptr_t, std::size_t) { + pinHolder = GuestAllocations::GuestAllocationsAcquireAll_nid_postfix(); + return true; +} + +static void CheckPinnedRangeIsLeftOutOfNewLeases() { + constexpr std::size_t page = 0x4000; + const auto failed = reinterpret_cast(static_cast(-1)); + void* raw = mmap_nid_postfix(nullptr, page * 2, 3, 0x1002, -1, 0); + Require(raw != failed); + const auto holds = [&] { + return std::count_if(pinHolder.begin(), pinHolder.end(), [&](const auto& range) { return range->address == reinterpret_cast(raw); }) == 1; + }; + pinHolder = GuestAllocations::GuestAllocationsAcquireAll_nid_postfix(); + Require(holds()); + GuestAllocations::GuestAllocationsSetPinWaiter_nid_postfix(PinWaiterTakesANewLease); + bool unmapped = false; + try { + unmapped = munmap_nid_postfix(raw, page * 2) == 0; + } catch (const std::exception&) { + } + GuestAllocations::GuestAllocationsSetPinWaiter_nid_postfix(nullptr); + Require(unmapped); + Require(!holds()); + pinHolder.clear(); + void* again = mmap_nid_postfix(nullptr, page, 3, 0x1002, -1, 0); + Require(again != failed); + auto lease = GuestAllocations::GuestAllocationsAcquireAll_nid_postfix(); + Require(std::count_if(lease.begin(), lease.end(), [&](const auto& range) { return range->address == reinterpret_cast(again); }) == 1); + lease.clear(); + Require(munmap_nid_postfix(again, page) == 0); +} + #ifdef _WIN32 static void RequireResidentMapping(const void* address) { MEMORY_BASIC_INFORMATION info{}; @@ -4067,6 +4141,8 @@ static void CheckArenaFirstFitAgainstPageModel() { int main(int argc, char** argv) { #ifdef _WIN32 _putenv_s("APS5_PIN_WAIT_MS", "1000"); +#else + setenv("APS5_PIN_WAIT_MS", "1000", 1); #endif if (argc == 2 && std::strcmp(argv[1], "fixed-anonymous") == 0) { CheckPosixFixedAnonymousMapping(); @@ -4107,7 +4183,9 @@ int main(int argc, char** argv) { CheckInternalNamedFlexibleMapping(); CheckFlexibleFixedReplacement(); CheckMunmapScopedFlexibleRelease(); +#ifndef __APPLE__ CheckGuestModuleUsesFlexibleMemory(); +#endif CheckBatchMapStopsAtInvalidEntry(); CheckBatchMapPlacesAtTheGivenStart(); CheckCheckedReleaseDirectMemory(); @@ -4118,6 +4196,10 @@ int main(int argc, char** argv) { CheckDirectMapRefusesExecute(); CheckPosixMprotectIgnoresUndefinedBits(); CheckDirectMemoryFollowsPhysicalPages(); + CheckDirectMemoryBecomesExecutable(); +#ifndef _WIN32 + CheckDirectMemoryReleasesHostDescriptors(); +#endif CheckSparseDirectMemoryBacking(); CheckSavedDirectMemorySurvivesCachePressure(); CheckReusedDirectMemorySections(); @@ -4181,6 +4263,7 @@ int main(int argc, char** argv) { CheckReleaseKeepsPhysicalUntilViewsUnmapped(sceKernelReleaseDirectMemory); CheckReleaseKeepsPhysicalUntilViewsUnmapped(sceKernelCheckedReleaseDirectMemory); CheckConcurrentCheckedReleasesUnmapViewsOnce(); + CheckPinnedRangeIsLeftOutOfNewLeases(); CheckBreakingAResidentRunKeepsConcurrentAccesses(); CheckRemappedDirectMemoryIsChargedOnce(); CheckFixedRemapReprotectsHost(); diff --git a/core/libs/tests/GuestMemsetExtent.cpp b/core/libs/tests/GuestMemsetExtent.cpp new file mode 100644 index 0000000000..681d38eced --- /dev/null +++ b/core/libs/tests/GuestMemsetExtent.cpp @@ -0,0 +1,82 @@ +#include "prx/libc/include/Api.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static void Require(bool condition) { + if (!condition) { + std::fputs("GuestMemsetExtent check failed\n", stderr); + std::abort(); + } +} + +#ifdef _WIN32 +#include +#else +#include +#include +#endif + +class Buffer { + unsigned char* allocation; + std::size_t page; +public: + Buffer() { +#ifdef _WIN32 + SYSTEM_INFO info{}; + GetSystemInfo(&info); + page = info.dwPageSize; + allocation = static_cast(VirtualAlloc(nullptr, page * 3, MEM_RESERVE | MEM_COMMIT, PAGE_NOACCESS)); + Require(allocation != nullptr); + DWORD previous; + Require(VirtualProtect(allocation + page, page, PAGE_READWRITE, &previous) != 0); +#else + const long size = sysconf(_SC_PAGESIZE); + Require(size > 0); + page = static_cast(size); + allocation = static_cast(mmap(nullptr, page * 3, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)); + Require(allocation != MAP_FAILED); + Require(mprotect(allocation + page, page, PROT_READ | PROT_WRITE) == 0); +#endif + std::memset(allocation + page, 0xa5, page); + } + ~Buffer() { +#ifdef _WIN32 + Require(VirtualFree(allocation, 0, MEM_RELEASE) != 0); +#else + Require(munmap(allocation, page * 3) == 0); +#endif + } + Buffer(const Buffer&) = delete; + Buffer& operator=(const Buffer&) = delete; + template + T* At(std::size_t count) { + Require(count <= (page - 32) / sizeof(T)); + return reinterpret_cast(allocation + page * 2 - (std::max)(count, std::size_t{1}) * sizeof(T)); + } +}; + +template +static void Prefix(const T* data) { + for (int index = 1; index <= 8; ++index) Require(data[-index] == static_cast(sizeof(T) == 1 ? 0xa5 : 0xa5a5)); +} + +int main() { + Buffer output; + for (std::size_t count = 0; count <= 256; ++count) { + auto* destination = output.At(count); + for (int value : {-1025, -256, -1, 0, 1, 127, 128, 255, 256, 0x1234, (-2147483647 - 1), 2147483647}) { + std::memset(destination - 16, 0xa5, (std::max)(count, std::size_t{1}) + 16); + Require(memset_nid_postfix(destination, value, count) == destination); + for (std::size_t index = 0; index < count; ++index) Require(destination[index] == static_cast(value)); + if (count == 0) Require(destination[0] == 0xa5); + Prefix(destination); + } + } +} diff --git a/core/libs/tests/GuestMktime.cpp b/core/libs/tests/GuestMktime.cpp index 8fd2fbeba0..69e1290f5b 100644 --- a/core/libs/tests/GuestMktime.cpp +++ b/core/libs/tests/GuestMktime.cpp @@ -113,7 +113,7 @@ static void CheckZone(const char* zone) { Require(setenv_nid_postfix("TZ", zone, 1) == 0, "Could not set the time zone"); tzset_nid_postfix(); - constexpr std::array converters{libc_mktime_nid_postfix, mktime_nid_postfix}; + constexpr std::array converters{libc_mktime_nid_postfix, mktime_nid_postfix, timelocal_nid_postfix}; constexpr std::array timers{-86401, -2, -1, 0, 1, 59, 60, 86400}; for (auto convert : converters) { for (const auto timer : timers) { @@ -124,6 +124,47 @@ static void CheckZone(const char* zone) { } } +static void CheckTimelocalDst() { + Require(setenv_nid_postfix("TZ", "EST5EDT", 1) == 0, "Could not set EST5EDT zone"); + tzset_nid_postfix(); + + constexpr std::int64_t summerTimer = 995198400; + GuestTm summer{}; + Require(localtime_s_nid_postfix(&summerTimer, &summer) == &summer, "Local conversion failed"); + Require(summer.tm_isdst == 1, "Summer isdst was not 1 under EST5EDT"); + + GuestTm forMktime = summer; + forMktime.tm_isdst = 0; + const std::int64_t mktimeResult = mktime_nid_postfix(&forMktime); + + GuestTm forTimelocal = summer; + forTimelocal.tm_isdst = 0; + const std::int64_t timelocalResult = timelocal_nid_postfix(&forTimelocal); + + Require(mktimeResult != -1 && timelocalResult != -1, "Conversion failed"); + Require(timelocalResult == summerTimer, "timelocal did not determine DST with tm_isdst=-1"); +#ifndef _WIN32 + Require(mktimeResult == summerTimer + 3600, "mktime did not respect forced standard time tm_isdst=0"); +#endif + Require(forTimelocal.tm_isdst == 1, "timelocal normalized tm_isdst was not 1 in summer DST"); + + GuestTm failure{}; + failure.tm_year = std::numeric_limits::max(); + failure.tm_mon = std::numeric_limits::max(); + failure.tm_mday = 1; + failure.tm_wday = 4; + failure.tm_yday = 12; + failure.tm_isdst = 5; + failure.tm_gmtoff = 123; + failure.tm_zone = "unchanged"; + + GuestTm expectedFailure = failure; + expectedFailure.tm_isdst = -1; + + Require(timelocal_nid_postfix(&failure) == -1, "timelocal accepted unrepresentable date"); + Require(Equal(failure, expectedFailure), "timelocal failure modified fields other than tm_isdst"); +} + int main() { CheckZone("UTC0"); CheckZone("UTC-2"); @@ -133,4 +174,5 @@ int main() { CheckEquivalentFutureCalendars("America/New_York"); CheckEquivalentFutureCalendars("Pacific/Kiritimati"); #endif + CheckTimelocalDst(); } diff --git a/core/libs/tests/GuestMsgDialogNative.cpp b/core/libs/tests/GuestMsgDialogNative.cpp index 49cf6cbffb..a50ac8ce88 100644 --- a/core/libs/tests/GuestMsgDialogNative.cpp +++ b/core/libs/tests/GuestMsgDialogNative.cpp @@ -76,6 +76,7 @@ void CancelAndFallbacks() { Require(sceMsgDialogOpen(&system) == 0 && sceMsgDialogGetStatus() == kStatusFinished && Session() != ModalDialogState::Showing); result = MsgDialogResult{}; Require(sceMsgDialogGetResult(&result) == 0 && result.button_id == kButtonIdOk); + Require(sceMsgDialogGetResult(reinterpret_cast(0x10)) == kErrArgNull); ModalDialogSetPresenter_nid_postfix(false); UserMessage focusNo{MsgDialog::ButtonTypeYesNoFocusNo, 0, "Delete?"}; @@ -220,6 +221,7 @@ int main() { Require(sceMsgDialogInitialize() == kErrAlreadyInitialized); Require(sceMsgDialogClose() == kErrNotRunning); Require(sceMsgDialogGetResult(&result) == kErrNotFinished); + Require(sceMsgDialogGetResult(reinterpret_cast(0x10)) == kErrNotFinished); Require(sceMsgDialogGetResult(nullptr) == kErrArgNull); Require(sceMsgDialogOpen(nullptr) == kErrArgNull); Require(sceMsgDialogUpdateStatus() == kStatusInitialized); diff --git a/core/libs/tests/GuestMsync.cpp b/core/libs/tests/GuestMsync.cpp index e5dc64d5ce..3022daf468 100644 --- a/core/libs/tests/GuestMsync.cpp +++ b/core/libs/tests/GuestMsync.cpp @@ -54,6 +54,8 @@ int main() { Require(Fails(mapped, Page, Async | Invalidate, Invalid)); const auto top = std::numeric_limits::max() & ~(Page - 1); Require(Fails(reinterpret_cast(top), 2 * Page, Sync, Invalid)); + Require(Fails(mapped, std::numeric_limits::max() - (Page - 1) + 1, Sync, Invalid)); + Require(Fails(mapped + 1, std::numeric_limits::max() - (Page - 1), Sync, Invalid)); Require(Fails(mapped, Page, Invalidate, Busy)); Require(Fails(mapped, 3 * Page, Sync | Invalidate, Busy)); Require(Fails(mapped + 1, 16, Invalidate, Busy)); diff --git a/core/libs/tests/GuestNetKernelForwards.cpp b/core/libs/tests/GuestNetKernelForwards.cpp new file mode 100644 index 0000000000..abbb70927b --- /dev/null +++ b/core/libs/tests/GuestNetKernelForwards.cpp @@ -0,0 +1,190 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include "prx/libSceNet/libSceNet.hpp" +#include "prx/libkernel/libkernel.hpp" +#ifdef _WIN32 +#include +#else +#include +#endif + +#define Require(condition) RequireAt((condition), __LINE__) + +static void RequireAt(bool condition, int line) { + if (condition) return; + std::fprintf(stderr, "failed at line %d, errno %d\n", line, *__error_nid_postfix()); + std::abort(); +} + +static bool Failed(std::int64_t result, int error) { + const bool matched = result == -1 && *__error_nid_postfix() == error; + *__error_nid_postfix() = 0; + return matched; +} + +static int Open(bool net, int type) { + const int protocol = type == 1 ? 6 : 17; + return net ? sceNetSocket("forward", 2, type, protocol) : socket_nid_postfix(2, type, protocol); +} + +static int Close(bool net, int descriptor) { + return net ? sceNetSocketClose(descriptor) : close_nid_postfix(descriptor); +} + +static char* UnreadablePage() { +#ifdef _WIN32 + return static_cast(VirtualAlloc(nullptr, 4096, MEM_RESERVE | MEM_COMMIT, PAGE_NOACCESS)); +#else + return static_cast(mmap(nullptr, 4096, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)); +#endif +} + +static void CheckDatagram(bool net) { + const int receiver = Open(net, 2); + const int sender = Open(net, 2); + Require(receiver >= 0 && sender >= 0); + std::array local{16, 2, 0, 0, 127, 0, 0, 1}; + Require(bind_nid_postfix(receiver, local.data(), local.size()) == 0); + Require(bind_nid_postfix(sender, local.data(), local.size()) == 0); + std::array receiverAddress{}; + std::array senderAddress{}; + std::uint32_t size = receiverAddress.size(); + Require(getsockname_nid_postfix(receiver, receiverAddress.data(), &size) == 0 && size == receiverAddress.size()); + Require((receiverAddress[2] | receiverAddress[3]) != 0); + size = senderAddress.size(); + Require(getsockname_nid_postfix(sender, senderAddress.data(), &size) == 0 && size == senderAddress.size()); + char received[8]{}; + Require(recvfrom_nid_postfix(receiver, received, 0, 0, nullptr, nullptr) == 0); + const char payload[] = "ping"; + for (const std::uint32_t capacity : {16u, 4u, 0u}) { + Require(sendto_nid_postfix(sender, payload, sizeof(payload), 0, receiverAddress.data(), receiverAddress.size()) == sizeof(payload)); + std::array from{}; + size = capacity; + Require(recvfrom_nid_postfix(receiver, received, sizeof(received), 0, from.data(), &size) == sizeof(payload)); + Require(std::strcmp(received, payload) == 0 && size == capacity); + if (capacity == from.size()) Require(from[2] == senderAddress[2] && from[3] == senderAddress[3]); + } + Require(sendto_nid_postfix(sender, payload, sizeof(payload), 0, receiverAddress.data(), receiverAddress.size()) == sizeof(payload)); + size = 123; + Require(recvfrom_nid_postfix(receiver, received, sizeof(received), 0, nullptr, &size) == sizeof(payload)); + Require(size == 123 && std::strcmp(received, payload) == 0); + Require(Close(net, receiver) == 0 && Close(net, sender) == 0); +} + +static void CheckArguments(bool net) { + const int socket = Open(net, 2); + Require(socket >= 0); + char* const unreadable = UnreadablePage(); + Require(unreadable != nullptr); + std::array local{16, 2, 0, 0, 127, 0, 0, 1}; + std::array otherFamily{16, 28}; + std::array longAddress{}; + Require(Failed(bind_nid_postfix(socket, nullptr, 16), 14)); + Require(Failed(bind_nid_postfix(socket, local.data(), 1), 22)); + Require(Failed(bind_nid_postfix(socket, local.data(), 17), 22)); + Require(Failed(bind_nid_postfix(socket, longAddress.data(), 256), 63)); + Require(Failed(bind_nid_postfix(socket, otherFamily.data(), 16), 47)); + Require(bind_nid_postfix(socket, local.data(), 16) == 0); + std::uint32_t size = 16; + Require(Failed(getsockname_nid_postfix(socket, nullptr, &size), 14)); + Require(Failed(getsockname_nid_postfix(socket, local.data(), nullptr), 14)); + Require(Failed(sendto_nid_postfix(socket, nullptr, 1, 0, local.data(), 16), 14)); + Require(Failed(sendto_nid_postfix(socket, "x", 1, 0, local.data(), 1), 22)); + Require(Failed(sendto_nid_postfix(socket, "x", 1, 0, local.data(), 17), 22)); + Require(Failed(sendto_nid_postfix(socket, "x", 1, 0, longAddress.data(), 256), 63)); + Require(Failed(sendto_nid_postfix(socket, "x", 1, 0, otherFamily.data(), 16), 47)); + Require(Failed(sendto_nid_postfix(socket, "x", 1, 0, unreadable, 16), 14)); + Require(Failed(sendto_nid_postfix(socket, "x", 1, 0, nullptr, 16), 22)); + Require(Failed(sendto_nid_postfix(socket, "x", 1, 0x2, local.data(), 16), 45)); + Require(Failed(sendto_nid_postfix(socket, "x", std::uint64_t{INT_MAX} + 1, 0, local.data(), 16), 40)); + char received[8]{}; + Require(Failed(recvfrom_nid_postfix(socket, nullptr, 1, 0, nullptr, nullptr), 14)); + Require(Failed(recvfrom_nid_postfix(socket, received, sizeof(received), 0, local.data(), nullptr), 14)); + Require(Failed(recvfrom_nid_postfix(socket, received, sizeof(received), 0, nullptr, reinterpret_cast(unreadable)), 14)); + Require(Failed(recvfrom_nid_postfix(socket, received, sizeof(received), 0, local.data(), reinterpret_cast(unreadable)), 14)); + Require(Failed(recvfrom_nid_postfix(socket, received, sizeof(received), 0x1, nullptr, nullptr), 45)); + Require(Failed(recvfrom_nid_postfix(socket, received, std::uint64_t{INT_MAX} + 1, 0, nullptr, nullptr), 40)); + Require(Failed(recvfrom_nid_postfix(socket, received, sizeof(received), 0x80, nullptr, nullptr), 35)); + Require(Close(net, socket) == 0); +} + +struct Stream { + int listener; + int client; + int accepted; +}; + +static Stream OpenStream(bool net) { + Stream stream{Open(net, 1), Open(net, 1), -1}; + Require(stream.listener >= 0 && stream.client >= 0); + std::array local{16, 2, 0, 0, 127, 0, 0, 1}; + Require(bind_nid_postfix(stream.listener, local.data(), local.size()) == 0); + std::uint32_t size = local.size(); + Require(getsockname_nid_postfix(stream.listener, local.data(), &size) == 0); + Require((net ? sceNetListen(stream.listener, 1) : listen_nid_postfix(stream.listener, 1)) == 0); + Require((net ? sceNetConnect(stream.client, local.data(), 16) : connect_nid_postfix(stream.client, local.data(), 16)) == 0); + stream.accepted = net ? sceNetAccept(stream.listener, nullptr, nullptr) : accept_nid_postfix(stream.listener, nullptr, nullptr); + Require(stream.accepted >= 0); + return stream; +} + +static void CheckStreamWaitAll(bool net) { + const int unconnected = Open(net, 1); + char unused = 0; + Require(unconnected >= 0 && Failed(recvfrom_nid_postfix(unconnected, &unused, 0, 0, nullptr, nullptr), 57)); + Require(Close(net, unconnected) == 0); + const auto stream = OpenStream(net); + constexpr int WaitAll = 0x40; + std::thread writer([&] { + Require(sendto_nid_postfix(stream.client, "ab", 2, 0, nullptr, 0) == 2); + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + Require(sendto_nid_postfix(stream.client, "cd", 2, 0, nullptr, 0) == 2); + }); + char received[4]{}; + std::array from; + from.fill(0xa5); + std::uint32_t size = from.size(); + Require(recvfrom_nid_postfix(stream.accepted, received, sizeof(received), WaitAll, from.data(), &size) == 4); + writer.join(); + Require(std::memcmp(received, "abcd", 4) == 0 && size == 0 && from[0] == 0xa5); + Require(sendto_nid_postfix(stream.client, "ef", 2, 0, nullptr, 0) == 2); + char peeked[2]{}; + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + Require(recvfrom_nid_postfix(stream.accepted, peeked, 0, 0, nullptr, nullptr) == 0); + const int peek = net ? WaitAll | 0x2 : 0x2; + Require(recvfrom_nid_postfix(stream.accepted, peeked, sizeof(peeked), peek, nullptr, nullptr) == 2); + Require(std::memcmp(peeked, "ef", 2) == 0); + Require(sendto_nid_postfix(stream.client, "g", 1, 0, nullptr, 0) == 1); + Require((net ? sceNetShutdown(stream.client, 1) : shutdown_nid_postfix(stream.client, 1)) == 0); + char rest[8]{}; + Require(recvfrom_nid_postfix(stream.accepted, rest, sizeof(rest), WaitAll, nullptr, nullptr) == 3); + Require(std::memcmp(rest, "efg", 3) == 0); + Require(Close(net, stream.accepted) == 0 && Close(net, stream.client) == 0 && Close(net, stream.listener) == 0); +} + +static void CheckUnknownDescriptor() { + constexpr int unknown = 0x7ffffff0; + std::array local{16, 2, 0, 0, 127, 0, 0, 1}; + std::uint32_t size = local.size(); + char byte = 0; + Require(Failed(bind_nid_postfix(unknown, local.data(), local.size()), 9)); + Require(Failed(getsockname_nid_postfix(unknown, local.data(), &size), 9)); + Require(Failed(sendto_nid_postfix(unknown, &byte, 1, 0, local.data(), local.size()), 9)); + Require(Failed(recvfrom_nid_postfix(unknown, &byte, 1, 0, nullptr, nullptr), 9)); +} + +int main() { + for (const bool net : {false, true}) { + CheckDatagram(net); + CheckArguments(net); + CheckStreamWaitAll(net); + } + CheckUnknownDescriptor(); + return 0; +} diff --git a/core/libs/tests/GuestNpCommon.cpp b/core/libs/tests/GuestNpCommon.cpp index 08313861e1..f007128cf5 100644 --- a/core/libs/tests/GuestNpCommon.cpp +++ b/core/libs/tests/GuestNpCommon.cpp @@ -4,7 +4,9 @@ #include "SceTypes.hpp" #include #include +#include #include +#include static constexpr int INVALID_ARGUMENT = static_cast(0x80550003); static constexpr int INVALID_PLATFORM_TYPE = static_cast(0x80550004); @@ -93,6 +95,68 @@ static void* APS5_VABI returnTicket(void* arg) { return reinterpret_cast(reinterpret_cast(arg) + 1); } +struct Wakeup { + PthreadCond cond = nullptr; + PthreadMutex mutex = nullptr; + bool lightweight = false; + int waiting = 0; + int woken = 0; + bool ready = false; +}; + +static void* APS5_VABI waitUntilReady(void* arg) { + auto* wakeup = static_cast(arg); + sceNpMutexLock(&wakeup->mutex); + ++wakeup->waiting; + int result = 0; + while (!wakeup->ready && result == 0) result = wakeup->lightweight ? sceNpLwCondWait(&wakeup->cond, &wakeup->mutex) : sceNpCondWait(&wakeup->cond, &wakeup->mutex); + if (result == 0) ++wakeup->woken; + sceNpMutexUnlock(&wakeup->mutex); + return nullptr; +} + +enum class Wake { One, All, To }; + +static bool wakeWaiters(bool lightweight, int count, Wake wake) { + Wakeup wakeup; + wakeup.lightweight = lightweight; + bool ok = expect(sceNpMutexInit(&wakeup.mutex, "np-wake", 0) == 0, "the wake mutex was not created"); + ok &= expect((lightweight ? sceNpLwCondInit(&wakeup.cond, "np-lw-wake") : sceNpCondInit(&wakeup.cond, "np-wake", 0)) == 0 && wakeup.cond, "the wake condition was not created"); + if (!ok) return false; + Pthread threads[2] = {}; + for (int index = 0; index < count; ++index) + if (!expect(scePthreadCreate(&threads[index], nullptr, waitUntilReady, &wakeup, "np-waiter") == 0, "a waiter was not created")) std::abort(); + for (;;) { + sceNpMutexLock(&wakeup.mutex); + const bool waiting = wakeup.waiting == count; + if (waiting) break; + sceNpMutexUnlock(&wakeup.mutex); + scePthreadYield(); + } + wakeup.ready = true; + int signalled = 0; + if (wake == Wake::All) signalled = lightweight ? sceNpLwCondSignalAll(&wakeup.cond) : sceNpCondSignalAll(&wakeup.cond); + else if (wake == Wake::To) signalled = lightweight ? sceNpLwCondSignalTo(&wakeup.cond, threads[0]) : sceNpCondSignalTo(&wakeup.cond, threads[0]); + else signalled = lightweight ? sceNpLwCondSignal(&wakeup.cond) : sceNpCondSignal(&wakeup.cond); + sceNpMutexUnlock(&wakeup.mutex); + if (!expect(signalled == 0, "signalling the waiters failed")) std::abort(); + for (int index = 0; index < count; ++index) ok &= expect(scePthreadJoin(threads[index], nullptr) == 0, "a waiter was not joined"); + ok &= expect(wakeup.woken == count, "not every waiter returned from its wait"); + ok &= expect((lightweight ? sceNpLwCondDestroy(&wakeup.cond) : sceNpCondDestroy(&wakeup.cond)) == 0, "the wake condition was not destroyed"); + ok &= expect(sceNpMutexDestroy(&wakeup.mutex) == 0, "the wake mutex was not destroyed"); + return ok; +} + +static int testWake() { + bool ok = true; + for (const bool lightweight : {false, true}) { + ok &= expect(wakeWaiters(lightweight, 1, Wake::One), lightweight ? "sceNpLwCondSignal did not wake its waiter" : "sceNpCondSignal did not wake its waiter"); + ok &= expect(wakeWaiters(lightweight, 2, Wake::All), lightweight ? "sceNpLwCondSignalAll did not wake both waiters" : "sceNpCondSignalAll did not wake both waiters"); + ok &= expect(wakeWaiters(lightweight, 1, Wake::To), lightweight ? "sceNpLwCondSignalTo did not wake its thread" : "sceNpCondSignalTo did not wake its thread"); + } + return ok ? 0 : 1; +} + static int testSync() { PthreadMutex plain = nullptr; PthreadMutex recursive = nullptr; @@ -141,7 +205,7 @@ static int testClock() { std::memset(version, 'x', sizeof(version)); sceNpGetSdkVersion(version); - ok &= expect(std::strcmp(version, "1.00") == 0, "the version is not 1.00"); + ok &= expect(std::memcmp(version, "1.00\0\0\0\0", 8) == 0, "the version is not 1.00 padded with zeros"); ok &= expect(version[8] == 'x', "the version was written past 8 bytes"); return ok ? 0 : 1; } @@ -152,6 +216,7 @@ int main(int argc, char** argv) { if (std::strcmp(argv[1], "ids") == 0) status = testIds(); else if (std::strcmp(argv[1], "sync") == 0) status = testSync(); else if (std::strcmp(argv[1], "clock") == 0) status = testClock(); + else if (std::strcmp(argv[1], "wake") == 0) status = testWake(); if (status == 0) std::printf("NpCommon %s tests passed\n", argv[1]); return status; } diff --git a/core/libs/tests/GuestNpCommonRuntime.cpp b/core/libs/tests/GuestNpCommonRuntime.cpp index 737602ce32..660ce02fd9 100644 --- a/core/libs/tests/GuestNpCommonRuntime.cpp +++ b/core/libs/tests/GuestNpCommonRuntime.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -37,6 +38,7 @@ constexpr int NpStreamBufferTooSmall = static_cast(0x80559E0D); constexpr int NpTitleIdInvalid = static_cast(0x80559E11); constexpr int NpUtilParseOutOfRange = static_cast(0x8055060A); constexpr int NpTitleIdBufferTooSmall = static_cast(0x80559E0D); +constexpr int NpTitleSecretInvalid = static_cast(0x80559E11); constexpr int NpStreamShort = static_cast(0x80559E10); constexpr int KernelNotPermitted = static_cast(0x80020001); constexpr int KernelNoSuchObject = static_cast(0x80020003); @@ -77,6 +79,12 @@ void TestSemaphoresAndFlags() { std::uint64_t result = 0; Require(sceNpPollEventFlag(flag, 1, EventFlagWaitAnd, &result) == NpEventFlagBusy, "a clear event flag reports the busy error"); Require(sceNpSetEventFlag(flag, 1) == 0 && sceNpPollEventFlag(flag, 1, EventFlagWaitAnd, &result) == 0 && result == 1, "a set bit is polled"); + Require(sceNpSetEventFlag(flag, 6) == 0 && sceNpClearEventFlag(flag, 5) == 0, "bits are set and then masked"); + Require(sceNpPollEventFlag(flag, 2, EventFlagWaitAnd, &result) == NpEventFlagBusy, "a bit outside the clear mask is cleared"); + Require(sceNpPollEventFlag(flag, 5, EventFlagWaitAnd, &result) == 0 && result == 5, "bits inside the clear mask stay set"); + int waiting = -1; + Require(sceNpCancelEventFlag(flag, 0x10, &waiting) == 0 && waiting == 0, "a cancel with no waiter reports none"); + Require(sceNpPollEventFlag(flag, 0x10, EventFlagWaitAnd, &result) == 0 && result == 0x10, "a cancel sets the pattern it is given"); Require(sceNpDeleteEventFlag(flag) == 0, "the event flag is deleted"); } @@ -286,6 +294,41 @@ void TestObjects() { "a failed allocation returns null"); } +void TestAllocatorEx() { + NpMemoryHeap memoryHeap {}; + Require(sceNpMemoryHeapInit(&memoryHeap, 0x1000, "np-test") == 0, "a memory heap is created"); + NpAllocatorEx allocator {}; + sceNpMemoryHeapGetAllocatorEx(&memoryHeap, 0x1234, &allocator); + + Require(_sceNpAllocatorExConvertAllocator(&allocator) == &allocator.allocator, "the extended allocator converts to its allocator"); + auto* block = static_cast(_sceNpAllocatorExMalloc(&allocator, 16)); + Require(block != nullptr, "the extended allocator hands out memory"); + block[15] = 0x5A; + block = static_cast(_sceNpAllocatorExRealloc(&allocator, block, 128)); + Require(block != nullptr && block[15] == 0x5A, "reallocation keeps the contents"); + _sceNpAllocatorExFree(&allocator, block); + char* copy = _sceNpAllocatorExStrdup(&allocator, "np-common"); + + Require(copy != nullptr && std::strcmp(copy, "np-common") == 0, "a string is duplicated"); + _sceNpAllocatorExFree(&allocator, copy); + copy = _sceNpAllocatorExStrndup(&allocator, "np-common", 2); + Require(copy != nullptr && std::strcmp(copy, "np") == 0, "a duplicated prefix is terminated"); + _sceNpAllocatorExFree(&allocator, copy); + copy = _sceNpAllocatorExStrndup(&allocator, "np", 16); + + Require(copy != nullptr && std::strcmp(copy, "np") == 0, "a short string is duplicated whole"); + _sceNpAllocatorExFree(&allocator, copy); + sceNpMemoryHeapDestroy(&memoryHeap); + AllocatorRecord record {}; + record.fail = true; + NpAllocatorEx failing {{RecordAllocate, nullptr, RecordRelease, &record}, 0}; + + Require(_sceNpAllocatorExStrdup(&failing, "abc") == nullptr && record.size == 4, "a failed duplicate returns null after asking for the string and its terminator"); + Require(_sceNpAllocatorExStrndup(&failing, "abcdef", 3) == nullptr && record.size == 4, "a failed prefix duplicate returns null"); + _sceNpAllocatorExFree(&failing, record.block.data()); + Require(record.released == record.block.data(), "freeing goes through the allocator"); +} + void TestObjectsThroughStoredAllocator() { AllocatorRecord record {}; NpAllocator allocator {RecordAllocate, nullptr, RecordRelease, &record}; @@ -318,6 +361,67 @@ void TestObjectsThroughStoredAllocator() { "a failed array allocation returns null"); } +struct ExRecord { + std::array block {}; + int allocations = 0; + int reallocations = 0; + int releases = 0; + void* reallocated = nullptr; + std::size_t size = 0; + void* released = nullptr; + bool fail = false; +}; + +void* APS5_VABI ExAllocate(std::size_t size, void* context) { + auto* record = static_cast(context); + ++record->allocations; + record->size = size; + return record->fail ? nullptr : record->block.data(); +} + +void* APS5_VABI ExReallocate(void* pointer, std::size_t size, void* context) { + auto* record = static_cast(context); + ++record->reallocations; + record->reallocated = pointer; + record->size = size; + return record->fail ? nullptr : record->block.data() + 8; +} + +void APS5_VABI ExRelease(void* pointer, void* context) { + auto* record = static_cast(context); + ++record->releases; + record->released = pointer; +} + +void TestAllocatorExRecorded() { + ExRecord record {}; + NpAllocatorEx allocator {{ExAllocate, ExReallocate, ExRelease, &record}, 0x1234}; + std::uint8_t old = 0; + + Require(_sceNpAllocatorExRealloc(&allocator, &old, 96) == record.block.data() + 8 && record.reallocated == &old && record.size == 96 + && record.reallocations == 1 && record.allocations == 0 && record.releases == 0, + "reallocation is one call to the allocator's reallocate with the pointer and size"); + record.fail = true; + Require(_sceNpAllocatorExRealloc(&allocator, &old, 32) == nullptr && record.reallocations == 2 && record.releases == 0, + "a failed reallocation returns null and keeps the old block"); + Require(_sceNpAllocatorExMalloc(&allocator, 24) == nullptr && record.allocations == 1 && record.size == 24, + "a failed allocation is one call to allocate"); + _sceNpAllocatorExFree(&allocator, nullptr); + Require(record.releases == 1 && record.released == nullptr, "freeing null still calls release"); + + record.fail = false; + record.size = 0; + Require(_sceNpAllocatorExStrndup(&allocator, "abc", 10) != nullptr && record.size == 4, + "a short string asks for its length and terminator, not the limit"); + Require(_sceNpAllocatorExStrndup(&allocator, "abc", SIZE_MAX) != nullptr && record.size == 4 + && std::memcmp(record.block.data(), "abc", 4) == 0, + "an unbounded limit duplicates the whole string"); + const char unterminated[3] = {'x', 'y', 'z'}; + Require(_sceNpAllocatorExStrndup(&allocator, unterminated, sizeof(unterminated)) != nullptr && record.size == 4 + && std::memcmp(record.block.data(), "xyz", 4) == 0, + "the limit bounds the read of an unterminated buffer"); +} + void TestTitleIdObjects() { NpTitleIdObject parsed {}; std::memset(&parsed, 0x77, sizeof(parsed)); @@ -372,6 +476,62 @@ void TestTitleIdObjects() { Require(record.released == record.block.data(), "the deleting destructor frees through the stored allocator"); } +void TestTitleSecretObjects() { + std::uint8_t data[200]; + for (std::size_t i = 0; i < sizeof(data); ++i) data[i] = static_cast(i * 7 + 1); + NpTitleSecretObject secret {}; + _ZN3sce2np13NpTitleSecretC1Ev(&secret); + Require(secret.vtable != nullptr && _ZNK3sce2np13NpTitleSecret7IsEmptyEv(&secret), "a default title secret is empty"); + Require(_ZN3sce2np13NpTitleSecret13SetSecretDataEPvm(&secret, data, 127) == NpTitleSecretInvalid && _ZNK3sce2np13NpTitleSecret7IsEmptyEv(&secret), + "secret data must be 128 bytes and a refused size changes nothing"); + Require(_ZN3sce2np13NpTitleSecret13SetSecretDataEPvm(&secret, data, 128) == 0 && std::memcmp(secret.secret.data, data, 128) == 0, + "128 bytes of secret data are stored"); + Require(_ZNK3sce2np13NpTitleSecret6EqualsEPvm(&secret, data, 128) && !_ZNK3sce2np13NpTitleSecret6EqualsEPvm(&secret, data, 127), + "comparing with raw data needs 128 bytes"); + + NpTitleSecretObject fromData {}; + _ZN3sce2np13NpTitleSecretC1EPKvm(&fromData, data, 128); + bool refused = false; + NpTitleSecretObject wrongSize {}; + try { + _ZN3sce2np13NpTitleSecretC1EPKvm(&wrongSize, data, 129); + } catch (const std::invalid_argument&) { + refused = true; + } + Require(refused && _ZN3sce2npeqERKNS0_13NpTitleSecretES3_(&fromData, &secret), "constructing from data asserts the 128-byte size"); + + NpTitleSecret plain = secret.secret; + plain.data[127] ^= 1; + Require(_ZN3sce2npneERK16SceNpTitleSecretRKNS0_13NpTitleSecretE(&plain, &secret) && !_ZN3sce2npeqERKNS0_13NpTitleSecretERK16SceNpTitleSecret(&secret, &plain) + && !_ZNK3sce2np13NpTitleSecret6EqualsERK16SceNpTitleSecret(&secret, &plain), "all 128 bytes take part in the comparison"); + NpTitleSecretObject copy {}; + _ZN3sce2np13NpTitleSecretC1ERK16SceNpTitleSecret(©, &plain); + Require(_ZN3sce2npeqERK16SceNpTitleSecretRKNS0_13NpTitleSecretE(&plain, ©) && _ZN3sce2npneERKNS0_13NpTitleSecretES3_(©, &secret), + "a secret built from a SceNpTitleSecret compares equal to it"); + _ZN3sce2np13NpTitleSecretaSERKS1_(©, &secret); + Require(_ZNK3sce2np13NpTitleSecret6EqualsERKS1_(©, &secret), "assignment copies the secret"); + + char hex[300]; + std::memset(hex, 'x', sizeof(hex)); + Require(_ZN3sce2np13NpTitleSecret8BuildHexERKS1_Pcmb(&secret, hex, 256, false) == NpBufferTooSmall && hex[0] == 'x', "the hex form needs 257 bytes"); + Require(_ZN3sce2np13NpTitleSecret8BuildHexERKS1_Pcmb(&secret, hex, 257, true) == 0 && std::strlen(hex) == 256 && std::strncmp(hex, "01080F", 6) == 0, + "the secret is written as 256 hex digits"); + NpTitleSecretObject parsed {}; + _ZN3sce2np13NpTitleSecretC1Ev(&parsed); + Require(_ZN3sce2np13NpTitleSecret8ParseHexEPS1_PKcm(&parsed, hex, 256) == 0 && _ZN3sce2npeqERKNS0_13NpTitleSecretES3_(&parsed, &secret), + "the hex form parses back to the same secret"); + Require(_ZN3sce2np13NpTitleSecret8ParseHexEPS1_PKcm(&parsed, hex, 255) == NpInvalidArgument + && _ZN3sce2np13NpTitleSecret8ParseHexEPS1_PKcm(&parsed, hex, 258) == NpBufferTooSmall, "parsing needs an even length of at most 256 digits"); + + AllocatorRecord record {}; + NpAllocator allocator {RecordAllocate, nullptr, RecordRelease, &record}; + auto* owned = static_cast(_ZN3sce2np6ObjectnwEmR14SceNpAllocator(sizeof(NpTitleSecretObject), &allocator)); + _ZN3sce2np13NpTitleSecretC1Ev(owned); + using Destructor = void APS5_VABI (*)(NpTitleSecretObject*); + reinterpret_cast(owned->vtable[1])(owned); + Require(record.released == record.block.data(), "the deleting destructor frees through the stored allocator"); +} + struct CalloutRecord { std::atomic order {0}; std::atomic first {0}; @@ -432,6 +592,40 @@ void TestCallouts() { } +struct SelfTermination { + NpCalloutContext* context {}; + std::atomic finished {0}; + std::string message; +}; + +SelfTermination g_selfTermination; + +void APS5_VABI TerminateOwnContext(void*) { + try { + sceNpCalloutTermCtx(g_selfTermination.context); + } catch (const std::runtime_error& error) { + g_selfTermination.message = error.what(); + } + g_selfTermination.finished.store(1, std::memory_order_release); +} + +void TestCalloutSelfTermination() { + NpCalloutContext context {}; + NpCallout callout {}; + g_selfTermination.context = &context; + Require(sceNpCalloutInitCtx(&context, "np-self-term", 0x4000, 0, 0) == 0, "a callout context starts"); + Require(sceNpCalloutStartOnCtx(&context, &callout, 0, TerminateOwnContext, nullptr) == 0, "the terminating callout is queued"); + for (int i = 0; i < 2000 && g_selfTermination.finished.load(std::memory_order_acquire) == 0; ++i) + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + Require(g_selfTermination.finished.load(std::memory_order_acquire) == 1, "the terminating callout returns"); + Require(g_selfTermination.message.find("sceNpCalloutTermCtx") == 0, "terminating a context from its own callout is refused"); + Require(context.initialized == 1, "a refused termination leaves the context running"); + + sceNpCalloutTermCtx(&context); + + Require(context.initialized == 0, "the context terminates from another thread"); +} + bool Equals(const char* actual, const char* expected) { return std::strcmp(actual, expected) == 0; } @@ -529,6 +723,78 @@ void TestJsonText() { Require(sceNpUtilHttpUrlEncode(url, 3, "@", 1, &length) == NpUtilBufferTooSmall, "encoding needs room for the terminator"); } +void TestXmlText() { + char escaped[64] = {}; + std::size_t length = 0; + const char* special = "a\"&'<>b"; + const char* specialEscaped = "a"&'<>b"; + Require(sceNpUtilXmlEscape(escaped, sizeof(escaped), special, std::strlen(special), &length) == 0 && Equals(escaped, specialEscaped) + && length == std::strlen(specialEscaped) + 1, + "xml escaping replaces the five special characters and counts the terminator"); + length = 0; + Require(sceNpUtilXmlEscape(nullptr, 0, special, std::strlen(special), &length) == 0 && length == std::strlen(specialEscaped) + 1, + "xml escaping without a buffer only measures"); + const char* text = "\t\n\r \xC3\xA9\xE2\x82\xAC\xF0\x9F\x98\x80"; + Require(sceNpUtilXmlEscape(escaped, sizeof(escaped), text, std::strlen(text), &length) == 0 && Equals(escaped, text) && length == std::strlen(text) + 1, + "xml escaping copies whitespace and UTF-8 sequences"); + Require(sceNpUtilXmlEscape(escaped, sizeof(escaped), "a\x01", 2, &length) == NpUtilInvalidCharacter, "xml escaping refuses control characters"); + Require(sceNpUtilXmlEscape(escaped, sizeof(escaped), "\x7F", 1, &length) == NpUtilInvalidCharacter, "xml escaping refuses DEL"); + Require(sceNpUtilXmlEscape(escaped, sizeof(escaped), "\0", 1, &length) == NpUtilInvalidCharacter, "xml escaping refuses a NUL byte"); + Require(sceNpUtilXmlEscape(escaped, sizeof(escaped), "\x80", 1, &length) == NpUtilInvalidCharacter, "xml escaping refuses a lone continuation byte"); + Require(sceNpUtilXmlEscape(escaped, sizeof(escaped), "\xC1\xBF", 2, &length) == NpUtilInvalidCharacter, "xml escaping refuses an overlong lead byte"); + Require(sceNpUtilXmlEscape(escaped, sizeof(escaped), "\xC3(", 2, &length) == NpUtilInvalidCharacter, "xml escaping refuses a broken sequence"); + Require(sceNpUtilXmlEscape(escaped, sizeof(escaped), "\xF8\x80\x80\x80", 4, &length) == NpUtilInvalidCharacter, "xml escaping refuses a five-byte lead"); + Require(sceNpUtilXmlEscape(escaped, sizeof(escaped), "\xC3\xA9", 1, &length) == 0 && Equals(escaped, "\xC3\xA9") && length == 3, + "a sequence that starts inside the source is copied whole"); + Require(sceNpUtilXmlEscape(escaped, 5, "&", 1, &length) == NpUtilBufferTooSmall, "an entity must fit whole"); + Require(sceNpUtilXmlEscape(escaped, 5, "abcde", 5, &length) == NpUtilBufferTooSmall, "the terminator must fit"); + Require(sceNpUtilXmlEscape(escaped, 6, "abcde", 5, &length) == 0 && Equals(escaped, "abcde"), "a buffer of the measured size is enough"); + Require(sceNpUtilXmlEscape(escaped, sizeof(escaped), nullptr, 1, &length) == NpUtilInvalid, "xml escaping needs a source"); + Require(sceNpUtilXmlEscape(escaped, sizeof(escaped), "a", 0, &length) == NpUtilInvalid, "xml escaping needs a source length"); + Require(sceNpUtilXmlEscape(nullptr, 0, "a", 1, nullptr) == NpUtilInvalid, "xml escaping needs a buffer or a length"); + + char unescaped[64] = {}; + const char* entities = "x<>&"'y"; + Require(sceNpUtilXmlUnescape(unescaped, sizeof(unescaped), entities, std::strlen(entities), &length) == 0 && Equals(unescaped, "x<>&\"'y") + && length == 8, + "xml unescaping replaces the five entities and counts the terminator"); + length = 0; + Require(sceNpUtilXmlUnescape(nullptr, 0, entities, std::strlen(entities), &length) == 0 && length == 8, "xml unescaping without a buffer only measures"); + const char* others = " &A&"; + Require(sceNpUtilXmlUnescape(unescaped, sizeof(unescaped), others, std::strlen(others), &length) == 0 && Equals(unescaped, others), + "other ampersands are copied"); + Require(sceNpUtilXmlUnescape(unescaped, sizeof(unescaped), "a\0b", 3, &length) == 0 && unescaped[0] == 'a' && unescaped[1] == '\0' + && unescaped[2] == 'b' && unescaped[3] == '\0' && length == 4, + "xml unescaping copies every other byte"); + Require(sceNpUtilXmlUnescape(unescaped, sizeof(unescaped), "&", 1, &length) == 0 && Equals(unescaped, "&") && length == 2, + "an entity that starts inside the source is read whole"); + Require(sceNpUtilXmlUnescape(unescaped, 3, "<ab", 6, &length) == NpUtilBufferTooSmall, "each character must leave room for the terminator"); + Require(sceNpUtilXmlUnescape(unescaped, 3, "<a", 5, &length) == 0 && Equals(unescaped, " g_jsonEvents; @@ -922,6 +1188,9 @@ void TestHexAndCopies() { Require(sceNpStrcpyToBuf(copy, 5, "Hello") == NpBufferTooSmall, "a string without room for its terminator is refused"); Require(sceNpStrncpyToBuf(copy, 6, "Hello World", 5) == 0 && Equals(copy, "Hello"), "the length limit sizes the copy"); Require(sceNpStrncpyToBuf(copy, 5, "Hello World", 5) == NpBufferTooSmall, "the limited string still needs a terminator"); + const char unterminated[] = {'A', 'B', 'C', 'D', 'E', 'F', '\0'}; + Require(sceNpStrncpyToBuf(copy, 8, unterminated, 4) == 0 && Equals(copy, "ABCD"), + "a source without a terminator within the limit is cut at the limit"); } } @@ -1191,9 +1460,12 @@ int main() { TestConditionWakeups(); TestHeaps(); TestObjects(); + TestAllocatorEx(); TestObjectsThroughStoredAllocator(); + TestAllocatorExRecorded(); TestMemoryStreams(); TestTitleIdObjects(); + TestTitleSecretObjects(); TestStreamHelpers(); TestRefObjects(); TestCommIdObjects(); @@ -1201,10 +1473,12 @@ int main() { TestNumbers(); TestHexAndCopies(); TestCallouts(); + TestCalloutSelfTermination(); TestIds(); TestJid(); TestLanguagesAndDebugIds(); TestJsonText(); + TestXmlText(); TestJsonParser(); TestTitleIds(); std::puts("guest np common runtime: PASS"); diff --git a/core/libs/tests/GuestNpCppWebApiIntrusivePtr.cpp b/core/libs/tests/GuestNpCppWebApiIntrusivePtr.cpp index 0a4d4e7e85..e9a056947e 100644 --- a/core/libs/tests/GuestNpCppWebApiIntrusivePtr.cpp +++ b/core/libs/tests/GuestNpCppWebApiIntrusivePtr.cpp @@ -442,6 +442,48 @@ static void TestUserProfileModelAndTransaction() { Require(_ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V125GetPublicProfilesResponseEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(transaction) == 0); } +static void TestUserProfilePublicProfileRequest() { + constexpr int NotSignedIn = static_cast(0x80552907); + Require(_ZN3sce2Np9CppWebApi11UserProfile2V115BasicProfileApi27ParameterToGetPublicProfileC1Ev() == 0); + Require(_ZN3sce2Np9CppWebApi11UserProfile2V115BasicProfileApi27ParameterToGetPublicProfile10initializeEPNS1_6Common10LibContextEPKc() == 0); + + std::uint64_t transaction[0x20]; + std::memset(transaction, 0xa5, sizeof(transaction)); + Require(_ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V124GetPublicProfileResponseEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(transaction) == 0); + Require(_ZN3sce2Np9CppWebApi11UserProfile2V115BasicProfileApi16getPublicProfileEiRKNS4_27ParameterToGetPublicProfileERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_24GetPublicProfileResponseEEENSA_INS8_18ResponseHeaderBaseEEEEE() == NotSignedIn); + Require(_ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V124GetPublicProfileResponseEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE() == NotSignedIn); + Require(_ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V124GetPublicProfileResponseEEENS4_INS2_18ResponseHeaderBaseEEEE11getResponseERS8_() == NotSignedIn); + Require(_ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V125GetPublicProfilesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE28setResponseInformationOptionEP37SceNpWebApi2ResponseInformationOption() == 0); + Require(_ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V124GetPublicProfileResponseEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(transaction) == 0); + + Require(_ZN3sce2Np9CppWebApi11UserProfile2V115BasicProfileApi27ParameterToGetPublicProfile9terminateEv() == 0); + Require(_ZN3sce2Np9CppWebApi11UserProfile2V115BasicProfileApi27ParameterToGetPublicProfileD1Ev() == 0); +} + +static void TestUserProfileBoolAndSize() { + int object = 0; + const IntrusivePtr empty{nullptr, RecordDelete, nullptr}; + const IntrusivePtr filled{&object, nullptr, nullptr}; + Require(!_ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V112BasicProfileEEcvbEv(&empty)); + Require(_ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V112BasicProfileEEcvbEv(&filled)); + Require(!_ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V113BasicPresenceEEcvbEv(&empty)); + Require(_ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V113BasicPresenceEEcvbEv(&filled)); + + Require(_ZNK3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_11UserProfile2V112BasicProfileEEEE4sizeEv(&empty) == 0); + Require(_ZNK3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_11UserProfile2V113BasicPresenceEEEE4sizeEv(&empty) == 0); + + // a body created by pushBack is host-side, so size reads it + int context = 0; + IntrusivePtr vector; + _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V126RequestPlayerSessionPlayerEEEEC1EPNS2_10LibContextE(&vector, &context); + const IntrusivePtr element{nullptr, nullptr, &context}; + Require(_ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V126RequestPlayerSessionPlayerEEEE8pushBackERKS8_(&vector, &element) == 0); + Require(_ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V126RequestPlayerSessionPlayerEEEE8pushBackERKS8_(&vector, &element) == 0); + Require(_ZNK3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_11UserProfile2V112BasicProfileEEEE4sizeEv(&vector) == 2); + Require(_ZNK3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_11UserProfile2V113BasicPresenceEEEE4sizeEv(&vector) == 2); + _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V126RequestPlayerSessionPlayerEEEED1Ev(&vector); +} + int main(int argc, char** argv) { if (argc == 2 && std::strcmp(argv[1], "alias-vector") == 0) { TestVectorAliasedGrowth(); return 0; } Require(sizeof(IntrusivePtr) == 0x18); @@ -461,6 +503,8 @@ int main(int argc, char** argv) { _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_13InGameCatalog2V59ContainerEEEEEEC1Ev, _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V125GetPublicProfilesResponseEEC1Ev, _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEC1Ev, + _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V124GetPublicProfileResponseEEC1Ev, + _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V114PersonalDetailEEC1Ev, }; for (const auto construct : constructors) TestConstruct(construct); @@ -502,6 +546,10 @@ int main(int argc, char** argv) { _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V112BasicProfileEEptEv, _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V125GetPublicProfilesResponseEEptEv, _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_11UserProfile2V112BasicProfileEEEEEEptEv, + _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_11UserProfile2V112BasicProfileEEEEEEdeEv, + _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V124GetPublicProfileResponseEEptEv, + _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V16AvatarEEptEv, + _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_11UserProfile2V16AvatarEEEEEEdeEv, }; for (const auto access : accessors) TestAccess(access); @@ -552,6 +600,10 @@ int main(int argc, char** argv) { {_ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V113BasicPresenceEED1Ev, 0}, {_ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEED1Ev, 0}, {_ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_11UserProfile2V112BasicProfileEEEEEED1Ev, VectorReferenceCountOffset}, + {_ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V124GetPublicProfileResponseEED1Ev, 0}, + {_ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_11UserProfile2V16AvatarEEEEEED1Ev, VectorReferenceCountOffset}, + {_ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS2_6StringEEEED1Ev, StringVectorReferenceCountOffset}, + {_ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorImEEED1Ev, ScalarVectorReferenceCountOffset}, }; for (const auto& testCase : destructors) TestDestroy(testCase); @@ -563,5 +615,7 @@ int main(int argc, char** argv) { TestScalarVectorAssignment(); TestUserProfileVectorAssign(); TestUserProfileModelAndTransaction(); + TestUserProfilePublicProfileRequest(); + TestUserProfileBoolAndSize(); return 0; } diff --git a/core/libs/tests/GuestNpCppWebApiMatches.cpp b/core/libs/tests/GuestNpCppWebApiMatches.cpp new file mode 100644 index 0000000000..10f6fe2c62 --- /dev/null +++ b/core/libs/tests/GuestNpCppWebApiMatches.cpp @@ -0,0 +1,251 @@ +#include "prx/libSceNpCppWebApi/include/Api.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void Require(bool value, std::source_location at = std::source_location::current()) { + if (!value) { + std::fprintf(stderr, "requirement failed at %s:%u\n", at.file_name(), at.line()); + std::abort(); + } +} + +template +T Read(const std::uint8_t* object, const std::size_t offset) { + T value; + std::memcpy(&value, object + offset, sizeof(value)); + return value; +} + +template +void Write(std::uint8_t* object, const std::size_t offset, const T value) { + std::memcpy(object + offset, &value, sizeof(value)); +} + +} + +int main() { + alignas(8) std::array request{}; + std::memset(request.data(), 0, request.size()); + Require(_ZN3sce2Np9CppWebApi7Matches2V111AddedPlayer12setAccountIdEPKc(request.data(), "value") == 0); + Require(Read(request.data(), 96 + 0x08) != nullptr && std::strcmp(Read(request.data(), 96 + 0x08), "value") == 0); + Require(request[128] == 1); + std::memset(request.data(), 0, request.size()); + Require(_ZN3sce2Np9CppWebApi7Matches2V111AddedPlayer13setPlayerNameEPKc(request.data(), "value") == 0); + Require(Read(request.data(), 56 + 0x08) != nullptr && std::strcmp(Read(request.data(), 56 + 0x08), "value") == 0); + Require(request[88] == 1); + std::memset(request.data(), 0, request.size()); + Require(_ZN3sce2Np9CppWebApi7Matches2V111AddedPlayer9setTeamIdEPKc(request.data(), "value") == 0); + Require(Read(request.data(), 136 + 0x08) != nullptr && std::strcmp(Read(request.data(), 136 + 0x08), "value") == 0); + Require(request[168] == 1); + std::memset(request.data(), 0, request.size()); + Require(_ZN3sce2Np9CppWebApi7Matches2V116RequestMatchTeam11setTeamNameEPKc(request.data(), "value") == 0); + Require(Read(request.data(), 48 + 0x08) != nullptr && std::strcmp(Read(request.data(), 48 + 0x08), "value") == 0); + Require(request[80] == 1); + std::memset(request.data(), 0, request.size()); + Require(_ZN3sce2Np9CppWebApi7Matches2V118CreateMatchRequest9setZoneIdEPKc(request.data(), "value") == 0); + Require(Read(request.data(), 56 + 0x08) != nullptr && std::strcmp(Read(request.data(), 56 + 0x08), "value") == 0); + Require(request[88] == 1); + std::memset(request.data(), 0, request.size()); + Require(_ZN3sce2Np9CppWebApi7Matches2V118RequestMatchPlayer12setAccountIdEPKc(request.data(), "value") == 0); + Require(Read(request.data(), 96 + 0x08) != nullptr && std::strcmp(Read(request.data(), 96 + 0x08), "value") == 0); + Require(request[128] == 1); + std::memset(request.data(), 0, request.size()); + Require(_ZN3sce2Np9CppWebApi7Matches2V118RequestMatchPlayer13setPlayerNameEPKc(request.data(), "value") == 0); + Require(Read(request.data(), 56 + 0x08) != nullptr && std::strcmp(Read(request.data(), 56 + 0x08), "value") == 0); + Require(request[88] == 1); + std::memset(request.data(), 0, request.size()); + Require(_ZN3sce2Np9CppWebApi7Matches2V124UpdateMatchDetailRequest9setZoneIdEPKc(request.data(), "value") == 0); + Require(Read(request.data(), 24 + 0x08) != nullptr && std::strcmp(Read(request.data(), 24 + 0x08), "value") == 0); + Require(request[56] == 1); + std::memset(request.data(), 0, request.size()); + const int intValue = 0x1234; + Require(_ZN3sce2Np9CppWebApi7Matches2V113RemovedPlayer9setReasonERKNS3_11LeaveReasonE(request.data(), &intValue) == 0); + Require(Read(request.data(), 48) == intValue && request[52] == 1); + std::memset(request.data(), 0, request.size()); + Require(_ZN3sce2Np9CppWebApi7Matches2V116JoinMatchRequest17setNpServiceLabelERKi(request.data(), &intValue) == 0); + Require(Read(request.data(), 16) == intValue && request[20] == 1); + std::memset(request.data(), 0, request.size()); + Require(_ZN3sce2Np9CppWebApi7Matches2V117LeaveMatchRequest17setNpServiceLabelERKi(request.data(), &intValue) == 0); + Require(Read(request.data(), 16) == intValue && request[20] == 1); + std::memset(request.data(), 0, request.size()); + Require(_ZN3sce2Np9CppWebApi7Matches2V118CreateMatchRequest17setExpirationTimeERKi(request.data(), &intValue) == 0); + Require(Read(request.data(), 92) == intValue && request[96] == 1); + std::memset(request.data(), 0, request.size()); + Require(_ZN3sce2Np9CppWebApi7Matches2V118CreateMatchRequest17setNpServiceLabelERKi(request.data(), &intValue) == 0); + Require(Read(request.data(), 16) == intValue && request[20] == 1); + std::memset(request.data(), 0, request.size()); + Require(_ZN3sce2Np9CppWebApi7Matches2V119RequestMatchResults20setCooperativeResultERKNS3_24RequestCooperativeResultE(request.data(), &intValue) == 0); + Require(Read(request.data(), 20) == intValue && request[24] == 1); + std::memset(request.data(), 0, request.size()); + Require(_ZN3sce2Np9CppWebApi7Matches2V120ReportResultsRequest17setNpServiceLabelERKi(request.data(), &intValue) == 0); + Require(Read(request.data(), 16) == intValue && request[20] == 1); + std::memset(request.data(), 0, request.size()); + Require(_ZN3sce2Np9CppWebApi7Matches2V124UpdateMatchDetailRequest17setExpirationTimeERKi(request.data(), &intValue) == 0); + Require(Read(request.data(), 60) == intValue && request[64] == 1); + std::memset(request.data(), 0, request.size()); + Require(_ZN3sce2Np9CppWebApi7Matches2V124UpdateMatchDetailRequest17setNpServiceLabelERKi(request.data(), &intValue) == 0); + Require(Read(request.data(), 16) == intValue && request[20] == 1); + std::memset(request.data(), 0, request.size()); + Require(_ZN3sce2Np9CppWebApi7Matches2V124UpdateMatchStatusRequest17setNpServiceLabelERKi(request.data(), &intValue) == 0); + Require(Read(request.data(), 16) == intValue && request[20] == 1); + std::memset(request.data(), 0, request.size()); + const double doubleValue = 1.5; + Require(_ZN3sce2Np9CppWebApi7Matches2V118RequestTeamResults8setScoreERKd(request.data(), &doubleValue) == 0); + Require(Read(request.data(), 56) == doubleValue && request[64] == 1); + std::memset(request.data(), 0, request.size()); + Require(_ZN3sce2Np9CppWebApi7Matches2V120RequestPlayerResults8setScoreERKd(request.data(), &doubleValue) == 0); + Require(Read(request.data(), 56) == doubleValue && request[64] == 1); + alignas(8) std::array objectBody{}; + Write(objectBody.data(), 0x00, 1); + const IntrusivePtr objectValue{objectBody.data(), nullptr, nullptr}; + std::memset(request.data(), 0, request.size()); + Require(_ZN3sce2Np9CppWebApi7Matches2V118CreateMatchRequest15setInGameRosterERKNS1_6Common12IntrusivePtrINS3_19RequestInGameRosterEEE(request.data(), &objectValue) == 0); + Require(Read(request.data(), 152) == objectBody.data() && request[176] == 1); + Require(Read(objectBody.data(), 0x00) >= 2); + std::memset(request.data(), 0, request.size()); + Require(_ZN3sce2Np9CppWebApi7Matches2V119RequestMatchResults20setCompetitiveResultERKNS1_6Common12IntrusivePtrINS3_24RequestCompetitiveResultEEE(request.data(), &objectValue) == 0); + Require(Read(request.data(), 32) == objectBody.data() && request[56] == 1); + Require(Read(objectBody.data(), 0x00) >= 2); + std::memset(request.data(), 0, request.size()); + Require(_ZN3sce2Np9CppWebApi7Matches2V120ReportResultsRequest18setMatchStatisticsERKNS1_6Common12IntrusivePtrINS3_22RequestMatchStatisticsEEE(request.data(), &objectValue) == 0); + Require(Read(request.data(), 48) == objectBody.data() && request[72] == 1); + Require(Read(objectBody.data(), 0x00) >= 2); + std::memset(request.data(), 0, request.size()); + Require(_ZN3sce2Np9CppWebApi7Matches2V124UpdateMatchDetailRequest15setInGameRosterERKNS1_6Common12IntrusivePtrINS3_19RequestInGameRosterEEE(request.data(), &objectValue) == 0); + Require(Read(request.data(), 112) == objectBody.data() && request[136] == 1); + Require(Read(objectBody.data(), 0x00) >= 2); + alignas(8) std::array vectorBody{}; + Write(vectorBody.data(), 0x30, 1); + const IntrusivePtr vectorValue{vectorBody.data(), nullptr, nullptr}; + std::memset(request.data(), 0, request.size()); + Require(_ZN3sce2Np9CppWebApi7Matches2V116RequestMatchTeam10setMembersERKNS1_6Common6VectorINS5_12IntrusivePtrINS3_13RequestMemberEEEEE(request.data(), &vectorValue) == 0); + Require(Read(request.data(), 88) == vectorBody.data() && request[112] == 1); + Require(Read(vectorBody.data(), 0x30) >= 2); + std::memset(request.data(), 0, request.size()); + Require(_ZN3sce2Np9CppWebApi7Matches2V118RequestTeamResults20setTeamMemberResultsERKNS1_6Common6VectorINS5_12IntrusivePtrINS3_23RequestTeamMemberResultEEEEE(request.data(), &vectorValue) == 0); + Require(Read(request.data(), 72) == vectorBody.data() && request[96] == 1); + Require(Read(vectorBody.data(), 0x30) >= 2); + std::memset(request.data(), 0, request.size()); + Require(_ZN3sce2Np9CppWebApi7Matches2V119RequestInGameRoster10setPlayersERKNS1_6Common6VectorINS5_12IntrusivePtrINS3_18RequestMatchPlayerEEEEE(request.data(), &vectorValue) == 0); + Require(Read(request.data(), 16) == vectorBody.data() && request[40] == 1); + Require(Read(vectorBody.data(), 0x30) >= 2); + std::memset(request.data(), 0, request.size()); + Require(_ZN3sce2Np9CppWebApi7Matches2V119RequestInGameRoster8setTeamsERKNS1_6Common6VectorINS5_12IntrusivePtrINS3_16RequestMatchTeamEEEEE(request.data(), &vectorValue) == 0); + Require(Read(request.data(), 48) == vectorBody.data() && request[72] == 1); + Require(Read(vectorBody.data(), 0x30) >= 2); + std::memset(request.data(), 0, request.size()); + Require(_ZN3sce2Np9CppWebApi7Matches2V120RequestTeamStatistic23setTeamMemberStatisticsERKNS1_6Common6VectorINS5_12IntrusivePtrINS3_26RequestTeamMemberStatisticEEEEE(request.data(), &vectorValue) == 0); + Require(Read(request.data(), 80) == vectorBody.data() && request[104] == 1); + Require(Read(vectorBody.data(), 0x30) >= 2); + std::memset(request.data(), 0, request.size()); + Require(_ZN3sce2Np9CppWebApi7Matches2V120RequestTeamStatistic8setStatsERKNS1_6Common6VectorINS5_12IntrusivePtrINS3_19AdditionalStatisticEEEEE(request.data(), &vectorValue) == 0); + Require(Read(request.data(), 48) == vectorBody.data() && request[72] == 1); + Require(Read(vectorBody.data(), 0x30) >= 2); + std::memset(request.data(), 0, request.size()); + Require(_ZN3sce2Np9CppWebApi7Matches2V122RequestMatchStatistics17setTeamStatisticsERKNS1_6Common6VectorINS5_12IntrusivePtrINS3_20RequestTeamStatisticEEEEE(request.data(), &vectorValue) == 0); + Require(Read(request.data(), 48) == vectorBody.data() && request[72] == 1); + Require(Read(vectorBody.data(), 0x30) >= 2); + std::memset(request.data(), 0, request.size()); + Require(_ZN3sce2Np9CppWebApi7Matches2V122RequestMatchStatistics19setPlayerStatisticsERKNS1_6Common6VectorINS5_12IntrusivePtrINS3_22RequestPlayerStatisticEEEEE(request.data(), &vectorValue) == 0); + Require(Read(request.data(), 16) == vectorBody.data() && request[40] == 1); + Require(Read(vectorBody.data(), 0x30) >= 2); + std::memset(request.data(), 0, request.size()); + Require(_ZN3sce2Np9CppWebApi7Matches2V124RequestCompetitiveResult14setTeamResultsERKNS1_6Common6VectorINS5_12IntrusivePtrINS3_18RequestTeamResultsEEEEE(request.data(), &vectorValue) == 0); + Require(Read(request.data(), 48) == vectorBody.data() && request[72] == 1); + Require(Read(vectorBody.data(), 0x30) >= 2); + std::memset(request.data(), 0, request.size()); + Require(_ZN3sce2Np9CppWebApi7Matches2V124RequestCompetitiveResult16setPlayerResultsERKNS1_6Common6VectorINS5_12IntrusivePtrINS3_20RequestPlayerResultsEEEEE(request.data(), &vectorValue) == 0); + Require(Read(request.data(), 16) == vectorBody.data() && request[40] == 1); + Require(Read(vectorBody.data(), 0x30) >= 2); + std::uint64_t vectorContext = 9; + alignas(8) IntrusivePtr emptyMembers{}; + _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_7Matches2V113RequestMemberEEEEC1EPNS2_10LibContextE(&emptyMembers, &vectorContext); + std::memset(request.data(), 0, request.size()); + Require(_ZN3sce2Np9CppWebApi7Matches2V116RequestMatchTeam10setMembersERKNS1_6Common6VectorINS5_12IntrusivePtrINS3_13RequestMemberEEEEE(request.data(), &emptyMembers) == 0); + Require(Read(request.data(), 88) == nullptr && request[112] == 1); + alignas(8) IntrusivePtr emptyTeamMemberResults{}; + _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_7Matches2V123RequestTeamMemberResultEEEEC1EPNS2_10LibContextE(&emptyTeamMemberResults, &vectorContext); + std::memset(request.data(), 0, request.size()); + Require(_ZN3sce2Np9CppWebApi7Matches2V118RequestTeamResults20setTeamMemberResultsERKNS1_6Common6VectorINS5_12IntrusivePtrINS3_23RequestTeamMemberResultEEEEE(request.data(), &emptyTeamMemberResults) == 0); + Require(Read(request.data(), 72) == nullptr && request[96] == 1); + alignas(8) IntrusivePtr emptyPlayers{}; + _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_7Matches2V118RequestMatchPlayerEEEEC1EPNS2_10LibContextE(&emptyPlayers, &vectorContext); + std::memset(request.data(), 0, request.size()); + Require(_ZN3sce2Np9CppWebApi7Matches2V119RequestInGameRoster10setPlayersERKNS1_6Common6VectorINS5_12IntrusivePtrINS3_18RequestMatchPlayerEEEEE(request.data(), &emptyPlayers) == 0); + Require(Read(request.data(), 16) == nullptr && request[40] == 1); + alignas(8) IntrusivePtr emptyTeams{}; + _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_7Matches2V116RequestMatchTeamEEEEC1EPNS2_10LibContextE(&emptyTeams, &vectorContext); + std::memset(request.data(), 0, request.size()); + Require(_ZN3sce2Np9CppWebApi7Matches2V119RequestInGameRoster8setTeamsERKNS1_6Common6VectorINS5_12IntrusivePtrINS3_16RequestMatchTeamEEEEE(request.data(), &emptyTeams) == 0); + Require(Read(request.data(), 48) == nullptr && request[72] == 1); + alignas(8) IntrusivePtr emptyTeamMemberStatistics{}; + _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_7Matches2V126RequestTeamMemberStatisticEEEEC1EPNS2_10LibContextE(&emptyTeamMemberStatistics, &vectorContext); + std::memset(request.data(), 0, request.size()); + Require(_ZN3sce2Np9CppWebApi7Matches2V120RequestTeamStatistic23setTeamMemberStatisticsERKNS1_6Common6VectorINS5_12IntrusivePtrINS3_26RequestTeamMemberStatisticEEEEE(request.data(), &emptyTeamMemberStatistics) == 0); + Require(Read(request.data(), 80) == nullptr && request[104] == 1); + alignas(8) IntrusivePtr emptyStats{}; + _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_7Matches2V119AdditionalStatisticEEEEC1EPNS2_10LibContextE(&emptyStats, &vectorContext); + std::memset(request.data(), 0, request.size()); + Require(_ZN3sce2Np9CppWebApi7Matches2V120RequestTeamStatistic8setStatsERKNS1_6Common6VectorINS5_12IntrusivePtrINS3_19AdditionalStatisticEEEEE(request.data(), &emptyStats) == 0); + Require(Read(request.data(), 48) == nullptr && request[72] == 1); + alignas(8) IntrusivePtr emptyTeamStatistics{}; + _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_7Matches2V120RequestTeamStatisticEEEEC1EPNS2_10LibContextE(&emptyTeamStatistics, &vectorContext); + std::memset(request.data(), 0, request.size()); + Require(_ZN3sce2Np9CppWebApi7Matches2V122RequestMatchStatistics17setTeamStatisticsERKNS1_6Common6VectorINS5_12IntrusivePtrINS3_20RequestTeamStatisticEEEEE(request.data(), &emptyTeamStatistics) == 0); + Require(Read(request.data(), 48) == nullptr && request[72] == 1); + alignas(8) IntrusivePtr emptyPlayerStatistics{}; + _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_7Matches2V122RequestPlayerStatisticEEEEC1EPNS2_10LibContextE(&emptyPlayerStatistics, &vectorContext); + std::memset(request.data(), 0, request.size()); + Require(_ZN3sce2Np9CppWebApi7Matches2V122RequestMatchStatistics19setPlayerStatisticsERKNS1_6Common6VectorINS5_12IntrusivePtrINS3_22RequestPlayerStatisticEEEEE(request.data(), &emptyPlayerStatistics) == 0); + Require(Read(request.data(), 16) == nullptr && request[40] == 1); + alignas(8) IntrusivePtr emptyTeamResults{}; + _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_7Matches2V118RequestTeamResultsEEEEC1EPNS2_10LibContextE(&emptyTeamResults, &vectorContext); + std::memset(request.data(), 0, request.size()); + Require(_ZN3sce2Np9CppWebApi7Matches2V124RequestCompetitiveResult14setTeamResultsERKNS1_6Common6VectorINS5_12IntrusivePtrINS3_18RequestTeamResultsEEEEE(request.data(), &emptyTeamResults) == 0); + Require(Read(request.data(), 48) == nullptr && request[72] == 1); + alignas(8) IntrusivePtr emptyPlayerResults{}; + _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_7Matches2V120RequestPlayerResultsEEEEC1EPNS2_10LibContextE(&emptyPlayerResults, &vectorContext); + std::memset(request.data(), 0, request.size()); + Require(_ZN3sce2Np9CppWebApi7Matches2V124RequestCompetitiveResult16setPlayerResultsERKNS1_6Common6VectorINS5_12IntrusivePtrINS3_20RequestPlayerResultsEEEEE(request.data(), &emptyPlayerResults) == 0); + Require(Read(request.data(), 16) == nullptr && request[40] == 1); + std::memset(request.data(), 0, request.size()); + Write(request.data(), 0x48, 1); + Require(_ZNK3sce2Np9CppWebApi7Matches2V119RequestInGameRoster10teamsIsSetEv(request.data())); + Write(request.data(), 0x28, 1); + Require(_ZNK3sce2Np9CppWebApi7Matches2V119RequestInGameRoster12playersIsSetEv(request.data())); + Require(_ZNK3sce2Np9CppWebApi7Matches2V119CreateMatchResponse10getMatchIdEv(request.data()) == request.data() + 0x30); + alignas(8) std::array joinParameter{}; + alignas(8) std::array joinBodyObject{}; + Write(joinBodyObject.data(), 0x00, 1); + const IntrusivePtr joinBody{joinBodyObject.data(), nullptr, nullptr}; + std::uint64_t context = 1; + Require(_ZN3sce2Np9CppWebApi7Matches2V18MatchApi20ParameterToJoinMatch10initializeEPNS1_6Common10LibContextEPKcNS6_12IntrusivePtrINS3_16JoinMatchRequestEEE(joinParameter.data(), &context, "room", &joinBody) == 0); + Require(Read(joinParameter.data(), 0x08) != nullptr); + Require(Read(joinBodyObject.data(), 0x00) == 2); + Require(_ZN3sce2Np9CppWebApi7Matches2V18MatchApi20ParameterToJoinMatch10initializeEPNS1_6Common10LibContextEPKcNS6_12IntrusivePtrINS3_16JoinMatchRequestEEE(joinParameter.data(), &context, "room", &joinBody) == static_cast(0x8055350f)); + Require(_ZN3sce2Np9CppWebApi7Matches2V18MatchApi20ParameterToJoinMatch9terminateEv(joinParameter.data()) == 0); + Require(Read(joinBodyObject.data(), 0x00) == 1); + Require(_ZN3sce2Np9CppWebApi7Matches2V18MatchApi20ParameterToJoinMatch9terminateEv(joinParameter.data()) == static_cast(0x80553510)); + + alignas(8) std::array emptyParameter{}; + const IntrusivePtr emptyBody{nullptr, nullptr, nullptr}; + Require(_ZN3sce2Np9CppWebApi7Matches2V18MatchApi20ParameterToJoinMatch10initializeEPNS1_6Common10LibContextEPKcNS6_12IntrusivePtrINS3_16JoinMatchRequestEEE(emptyParameter.data(), &context, "room", &emptyBody) == static_cast(0x80553512)); + Require(_ZN3sce2Np9CppWebApi7Matches2V18MatchApi20ParameterToJoinMatch10initializeEPNS1_6Common10LibContextEPKcNS6_12IntrusivePtrINS3_16JoinMatchRequestEEE(emptyParameter.data(), nullptr, "room", &joinBody) == static_cast(0x80553503)); + std::uint64_t zero = 0; + Require(_ZN3sce2Np9CppWebApi7Matches2V18MatchApi20ParameterToJoinMatch10initializeEPNS1_6Common10LibContextEPKcNS6_12IntrusivePtrINS3_16JoinMatchRequestEEE(emptyParameter.data(), &zero, "room", &joinBody) == static_cast(0x80553502)); + Require(_ZN3sce2Np9CppWebApi7Matches2V18MatchApi20ParameterToJoinMatch10initializeEPNS1_6Common10LibContextEPKcNS6_12IntrusivePtrINS3_16JoinMatchRequestEEE(emptyParameter.data(), &context, nullptr, &joinBody) == static_cast(0x80553503)); + + alignas(8) std::array createParameter{}; + Require(_ZN3sce2Np9CppWebApi7Matches2V18MatchApi22ParameterToCreateMatch10initializeEPNS1_6Common10LibContextENS6_12IntrusivePtrINS3_18CreateMatchRequestEEE(createParameter.data(), &context, &joinBody) == 0); + Require(_ZN3sce2Np9CppWebApi7Matches2V18MatchApi22ParameterToCreateMatch9terminateEv(createParameter.data()) == 0); + + return 0; +} diff --git a/core/libs/tests/GuestNpCppWebApiOffline.cpp b/core/libs/tests/GuestNpCppWebApiOffline.cpp index 40a67516ed..d66fca391b 100644 --- a/core/libs/tests/GuestNpCppWebApiOffline.cpp +++ b/core/libs/tests/GuestNpCppWebApiOffline.cpp @@ -268,6 +268,66 @@ static void TestUserProfileParameterLifecycles() { }); } +template +static int InitializeSessionWithBody(TInitialize initialize, void* parameter, const std::uint64_t* context, bool present) { + if (present) return initialize(parameter, context, "session", &body); + Require(initialize(parameter, context, "session", &missingBody) == ParameterBodyMissing, "initialize accepted a missing request body"); + return initialize(parameter, context, nullptr, &body); +} + +static void TestPlayerSessionParameterLifecycles() { + TestParameterLifecycle( + { + _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi28ParameterToGetPlayerSessionsC1Ev, + _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi28ParameterToGetPlayerSessionsD1Ev, + _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi28ParameterToGetPlayerSessions9terminateEv, + nullptr, + }, + [](void* parameter, const std::uint64_t* context, bool present) { + return _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi28ParameterToGetPlayerSessions10initializeEPNS1_6Common10LibContextEPKc(parameter, context, present ? "session" : nullptr); + }); + TestParameterLifecycle( + { + _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi39ParameterToSendPlayerSessionInvitationsC1Ev, + _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi39ParameterToSendPlayerSessionInvitationsD1Ev, + _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi39ParameterToSendPlayerSessionInvitations9terminateEv, + nullptr, + }, + [](void* parameter, const std::uint64_t* context, bool present) { + return InitializeSessionWithBody(_ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi39ParameterToSendPlayerSessionInvitations10initializeEPNS1_6Common10LibContextEPKcNS6_12IntrusivePtrINS3_49PostPlayerSessionsSessionIdInvitationsRequestBodyEEE, parameter, context, present); + }); + TestParameterLifecycle( + { + _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi37ParameterToSetPlayerSessionPropertiesC1Ev, + _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi37ParameterToSetPlayerSessionPropertiesD1Ev, + _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi37ParameterToSetPlayerSessionProperties9terminateEv, + nullptr, + }, + [](void* parameter, const std::uint64_t* context, bool present) { + return InitializeSessionWithBody(_ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi37ParameterToSetPlayerSessionProperties10initializeEPNS1_6Common10LibContextEPKcNS6_12IntrusivePtrINS3_39PatchPlayerSessionsSessionIdRequestBodyEEE, parameter, context, present); + }); + TestParameterLifecycle( + { + _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi36ParameterToChangePlayerSessionLeaderC1Ev, + _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi36ParameterToChangePlayerSessionLeaderD1Ev, + _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi36ParameterToChangePlayerSessionLeader9terminateEv, + nullptr, + }, + [](void* parameter, const std::uint64_t* context, bool present) { + return InitializeSessionWithBody(_ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi36ParameterToChangePlayerSessionLeader10initializeEPNS1_6Common10LibContextEPKcNS6_12IntrusivePtrINS3_43PutPlayerSessionsSessionIdLeaderRequestBodyEEE, parameter, context, present); + }); + TestParameterLifecycle( + { + _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi49ParameterToAddPlayerSessionJoinableSpecifiedUsersC1Ev, + _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi49ParameterToAddPlayerSessionJoinableSpecifiedUsersD1Ev, + _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi49ParameterToAddPlayerSessionJoinableSpecifiedUsers9terminateEv, + nullptr, + }, + [](void* parameter, const std::uint64_t* context, bool present) { + return InitializeSessionWithBody(_ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi49ParameterToAddPlayerSessionJoinableSpecifiedUsers10initializeEPNS1_6Common10LibContextEPKcNS6_12IntrusivePtrINS3_60PostPlayerSessionsSessionIdJoinableSpecifiedUsersRequestBodyEEE, parameter, context, present); + }); +} + static void TestIteratorDestructors() { unsigned char iterator[0x20]; std::memset(iterator, 0x5a, sizeof(iterator)); @@ -331,6 +391,7 @@ int main() { TestSessionManagerParameterLifecycles(); TestLeaderboardsParameterLifecycles(); TestUserProfileParameterLifecycles(); + TestPlayerSessionParameterLifecycles(); TestIteratorDestructors(); TestVectorSizeAndElements(); std::puts("NpCppWebApi offline tests passed"); diff --git a/core/libs/tests/GuestNpCppWebApiString.cpp b/core/libs/tests/GuestNpCppWebApiString.cpp index 8be00b89d9..92b90704c1 100644 --- a/core/libs/tests/GuestNpCppWebApiString.cpp +++ b/core/libs/tests/GuestNpCppWebApiString.cpp @@ -24,6 +24,7 @@ constexpr std::size_t kReferenceCountOffset = 0x00; constexpr std::size_t kBufferOffset = 0x08; constexpr std::size_t kBufferSizeOffset = 0x10; constexpr std::size_t kLibContextOffset = 0x18; +constexpr int kStringInvalidArgument = static_cast(0x80553503); constexpr unsigned char kFill = 0xA5; constexpr std::size_t kVectorSize = 0x48; @@ -35,6 +36,10 @@ constexpr std::size_t kBodyElementKindOffset = 0x34; constexpr std::size_t kBodyElementsOffset = 0x38; constexpr std::size_t kBodySizeOffset = 0x40; +constexpr std::size_t kIteratorSize = 0x10; +constexpr std::size_t kIteratorReferenceCountOffset = 0x00; +constexpr std::size_t kIteratorElementOffset = 0x08; + struct Storage { alignas(8) unsigned char bytes[kStringSize + kGuardSize]; }; @@ -43,6 +48,10 @@ struct VectorStorage { alignas(8) unsigned char bytes[kVectorSize + kGuardSize]; }; +struct IteratorStorage { + alignas(8) unsigned char bytes[kIteratorSize + kGuardSize]; +}; + template TValue Load(const Storage& storage, std::size_t offset) { TValue value; @@ -76,6 +85,20 @@ bool VectorGuardIntact(const VectorStorage& storage) { return true; } +bool IteratorGuardIntact(const IteratorStorage& storage) { + for (std::size_t i = kIteratorSize; i < sizeof(storage.bytes); i++) { + if (storage.bytes[i] != kFill) return false; + } + return true; +} + +template +TValue LoadIterator(const IteratorStorage& storage, std::size_t offset) { + TValue value; + std::memcpy(&value, storage.bytes + offset, sizeof(value)); + return value; +} + void TestConstructedStringIsEmpty() { std::uint64_t libContext = 1; Storage storage; @@ -87,6 +110,8 @@ void TestConstructedStringIsEmpty() { Require(Load(storage, kBufferSizeOffset) == 0); Require(Load(storage, kLibContextOffset) == &libContext); Require(GuardIntact(storage)); + Require(_ZNK3sce2Np9CppWebApi6Common6String4sizeEv(reinterpret_cast(storage.bytes)) == 0); + Require(_ZNK3sce2Np9CppWebApi6Common6String6lengthEv(reinterpret_cast(storage.bytes)) == 0); const char* text = _ZNK3sce2Np9CppWebApi6Common6String5c_strEv(reinterpret_cast(storage.bytes)); Require(text != nullptr && text[0] == '\0'); @@ -117,8 +142,10 @@ void TestAppendBuildsAndClearsBuffer() { Require(_ZN3sce2Np9CppWebApi6Common6String6appendEPKc(reinterpret_cast(storage.bytes), "rank") == 0); Require(std::strcmp(_ZNK3sce2Np9CppWebApi6Common6String5c_strEv(reinterpret_cast(storage.bytes)), "rank") == 0); + Require(Load(storage, kBufferSizeOffset) == 5); Require(_ZN3sce2Np9CppWebApi6Common6String6appendEPKc(reinterpret_cast(storage.bytes), "ing") == 0); Require(std::strcmp(_ZNK3sce2Np9CppWebApi6Common6String5c_strEv(reinterpret_cast(storage.bytes)), "ranking") == 0); + Require(Load(storage, kBufferSizeOffset) == 8); Require(Load(storage, kBufferOffset) != nullptr); Require(GuardIntact(storage)); @@ -134,13 +161,83 @@ void TestAppendNullKeepsEmpty() { std::memset(storage.bytes, kFill, sizeof(storage.bytes)); _ZN3sce2Np9CppWebApi6Common6StringC1EPNS2_10LibContextE(reinterpret_cast(storage.bytes), &libContext); - Require(_ZN3sce2Np9CppWebApi6Common6String6appendEPKc(reinterpret_cast(storage.bytes), nullptr) == 0); + Require(_ZN3sce2Np9CppWebApi6Common6String6appendEPKc(reinterpret_cast(storage.bytes), nullptr) == kStringInvalidArgument); + Require(_ZN3sce2Np9CppWebApi6Common6String6appendEPKc(reinterpret_cast(storage.bytes), "") == 0); Require(Load(storage, kBufferOffset) == nullptr); Require(_ZNK3sce2Np9CppWebApi6Common6String5c_strEv(reinterpret_cast(storage.bytes))[0] == '\0'); _ZN3sce2Np9CppWebApi6Common6StringD1Ev(reinterpret_cast(storage.bytes)); } +void TestStringCopyFromAndObservers() { + std::uint64_t libContext = 1; + std::uint64_t sourceContext = 2; + Storage storage; + Storage source; + std::memset(storage.bytes, kFill, sizeof(storage.bytes)); + std::memset(source.bytes, kFill, sizeof(source.bytes)); + + _ZN3sce2Np9CppWebApi6Common6StringC1EPNS2_10LibContextE(reinterpret_cast(storage.bytes), &libContext); + Require(_ZN3sce2Np9CppWebApi6Common6String8copyFromEPKc(reinterpret_cast(storage.bytes), "alpha") == 0); + Require(Load(storage, kBufferSizeOffset) == 6); + Require(_ZNK3sce2Np9CppWebApi6Common6String4sizeEv(reinterpret_cast(storage.bytes)) == 5); + Require(_ZNK3sce2Np9CppWebApi6Common6String6lengthEv(reinterpret_cast(storage.bytes)) == 5); + Require(std::strcmp(_ZNK3sce2Np9CppWebApi6Common6String5c_strEv(reinterpret_cast(storage.bytes)), "alpha") == 0); + Require(_ZN3sce2Np9CppWebApi6Common6String8copyFromEPKc(reinterpret_cast(storage.bytes), nullptr) == kStringInvalidArgument); + Require(std::strcmp(_ZNK3sce2Np9CppWebApi6Common6String5c_strEv(reinterpret_cast(storage.bytes)), "alpha") == 0); + + _ZN3sce2Np9CppWebApi6Common6StringC1EPNS2_10LibContextE(reinterpret_cast(source.bytes), &sourceContext); + Require(_ZN3sce2Np9CppWebApi6Common6String6appendEPKc(reinterpret_cast(source.bytes), "beta") == 0); + Require(_ZN3sce2Np9CppWebApi6Common6String8copyFromERKS3_(reinterpret_cast(storage.bytes), reinterpret_cast(source.bytes)) == 0); + Require(std::strcmp(_ZNK3sce2Np9CppWebApi6Common6String5c_strEv(reinterpret_cast(storage.bytes)), "beta") == 0); + Require(Load(storage, kBufferSizeOffset) == 5); + Require(_ZN3sce2Np9CppWebApi6Common6StringeqERKS3_(reinterpret_cast(storage.bytes), reinterpret_cast(source.bytes)) == 1); + Require(_ZN3sce2Np9CppWebApi6Common6String6appendERKS3_(reinterpret_cast(storage.bytes), reinterpret_cast(source.bytes)) == 0); + Require(std::strcmp(_ZNK3sce2Np9CppWebApi6Common6String5c_strEv(reinterpret_cast(storage.bytes)), "betabeta") == 0); + + Storage emptySource; + std::memset(emptySource.bytes, kFill, sizeof(emptySource.bytes)); + _ZN3sce2Np9CppWebApi6Common6StringC1EPNS2_10LibContextE(reinterpret_cast(emptySource.bytes), &sourceContext); + Require(_ZN3sce2Np9CppWebApi6Common6String8copyFromERKS3_(reinterpret_cast(storage.bytes), reinterpret_cast(emptySource.bytes)) == 0); + Require(Load(storage, kBufferOffset) == nullptr); + Require(Load(storage, kBufferSizeOffset) == 0); + + Storage contextLess; + std::memset(contextLess.bytes, kFill, sizeof(contextLess.bytes)); + _ZN3sce2Np9CppWebApi6Common6StringC1Ev(reinterpret_cast(contextLess.bytes)); + Require(Load(contextLess, kLibContextOffset) == nullptr); + Require(_ZN3sce2Np9CppWebApi6Common6String8copyFromERKS3_(reinterpret_cast(contextLess.bytes), reinterpret_cast(source.bytes)) == 0); + Require(Load(contextLess, kLibContextOffset) == &sourceContext); + + Storage emptyContextLess; + std::memset(emptyContextLess.bytes, kFill, sizeof(emptyContextLess.bytes)); + _ZN3sce2Np9CppWebApi6Common6StringC1Ev(reinterpret_cast(emptyContextLess.bytes)); + Require(_ZN3sce2Np9CppWebApi6Common6String8copyFromERKS3_(reinterpret_cast(emptyContextLess.bytes), reinterpret_cast(emptySource.bytes)) == 0); + Require(Load(emptyContextLess, kLibContextOffset) == &sourceContext); + Require(Load(emptyContextLess, kBufferOffset) == nullptr); + + _ZN3sce2Np9CppWebApi6Common6String10setContextEPNS2_10LibContextE(reinterpret_cast(contextLess.bytes), &libContext); + Require(Load(contextLess, kLibContextOffset) == &libContext); + Require(Load(contextLess, kReferenceCountOffset) == 0); + Require(_ZN3sce2Np9CppWebApi6Common6String21intrusive_ptr_add_refEPS3_(reinterpret_cast(contextLess.bytes)) == 1); + Require(_ZN3sce2Np9CppWebApi6Common6String21intrusive_ptr_add_refEPS3_(reinterpret_cast(contextLess.bytes)) == 2); + Require(_ZN3sce2Np9CppWebApi6Common6String21intrusive_ptr_sub_refEPS3_(reinterpret_cast(contextLess.bytes)) == 2); + Require(Load(contextLess, kReferenceCountOffset) == 1); + + _ZN3sce2Np9CppWebApi6Common6String5clearEv(reinterpret_cast(storage.bytes)); + Require(Load(storage, kBufferOffset) == nullptr); + Require(Load(storage, kBufferSizeOffset) == 0); + Require(_ZNK3sce2Np9CppWebApi6Common6String5emptyEv(reinterpret_cast(storage.bytes)) == 1); + Require(_ZN3sce2Np9CppWebApi6Common6StringeqERKS3_(reinterpret_cast(storage.bytes), reinterpret_cast(emptySource.bytes)) == 1); + + _ZN3sce2Np9CppWebApi6Common6StringD1Ev(reinterpret_cast(storage.bytes)); + _ZN3sce2Np9CppWebApi6Common6StringD1Ev(reinterpret_cast(source.bytes)); + _ZN3sce2Np9CppWebApi6Common6StringD1Ev(reinterpret_cast(emptySource.bytes)); + _ZN3sce2Np9CppWebApi6Common6StringD2Ev(reinterpret_cast(contextLess.bytes)); + _ZN3sce2Np9CppWebApi6Common6StringD1Ev(reinterpret_cast(emptyContextLess.bytes)); + Require(GuardIntact(storage) && GuardIntact(source)); +} + void TestVectorPushBackCopiesStringContent() { std::uint64_t libContext = 1; VectorStorage vector; @@ -182,6 +279,93 @@ void TestVectorPushBackCopiesStringContent() { Require(VectorGuardIntact(vector)); } +void TestVectorIterationVisitsStringsInOrder() { + std::uint64_t libContext = 1; + VectorStorage vector; + IteratorStorage first; + IteratorStorage last; + std::memset(vector.bytes, kFill, sizeof(vector.bytes)); + std::memset(first.bytes, kFill, sizeof(first.bytes)); + std::memset(last.bytes, kFill, sizeof(last.bytes)); + auto* self = reinterpret_cast(vector.bytes); + auto* firstIterator = reinterpret_cast(first.bytes); + auto* lastIterator = reinterpret_cast(last.bytes); + + _ZN3sce2Np9CppWebApi6Common6VectorINS2_6StringEEC1EPNS2_10LibContextE(self, &libContext); + Require(_ZN3sce2Np9CppWebApi6Common6VectorINS2_6StringEE5beginEv(firstIterator, self) == firstIterator); + Require(_ZN3sce2Np9CppWebApi6Common6VectorINS2_6StringEE3endEv(lastIterator, self) == lastIterator); + Require(!_ZNK3sce2Np9CppWebApi6Common8IteratorINS2_6StringEEneERKS5_(firstIterator, lastIterator)); + + const char* const texts[] = {"first", "second", "third"}; + for (const char* text : texts) { + Storage element; + std::memset(element.bytes, kFill, sizeof(element.bytes)); + _ZN3sce2Np9CppWebApi6Common6StringC1EPNS2_10LibContextE(reinterpret_cast(element.bytes), &libContext); + Require(_ZN3sce2Np9CppWebApi6Common6String6appendEPKc(reinterpret_cast(element.bytes), text) == 0); + Require(_ZN3sce2Np9CppWebApi6Common6VectorINS2_6StringEE8pushBackERKS4_(self, element.bytes) == 0); + _ZN3sce2Np9CppWebApi6Common6StringD1Ev(reinterpret_cast(element.bytes)); + } + + std::memset(first.bytes, kFill, sizeof(first.bytes)); + std::memset(last.bytes, kFill, sizeof(last.bytes)); + _ZN3sce2Np9CppWebApi6Common6VectorINS2_6StringEE5beginEv(firstIterator, self); + _ZN3sce2Np9CppWebApi6Common6VectorINS2_6StringEE3endEv(lastIterator, self); + Require(LoadIterator(first, kIteratorReferenceCountOffset) == 0); + Require(LoadIterator(last, kIteratorReferenceCountOffset) == 0); + Require(LoadIterator(last, kIteratorElementOffset) - LoadIterator(first, kIteratorElementOffset) == 3 * kStringSize); + Require(IteratorGuardIntact(first) && IteratorGuardIntact(last)); + + std::size_t visited = 0; + while (_ZNK3sce2Np9CppWebApi6Common8IteratorINS2_6StringEEneERKS5_(firstIterator, lastIterator)) { + Require(visited < 3); + const CommonString* element = _ZNK3sce2Np9CppWebApi6Common8IteratorINS2_6StringEEdeEv(firstIterator); + Require(static_cast(element) == LoadIterator(first, kIteratorElementOffset)); + Require(std::strcmp(_ZNK3sce2Np9CppWebApi6Common6String5c_strEv(element), texts[visited]) == 0); + Require(_ZN3sce2Np9CppWebApi6Common8IteratorINS2_6StringEEppEv(firstIterator) == firstIterator); + ++visited; + } + Require(visited == 3); + Require(IteratorGuardIntact(first)); + + _ZN3sce2Np9CppWebApi6Common13ConstIteratorINS2_6StringEED2Ev(firstIterator); + _ZN3sce2Np9CppWebApi6Common13ConstIteratorINS2_6StringEED2Ev(lastIterator); + _ZN3sce2Np9CppWebApi6Common6VectorINS2_6StringEED1Ev(self); + Require(VectorGuardIntact(vector)); +} + +bool BeginThrows(const IntrusivePtr* vector) { + IteratorStorage iterator; + try { + _ZN3sce2Np9CppWebApi6Common6VectorINS2_6StringEE5beginEv(reinterpret_cast(iterator.bytes), vector); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + +bool EndThrows(const IntrusivePtr* vector) { + IteratorStorage iterator; + try { + _ZN3sce2Np9CppWebApi6Common6VectorINS2_6StringEE3endEv(reinterpret_cast(iterator.bytes), vector); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + +void TestVectorIterationRejectsOtherBodies() { + std::uint64_t libContext = 1; + VectorStorage vector; + std::memset(vector.bytes, kFill, sizeof(vector.bytes)); + auto* self = reinterpret_cast(vector.bytes); + const IntrusivePtr empty{nullptr, nullptr, &libContext}; + _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V112JoinableUserEEEEC1EPNS2_10LibContextE(self, &libContext); + Require(_ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V112JoinableUserEEEE8pushBackERKS8_(self, &empty) == 0); + Require(BeginThrows(self)); + Require(EndThrows(self)); + _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V112JoinableUserEEEED1Ev(self); +} + void TestStringEmptyAndEquals() { std::uint64_t libContext = 1; Storage storage; @@ -331,7 +515,10 @@ int main(int argc, char** argv) { TestAppendBuildsAndClearsBuffer(); TestAppendNullKeepsEmpty(); TestVectorPushBackCopiesStringContent(); + TestVectorIterationVisitsStringsInOrder(); + TestVectorIterationRejectsOtherBodies(); TestStringEmptyAndEquals(); + TestStringCopyFromAndObservers(); TestIntrusivePtrVectorPushBack(); TestOfflineResponses(); } diff --git a/core/libs/tests/GuestNpFriendListDialog.cpp b/core/libs/tests/GuestNpFriendListDialog.cpp index b71297ccdc..10f738abb4 100644 --- a/core/libs/tests/GuestNpFriendListDialog.cpp +++ b/core/libs/tests/GuestNpFriendListDialog.cpp @@ -4,6 +4,7 @@ #include "prx/libSceIme/include/PsnDialogNotice.hpp" #include #include +#include namespace { @@ -51,7 +52,8 @@ void RequireNotices() { int main() { std::int64_t param = 0; - std::int32_t result[2] = {-1, -1}; + std::int32_t result[2]; + std::memset(result, 0x5a, sizeof(result)); Require(sceNpFriendListDialogTerminate() == kErrNotInitialized); Require(sceNpFriendListDialogOpenA(nullptr) == kErrNotInitialized); @@ -68,7 +70,10 @@ int main() { Require(sceNpFriendListDialogUpdateStatus() == kStatusInitialized); Require(sceNpFriendListDialogOpenA(nullptr) == kErrArgNull); + Require(sceNpFriendListDialogGetResultA(nullptr) == kErrArgNull); Require(sceNpFriendListDialogGetResultA(result) == kErrNotFinished); + Require(result[0] == 0x5a5a5a5a); + Require(result[1] == 0x5a5a5a5a); Require(sceNpFriendListDialogOpenA(¶m) == 0); Require(sceNpFriendListDialogGetStatus() == kStatusFinished); @@ -77,7 +82,7 @@ int main() { Require(sceNpFriendListDialogGetResultA(nullptr) == kErrArgNull); Require(sceNpFriendListDialogGetResultA(result) == 0); Require(result[0] == kResultUserCanceled); - Require(result[1] == -1); + Require(result[1] == 0x5a5a5a5a); Require(sceNpFriendListDialogClose() == 0); Require(sceNpFriendListDialogTerminate() == 0); @@ -94,9 +99,10 @@ int main() { Require(sceNpFriendListDialogUpdateStatus() == kStatusInitialized); Require(sceNpFriendListDialogClose() == 0); Require(sceNpFriendListDialogUpdateStatus() == kStatusFinished); - result[0] = -1; + std::memset(result, 0x5a, sizeof(result)); Require(sceNpFriendListDialogGetResultA(result) == 0); Require(result[0] == kResultUserCanceled); + Require(result[1] == 0x5a5a5a5a); Require(sceNpFriendListDialogTerminate() == 0); Require(sceNpFriendListDialogTerminate() == kErrNotInitialized); Require(sceNpFriendListDialogOpenA(nullptr) == kErrNotInitialized); diff --git a/core/libs/tests/GuestNpIntegerRadices.cpp b/core/libs/tests/GuestNpIntegerRadices.cpp new file mode 100644 index 0000000000..17a5b2061d --- /dev/null +++ b/core/libs/tests/GuestNpIntegerRadices.cpp @@ -0,0 +1,69 @@ +#include "prx/libSceNpCommon/include/Api.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static void Require(bool value, std::source_location location = std::source_location::current()) { + if (!value) { + std::fprintf(stderr, "check failed at line %u\n", location.line()); + std::abort(); + } +} + +template +static void Check(TValue value, std::uint32_t radix, std::uint32_t flags, TFormat format) { + std::array reference{}; + const auto converted = std::to_chars(reference.data(), reference.data() + reference.size(), value, radix); + Require(converted.ec == std::errc{}); + std::string expected(reference.data(), converted.ptr); + std::string sign; + if (expected.front() == '-') { sign = "-"; expected.erase(0, 1); } + else if (value != 0 && (flags & 4)) sign = "+"; + if (flags & 1) { + for (char& c : expected) if (c >= 'a' && c <= 'z') c = static_cast(c - 'a' + 'A'); + } + if ((flags & 2) && (radix == 8 || radix == 16)) expected.insert(0, radix == 8 ? "0" : (flags & 1) ? "0X" : "0x"); + expected.insert(0, sign); + std::size_t length = 999; + Require(format(nullptr, 0, value, radix, flags, &length) == 0 && length == expected.size()); + std::array output{}; + for (const auto capacity : {length, length + 1}) { + output.fill('!'); + std::size_t actual = 777; + const int status = format(output.data() + 1, capacity, value, radix, flags, &actual); + if (capacity == length) { + Require(status == static_cast(0x80558008) && actual == 777); + Require(std::all_of(output.begin(), output.end(), [](char c) { return c == '!'; })); + } else { + Require(status == 0 && actual == expected.size()); + Require(std::string(output.data() + 1) == expected); + Require(output.front() == '!' && output[capacity + 1] == '!'); + } + } +} + +int main() { + for (std::uint32_t radix = 2; radix <= 36; ++radix) { + for (std::uint32_t flags = 0; flags < 8; ++flags) { + for (std::uint64_t value : {std::uint64_t{0}, std::uint64_t{1}, std::uint64_t{35}, std::numeric_limits::max()}) + Check(value, radix, flags, sceNpUInt64ToStr); + for (std::uint32_t value : {std::uint32_t{0}, std::uint32_t{1}, std::numeric_limits::max()}) + Check(value, radix, flags, sceNpUInt32ToStr); + for (std::int32_t value : {0, 1, -1, std::numeric_limits::min(), std::numeric_limits::max()}) + Check(value, radix, flags, sceNpInt32ToStr); + for (std::int64_t value : {std::int64_t{0}, std::int64_t{1}, std::numeric_limits::max()}) + Check(value, radix, flags, sceNpInt64ToStr); + if (flags & 4) { + Check(std::int64_t{-1}, radix, flags, sceNpInt64ToStr); + Check(std::numeric_limits::min(), radix, flags, sceNpInt64ToStr); + } + } + } +} diff --git a/core/libs/tests/GuestNpManager.cpp b/core/libs/tests/GuestNpManager.cpp index b467b90a8b..61724a38b8 100644 --- a/core/libs/tests/GuestNpManager.cpp +++ b/core/libs/tests/GuestNpManager.cpp @@ -1,4 +1,5 @@ #include "prx/libSceNpManager/include/Api.hpp" +#include "prx/libSceNpManager/include/InGameMessageIds.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include "SceTypes.hpp" #include @@ -23,6 +24,8 @@ constexpr int IdMapperRefusedArgument = static_cast(0x80553083u); constexpr int IdMapperRefusedRequestId = static_cast(0x80553085u); constexpr int ManagerIntTableFull = static_cast(0x80559D05u); constexpr int ManagerIntNotInTable = static_cast(0x80559D0Au); +constexpr int InGameMessageContextNotFound = static_cast(0x80552B03u); +constexpr int InGameMessageHandleNotFound = static_cast(0x80552B05u); void Require(bool condition, const char* message) { if (!condition) { @@ -501,7 +504,18 @@ void CheckUserIdQueries() { 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(sceNpSetGamePresenceOnlineA(request, userId, 0) == SignedOut, "sceNpSetGamePresenceOnlineA must report NP as signed out"); } + NpCreateAsyncRequestParameter asyncParam{}; + const int asyncRequest = sceNpCreateAsyncRequest(&asyncParam); + Require(asyncRequest > 0, "sceNpCreateAsyncRequest must return a positive request id"); + Require(sceNpSetGamePresenceOnlineA(asyncRequest, InitialUserId, 0) == SignedOut, "sceNpSetGamePresenceOnlineA must report NP as signed out"); + Require(sceNpDeleteRequest(asyncRequest) == 0, "sceNpDeleteRequest failed"); + Require(sceNpSetGamePresenceOnlineA(asyncRequest, InitialUserId, 0) == RequestNotFound, "sceNpSetGamePresenceOnlineA must reject a deleted request"); + Require(sceNpSetGamePresenceOnlineA(request, InvalidUserId, 0) == InvalidArgument, "sceNpSetGamePresenceOnlineA must reject the invalid user id"); + Require(sceNpSetGamePresenceOnlineA(0, InitialUserId, 0) == InvalidArgument, "sceNpSetGamePresenceOnlineA must reject request id 0"); + Require(sceNpSetGamePresenceOnlineA(NeverCreated, InitialUserId, 0) == RequestNotFound, "sceNpSetGamePresenceOnlineA must reject a request that was never created"); + Require(sceNpSetGamePresenceOnlineA(NeverCreated, InvalidUserId, 0) == InvalidArgument, "sceNpSetGamePresenceOnlineA must check the user before the request"); 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"); @@ -523,6 +537,7 @@ void CheckUserIdQueries() { 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(sceNpSetGamePresenceOnlineA(request, InitialUserId, 0) == RequestNotFound, "sceNpSetGamePresenceOnlineA 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; @@ -683,5 +698,34 @@ int main() { std::memset(&untouchedPremiumResult, 0x5a, sizeof(untouchedPremiumResult)); Require(sceNpCheckPremium(1, &premiumParameter, &premiumResult) == SignedOut, "sceNpCheckPremium must report the user as signed out"); Require(std::memcmp(&premiumResult, &untouchedPremiumResult, sizeof(premiumResult)) == 0, "sceNpCheckPremium must leave the result untouched"); + const int messageContext = sceNpInGameMessageInitialize(0x4000, nullptr); + Require(messageContext > 0, "sceNpInGameMessageInitialize must return a context id"); + const int otherMessageContext = sceNpInGameMessageInitialize(0x4000, nullptr); + Require(otherMessageContext > 0 && otherMessageContext != messageContext, "sceNpInGameMessageInitialize must return a new id for each context"); + Require(sceNpInGameMessageCreateHandle(-1) == InGameMessageContextNotFound, "sceNpInGameMessageCreateHandle must reject an unknown context"); + const int messageHandle = sceNpInGameMessageCreateHandle(messageContext); + Require(messageHandle > 0, "sceNpInGameMessageCreateHandle must return a handle id"); + const int secondMessageHandle = sceNpInGameMessageCreateHandle(messageContext); + Require(secondMessageHandle > 0 && secondMessageHandle != messageHandle, "sceNpInGameMessageCreateHandle must return a new id for each handle"); + Require(sceNpInGameMessageDeleteHandle(messageContext, secondMessageHandle) == 0, "sceNpInGameMessageDeleteHandle must delete the second handle"); + Require(sceNpInGameMessageDeleteHandle(otherMessageContext, messageHandle) == InGameMessageHandleNotFound, "a handle must belong to the context that created it"); + Require(sceNpInGameMessageAbortHandle(-1, messageHandle) == InGameMessageContextNotFound, "sceNpInGameMessageAbortHandle must reject an unknown context"); + Require(sceNpInGameMessageAbortHandle(messageContext, -1) == InGameMessageHandleNotFound, "sceNpInGameMessageAbortHandle must reject an unknown handle"); + Require(sceNpInGameMessageAbortHandle(messageContext, messageHandle) == 0, "sceNpInGameMessageAbortHandle must accept a live handle"); + Require(sceNpInGameMessageDeleteHandle(messageContext, messageHandle) == 0, "sceNpInGameMessageDeleteHandle must delete the handle"); + Require(sceNpInGameMessageDeleteHandle(messageContext, messageHandle) == InGameMessageHandleNotFound, "a deleted handle must not be found"); + Require(sceNpInGameMessageTerminate(messageContext) == 0, "sceNpInGameMessageTerminate must end the context"); + Require(sceNpInGameMessageTerminate(messageContext) == InGameMessageContextNotFound, "a terminated context must not be found"); + Require(sceNpInGameMessageCreateHandle(messageContext) == InGameMessageContextNotFound, "a terminated context must not create handles"); + Require(sceNpInGameMessageTerminate(otherMessageContext) == 0, "sceNpInGameMessageTerminate must end the second context"); + std::int32_t nextMessageId = std::numeric_limits::max() - 1; + Require(TakeInGameMessageId(nextMessageId, "ids") == std::numeric_limits::max() - 1, "the last in-game message id below the limit must be handed out"); + try { + TakeInGameMessageId(nextMessageId, "ids"); + Require(false, "the in-game message ids must not pass the 32-bit limit"); + } catch (const std::runtime_error& error) { + Require(std::string(error.what()) == "ids: the in-game message ids are exhausted", "the exhausted in-game message ids threw"); + } + Require(nextMessageId == std::numeric_limits::max(), "an exhausted in-game message id must not wrap"); return 0; } diff --git a/core/libs/tests/GuestNpMatching2Offline.cpp b/core/libs/tests/GuestNpMatching2Offline.cpp index 67687e16d6..79f12f6d62 100644 --- a/core/libs/tests/GuestNpMatching2Offline.cpp +++ b/core/libs/tests/GuestNpMatching2Offline.cpp @@ -1,9 +1,16 @@ #include "prx/libSceNpMatching2/include/Api.hpp" #include "SceTypes.hpp" +#include #include #include +#include #include #include +#ifdef _WIN32 +#include +#else +#include +#endif @@ -18,6 +25,14 @@ constexpr int ContextMax = static_cast(0x80550C04u); constexpr int ContextNotStarted = static_cast(0x80550C08u); constexpr int InvalidArgument = static_cast(0x80550C0Au); constexpr int InvalidContextId = static_cast(0x80550C0Bu); +constexpr int ContextNotFound = static_cast(0x80550C06u); +constexpr int InvalidServerId = static_cast(0x80550C0Cu); +constexpr int InvalidRoomId = static_cast(0x80550C0Fu); +constexpr int ServerNotAvailable = static_cast(0x80550C28u); +constexpr int SessionNotFound = static_cast(0x80550C3Au); +constexpr int SignalingContextNotFound = static_cast(0x80550E05u); +constexpr int SignalingInvalidArgument = static_cast(0x80550E15u); +constexpr int SignalingPeerNotFound = static_cast(0x80550E19u); constexpr std::int32_t ConnStatusInactive = 0; constexpr std::int32_t StatusSentinel = 0x5A5A5A5A; constexpr std::uint32_t PeerAddrSentinel = 0xA5A5A5A5u; @@ -31,6 +46,104 @@ void Require(bool value, const char* name, const char* what) { ++failures; } +using CreateContextAFn = decltype(&sceNpMatching2CreateContextA); +using RegisterLobbyCallbackFn = int (APS5_VABI*)(std::uint16_t, void*, void*); +using RequestFn = int (APS5_VABI*)(std::uint16_t, const void*, const void*, std::uint32_t*); +using GetServerIdFn = int (APS5_VABI*)(std::uint16_t, std::uint16_t*); +using SlotMaskFn = int (APS5_VABI*)(std::uint16_t, std::uint64_t, std::uint64_t*); +using WorldIdsFn = int (APS5_VABI*)(std::uint16_t, void*, std::uint64_t, void*); +using ConnectionInfoFn = int (APS5_VABI*)(std::uint16_t, std::uint64_t, std::uint16_t, std::int32_t, void*); +using GetPortFn = int (APS5_VABI*)(std::uint16_t*); +using SetPortFn = int (APS5_VABI*)(std::uint16_t); + +template +Function Resolve(const char* name) { +#ifdef _WIN32 + void* symbol = reinterpret_cast(GetProcAddress(GetModuleHandleA("libSceNpMatching2.prx"), name)); +#else + void* symbol = dlsym(RTLD_DEFAULT, name); +#endif + if (!symbol) { + std::fprintf(stderr, "%s is not exported by libSceNpMatching2\n", name); + std::abort(); + } + return reinterpret_cast(symbol); +} + +void CheckOfflineRequests(CreateContextAFn createContextA) { + const auto getWorldInfoList = Resolve("sceNpMatching2GetWorldInfoList"); + const auto setUserInfo = Resolve("sceNpMatching2SetUserInfo"); + const auto setRoomDataExternal = Resolve("sceNpMatching2SetRoomDataExternal"); + const auto setRoomDataInternal = Resolve("sceNpMatching2SetRoomDataInternal"); + const auto getRoomDataInternal = Resolve("sceNpMatching2GetRoomDataInternal"); + const auto getPingInfo = Resolve("sceNpMatching2SignalingGetPingInfo"); + const auto leaveRoom = Resolve("sceNpMatching2LeaveRoom"); + const auto getServerId = Resolve("sceNpMatching2GetServerId"); + const auto getSlotMask = Resolve("sceNpMatching2GetRoomJoinedSlotMaskLocal"); + const auto getWorldIds = Resolve("sceNpMatching2GetWorldIdArrayForAllServers"); + const auto getConnectionInfo = Resolve("sceNpMatching2SignalingGetConnectionInfo"); + const auto getConnectionInfoA = Resolve("sceNpMatching2SignalingGetConnectionInfoA"); + const auto getPort = Resolve("sceNpMatching2SignalingGetPort"); + const auto setPort = Resolve("sceNpMatching2SignalingSetPort"); + unsigned char serverRequest[32] = {1}; + unsigned char roomRequest[64] = {5}; + unsigned char zeroed[64] = {}; + std::uint32_t requestId = 77; + + Require(getWorldInfoList(1, serverRequest, nullptr, &requestId) == NotInitialized && requestId == 77, "sceNpMatching2GetWorldInfoList", "a request before initialization did not return NOT_INITIALIZED"); + NpMatching2InitializeParameter init{}; + init.size = 0x30; + NpMatching2CreateContextParameter create{}; + create.size = 0x28; + NpMatching2CreateContextParameterA createA{1, 0, 0x10}; + std::uint16_t context = 0; + std::uint16_t contextA = 0; + Require(sceNpMatching2Initialize(&init) == 0, "sceNpMatching2Initialize", "initializing for the requests failed"); + Require(sceNpMatching2CreateContext(&create, &context) == 0 && context == 1, "sceNpMatching2CreateContext", "the request context is not id 1"); + Require(createContextA(&createA, &contextA) == 0 && contextA == 0x102, "sceNpMatching2CreateContextA", "the request A context is not id 0x102"); + + Require(getWorldInfoList(context, nullptr, nullptr, &requestId) == InvalidArgument, "sceNpMatching2GetWorldInfoList", "a null request was taken"); + Require(getWorldInfoList(context, serverRequest, nullptr, nullptr) == InvalidArgument, "sceNpMatching2GetWorldInfoList", "a null request id was taken"); + for (const RequestFn request : {getWorldInfoList, setUserInfo}) { + Require(request(context, zeroed, nullptr, &requestId) == InvalidServerId, "server request", "server id 0 did not return INVALID_SERVER_ID"); + Require(request(99, zeroed, nullptr, &requestId) == InvalidServerId, "server request", "the server id was not checked before the context"); + Require(request(99, serverRequest, nullptr, &requestId) == ContextNotFound && request(0, serverRequest, nullptr, &requestId) == ContextNotFound, "server request", "an unknown context did not return CONTEXT_NOT_FOUND"); + } + for (const RequestFn request : {setRoomDataExternal, setRoomDataInternal, getRoomDataInternal, getPingInfo, leaveRoom}) { + Require(request(context, zeroed, nullptr, &requestId) == InvalidRoomId, "room request", "room id 0 did not return INVALID_ROOM_ID"); + Require(request(99, zeroed, nullptr, &requestId) == InvalidRoomId, "room request", "the room id was not checked before the context"); + Require(request(99, roomRequest, nullptr, &requestId) == ContextNotFound, "room request", "an unknown context did not return CONTEXT_NOT_FOUND"); + } + Require(requestId == 77, "request", "a refused request wrote a request id"); + Require(leaveRoom(context, roomRequest, nullptr, &requestId) == SessionNotFound && leaveRoom(contextA, roomRequest, nullptr, &requestId) == SessionNotFound && requestId == 77, "sceNpMatching2LeaveRoom", "leaving with no room joined did not return SESSION_NOT_FOUND"); + + std::uint16_t serverId = 0xA5A5; + Require(getServerId(99, nullptr) == InvalidArgument, "sceNpMatching2GetServerId", "a null output was not refused before the context"); + Require(getServerId(99, &serverId) == ContextNotFound, "sceNpMatching2GetServerId", "an unknown context did not return CONTEXT_NOT_FOUND"); + Require(getServerId(context, &serverId) == ContextNotStarted && getServerId(contextA, &serverId) == ContextNotStarted && serverId == 0xA5A5, "sceNpMatching2GetServerId", "a never-started context did not return CONTEXT_NOT_STARTED without writing"); + std::uint64_t mask = 0xA5A5A5A5A5A5A5A5ull; + Require(getSlotMask(99, 5, nullptr) == InvalidArgument, "sceNpMatching2GetRoomJoinedSlotMaskLocal", "a null mask was not refused before the context"); + Require(getSlotMask(context, 0, &mask) == InvalidServerId && getSlotMask(contextA, 0, &mask) == InvalidServerId, "sceNpMatching2GetRoomJoinedSlotMaskLocal", "room 0 did not return 0x80550C0C"); + Require(getSlotMask(99, 0, &mask) == ContextNotFound, "sceNpMatching2GetRoomJoinedSlotMaskLocal", "room 0 was checked before the context"); + Require(getSlotMask(99, 5, &mask) == ContextNotFound, "sceNpMatching2GetRoomJoinedSlotMaskLocal", "an unknown context did not return CONTEXT_NOT_FOUND"); + Require(getSlotMask(context, 5, &mask) == ContextNotStarted && mask == 0xA5A5A5A5A5A5A5A5ull, "sceNpMatching2GetRoomJoinedSlotMaskLocal", "a never-started context did not return CONTEXT_NOT_STARTED without writing"); + std::uint32_t worldIds[4] = {0xA5A5A5A5u, 0xA5A5A5A5u, 0xA5A5A5A5u, 0xA5A5A5A5u}; + std::uint32_t worldIdCount = 0xA5A5A5A5u; + Require(getWorldIds(99, nullptr, 0, nullptr) == ContextNotFound, "sceNpMatching2GetWorldIdArrayForAllServers", "the context was not checked before the pointers"); + Require(getWorldIds(context, nullptr, 0, nullptr) == InvalidArgument, "sceNpMatching2GetWorldIdArrayForAllServers", "null pointers were taken"); + Require(getWorldIds(context, worldIds, 4, &worldIdCount) == ServerNotAvailable && worldIds[0] == 0xA5A5A5A5u && worldIdCount == 0xA5A5A5A5u, "sceNpMatching2GetWorldIdArrayForAllServers", "a never-started context did not return SERVER_NOT_AVAILABLE without writing"); + unsigned char info[64] = {}; + for (const ConnectionInfoFn connectionInfo : {getConnectionInfo, getConnectionInfoA}) { + Require(connectionInfo(99, 5, 1, 1, nullptr) == SignalingInvalidArgument, "connection info", "a null info was not refused with 0x80550E15 before the context"); + Require(connectionInfo(99, 5, 1, 1, info) == SignalingContextNotFound, "connection info", "an unknown context did not return 0x80550E05"); + Require(connectionInfo(context, 5, 1, 1, info) == SignalingPeerNotFound, "connection info", "a never-started context did not return 0x80550E19"); + } + std::uint16_t port = 0xA5A5; + Require(getPort(&port) == 0 && port == 0, "sceNpMatching2SignalingGetPort", "the port did not start at 0"); + Require(setPort(3659) == 0 && getPort(&port) == 0 && port == 3659, "sceNpMatching2SignalingSetPort", "the port that was set is not reported"); + Require(sceNpMatching2Terminate() == 0, "sceNpMatching2Terminate", "terminating after the requests failed"); +} + bool NullStatusThrows(std::uint16_t ctxId) { try { sceNpMatching2SignalingGetConnectionStatus(ctxId, 1, 1, nullptr, nullptr, nullptr); @@ -43,9 +156,13 @@ bool NullStatusThrows(std::uint16_t ctxId) { } int main() { + const auto createContextA = Resolve("sceNpMatching2CreateContextA"); + const auto registerLobbyEvent = Resolve("sceNpMatching2RegisterLobbyEventCallback"); + const auto registerLobbyMessage = Resolve("sceNpMatching2RegisterLobbyMessageCallback"); NpMatching2InitializeParameter init{}; NpMatching2CreateContextParameter create{}; create.size = 0x28; + NpMatching2CreateContextParameterA createA{1, 0, 0x10}; unsigned char requestOpt[24] = {}; std::uint16_t context = 77; @@ -61,6 +178,9 @@ int main() { Require(sceNpMatching2RegisterRoomEventCallback(1, nullptr, nullptr) == NotInitialized, "sceNpMatching2RegisterRoomEventCallback", "a call before initialization did not return NOT_INITIALIZED"); Require(sceNpMatching2RegisterRoomMessageCallback(1, nullptr, nullptr) == NotInitialized, "sceNpMatching2RegisterRoomMessageCallback", "a call before initialization did not return NOT_INITIALIZED"); Require(sceNpMatching2RegisterSignalingCallback(1, nullptr, nullptr) == NotInitialized, "sceNpMatching2RegisterSignalingCallback", "a call before initialization did not return NOT_INITIALIZED"); + Require(createContextA(nullptr, nullptr) == NotInitialized, "sceNpMatching2CreateContextA", "a call before initialization did not return NOT_INITIALIZED first"); + Require(registerLobbyEvent(1, nullptr, nullptr) == NotInitialized, "sceNpMatching2RegisterLobbyEventCallback", "a call before initialization did not return NOT_INITIALIZED"); + Require(registerLobbyMessage(1, nullptr, nullptr) == NotInitialized, "sceNpMatching2RegisterLobbyMessageCallback", "a call before initialization did not return NOT_INITIALIZED"); Require(sceNpMatching2SetDefaultRequestOptParam(1, nullptr) == NotInitialized, "sceNpMatching2SetDefaultRequestOptParam", "a call before initialization did not return NOT_INITIALIZED first"); Require(sceNpMatching2Terminate() == NotInitialized, "sceNpMatching2Terminate", "terminating before initialization did not return NOT_INITIALIZED"); Require(sceNpMatching2Initialize(nullptr) == InvalidArgument, "sceNpMatching2Initialize", "a null parameter did not return INVALID_ARGUMENT"); @@ -120,6 +240,23 @@ int main() { Require(sceNpMatching2RegisterRoomMessageCallback(1, nullptr, nullptr) == 0, "sceNpMatching2RegisterRoomMessageCallback", "registering failed"); Require(sceNpMatching2RegisterSignalingCallback(99, nullptr, nullptr) == InvalidContextId, "sceNpMatching2RegisterSignalingCallback", "an unknown context was taken"); Require(sceNpMatching2RegisterSignalingCallback(1, nullptr, nullptr) == 0, "sceNpMatching2RegisterSignalingCallback", "registering failed"); + Require(registerLobbyEvent(99, nullptr, nullptr) == InvalidContextId, "sceNpMatching2RegisterLobbyEventCallback", "an unknown context was taken"); + Require(registerLobbyEvent(1, nullptr, nullptr) == 0, "sceNpMatching2RegisterLobbyEventCallback", "registering failed"); + Require(registerLobbyMessage(99, nullptr, nullptr) == InvalidContextId, "sceNpMatching2RegisterLobbyMessageCallback", "an unknown context was taken"); + Require(registerLobbyMessage(1, nullptr, nullptr) == 0, "sceNpMatching2RegisterLobbyMessageCallback", "registering failed"); + created = 77; + Require(createContextA(nullptr, &created) == InvalidArgument, "sceNpMatching2CreateContextA", "a null parameter was taken"); + createA.size = 0x28; + Require(createContextA(&createA, &created) == InvalidArgument, "sceNpMatching2CreateContextA", "parameter size 0x28 was taken"); + createA.size = 0x10; + Require(createContextA(&createA, nullptr) == InvalidArgument, "sceNpMatching2CreateContextA", "a null output was taken"); + Require(created == 77, "sceNpMatching2CreateContextA", "a refused call wrote the context id"); + Require(createContextA(&createA, &created) == 0 && created == 0x102, "sceNpMatching2CreateContextA", "the freed slot 2 was not handed out as A context 0x102"); + created = 77; + Require(createContextA(&createA, &created) == ContextMax && created == 77, "sceNpMatching2CreateContextA", "an eleventh context did not return CONTEXT_MAX"); + Require(sceNpMatching2ContextStop(0x102) == ContextNotStarted, "sceNpMatching2CreateContextA", "the context it created is not a regular context"); + Require(sceNpMatching2DestroyContext(0x102) == 0, "sceNpMatching2CreateContextA", "the A context was not destroyed"); + Require(sceNpMatching2CreateContext(&create, &created) == 0 && created == 2, "sceNpMatching2CreateContextA", "destroying the A context did not free slot 2"); Require(sceNpMatching2SetDefaultRequestOptParam(99, nullptr) == InvalidArgument, "sceNpMatching2SetDefaultRequestOptParam", "a null option was not refused before the context"); Require(sceNpMatching2SetDefaultRequestOptParam(99, requestOpt) == InvalidContextId, "sceNpMatching2SetDefaultRequestOptParam", "an unknown context was taken"); Require(sceNpMatching2SetDefaultRequestOptParam(1, requestOpt) == 0, "sceNpMatching2SetDefaultRequestOptParam", "setting the default option failed"); @@ -139,6 +276,10 @@ int main() { Require(sceNpMatching2ContextStop(1) == NotInitialized, "sceNpMatching2ContextStop", "a call after termination did not return NOT_INITIALIZED"); Require(sceNpMatching2RegisterContextCallback(nullptr, nullptr) == NotInitialized, "sceNpMatching2RegisterContextCallback", "a call after termination did not return NOT_INITIALIZED"); Require(sceNpMatching2RegisterSignalingCallback(1, nullptr, nullptr) == NotInitialized, "sceNpMatching2RegisterSignalingCallback", "a call after termination did not return NOT_INITIALIZED"); + Require(registerLobbyEvent(1, nullptr, nullptr) == NotInitialized, "sceNpMatching2RegisterLobbyEventCallback", "a call after termination did not return NOT_INITIALIZED"); + Require(createContextA(&createA, &created) == NotInitialized, "sceNpMatching2CreateContextA", "a call after termination did not return NOT_INITIALIZED"); + + CheckOfflineRequests(createContextA); if (failures != 0) return 1; std::puts("NpMatching2 offline tests passed"); diff --git a/core/libs/tests/GuestNpProfileDialog.cpp b/core/libs/tests/GuestNpProfileDialog.cpp index 49ba4eb805..2fb03b3baf 100644 --- a/core/libs/tests/GuestNpProfileDialog.cpp +++ b/core/libs/tests/GuestNpProfileDialog.cpp @@ -176,6 +176,7 @@ int main() { Require(sceNpProfileDialogGetStatus() == kStatusFinished); Require(sceNpProfileDialogUpdateStatus() == kStatusFinished); Require(sceNpProfileDialogGetResult(nullptr) == kErrArgNull); + Require(sceNpProfileDialogGetResult(reinterpret_cast(0x10)) == static_cast(0x80B8000Au)); Require(sceNpProfileDialogGetResult(reinterpret_cast(result)) == 0); RequireCanceled(result, &first); RequireThrows([] { sceNpProfileDialogClose(); }); diff --git a/core/libs/tests/GuestNpScoreOffline.cpp b/core/libs/tests/GuestNpScoreOffline.cpp index 15cc132c95..285a181019 100644 --- a/core/libs/tests/GuestNpScoreOffline.cpp +++ b/core/libs/tests/GuestNpScoreOffline.cpp @@ -2,7 +2,14 @@ #include "SceTypes.hpp" #include #include +#include #include +#include +#ifdef _WIN32 +#include +#else +#include +#endif namespace { @@ -27,6 +34,42 @@ unsigned char infos[2 * 200]; unsigned char board[24]; unsigned char manyNpIds[101 * 36]; unsigned char manyRanks[101 * 0x88]; +unsigned char ranksA[2 * 0x98]; +std::uint64_t accountIds[2] = {1, 2}; +unsigned char npIdPcIds[2 * 44]; +unsigned char accountIdPcIds[2 * 16]; + +using RankingFn = int (APS5_VABI*)(int, std::uint32_t, const void*, std::uint64_t, void*, std::uint64_t, void*, std::uint64_t, void*, std::uint64_t, std::uint64_t, void*, void*, void*); +using AccountRankingFn = int (APS5_VABI*)(int, std::uint32_t, const std::uint64_t*, std::uint64_t, void*, std::uint64_t, void*, std::uint64_t, void*, std::uint64_t, std::uint64_t, void*, void*, void*); +using RangeFn = int (APS5_VABI*)(int, std::uint32_t, std::uint32_t, void*, std::uint64_t, void*, std::uint64_t, void*, std::uint64_t, std::uint64_t, void*, void*, void*); +using FriendsFn = int (APS5_VABI*)(int, std::uint32_t, int, void*, std::uint64_t, void*, std::uint64_t, void*, std::uint64_t, std::uint64_t, void*, void*, const NpScoreFriendRankingOption*); +using GameDataFn = int (APS5_VABI*)(int, std::uint32_t, const void*, std::uint64_t*, std::uint64_t, void*, void*); +using GameDataByAccountIdFn = int (APS5_VABI*)(int, std::uint32_t, std::uint64_t, std::uint64_t*, std::uint64_t, void*, void*); +using BoardInfoFn = int (APS5_VABI*)(int, std::uint32_t, void*, void*); +using CreateTitleCtxAFn = int (APS5_VABI*)(std::uint32_t, int); +using SetPlayerCharacterIdFn = int (APS5_VABI*)(int, int); +using WaitAsyncFn = int (APS5_VABI*)(int, int*); +using SetTimeoutFn = int (APS5_VABI*)(int, int, int, int, int, int); +using SetThreadParamFn = int (APS5_VABI*)(int, std::uint64_t); +using CreateTitleCtxFn = int (APS5_VABI*)(const void*, const void*, const void*); +using CensorFn = int (APS5_VABI*)(int, const char*, void*); +using SanitizeFn = int (APS5_VABI*)(int, const char*, char*, void*); +using RecordGameDataFn = int (APS5_VABI*)(int, std::uint32_t, std::int64_t, std::uint64_t, std::uint64_t, const void*, void*); +using ChangeModeFn = int (APS5_VABI*)(int, std::uint32_t); + +template +Function Resolve(const char* name) { +#ifdef _WIN32 + void* symbol = reinterpret_cast(GetProcAddress(GetModuleHandleA("libSceNpScoreRanking.prx"), name)); +#else + void* symbol = dlsym(RTLD_DEFAULT, name); +#endif + if (!symbol) { + std::fprintf(stderr, "%s is not exported by libSceNpScoreRanking\n", name); + std::abort(); + } + return reinterpret_cast(symbol); +} void Require(bool value, const char* name, const char* what) { if (value) return; @@ -43,6 +86,7 @@ bool Untouched() { for (const unsigned char value : comments) if (value != 0xA5) return false; for (const unsigned char value : infos) if (value != 0xA5) return false; for (const unsigned char value : board) if (value != 0xA5) return false; + for (const unsigned char value : ranksA) if (value != 0xA5) return false; return true; } @@ -179,6 +223,234 @@ void CheckBoard() { } +void CheckRangeAndFriendsA() { + const auto rangeA = Resolve("sceNpScoreGetRankingByRangeA"); + const auto rangeAAsync = Resolve("sceNpScoreGetRankingByRangeAAsync"); + const auto friendsA = Resolve("sceNpScoreGetFriendsRankingA"); + const auto friendsAAsync = Resolve("sceNpScoreGetFriendsRankingAAsync"); + const int req = NewRequest(); + Require(rangeA(req, 1, 0, ranksA, 0x90, nullptr, 0, nullptr, 0, 1, nullptr, nullptr, nullptr) == InvalidArgument, "sceNpScoreGetRankingByRangeA", "rank 0 was taken"); + Require(rangeA(req, 1, 1, ranksA, 0x80, nullptr, 0, nullptr, 0, 1, nullptr, nullptr, nullptr) == InvalidAlignment, "sceNpScoreGetRankingByRangeA", "a rank array size other than 0x90 per rank was taken"); + Require(rangeA(req, 1, 1, ranksA, 0x120, comments, 64, nullptr, 0, 2, nullptr, nullptr, nullptr) == InvalidAlignment, "sceNpScoreGetRankingByRangeA", "a comment array size other than 64 per rank was taken"); + CheckOffline("sceNpScoreGetRankingByRangeA", rangeA(req, 1, 1, ranksA, 0x120, comments, 128, infos, 400, 2, nullptr, nullptr, nullptr), req); + Require(rangeAAsync(req, 1, 1, ranksA, 0x90, nullptr, 0, nullptr, 0, 1, nullptr, nullptr, nullptr) == SignedOut, "sceNpScoreGetRankingByRangeAAsync", "valid arguments did not return SIGNED_OUT"); + Require(friendsA(req, 1, 1, nullptr, 0x90, nullptr, 0, nullptr, 0, 1, nullptr, nullptr, nullptr) == InvalidArgument, "sceNpScoreGetFriendsRankingA", "a null rank array was taken"); + Require(friendsA(req, 1, 1, ranksA, 0x80, nullptr, 0, nullptr, 0, 1, nullptr, nullptr, nullptr) == InvalidAlignment, "sceNpScoreGetFriendsRankingA", "a rank array size other than 0x90 per friend was taken"); + Require(friendsA(req, 1, 1, ranksA, 0x90, nullptr, 0, infos, 400, 1, nullptr, nullptr, nullptr) == InvalidAlignment, "sceNpScoreGetFriendsRankingA", "an info array size other than 200 per friend was taken"); + CheckOffline("sceNpScoreGetFriendsRankingA", friendsA(req, 1, 1, ranksA, 0x120, nullptr, 0, nullptr, 0, 2, nullptr, nullptr, nullptr), req); + Require(friendsAAsync(req, 1, 0, ranksA, 0x90, nullptr, 0, nullptr, 0, 1, nullptr, nullptr, nullptr) == SignedOut, "sceNpScoreGetFriendsRankingAAsync", "valid arguments did not return SIGNED_OUT"); + sceNpScoreDeleteRequest(req); +} + +void CheckAccountIds() { + const auto byAccountId = Resolve("sceNpScoreGetRankingByAccountId"); + const auto byAccountIdAsync = Resolve("sceNpScoreGetRankingByAccountIdAsync"); + const auto byAccountIdPcId = Resolve("sceNpScoreGetRankingByAccountIdPcId"); + const auto byAccountIdPcIdAsync = Resolve("sceNpScoreGetRankingByAccountIdPcIdAsync"); + int req = NewRequest(); + Require(byAccountId(req, 1, accountIds, 16, ranksA, 0x130, nullptr, 0, nullptr, 0, 2, nullptr, nullptr, board) == InvalidArgument, "sceNpScoreGetRankingByAccountId", "an option was taken"); + Require(byAccountId(req, 1, nullptr, 0, nullptr, 0, nullptr, 0, nullptr, 0, 102, nullptr, nullptr, nullptr) == TooManyNpId, "sceNpScoreGetRankingByAccountId", "102 account ids were not refused before the null arrays"); + Require(byAccountId(req, 1, accountIds, 0, ranksA, 0, nullptr, 0, nullptr, 0, 0, nullptr, nullptr, nullptr) == InsufficientArgument, "sceNpScoreGetRankingByAccountId", "zero account ids were taken"); + Require(byAccountId(req, 1, nullptr, 16, ranksA, 0x130, nullptr, 0, nullptr, 0, 2, nullptr, nullptr, nullptr) == InsufficientArgument, "sceNpScoreGetRankingByAccountId", "a null account id array was taken"); + Require(byAccountId(req, 1, accountIds, 8, ranksA, 0x130, nullptr, 0, nullptr, 0, 2, nullptr, nullptr, nullptr) == InvalidAlignment, "sceNpScoreGetRankingByAccountId", "an account id array size other than 8 per id was taken"); + Require(byAccountId(req, 1, accountIds, 16, ranksA, 0x110, nullptr, 0, nullptr, 0, 2, nullptr, nullptr, nullptr) == InvalidAlignment, "sceNpScoreGetRankingByAccountId", "a rank array size other than 0x98 per id was taken"); + Require(byAccountId(req + 100, 1, accountIds, 16, ranksA, 0x130, nullptr, 0, nullptr, 0, 2, nullptr, nullptr, nullptr) == InvalidId, "sceNpScoreGetRankingByAccountId", "an unknown request was taken"); + CheckOffline("sceNpScoreGetRankingByAccountId", byAccountId(req, 1, accountIds, 16, ranksA, 0x130, comments, 128, infos, 400, 2, nullptr, nullptr, nullptr), req); + Require(byAccountIdAsync(req, 1, accountIds, 8, ranksA, 0x98, nullptr, 0, nullptr, 0, 1, nullptr, nullptr, nullptr) == SignedOut, "sceNpScoreGetRankingByAccountIdAsync", "valid arguments did not return SIGNED_OUT"); + sceNpScoreDeleteRequest(req); + + req = NewRequest(); + Require(byAccountIdPcId(req, 1, accountIdPcIds, 16, ranksA, 0x130, nullptr, 0, nullptr, 0, 2, nullptr, nullptr, nullptr) == InvalidAlignment, "sceNpScoreGetRankingByAccountIdPcId", "an id array size other than 16 per id was taken"); + Require(byAccountIdPcId(req, 1, accountIdPcIds, 32, ranksA, 0x120, nullptr, 0, nullptr, 0, 2, nullptr, nullptr, nullptr) == InvalidAlignment, "sceNpScoreGetRankingByAccountIdPcId", "a rank array size other than 0x98 per id was taken"); + CheckOffline("sceNpScoreGetRankingByAccountIdPcId", byAccountIdPcId(req, 1, accountIdPcIds, 32, ranksA, 0x130, nullptr, 0, nullptr, 0, 2, nullptr, nullptr, nullptr), req); + Require(byAccountIdPcIdAsync(req, 1, accountIdPcIds, 16, ranksA, 0x98, nullptr, 0, nullptr, 0, 1, nullptr, nullptr, nullptr) == SignedOut, "sceNpScoreGetRankingByAccountIdPcIdAsync", "valid arguments did not return SIGNED_OUT"); + sceNpScoreDeleteRequest(req); +} + +void CheckNpIdPcId() { + const auto byNpIdPcId = Resolve("sceNpScoreGetRankingByNpIdPcId"); + const auto byNpIdPcIdAsync = Resolve("sceNpScoreGetRankingByNpIdPcIdAsync"); + const int req = NewRequest(); + Require(byNpIdPcId(req, 1, nullptr, 0, nullptr, 0, nullptr, 0, nullptr, 0, 102, nullptr, nullptr, nullptr) == InsufficientArgument, "sceNpScoreGetRankingByNpIdPcId", "null arrays were not refused before 102 ids"); + Require(byNpIdPcId(req, 1, npIdPcIds, 102 * 44, manyRanks, 102 * 0x88, nullptr, 0, nullptr, 0, 102, nullptr, nullptr, nullptr) == TooManyNpId, "sceNpScoreGetRankingByNpIdPcId", "102 ids were taken"); + Require(byNpIdPcId(req, 1, npIdPcIds, 72, ranks, 0x110, nullptr, 0, nullptr, 0, 2, nullptr, nullptr, nullptr) == InvalidAlignment, "sceNpScoreGetRankingByNpIdPcId", "an id array size other than 44 per id was taken"); + Require(byNpIdPcId(req, 1, npIdPcIds, 88, ranks, 0x100, nullptr, 0, nullptr, 0, 2, nullptr, nullptr, nullptr) == InvalidAlignment, "sceNpScoreGetRankingByNpIdPcId", "a rank array size other than 0x88 per id was taken"); + CheckOffline("sceNpScoreGetRankingByNpIdPcId", byNpIdPcId(req, 1, npIdPcIds, 88, ranks, 0x110, comments, 128, infos, 400, 2, nullptr, nullptr, nullptr), req); + Require(byNpIdPcIdAsync(req, 1, npIdPcIds, 44, ranks, 0x88, nullptr, 0, nullptr, 0, 1, nullptr, nullptr, nullptr) == SignedOut, "sceNpScoreGetRankingByNpIdPcIdAsync", "valid arguments did not return SIGNED_OUT"); + sceNpScoreDeleteRequest(req); +} + +void CheckGameDataAndBoard() { + const auto gameData = Resolve("sceNpScoreGetGameData"); + const auto gameDataAsync = Resolve("sceNpScoreGetGameDataAsync"); + const auto byAccountId = Resolve("sceNpScoreGetGameDataByAccountId"); + const auto byAccountIdAsync = Resolve("sceNpScoreGetGameDataByAccountIdAsync"); + const auto boardInfoAsync = Resolve("sceNpScoreGetBoardInfoAsync"); + unsigned char npId[36] = {'p', 'l', 'a', 'y', 'e', 'r'}; + const unsigned char emptyNpId[36] = {}; + std::uint64_t totalSize = 9; + unsigned char data[16]; + int req = NewRequest(); + Require(gameData(req, 1, nullptr, &totalSize, sizeof(data), data, board) == InsufficientArgument, "sceNpScoreGetGameData", "a null NpId was not refused before the option"); + Require(gameData(req, 1, npId, &totalSize, sizeof(data), data, board) == InvalidArgument, "sceNpScoreGetGameData", "an option was taken"); + Require(gameData(req + 100, 1, npId, &totalSize, sizeof(data), data, nullptr) == InvalidId, "sceNpScoreGetGameData", "an unknown request was taken"); + CheckOffline("sceNpScoreGetGameData", gameData(req, 1, npId, &totalSize, sizeof(data), data, nullptr), req); + Require(gameDataAsync(req, 1, npId, &totalSize, sizeof(data), data, nullptr) == SignedOut, "sceNpScoreGetGameDataAsync", "valid arguments did not return SIGNED_OUT"); + Require(totalSize == 9, "sceNpScoreGetGameData", "the total size was written"); + sceNpScoreDeleteRequest(req); + req = NewRequest(); + int result = 0; + Require(gameData(req, 1, emptyNpId, &totalSize, sizeof(data), data, nullptr) == InvalidArgument, "sceNpScoreGetGameData", "an empty online id was taken"); + Require(sceNpScorePollAsync(req, &result) == PollFinished && result == InvalidArgument, "sceNpScoreGetGameData", "an empty online id did not finish the request with INVALID_ARGUMENT"); + sceNpScoreDeleteRequest(req); + + req = NewRequest(); + Require(byAccountId(req, 1, 5, &totalSize, sizeof(data), data, board) == InvalidArgument, "sceNpScoreGetGameDataByAccountId", "an option was taken"); + Require(byAccountId(req, 1, 0, &totalSize, sizeof(data), data, nullptr) == InvalidArgument, "sceNpScoreGetGameDataByAccountId", "account id 0 was taken"); + Require(sceNpScorePollAsync(req, nullptr) == PollRunning, "sceNpScoreGetGameDataByAccountId", "a refused call finished the request"); + CheckOffline("sceNpScoreGetGameDataByAccountId", byAccountId(req, 1, 5, &totalSize, sizeof(data), data, nullptr), req); + Require(byAccountIdAsync(req, 1, 5, &totalSize, sizeof(data), data, nullptr) == SignedOut, "sceNpScoreGetGameDataByAccountIdAsync", "valid arguments did not return SIGNED_OUT"); + sceNpScoreDeleteRequest(req); + + req = NewRequest(); + Require(boardInfoAsync(req, 1, board, board) == InvalidArgument, "sceNpScoreGetBoardInfoAsync", "an option was taken"); + CheckOffline("sceNpScoreGetBoardInfoAsync", boardInfoAsync(req, 1, board, nullptr), req); + sceNpScoreDeleteRequest(req); +} + +void CheckContextsAndWaiting() { + const auto createA = Resolve("sceNpScoreCreateNpTitleCtxA"); + const auto setPcId = Resolve("sceNpScoreSetPlayerCharacterId"); + const auto waitAsync = Resolve("sceNpScoreWaitAsync"); + Require(createA(0, 1) == SignedOut, "sceNpScoreCreateNpTitleCtxA", "a context was created for a signed-out user"); + Require(createA(0xFFFFFFFFu, 1) == SignedOut, "sceNpScoreCreateNpTitleCtxA", "the service label was checked before the sign-in"); + const int req = NewRequest(); + Require(setPcId(context, -1) == InvalidArgument, "sceNpScoreSetPlayerCharacterId", "a negative character id was taken"); + Require(setPcId(context, 3) == 0, "sceNpScoreSetPlayerCharacterId", "a title context was not found"); + Require(setPcId(req, 3) == 0, "sceNpScoreSetPlayerCharacterId", "a request was not found"); + Require(setPcId(999, 3) == InvalidId, "sceNpScoreSetPlayerCharacterId", "an unknown id was taken"); + int result = 0; + Require(waitAsync(req + 100, &result) == InvalidId, "sceNpScoreWaitAsync", "an unknown request was taken"); + Require(sceNpScoreRecordScoreAsync(req, 1, 5, nullptr, nullptr, nullptr, nullptr, nullptr) == SignedOut, "sceNpScoreRecordScoreAsync", "valid arguments did not return SIGNED_OUT"); + Require(waitAsync(req, &result) == 0 && result == SignedOut, "sceNpScoreWaitAsync", "a finished request did not report SIGNED_OUT"); + Require(waitAsync(req, nullptr) == 0, "sceNpScoreWaitAsync", "a null result was not accepted"); + sceNpScoreDeleteRequest(req); +} + +void CheckCrossSave() { + const auto byRange = Resolve("sceNpScoreGetRankingByRangeForCrossSave"); + const auto byRangeAsync = Resolve("sceNpScoreGetRankingByRangeForCrossSaveAsync"); + const auto friends = Resolve("sceNpScoreGetFriendsRankingForCrossSave"); + const auto friendsAsync = Resolve("sceNpScoreGetFriendsRankingForCrossSaveAsync"); + const auto byAccountId = Resolve("sceNpScoreGetRankingByAccountIdForCrossSave"); + const auto byAccountIdAsync = Resolve("sceNpScoreGetRankingByAccountIdForCrossSaveAsync"); + const auto byAccountIdPcId = Resolve("sceNpScoreGetRankingByAccountIdPcIdForCrossSave"); + const auto byAccountIdPcIdAsync = Resolve("sceNpScoreGetRankingByAccountIdPcIdForCrossSaveAsync"); + int req = NewRequest(); + Require(sceNpScoreGetRankingByRange(req, 1, 1, ranksA, 2 * 0x90, nullptr, 0, nullptr, 0, 2, nullptr, nullptr, nullptr) == SignedOut, "sceNpScoreGetRankingByRange", "0x90 bytes per rank were not taken"); + Require(byRange(req, 1, 1, ranksA, 0x88, nullptr, 0, nullptr, 0, 1, nullptr, nullptr, nullptr) == InvalidAlignment, "sceNpScoreGetRankingByRangeForCrossSave", "0x88 bytes per rank were taken"); + Require(byRange(req, 1, 0, ranksA, 0x80, nullptr, 0, nullptr, 0, 1, nullptr, nullptr, nullptr) == InvalidArgument, "sceNpScoreGetRankingByRangeForCrossSave", "rank 0 was taken"); + CheckOffline("sceNpScoreGetRankingByRangeForCrossSave", byRange(req, 1, 1, ranksA, 0x90, nullptr, 0, nullptr, 0, 1, nullptr, nullptr, nullptr), req); + Require(byRangeAsync(req, 1, 1, ranks, 0x80, nullptr, 0, nullptr, 0, 1, nullptr, nullptr, nullptr) == SignedOut, "sceNpScoreGetRankingByRangeForCrossSaveAsync", "valid arguments did not return SIGNED_OUT"); + sceNpScoreDeleteRequest(req); + + req = NewRequest(); + Require(friends(req, 1, 1, ranks, 0x88, nullptr, 0, nullptr, 0, 1, nullptr, nullptr, nullptr) == InvalidAlignment, "sceNpScoreGetFriendsRankingForCrossSave", "0x88 bytes per friend were taken"); + CheckOffline("sceNpScoreGetFriendsRankingForCrossSave", friends(req, 1, 1, ranks, 0x80, nullptr, 0, nullptr, 0, 1, nullptr, nullptr, nullptr), req); + Require(friendsAsync(req, 1, 0, ranks, 0x80, nullptr, 0, nullptr, 0, 1, nullptr, nullptr, nullptr) == SignedOut, "sceNpScoreGetFriendsRankingForCrossSaveAsync", "valid arguments did not return SIGNED_OUT"); + sceNpScoreDeleteRequest(req); + + req = NewRequest(); + Require(byAccountId(req, 1, accountIds, 4, ranksA, 0x98, nullptr, 0, nullptr, 0, 1, nullptr, nullptr, nullptr) == InvalidAlignment, "sceNpScoreGetRankingByAccountIdForCrossSave", "an account id array size other than 8 per id was taken"); + CheckOffline("sceNpScoreGetRankingByAccountIdForCrossSave", byAccountId(req, 1, accountIds, 16, ranksA, 0x130, nullptr, 0, nullptr, 0, 2, nullptr, nullptr, nullptr), req); + Require(byAccountIdAsync(req, 1, accountIds, 8, ranksA, 0x98, nullptr, 0, nullptr, 0, 1, nullptr, nullptr, nullptr) == SignedOut, "sceNpScoreGetRankingByAccountIdForCrossSaveAsync", "valid arguments did not return SIGNED_OUT"); + Require(byAccountIdPcId(req, 1, accountIdPcIds, 8, ranksA, 0x98, nullptr, 0, nullptr, 0, 1, nullptr, nullptr, nullptr) == InvalidAlignment, "sceNpScoreGetRankingByAccountIdPcIdForCrossSave", "an id array size other than 16 per id was taken"); + Require(byAccountIdPcId(req, 1, accountIdPcIds, 16, ranksA, 0x98, nullptr, 0, nullptr, 0, 1, nullptr, nullptr, nullptr) == SignedOut, "sceNpScoreGetRankingByAccountIdPcIdForCrossSave", "valid arguments did not return SIGNED_OUT"); + Require(byAccountIdPcIdAsync(req, 1, accountIdPcIds, 16, ranksA, 0x98, nullptr, 0, nullptr, 0, 1, nullptr, nullptr, nullptr) == SignedOut, "sceNpScoreGetRankingByAccountIdPcIdForCrossSaveAsync", "valid arguments did not return SIGNED_OUT"); + sceNpScoreDeleteRequest(req); +} + +void CheckSettings() { + const auto setTimeout = Resolve("sceNpScoreSetTimeout"); + const auto setThreadParam = Resolve("sceNpScoreSetThreadParam"); + const auto createTitleCtx = Resolve("sceNpScoreCreateTitleCtx"); + const int req = NewRequest(); + Require(setTimeout(context, 0, 0, 0, 0, 0) == 0, "sceNpScoreSetTimeout", "a title context was not found"); + Require(setTimeout(req, 1, 1000000, 10000000, 10000000, 10000000) == 0, "sceNpScoreSetTimeout", "the minimum timeouts were refused for a request"); + Require(setTimeout(999, 0, 0, 0, 0, 0) == InvalidId, "sceNpScoreSetTimeout", "an unknown id was taken"); + Require(setTimeout(999, -1, 0, 0, 0, 0) == InvalidArgument, "sceNpScoreSetTimeout", "a negative retry count was not refused before the id"); + Require(setTimeout(req, 0, 999999, 0, 0, 0) == InvalidArgument, "sceNpScoreSetTimeout", "a resolve timeout below 1 s was taken"); + Require(setTimeout(req, 0, -1, 0, 0, 0) == InvalidArgument, "sceNpScoreSetTimeout", "a negative resolve timeout was taken"); + Require(setTimeout(req, 0, 0, 9999999, 0, 0) == InvalidArgument, "sceNpScoreSetTimeout", "a connect timeout below 10 s was taken"); + Require(setTimeout(req, 0, 0, 0, 9999999, 0) == InvalidArgument, "sceNpScoreSetTimeout", "a send timeout below 10 s was taken"); + Require(setTimeout(req, 0, 0, 0, 0, 9999999) == InvalidArgument, "sceNpScoreSetTimeout", "a receive timeout below 10 s was taken"); + Require(setThreadParam(700, 0) == 0, "sceNpScoreSetThreadParam", "setting the worker thread parameters failed"); + unsigned char titleId[12] = {}; + unsigned char titleSecret[128] = {}; + Require(createTitleCtx(nullptr, titleSecret, selfNpId) == InsufficientArgument, "sceNpScoreCreateTitleCtx", "a null title id was taken"); + Require(createTitleCtx(titleId, nullptr, selfNpId) == InsufficientArgument, "sceNpScoreCreateTitleCtx", "a null title secret was taken"); + Require(createTitleCtx(titleId, titleSecret, nullptr) == InsufficientArgument, "sceNpScoreCreateTitleCtx", "a null NpId was taken"); + const int created = createTitleCtx(titleId, titleSecret, selfNpId); + Require(created > 0, "sceNpScoreCreateTitleCtx", "no title context was created"); + Require(sceNpScoreCreateRequest(created) > 0, "sceNpScoreCreateTitleCtx", "the context does not take requests"); + Require(sceNpScoreDeleteNpTitleCtx(created) == 0, "sceNpScoreCreateTitleCtx", "the context could not be deleted"); + sceNpScoreDeleteRequest(req); +} + +} + +void CheckCommentsAndGameData() { + const auto censor = Resolve("sceNpScoreCensorComment"); + const auto censorAsync = Resolve("sceNpScoreCensorCommentAsync"); + const auto sanitize = Resolve("sceNpScoreSanitizeComment"); + const auto sanitizeAsync = Resolve("sceNpScoreSanitizeCommentAsync"); + const auto recordGameData = Resolve("sceNpScoreRecordGameData"); + const auto recordGameDataAsync = Resolve("sceNpScoreRecordGameDataAsync"); + const auto changeMode = Resolve("sceNpScoreChangeModeForOtherSaveDataOwners"); + char longest[256]; + std::memset(longest, 'a', 255); + longest[255] = '\0'; + char tooLong[257]; + std::memset(tooLong, 'a', 256); + tooLong[256] = '\0'; + char sanitized[256]; + std::memset(sanitized, 0xA5, sizeof(sanitized)); + int option = 0; + const unsigned char data[16] = {}; + + for (const CensorFn call : {censor, censorAsync}) { + const int req = NewRequest(); + Require(call(req, longest, &option) == InvalidArgument, "sceNpScoreCensorComment", "an option was taken"); + Require(call(req + 100, nullptr, nullptr) == InsufficientArgument, "sceNpScoreCensorComment", "a null comment was not refused before the request lookup"); + Require(call(req + 100, tooLong, nullptr) == InvalidId, "sceNpScoreCensorComment", "the request was not looked up before the length"); + Require(call(req, tooLong, nullptr) == InvalidArgument, "sceNpScoreCensorComment", "a 256-character comment was taken"); + Require(sceNpScorePollAsync(req, nullptr) == PollRunning, "sceNpScoreCensorComment", "a refused call finished the request"); + CheckOffline("sceNpScoreCensorComment", call(req, longest, nullptr), req); + sceNpScoreDeleteRequest(req); + } + for (const SanitizeFn call : {sanitize, sanitizeAsync}) { + const int req = NewRequest(); + Require(call(req, longest, sanitized, &option) == InvalidArgument, "sceNpScoreSanitizeComment", "an option was taken"); + Require(call(req + 100, nullptr, sanitized, nullptr) == InsufficientArgument, "sceNpScoreSanitizeComment", "a null comment was not refused before the request lookup"); + Require(call(req, tooLong, sanitized, nullptr) == InvalidArgument, "sceNpScoreSanitizeComment", "a 256-character comment was taken"); + CheckOffline("sceNpScoreSanitizeComment", call(req, longest, nullptr, nullptr), req); + sceNpScoreDeleteRequest(req); + } + for (const unsigned char value : sanitized) Require(value == 0xA5, "sceNpScoreSanitizeComment", "the sanitized comment was written"); + for (const RecordGameDataFn call : {recordGameData, recordGameDataAsync}) { + const int req = NewRequest(); + Require(call(req, 1, 5, 16, 16, data, &option) == InvalidArgument, "sceNpScoreRecordGameData", "an option was taken"); + Require(call(req + 100, 1, 5, 16, 16, nullptr, nullptr) == InsufficientArgument, "sceNpScoreRecordGameData", "null data was not refused before the request lookup"); + Require(call(req + 100, 1, 5, 16, 16, data, nullptr) == InvalidId, "sceNpScoreRecordGameData", "an unknown request was taken"); + CheckOffline("sceNpScoreRecordGameData", call(req, 1, 5, 4, 16, data, nullptr), req); + sceNpScoreDeleteRequest(req); + } + const int req = NewRequest(); + Require(changeMode(req + 100, 4) == InvalidArgument && changeMode(req + 100, 0xFFFFFFFFu) == InvalidArgument, "sceNpScoreChangeModeForOtherSaveDataOwners", "a mode past 3 was not refused before the id"); + Require(changeMode(req + 100, 3) == InvalidId, "sceNpScoreChangeModeForOtherSaveDataOwners", "an unknown id was taken"); + Require(changeMode(context, 3) == 0 && changeMode(req, 0) == 0, "sceNpScoreChangeModeForOtherSaveDataOwners", "a title context or request was refused"); + Require(sceNpScorePollAsync(req, nullptr) == PollRunning, "sceNpScoreChangeModeForOtherSaveDataOwners", "changing the mode finished the request"); + sceNpScoreDeleteRequest(req); } int main() { @@ -186,6 +458,7 @@ int main() { std::memset(comments, 0xA5, sizeof(comments)); std::memset(infos, 0xA5, sizeof(infos)); std::memset(board, 0xA5, sizeof(board)); + std::memset(ranksA, 0xA5, sizeof(ranksA)); CheckContexts(); CheckRequests(); CheckRecord(); @@ -193,6 +466,14 @@ int main() { CheckNpId(); CheckFriends(); CheckBoard(); + CheckRangeAndFriendsA(); + CheckAccountIds(); + CheckNpIdPcId(); + CheckGameDataAndBoard(); + CheckContextsAndWaiting(); + CheckCrossSave(); + CheckSettings(); + CheckCommentsAndGameData(); Require(Untouched(), "libSceNpScoreRanking", "an output array was written"); if (failures != 0) return 1; std::puts("NpScoreRanking offline tests passed"); diff --git a/core/libs/tests/GuestNpTrophy.cpp b/core/libs/tests/GuestNpTrophy.cpp index 75c31429a3..c492e40b60 100644 --- a/core/libs/tests/GuestNpTrophy.cpp +++ b/core/libs/tests/GuestNpTrophy.cpp @@ -22,6 +22,7 @@ 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 InvalidTrpFileFormat = static_cast(0x80551616u); constexpr int TitleConfNotInstalled = static_cast(0x80551626u); constexpr int TrophyNotUnlocked = static_cast(0x8055161au); @@ -53,6 +54,62 @@ void WritePackage(const std::filesystem::path& path, const std::vector(bytes.data()), static_cast(bytes.size())); } +void PatchPackage(const std::filesystem::path& path, std::size_t at, std::uint64_t value, int width) { + std::vector bytes(width); + PutBigEndian(bytes, 0, value, width); + std::fstream file(path, std::ios::binary | std::ios::in | std::ios::out); + file.seekp(static_cast(at)); + file.write(reinterpret_cast(bytes.data()), static_cast(bytes.size())); + Require(static_cast(file), "patch the trophy package"); +} + +void TestPackageBounds() { + const std::filesystem::path path("app0/sce_sys/trophy/trophy02.trp"); + const std::vector> files{{"TROPCONF.ESFM", "conf"}, {"ICON0.PNG", "game-icon"}, {"TROP000.PNG", "first"}, {"TROP001.PNG", "second"}}; + WritePackage(path, files); + const auto fileSize = std::filesystem::file_size(path); + int handle = 0; + Require(sceNpTrophyCreateHandle(&handle) == 0, "create a package bounds handle"); + for (const auto& [offset, size] : std::vector>{ + {fileSize + 1, 0}, {fileSize + 1, 1}, {fileSize, 1}, {fileSize - 1, 2}, {UINT64_MAX, 1}, {1, UINT64_MAX}, {fileSize - 1, UINT64_MAX}}) { + for (const std::size_t index : {0u, 1u, 3u}) { + WritePackage(path, files); + PatchPackage(path, 0x60 + index * 0x40 + 32, offset, 8); + PatchPackage(path, 0x60 + index * 0x40 + 40, size, 8); + int context = 0; + Require(sceNpTrophyCreateContext(&context, 1, 2, 0) == 0, "create a package bounds context"); + Require(sceNpTrophyRegisterContext(context, handle, 0) == InvalidTrpFileFormat, "register a package whose entry extends past the file"); + TrophyFlagArray flags{}; + std::uint32_t count = 0; + Require(sceNpTrophyGetTrophyUnlockState(context, handle, &flags, &count) == NotRegistered, "a rejected package registered its context"); + WritePackage(path, files); + Require(sceNpTrophyRegisterContext(context, handle, 0) == 0, "retry registration after repairing the package"); + std::uint64_t iconSize = 0; + Require(sceNpTrophyGetGameIcon(context, handle, nullptr, &iconSize) == 0 && iconSize == 9, "a rejected package left stale icon metadata"); + Require(sceNpTrophyDestroyContext(context) == 0, "destroy a package bounds context"); + } + } + for (const std::uint64_t size : {0u, 4u}) { + WritePackage(path, files); + PatchPackage(path, 0x60 + 0x40 + 32, fileSize - size, 8); + PatchPackage(path, 0x60 + 0x40 + 40, size, 8); + int context = 0; + Require(sceNpTrophyCreateContext(&context, 1, 2, 0) == 0 && sceNpTrophyRegisterContext(context, handle, 0) == 0, "an entry ending exactly at EOF was rejected"); + std::uint64_t iconSize = 0; + Require(sceNpTrophyGetGameIcon(context, handle, nullptr, &iconSize) == 0 && iconSize == size, "the EOF entry has the wrong size"); + if (size != 0) { + char bytes[4]{}; + Require(sceNpTrophyGetGameIcon(context, handle, bytes, &iconSize) == 0 && std::memcmp(bytes, "cond", 4) == 0, "the EOF entry has the wrong bytes"); + } + Require(sceNpTrophyDestroyContext(context) == 0, "destroy the EOF entry context"); + } + WritePackage(path, files); + PatchPackage(path, 0x10, UINT32_MAX, 4); + int context = 0; + Require(sceNpTrophyCreateContext(&context, 1, 2, 0) == 0 && sceNpTrophyRegisterContext(context, handle, 0) == InvalidTrpFileFormat, "a table extending past the file was accepted"); + Require(sceNpTrophyDestroyContext(context) == 0 && sceNpTrophyDestroyHandle(handle) == 0, "destroy the package bounds context and handle"); +} + } int main() { @@ -146,6 +203,8 @@ int main() { Require(sceNpTrophyAbortHandle(handle) == InvalidHandle, "abort a destroyed handle"); Require(sceNpTrophyDestroyContext(context) == InvalidContext, "destroy the context twice"); + TestPackageBounds(); + std::filesystem::current_path(root.parent_path()); std::error_code error; std::filesystem::remove_all(root, error); diff --git a/core/libs/tests/GuestNpTrophy2.cpp b/core/libs/tests/GuestNpTrophy2.cpp index 923c21abd2..50365c3bbc 100644 --- a/core/libs/tests/GuestNpTrophy2.cpp +++ b/core/libs/tests/GuestNpTrophy2.cpp @@ -2,6 +2,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libSceNpTrophy2/include/NpTrophy2Constants.hpp" #include "prx/libSceNpTrophy2/include/NpTrophy2Types.hpp" +#include "tests/ExportLookup.hpp" #include #include #include @@ -10,6 +11,38 @@ static void Require(bool value) { if (!value) std::abort(); } static void APS5_VABI OnUnlock(int, int, void*) {} +static void ResolvePendingExports(const char* path) { + struct Export { + int (APS5_VABI* function)(void); + const char* name; + }; + const Export exports[] = { + {sceNpTrophy2SystemAbortHandle, "sceNpTrophy2SystemAbortHandle"}, + {sceNpTrophy2SystemBuildGroupIconUri, "sceNpTrophy2SystemBuildGroupIconUri"}, + {sceNpTrophy2SystemBuildRewardIconUri, "sceNpTrophy2SystemBuildRewardIconUri"}, + {sceNpTrophy2SystemBuildTrophyIconUri, "sceNpTrophy2SystemBuildTrophyIconUri"}, + {sceNpTrophy2SystemCheckCallback, "sceNpTrophy2SystemCheckCallback"}, + {sceNpTrophy2SystemCheckNetSyncTitles, "sceNpTrophy2SystemCheckNetSyncTitles"}, + {sceNpTrophy2SystemCreateHandle, "sceNpTrophy2SystemCreateHandle"}, + {sceNpTrophy2SystemDebugLockTrophy, "sceNpTrophy2SystemDebugLockTrophy"}, + {sceNpTrophy2SystemDebugUnlockTrophy, "sceNpTrophy2SystemDebugUnlockTrophy"}, + {sceNpTrophy2SystemDestroyContext, "sceNpTrophy2SystemDestroyContext"}, + {sceNpTrophy2SystemGetTrophyDetails, "sceNpTrophy2SystemGetTrophyDetails"}, + {sceNpTrophy2SystemGetTrophyGroupConf, "sceNpTrophy2SystemGetTrophyGroupConf"}, + {sceNpTrophy2SystemGetTrophySetArray, "sceNpTrophy2SystemGetTrophySetArray"}, + {sceNpTrophy2SystemGetTrophyTitleConf, "sceNpTrophy2SystemGetTrophyTitleConf"}, + {sceNpTrophy2SystemGetTrophyTitleDetails, "sceNpTrophy2SystemGetTrophyTitleDetails"}, + {sceNpTrophy2SystemGetTrpIconByUri, "sceNpTrophy2SystemGetTrpIconByUri"}, + {sceNpTrophy2SystemIsServerAvailable, "sceNpTrophy2SystemIsServerAvailable"}, + {sceNpTrophy2SystemRegisterTitleSyncedCallback, "sceNpTrophy2SystemRegisterTitleSyncedCallback"}, + {sceNpTrophy2SystemRegisterTitleUpdateCallback, "sceNpTrophy2SystemRegisterTitleUpdateCallback"}, + {sceNpTrophy2SystemRemoveAll, "sceNpTrophy2SystemRemoveAll"}, + {sceNpTrophy2SystemRemoveTitleData, "sceNpTrophy2SystemRemoveTitleData"}, + {sceNpTrophy2SystemRemoveUserData, "sceNpTrophy2SystemRemoveUserData"}, + }; + Require(ExportsResolveByName(path, exports, "sceNpTrophy2SystemNotExported")); +} + template static bool ThrowsInvalidArgument(Call call) { try { call(); @@ -19,7 +52,7 @@ template static bool ThrowsInvalidArgument(Call call) { return false; } -int main() { +int main(int argc, char** argv) { int userdata = 0; Require(sceNpTrophy2UnregisterUnlockCallback() == 0); Require(sceNpTrophy2RegisterUnlockCallback(reinterpret_cast(&OnUnlock), &userdata) == 0); @@ -31,6 +64,7 @@ int main() { auto* volatile linked = &sceNpTrophy2SystemCreateContext; Require(linked != nullptr); + ResolvePendingExports(argc > 1 ? argv[1] : nullptr); NpTrophy2GameDetails details{}; NpTrophy2GameData data{}; diff --git a/core/libs/tests/GuestNpTrophyData.cpp b/core/libs/tests/GuestNpTrophyData.cpp index 71275707f9..7c7c1f1fcc 100644 --- a/core/libs/tests/GuestNpTrophyData.cpp +++ b/core/libs/tests/GuestNpTrophyData.cpp @@ -3,6 +3,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libSceNpTrophy2/include/NpTrophy2Types.hpp" #include "prx/libSceNpTrophy2/include/TrophyStore.hpp" +#include "tests/ExportLookup.hpp" #include #include @@ -228,7 +229,28 @@ static void ReportExportLinked() { Require(linked != nullptr, "sceNpUniversalDataSystemIntInitialize is not linked"); } -int main() { +static void ResolveInternalExports(const char* path) { + struct Export { + int (APS5_VABI* function)(void); + const char* name; + }; + const Export exports[] = { + {sceNpUniversalDataSystemIntAbortHandle, "sceNpUniversalDataSystemIntAbortHandle"}, + {sceNpUniversalDataSystemIntCheckNetSyncTitles, "sceNpUniversalDataSystemIntCheckNetSyncTitles"}, + {sceNpUniversalDataSystemIntCreateContext, "sceNpUniversalDataSystemIntCreateContext"}, + {sceNpUniversalDataSystemIntCreateHandle, "sceNpUniversalDataSystemIntCreateHandle"}, + {sceNpUniversalDataSystemIntDeleteAllData, "sceNpUniversalDataSystemIntDeleteAllData"}, + {sceNpUniversalDataSystemIntDeleteUserDataWithServerData, "sceNpUniversalDataSystemIntDeleteUserDataWithServerData"}, + {sceNpUniversalDataSystemIntDestroyContext, "sceNpUniversalDataSystemIntDestroyContext"}, + {sceNpUniversalDataSystemIntDestroyHandle, "sceNpUniversalDataSystemIntDestroyHandle"}, + {sceNpUniversalDataSystemIntNetSyncTitles, "sceNpUniversalDataSystemIntNetSyncTitles"}, + {sceNpUniversalDataSystemIntTerminate, "sceNpUniversalDataSystemIntTerminate"}, + }; + Require(ExportsResolveByName(path, exports, "sceNpUniversalDataSystemIntNotExported"), + "the internal exports do not all resolve by name"); +} + +int main(int argc, char** argv) { const auto previous = std::filesystem::current_path(); const auto root = std::filesystem::temp_directory_path() / ("anyps5-trophy-data-" + std::to_string(std::random_device{}())); std::filesystem::create_directories(root); @@ -242,6 +264,7 @@ int main() { PostUnlocks(context); ReportPlaceholders(); ReportExportLinked(); + ResolveInternalExports(argc > 1 ? argv[1] : nullptr); std::filesystem::current_path(previous); std::filesystem::remove_all(root); diff --git a/core/libs/tests/GuestNpTrophyHelpers.cpp b/core/libs/tests/GuestNpTrophyHelpers.cpp new file mode 100644 index 0000000000..7f20a62d3b --- /dev/null +++ b/core/libs/tests/GuestNpTrophyHelpers.cpp @@ -0,0 +1,90 @@ +#include "prx/libSceNpTrophy/include/Api.hpp" + +#include +#include +#include +#include +#ifdef _WIN32 +#include +#else +#include +#endif + +namespace { + +using WordsFn = int (APS5_VABI*)(const std::uint32_t*); +using BytesFn = int (APS5_VABI*)(const std::uint8_t*); +using FlagArrayFn = int (APS5_VABI*)(const std::uint8_t*, std::uint8_t*); + +template +Function Resolve(const char* name) { +#ifdef _WIN32 + void* symbol = reinterpret_cast(GetProcAddress(LoadLibraryA("libSceNpTrophy.prx"), name)); +#else + void* symbol = dlsym(RTLD_DEFAULT, name); +#endif + if (!symbol) { + std::fprintf(stderr, "%s is not exported by libSceNpTrophy\n", name); + std::abort(); + } + return reinterpret_cast(symbol); +} + +void Require(bool value, const char* what) { + if (value) return; + std::fprintf(stderr, "NpTrophy helpers: %s\n", what); + std::abort(); +} + +} + +int main() { + const auto numInfoGetTotal = Resolve("sceNpTrophyNumInfoGetTotal"); + const auto groupArrayGetNum = Resolve("sceNpTrophyGroupArrayGetNum"); + const auto setInfoGetTrophyNum = Resolve("sceNpTrophySetInfoGetTrophyNum"); + const auto setInfoGetTrophyFlagArray = Resolve("sceNpTrophySetInfoGetTrophyFlagArray"); + const auto configHasGroupFeature = Resolve("sceNpTrophyConfigHasGroupFeature"); + + const std::uint32_t counts[4] = {1, 20, 300, 4000}; + Require(numInfoGetTotal(counts) == 4321, "the four counts were not summed"); + const std::uint32_t wrapping[4] = {0xFFFFFFFFu, 2, 0, 0}; + Require(numInfoGetTotal(wrapping) == 1, "the sum did not wrap at 32 bits"); + + const std::uint32_t groups[4] = {0x12345678u, 0xFFFFFFFFu, 0xFFFFFFFFu, 0xFFFFFFFFu}; + Require(groupArrayGetNum(groups) == 13, "the group count read more than the first word"); + const std::uint32_t allGroups[4] = {0xFFFFFFFFu, 0, 0, 0}; + Require(groupArrayGetNum(allGroups) == 32, "32 groups were not counted"); + + std::uint8_t setInfo[128]; + std::memset(setInfo, 0x0F, 64); + std::memset(setInfo + 64, 0xFF, 64); + Require(setInfoGetTrophyNum(setInfo) == 256, "the trophy count read past the four flag arrays"); + + std::memset(setInfo, 0, sizeof(setInfo)); + std::memset(setInfo + 32, 0xFF, 16); + std::uint8_t flags[32]; + std::memset(flags, 0xA5, sizeof(flags)); + Require(setInfoGetTrophyFlagArray(setInfo, flags) == 128, "a full flag array did not return 128 set bits"); + for (std::size_t index = 0; index < 16; ++index) Require(flags[index] == 0xFF, "one full flag array did not fill the output"); + for (std::size_t index = 16; index < 32; ++index) Require(flags[index] == 0xA5, "bytes past the 16-byte output were written"); + std::memset(setInfo, 0, sizeof(setInfo)); + setInfo[0] = 0x01; + setInfo[16] = 0x02; + setInfo[32] = 0x04; + setInfo[48 + 15] = 0x80; + Require(setInfoGetTrophyFlagArray(setInfo, flags) == 4 && flags[0] == 0x07 && flags[15] == 0x80 && flags[1] == 0, "the four flag arrays were not combined with OR, or the set bits were not counted"); + std::memset(setInfo, 0, sizeof(setInfo)); + Require(setInfoGetTrophyFlagArray(setInfo, flags) == 0 && flags[0] == 0, "an empty set did not return 0"); + setInfo[0] = 0x01; + Require(setInfoGetTrophyFlagArray(setInfo, flags) == 1, "a single trophy did not return 1"); + + std::uint8_t config[0x100] = {}; + Require(configHasGroupFeature(nullptr) == 0, "a null config did not answer 0"); + config[0x90] = 0x03; + Require(configHasGroupFeature(config) == 1, "bit 0 of the feature byte was not reported"); + config[0x90] = 0x02; + Require(configHasGroupFeature(config) == 0, "a bit other than bit 0 was reported"); + + std::puts("NpTrophy helper tests passed"); + return 0; +} diff --git a/core/libs/tests/GuestPad.cpp b/core/libs/tests/GuestPad.cpp index d894f17ec9..960817c100 100644 --- a/core/libs/tests/GuestPad.cpp +++ b/core/libs/tests/GuestPad.cpp @@ -4,6 +4,9 @@ #include "prx/libScePad/include/InputCommandFile.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include +#include +#include +#include #include #include #include @@ -154,11 +157,23 @@ static void CheckStickDeadZones(int handle) { Require(info.stickInfo.deadZoneLeft == 13 && info.stickInfo.deadZoneRight == 13); } -static void CheckTouchContact() { +static void CheckTouchContact(int handle) { + Require(scePadReadState(handle, nullptr) == PAD_ERROR_INVALID_ARG); + Require(scePadRead_nid_postfix(handle, nullptr, 1) == PAD_ERROR_INVALID_ARG); + for (const int invalid : {-1, 0, handle + 1, 9999}) { + Require(scePadReadState(invalid, nullptr) == PAD_ERROR_INVALID_HANDLE); + Require(scePadRead_nid_postfix(invalid, nullptr, 1) == PAD_ERROR_INVALID_HANDLE); + } PadInputState touch; touch.buttons = static_cast(Pad::PadButton::TouchPad); touch.touch[0] = {true, 960, 471, 0}; PadPublishInput_nid_postfix(touch); + PadData stateData{}; + Require(scePadReadState(handle, &stateData) == PAD_OK); + Require((stateData.buttons & static_cast(Pad::PadButton::TouchPad)) != 0); + Require(stateData.touch_data_touch_num == 1); + Require(stateData.touch_data_touch0_x == 960); + Require(stateData.touch_data_touch0_y == 471); const auto data = Pad::ReadState(); Require((data.buttons & static_cast(Pad::PadButton::TouchPad)) != 0); Require(data.touch_data_touch_num == 1); @@ -215,6 +230,10 @@ static void CheckHandleQueries(int user, int handle) { PadMotionSensorPosition position{1.0f, 2.0f, 3.0f}; Require(scePadGetMotionSensorPosition(handle, nullptr) == PAD_ERROR_INVALID_ARG); Require(scePadGetMotionSensorPosition(closed, &position) == PAD_ERROR_INVALID_HANDLE); + for (const int invalid : {-1, 0, 9999}) { + Require(scePadGetMotionSensorPosition(invalid, &position) == PAD_ERROR_INVALID_HANDLE); + Require(scePadGetMotionSensorPosition(invalid, nullptr) == PAD_ERROR_INVALID_ARG); + } Require(scePadGetMotionSensorPosition(handle, &position) == notConnected); Require(position.x == 1.0f && position.y == 2.0f && position.z == 3.0f); @@ -249,10 +268,15 @@ static void CheckHandleQueries(int user, int handle) { Require(!scePadIsValidHandle(special, PAD_PORT_TYPE_SPECIAL)); } -static void CheckVirtualTouchContact() { +static void CheckVirtualTouchContact(int handle) { PadInputState left; left.touchLeft = true; PadPublishInput_nid_postfix(left); + PadData leftState{}; + Require(scePadReadState(handle, &leftState) == PAD_OK); + Require(leftState.touch_data_touch_num == 1); + Require(leftState.touch_data_touch0_x == kTouchPadWidth / 4); + Require(leftState.touch_data_touch0_y == kTouchPadHeight / 2); const auto leftData = Pad::ReadState(); Require(leftData.touch_data_touch_num == 1); Require(leftData.touch_data_touch0_x == kTouchPadWidth / 4); @@ -261,6 +285,11 @@ static void CheckVirtualTouchContact() { PadInputState right; right.touchRight = true; PadPublishInput_nid_postfix(right); + PadData rightState{}; + Require(scePadReadState(handle, &rightState) == PAD_OK); + Require(rightState.touch_data_touch_num == 1); + Require(rightState.touch_data_touch0_x == kTouchPadWidth * 3 / 4); + Require(rightState.touch_data_touch0_y == kTouchPadHeight / 2); const auto rightData = Pad::ReadState(); Require(rightData.touch_data_touch_num == 1); Require(rightData.touch_data_touch0_x == kTouchPadWidth * 3 / 4); @@ -489,6 +518,12 @@ static void CheckSpecialPort(int user, int handle) { Require(scePadRead_nid_postfix(special, data, 1) == PAD_ERROR_INVALID_HANDLE); Require(scePadReadState(special, data) == PAD_ERROR_INVALID_HANDLE); Require(scePadGetControllerInformation(special, &info) == PAD_ERROR_INVALID_HANDLE); + Require(scePadSetVibration(special, &vibration) == PAD_ERROR_INVALID_HANDLE); + Require(scePadSetVibration(special, nullptr) == PAD_ERROR_INVALID_HANDLE); + for (const int invalidHandle : {-1, -2, -100}) { + Require(scePadSetVibration(invalidHandle, &vibration) == PAD_ERROR_INVALID_HANDLE); + Require(scePadSetVibration(invalidHandle, nullptr) == PAD_ERROR_INVALID_HANDLE); + } Require(scePadGetHandle(user, 0, 0) == handle); Require(scePadReadState(handle, data) == PAD_OK); const int other = user + 1; @@ -644,6 +679,8 @@ static void CheckVibrationForce(int user, int handle) { Require(scePadSetVibrationForce(special, &strong) == PAD_OK && VibrationIs(0, 5, 6)); Require(scePadSetVibrationForce(special, nullptr) == PAD_ERROR_INVALID_ARG); Require(scePadClose_nid_postfix(special) == PAD_OK); + Require(scePadSetVibration(special, &strong) == PAD_ERROR_INVALID_HANDLE); + Require(scePadSetVibration(special, nullptr) == PAD_ERROR_INVALID_HANDLE); const PadVibrationParam off{0, 0}; Require(scePadSetVibrationForce(handle, &off) == PAD_OK && VibrationIs(0, 0, 0)); } @@ -663,6 +700,18 @@ static void CheckTriggerEffect(int user, int handle) { const std::array weapon{0x25, 0x24, 0x00, 0x07}; const std::array feedback{0x21, 0x80, 0x03, 0x00, 0x00, 0x40, 0x12}; const std::array off{0x05}; + Require(scePadSetTriggerEffect(handle, nullptr) == PAD_ERROR_INVALID_ARG); + for (const int other : {handle + 1, 0x999, -1}) Require(scePadSetTriggerEffect(other, param.data()) == PAD_ERROR_INVALID_HANDLE); + for (const int other : {handle + 1, 0x999, -1}) Require(scePadSetTriggerEffect(other, nullptr) == PAD_ERROR_INVALID_HANDLE); + param[0] = 0x4; + Require(scePadSetTriggerEffect(handle, param.data()) == PAD_ERROR_INVALID_ARG); + param[0] = 0; + command(0, 7, {0, 0, 0}); + Require(scePadSetTriggerEffect(handle, param.data()) == PAD_ERROR_INVALID_ARG); + command(0, 0, {0, 0, 0}); + command(1, 7, {0, 0, 0}); + Require(scePadSetTriggerEffect(handle, param.data()) == PAD_ERROR_INVALID_ARG); + command(1, 0, {0, 0, 0}); command(0, 2, {2, 5, 8}); command(1, 1, {7, 3, 0}); param[0] = 0x1; @@ -821,6 +870,29 @@ static void CheckInjectedQueueTimes(const std::filesystem::path& path) { PadPublishInput_nid_postfix(PadInputState{}); } +void CheckConcurrentSpecialOpens() { + constexpr int threads = 8; + for (int round = 0; round < 200; ++round) { + std::array handles{}; + std::atomic ready{0}; + std::vector workers; + for (int index = 0; index < threads; ++index) { + workers.emplace_back([&, index] { + ready.fetch_add(1); + while (ready.load() < threads) std::this_thread::yield(); + handles[index] = scePadOpen_nid_postfix(0x20000000 + index, PAD_PORT_TYPE_SPECIAL, 0, nullptr); + }); + } + for (auto& worker : workers) worker.join(); + const std::set distinct(handles.begin(), handles.end()); + Require(distinct.size() == threads); + for (const auto handle : handles) { + Require(handle >= 0); + Require(scePadClose_nid_postfix(handle) == PAD_OK); + } + } +} + static void CheckPortZeroWithdrawn() { constexpr int user = 0x10000000; constexpr int noHandle = static_cast(0x80920008); @@ -866,6 +938,7 @@ static void CheckPortZeroWithdrawn() { } int main() { + CheckConcurrentSpecialOpens(); CheckInputCommands(); std::uint32_t accessibilityState = 7; Require(scePadGetAccessibilityControllerState(PAD_HANDLE, nullptr) == PAD_ERROR_NOT_INITIALIZED); @@ -935,8 +1008,10 @@ int main() { constexpr int user = 0x10000000; Require(scePadGetHandle(user, 0, 0) == noHandle); + for (const int invalidUser : {-1, 0, 0x10000005}) Require(scePadGetHandle(invalidUser, 0, 0) == noHandle); Require(scePadOpen_nid_postfix(user, 1, 0, nullptr) == PAD_ERROR_INVALID_ARG); Require(scePadOpen_nid_postfix(user, 0, 1, nullptr) == static_cast(0x809b0003)); + for (const int type : {3, 4}) Require(scePadOpen_nid_postfix(user, type, 0, nullptr) == PAD_ERROR_INVALID_ARG); for (const int type : {0, 2, 3, 4}) Require(scePadOpen_nid_postfix(-1, type, 0, nullptr) == static_cast(0x809b0001)); Require(scePadOpen_nid_postfix(-1, 0, 1, nullptr) == static_cast(0x809b0001)); Require(scePadOpen_nid_postfix(-1, 1, 0, nullptr) == PAD_ERROR_INVALID_ARG); @@ -967,12 +1042,13 @@ int main() { Require(handle > 0); Require(scePadUnknown_n3kSX62fgNo() == PAD_OK); Require(scePadGetHandle(user, 0, 0) == handle); + for (const int invalidUser : {-1, 0, 0x10000005}) Require(scePadGetHandle(invalidUser, 0, 0) == noHandle); CheckSpecialPort(user, handle); CheckTiltCorrection(handle); CheckAngularVelocityDeadband(handle); - CheckTouchContact(); + CheckTouchContact(handle); CheckHandleQueries(user, handle); - CheckVirtualTouchContact(); + CheckVirtualTouchContact(handle); CheckTouchPadInformation(handle); CheckStickDeadZones(handle); CheckExtControllerInformation(handle); @@ -1002,7 +1078,10 @@ int main() { Require(scePadResetLightBar(handle + 1) == PAD_ERROR_INVALID_HANDLE); Require(scePadSetMotionSensorState(handle, true) == PAD_OK); Require(scePadSetMotionSensorState(handle, false) == PAD_OK); - Require(scePadSetMotionSensorState(handle + 1, true) == PAD_ERROR_INVALID_HANDLE); + for (const int invalid : {-1, 0, handle + 1, 9999}) { + Require(scePadSetMotionSensorState(invalid, true) == PAD_ERROR_INVALID_HANDLE); + Require(scePadSetMotionSensorState(invalid, false) == PAD_ERROR_INVALID_HANDLE); + } Require(scePadSetVibrationMode(handle, 1) == 0); Require(scePadSetVibrationMode(handle, 2) == 0); Require(scePadSetVibrationMode(handle, 3) == 0); @@ -1079,7 +1158,11 @@ int main() { Require(scePadSetVibration(handle2, &vibFull) == PAD_OK); Require(scePadSetVibration(handle2, &vibMixed) == PAD_OK); Require(scePadSetVibration(handle2 + 99, &vibMixed) == PAD_ERROR_INVALID_HANDLE); - Require(scePadSetVibration(-1, &vibMixed) == PAD_ERROR_INVALID_HANDLE); + for (const int invalidHandle : {-1, -2, -100, -9999}) { + Require(scePadSetVibration(invalidHandle, &vibMixed) == PAD_ERROR_INVALID_HANDLE); + Require(scePadSetVibration(invalidHandle, nullptr) == PAD_ERROR_INVALID_HANDLE); + } + Require(scePadSetVibration(handle2 + 99, nullptr) == PAD_ERROR_INVALID_HANDLE); Require(scePadSetVibration(handle2, nullptr) == PAD_ERROR_INVALID_ARG); const PadLightBarParam lightOff{0, 0, 0}; @@ -1092,6 +1175,12 @@ int main() { Require(scePadSetLightBar(-1, &lightColor) == PAD_ERROR_INVALID_HANDLE); Require(scePadSetLightBar(handle2, nullptr) == PAD_ERROR_INVALID_ARG); + const std::array triggerZero{}; + Require(scePadSetTriggerEffect(handle2, triggerZero.data()) == PAD_OK); + Require(scePadSetTriggerEffect(handle2 + 99, triggerZero.data()) == PAD_ERROR_INVALID_HANDLE); + Require(scePadSetTriggerEffect(-1, triggerZero.data()) == PAD_ERROR_INVALID_HANDLE); + Require(scePadSetTriggerEffect(handle2, nullptr) == PAD_ERROR_INVALID_ARG); + PadInputState p0MotionSample{}; p0MotionSample.hasMotion = true; p0MotionSample.accel = {0.0f, 9.80665f, 0.0f}; diff --git a/core/libs/tests/GuestPadAudioRoute.cpp b/core/libs/tests/GuestPadAudioRoute.cpp new file mode 100644 index 0000000000..c1edff0d25 --- /dev/null +++ b/core/libs/tests/GuestPadAudioRoute.cpp @@ -0,0 +1,82 @@ +#include "SceTypes.hpp" +#include "prx/libScePad/include/Pad.hpp" +#include "prx/libScePad/include/PadState.hpp" +#include +#include + +static void RequireAt(bool value, int line) { if (!value) { std::fprintf(stderr, "check failed at line %d\n", line); std::abort(); } } +#define Require(value) RequireAt((value), __LINE__) + +static PadOutputState FetchAt(int port, std::uint32_t& seen, int line) { + PadOutputState out{}; + RequireAt(PadFetchOutputForPort_nid_postfix(port, &seen, &out), line); + return out; +} + +int main() { + const PadVolumeGain gain{40, 50, 0, 60}; + Require(scePadIsSupportedAudioFunction(1) == PAD_ERROR_NOT_INITIALIZED); + Require(scePadSetAudioOutPath(1, 3) == PAD_ERROR_NOT_INITIALIZED); + Require(scePadSetAudioOutPath(1, 9) == PAD_ERROR_NOT_INITIALIZED); + Require(scePadSetVolumeGain(1, &gain) == PAD_ERROR_NOT_INITIALIZED); + Require(scePadSetVolumeGain(1, nullptr) == PAD_ERROR_NOT_INITIALIZED); + + Require(scePadInit_nid_postfix() == PAD_OK); + for (int handle : {-1, 0, 1, 12345}) { + Require(scePadIsSupportedAudioFunction(handle) == PAD_ERROR_INVALID_HANDLE); + Require(scePadSetAudioOutPath(handle, 3) == PAD_ERROR_INVALID_HANDLE); + Require(scePadSetAudioOutPath(handle, 5) == PAD_ERROR_INVALID_ARG); + Require(scePadSetAudioOutPath(handle, -1) == PAD_ERROR_INVALID_ARG); + Require(scePadSetVolumeGain(handle, &gain) == PAD_ERROR_INVALID_HANDLE); + Require(scePadSetVolumeGain(handle, nullptr) == PAD_ERROR_INVALID_ARG); + } + + const int handle = scePadOpen_nid_postfix(0x10000000, PAD_PORT_TYPE_STANDARD, 0, nullptr); + Require(handle > 0); + std::uint32_t seen = 0xffffffffu; + PadOutputState out = FetchAt(0, seen, __LINE__); + Require(out.audioOutPath == 0 && !out.volumeGainValid); + + Require(scePadIsSupportedAudioFunction(handle) == 1); + for (int path : {0, 1, 2, 3, 4}) { + Require(scePadSetAudioOutPath(handle, path) == PAD_OK); + if (path != 0) { + out = FetchAt(0, seen, __LINE__); + Require(out.audioOutPath == path); + } + } + Require(scePadSetAudioOutPath(handle, 5) == PAD_ERROR_INVALID_ARG); + Require(scePadSetAudioOutPath(handle, 0x7fffffff) == PAD_ERROR_INVALID_ARG); + Require(!PadFetchOutputForPort_nid_postfix(0, &seen, &out)); + Require(scePadSetAudioOutPath(handle, 4) == PAD_OK); + Require(!PadFetchOutputForPort_nid_postfix(0, &seen, &out)); + + Require(scePadSetVolumeGain(handle, nullptr) == PAD_ERROR_INVALID_ARG); + Require(scePadSetVolumeGain(handle, &gain) == PAD_OK); + out = FetchAt(0, seen, __LINE__); + Require(out.volumeGainValid && out.speakerVolume == 40 && out.headsetVolume == 50 && out.micGain == 60); + Require(scePadSetVolumeGain(handle, &gain) == PAD_OK); + Require(!PadFetchOutputForPort_nid_postfix(0, &seen, &out)); + const PadVolumeGain other{0, 255, 7, 1}; + Require(scePadSetVolumeGain(handle, &other) == PAD_OK); + out = FetchAt(0, seen, __LINE__); + Require(out.speakerVolume == 0 && out.headsetVolume == 255 && out.micGain == 1); + + PadSetPortConnected_nid_postfix(0, false); + Require(scePadIsSupportedAudioFunction(handle) == static_cast(0x80920007)); + Require(scePadSetAudioOutPath(handle, 1) == PAD_OK); + PadSetPortConnected_nid_postfix(0, true); + Require(scePadClose_nid_postfix(handle) == PAD_OK); + Require(scePadSetAudioOutPath(handle, 2) == PAD_OK); + + const int special = scePadOpen_nid_postfix(0x10000000, PAD_PORT_TYPE_SPECIAL, 0, nullptr); + Require(special > 0); + Require(scePadIsSupportedAudioFunction(special) == static_cast(0x80920007)); + Require(scePadSetAudioOutPath(special, 1) == PAD_OK); + Require(scePadSetAudioOutPath(special, 5) == PAD_ERROR_INVALID_ARG); + Require(scePadSetVolumeGain(special, &gain) == PAD_OK); + Require(scePadSetVolumeGain(special, nullptr) == PAD_ERROR_INVALID_ARG); + out = FetchAt(0, seen, __LINE__); + Require(out.audioOutPath == 2); + Require(scePadClose_nid_postfix(special) == PAD_OK); +} diff --git a/core/libs/tests/GuestPadControllerQueries.cpp b/core/libs/tests/GuestPadControllerQueries.cpp new file mode 100644 index 0000000000..e2f48f6480 --- /dev/null +++ b/core/libs/tests/GuestPadControllerQueries.cpp @@ -0,0 +1,69 @@ +#include "SceTypes.hpp" +#include "prx/libScePad/include/Pad.hpp" +#include "prx/libScePad/include/PadState.hpp" +#include +#include + +static void Require(bool value) { if (!value) std::abort(); } + +int main() { + int busType = -1; + int jack = -1; + PadContainerIdInformation container{}; + Require(scePadGetParticularMode() == PAD_ERROR_NOT_INITIALIZED); + Require(scePadGetControllerBusType(1, &busType) == PAD_ERROR_NOT_INITIALIZED); + Require(scePadGetControllerBusType(1, nullptr) == PAD_ERROR_NOT_INITIALIZED); + Require(scePadGetJackState(1, &jack) == PAD_ERROR_NOT_INITIALIZED); + Require(scePadIsControllerUpdateRequired(1) == PAD_ERROR_NOT_INITIALIZED); + Require(scePadGetContainerIdInformation(1, &container) == PAD_ERROR_NOT_INITIALIZED); + Require(busType == -1 && jack == -1); + + Require(scePadInit_nid_postfix() == PAD_OK); + Require(scePadGetParticularMode() == 0); + Require(scePadSetParticularMode(true) == PAD_OK); + Require(scePadGetParticularMode() == 1); + Require(scePadSetParticularMode(false) == PAD_OK); + Require(scePadGetParticularMode() == 0); + + for (int handle : {-1, 0, 1, 12345}) { + Require(scePadGetControllerBusType(handle, nullptr) == PAD_ERROR_INVALID_ARG); + Require(scePadGetJackState(handle, nullptr) == PAD_ERROR_INVALID_ARG); + Require(scePadGetContainerIdInformation(handle, nullptr) == PAD_ERROR_INVALID_ARG); + Require(scePadGetControllerBusType(handle, &busType) == PAD_ERROR_INVALID_HANDLE); + Require(scePadGetJackState(handle, &jack) == PAD_ERROR_INVALID_HANDLE); + Require(scePadIsControllerUpdateRequired(handle) == PAD_ERROR_INVALID_HANDLE); + Require(scePadGetContainerIdInformation(handle, &container) == PAD_ERROR_INVALID_HANDLE); + } + Require(busType == -1 && jack == -1 && container.size == 0); + + const int handle = scePadOpen_nid_postfix(0x10000000, PAD_PORT_TYPE_STANDARD, 0, nullptr); + Require(handle > 0); + Require(scePadGetControllerBusType(handle, nullptr) == PAD_ERROR_INVALID_ARG); + Require(scePadGetControllerBusType(handle, &busType) == PAD_OK); + Require(busType == PAD_CONNECT_TYPE_BLUETOOTH); + Require(scePadGetJackState(handle, &jack) == PAD_OK); + Require(jack == 0); + Require(scePadIsControllerUpdateRequired(handle) == 0); + std::memset(&container, 0x5a, sizeof(container)); + Require(scePadGetContainerIdInformation(handle, &container) == PAD_OK); + Require(container.size == 0 && container.id[0] == '\0'); + + PadSetPortConnected_nid_postfix(0, false); + Require(scePadGetControllerBusType(handle, &busType) == static_cast(0x80920007)); + Require(scePadGetJackState(handle, &jack) == static_cast(0x80920007)); + Require(scePadIsControllerUpdateRequired(handle) == static_cast(0x80920007)); + Require(scePadGetContainerIdInformation(handle, &container) == static_cast(0x80920007)); + PadSetPortConnected_nid_postfix(0, true); + Require(scePadGetJackState(handle, &jack) == PAD_OK); + + Require(scePadClose_nid_postfix(handle) == PAD_OK); + Require(scePadGetJackState(handle, &jack) == PAD_OK); + + const int special = scePadOpen_nid_postfix(0x10000000, PAD_PORT_TYPE_SPECIAL, 0, nullptr); + Require(special > 0); + Require(scePadGetControllerBusType(special, &busType) == static_cast(0x80920007)); + Require(scePadGetJackState(special, &jack) == static_cast(0x80920007)); + Require(scePadIsControllerUpdateRequired(special) == static_cast(0x80920007)); + Require(scePadGetContainerIdInformation(special, &container) == static_cast(0x80920007)); + Require(scePadClose_nid_postfix(special) == PAD_OK); +} diff --git a/core/libs/tests/GuestPadFirmwareChecks.cpp b/core/libs/tests/GuestPadFirmwareChecks.cpp new file mode 100644 index 0000000000..3c359ade31 --- /dev/null +++ b/core/libs/tests/GuestPadFirmwareChecks.cpp @@ -0,0 +1,62 @@ +#include "SceTypes.hpp" +#include "prx/libScePad/include/Pad.hpp" +#include "prx/libScePad/include/PadState.hpp" +#include +#include +#include + +using ReadHistoryFn = int (APS5_VABI*)(std::int32_t, PadHistoryData*, std::uint32_t); +using OpenExt2Fn = int (APS5_VABI*)(std::int32_t, std::int32_t, const void*); + +static void Require(bool value) { if (!value) std::abort(); } + +template +static Function As(Export function) { + return reinterpret_cast(function); +} + +int main() { + const auto readHistory = As(&scePadReadHistory); + const auto openExt2 = As(&scePadOpenExt2); + alignas(8) std::array buffer{}; + std::array param{}; + std::uint32_t capability = 7; + std::uint64_t count = 7; + std::uint64_t activeTime = 7; + std::int32_t deviceId = 7; + auto* data = buffer.data(); + + Require(scePadGetDeviceInfo(0, reinterpret_cast(data)) == PAD_ERROR_NOT_INITIALIZED); + Require(scePadGetCapability(0, &capability) == PAD_ERROR_NOT_INITIALIZED && capability == 7); + Require(scePadGetVersionInfo(1, reinterpret_cast(data)) == PAD_ERROR_NOT_INITIALIZED); + Require(scePadGetIdleCount(1) == 0); + Require(scePadGetControllerIdleCount(1, &count) == PAD_ERROR_NOT_INITIALIZED && count == 7); + Require(scePadGetActiveTime(1, 0, &activeTime) == PAD_ERROR_NOT_INITIALIZED && activeTime == 7); + Require(scePadSetPS4BcVibrationMode(0) == PAD_ERROR_NOT_INITIALIZED); + Require(scePadGetInfoByPortType(1, reinterpret_cast(data)) == PAD_ERROR_NOT_INITIALIZED); + Require(scePadGetInfoByPortType(0, reinterpret_cast(data)) == PAD_ERROR_NOT_INITIALIZED); + Require(scePadGetInfoByPortType(2, nullptr) == PAD_ERROR_INVALID_ARG && scePadGetInfoByPortType(1, nullptr) == PAD_ERROR_INVALID_ARG); + Require(scePadGetDeviceId(1, &deviceId) == PAD_ERROR_NOT_INITIALIZED && deviceId == 7); + Require(scePadGetExtensionUnitInfo(1, reinterpret_cast(data)) == PAD_ERROR_NOT_INITIALIZED); + Require(readHistory(1, reinterpret_cast(data), 0x960) == PAD_ERROR_NOT_INITIALIZED); + Require(openExt2(0x10000000, 0x10, param.data()) == PAD_ERROR_NOT_INITIALIZED); + + Require(scePadInit_nid_postfix() == PAD_OK); + Require(scePadGetDeviceInfo(0, nullptr) == PAD_ERROR_INVALID_ARG); + Require(scePadGetCapability(0, nullptr) == PAD_ERROR_INVALID_ARG); + Require(scePadGetVersionInfo(1, nullptr) == PAD_ERROR_INVALID_ARG); + Require(scePadGetControllerIdleCount(0, &count) == PAD_ERROR_INVALID_ARG && scePadGetControllerIdleCount(1, nullptr) == PAD_ERROR_INVALID_ARG); + Require(scePadGetActiveTime(1, 0, nullptr) == PAD_ERROR_INVALID_ARG); + for (int type : {1, 3, 4, -1}) Require(scePadGetInfoByPortType(type, reinterpret_cast(data)) == PAD_ERROR_INVALID_ARG); + Require(scePadGetInfoByPortType(1, nullptr) == PAD_ERROR_INVALID_ARG); + Require(scePadGetDeviceId(1, nullptr) == PAD_ERROR_INVALID_ARG); + Require(scePadGetDeviceId(12345, &deviceId) == PAD_ERROR_INVALID_HANDLE && deviceId == -1); + Require(scePadGetExtensionUnitInfo(1, nullptr) == PAD_ERROR_INVALID_ARG); + for (std::uint32_t size : {0u, 0x48u, 0x958u, 0x961u}) Require(readHistory(1, reinterpret_cast(data), size) == PAD_ERROR_INVALID_ARG); + Require(openExt2(0x10000000, 0x8, param.data()) == PAD_ERROR_INVALID_ARG && openExt2(0x10000000, 0x10, nullptr) == PAD_ERROR_INVALID_ARG); + + const int handle = scePadOpen_nid_postfix(0x10000000, PAD_PORT_TYPE_STANDARD, 0, nullptr); + Require(handle > 0); + Require(scePadReadStateExt(handle, nullptr) == PAD_ERROR_INVALID_ARG); + return 0; +} diff --git a/core/libs/tests/GuestPathErrors.cpp b/core/libs/tests/GuestPathErrors.cpp index 9eb21a8c7a..ca40f6829d 100644 --- a/core/libs/tests/GuestPathErrors.cpp +++ b/core/libs/tests/GuestPathErrors.cpp @@ -56,6 +56,13 @@ void OpenAndUnlink() { Check(sceKernelOpen(LongName.c_str(), SCE_KERNEL_O_RDONLY, 0) == SceEnametoolong); Check(sceKernelOpen((LongName + "/inside").c_str(), SCE_KERNEL_O_RDONLY, 0) == SceEnametoolong); Check(sceKernelOpen(LongName.c_str(), SCE_KERNEL_O_WRONLY | SCE_KERNEL_O_CREAT, 0644) == SceEnametoolong); + Check(sceKernelOpen("missing-create/", SCE_KERNEL_O_WRONLY | SCE_KERNEL_O_CREAT, 0644) == SCE_KERNEL_ERROR_ENOENT); + Check(PosixFails(open_nid_postfix("missing-create/", SCE_KERNEL_O_WRONLY | SCE_KERNEL_O_CREAT, 0644), 2)); + Check(sceKernelOpen("missing-create-exclusive/", SCE_KERNEL_O_WRONLY | SCE_KERNEL_O_CREAT | SCE_KERNEL_O_EXCL, 0644) == SCE_KERNEL_ERROR_ENOENT); + Check(PosixFails(open_nid_postfix("missing-create-exclusive/", SCE_KERNEL_O_WRONLY | SCE_KERNEL_O_CREAT | SCE_KERNEL_O_EXCL, 0644), 2)); + Check(!std::filesystem::exists("missing-create") && !std::filesystem::exists("missing-create-exclusive")); + Check(sceKernelOpen("loop/", SCE_KERNEL_O_WRONLY | SCE_KERNEL_O_CREAT, 0644) == SceEloop); + Check(PosixFails(open_nid_postfix("loop/", SCE_KERNEL_O_WRONLY | SCE_KERNEL_O_CREAT, 0644), GuestEloop)); Check(PosixFails(open_nid_postfix("loop", SCE_KERNEL_O_RDONLY, 0), GuestEloop)); Check(PosixFails(open_nid_postfix(LongName.c_str(), SCE_KERNEL_O_RDONLY, 0), GuestEnametoolong)); Check(sceKernelUnlink(LongName.c_str()) == SceEnametoolong); diff --git a/core/libs/tests/GuestPathNameLength.cpp b/core/libs/tests/GuestPathNameLength.cpp index c6197d2e8f..0276453546 100644 --- a/core/libs/tests/GuestPathNameLength.cpp +++ b/core/libs/tests/GuestPathNameLength.cpp @@ -1,4 +1,6 @@ #include "prx/libkernel/include/Api.hpp" +#include "prx/libkernel/File/include/File.hpp" +#include "prx/libkernel/KernelErrors.hpp" #include "prx/libc/include/Api.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include @@ -24,6 +26,18 @@ bool TooLong(const std::string& path) { return Fails(stat_nid_postfix(path.c_str(), reinterpret_cast(status)), ErrorNameTooLong); } +void RequireSceTooLong(const std::string& path, const char* what) { + unsigned char status[256]; + const char* name = path.c_str(); + const int results[] = {sceKernelStat(name, reinterpret_cast(status)), sceKernelUnlink(name), sceKernelMkdir(name, 0755), sceKernelRmdir(name), sceKernelChmod_nid_postfix(name, 0644), sceKernelTruncate_nid_postfix(name, 0), sceKernelUtimes_nid_postfix(name, nullptr)}; + const char* calls[] = {"sceKernelStat", "sceKernelUnlink", "sceKernelMkdir", "sceKernelRmdir", "sceKernelChmod", "sceKernelTruncate", "sceKernelUtimes"}; + for (int index = 0; index < 7; ++index) { + if (results[index] == SCE_KERNEL_ERROR_ENAMETOOLONG) continue; + std::fprintf(stderr, "%s: %s returned 0x%X\n", what, calls[index], static_cast(results[index])); + std::abort(); + } +} + } int main() { @@ -49,9 +63,11 @@ int main() { Require(!TooLong(block + block + block + name255), "a path of exactly 1023 bytes must not be too long"); #endif Require(TooLong(block + block + block + block), "a path of exactly 1024 bytes must fail with ENAMETOOLONG (63)"); + RequireSceTooLong(block + block + block + block, "the sce path calls must fail a path of exactly 1024 bytes with SCE_KERNEL_ERROR_ENAMETOOLONG"); Require(rmdir_nid_postfix("anyps5_name_length") == 0, "rmdir of the test directory failed"); Require(Fails(stat_nid_postfix((longName + "/x").c_str(), reinterpret_cast(status)), ErrorNameTooLong), "stat of a long first component must fail with ENAMETOOLONG (63)"); Require(Fails(stat_nid_postfix(longPath.c_str(), reinterpret_cast(status)), ErrorNameTooLong), "stat of a path of 1100 bytes must fail with ENAMETOOLONG (63)"); + RequireSceTooLong(longPath, "the sce path calls must fail a path of 1100 bytes with SCE_KERNEL_ERROR_ENAMETOOLONG"); Require(Fails(access_nid_postfix(longName.c_str(), 0), ErrorNameTooLong), "access of a long name must fail with ENAMETOOLONG (63)"); Require(Fails(chflags_nid_postfix(longName.c_str(), 0), ErrorNameTooLong), "chflags of a long name must fail with ENAMETOOLONG (63)"); Require(Fails(unlink_nid_postfix(longName.c_str()), ErrorNameTooLong), "unlink of a long name must fail with ENAMETOOLONG (63)"); diff --git a/core/libs/tests/GuestPlayGo.cpp b/core/libs/tests/GuestPlayGo.cpp index d0321f63e9..8e62a04c2b 100644 --- a/core/libs/tests/GuestPlayGo.cpp +++ b/core/libs/tests/GuestPlayGo.cpp @@ -291,6 +291,79 @@ static int CheckNoPlayGoFileAnswered(const char* root, const char* answer) { return 0; } +static int CheckNoPlayGoFileExplicit(const char* root, const char* value) { + constexpr int badChunkId = static_cast(0x80B2000C); + SetUnlistedChunks(value); + std::filesystem::current_path(root); + PlayGoInitParams init{}; + int handle = 0; + Require(scePlayGoInitialize(&init) == 0); + Require(scePlayGoOpen(&handle, nullptr) == 0); + const std::uint16_t listed[2] = {0, 1}; + std::int8_t loci[2] = {0, 0}; + Require(scePlayGoGetLocus(handle, listed, 2, loci) == 0 && loci[0] == 3 && loci[1] == 3); + const std::uint16_t unlisted[2] = {2, 999}; + Require(scePlayGoGetLocus(handle, unlisted, 2, loci) == badChunkId); + std::uint32_t chunkCount = 0; + Require(scePlayGoGetChunkId(handle, nullptr, 0, &chunkCount) == 0 && chunkCount == 2); + std::uint16_t ids[2] = {0xFFFF, 0xFFFF}; + Require(scePlayGoGetChunkId(handle, ids, 2, &chunkCount) == 0 && chunkCount == 2 && ids[0] == 0 && ids[1] == 1); + PlayGoProgress progress{}; + Require(scePlayGoGetProgress(handle, listed, 2, &progress) == 0 && progress.progress_size == 1 && progress.total_size == 1); + Require(scePlayGoGetProgress(handle, unlisted, 2, &progress) == badChunkId); + Require(scePlayGoClose(handle) == 0); + return 0; +} + +static int CheckNoPlayGoFileExplicitInvalid(const char* root, const char* value) { + SetUnlistedChunks(value); + std::filesystem::current_path(root); + PlayGoInitParams init{}; + int handle = 0; + Require(scePlayGoInitialize(&init) == 0); + Require(scePlayGoOpen(&handle, nullptr) == 0); + const std::uint16_t chunk = 0; + std::int8_t locus = 0; + bool threw = false; + try { + scePlayGoGetLocus(handle, &chunk, 1, &locus); + } catch (const std::runtime_error& error) { + threw = std::string_view(error.what()).find("APS5_PLAYGO_UNLISTED_CHUNKS must be installed, absent, or a comma-separated list") != std::string_view::npos; + } + Require(threw); + return 0; +} + +static int CheckPackageChunkTableExplicit(const char* root) { + SetUnlistedChunks("0,1"); + return CheckPackageChunkTable(root); +} + +static int CheckScenarioWithoutChunkTableExplicit(const char* root) { + constexpr int badChunkId = static_cast(0x80B2000C); + SetUnlistedChunks("0,1"); + std::filesystem::current_path(root); + PlayGoInitParams init{}; + int handle = 0; + Require(scePlayGoInitialize(&init) == 0); + Require(scePlayGoOpen(&handle, nullptr) == 0); + const std::uint16_t listed[2] = {0, 999}; + std::int8_t loci[2] = {0, 0}; + Require(scePlayGoGetLocus(handle, listed, 2, loci) == 0 && loci[0] == 3 && loci[1] == 3); + std::uint32_t chunkCount = 0; + Require(scePlayGoGetChunkId(handle, nullptr, 0, &chunkCount) == 0 && chunkCount == 1000); + const std::uint16_t unlisted = 1000; + bool threw = false; + try { + scePlayGoGetLocus(handle, &unlisted, 1, loci); + } catch (const std::runtime_error& error) { + threw = std::string_view(error.what()).find("APS5_PLAYGO_UNLISTED_CHUNKS=installed or =absent") != std::string_view::npos; + } + Require(threw); + Require(scePlayGoClose(handle) == 0); + return 0; +} + static int CheckScenarioWithoutChunkTableAnswered(const char* root, const char* answer) { constexpr int badChunkId = static_cast(0x80B2000C); SetUnlistedChunks(answer); @@ -326,7 +399,7 @@ static int CheckUnknownUnlistedChunkAnswer(const char* root) { scePlayGoGetLocus(handle, &unlisted, 1, &locus); } catch (const std::runtime_error& error) { - threw = std::string_view(error.what()).find("APS5_PLAYGO_UNLISTED_CHUNKS must be installed or absent") != std::string_view::npos; + threw = std::string_view(error.what()).find("APS5_PLAYGO_UNLISTED_CHUNKS must be installed, absent, or a comma-separated list") != std::string_view::npos; } Require(threw); return 0; @@ -362,7 +435,14 @@ int main(int argc, char** argv) { if (mode == "--no-file-installed") return CheckNoPlayGoFileAnswered(argv[2], "installed"); if (mode == "--no-file-absent") return CheckNoPlayGoFileAnswered(argv[2], "absent"); if (mode == "--no-file-unknown-answer") return CheckUnknownUnlistedChunkAnswer(argv[2]); + if (mode == "--no-file-explicit") return CheckNoPlayGoFileExplicit(argv[2], "0,1"); + if (mode == "--no-file-explicit-one") return CheckNoPlayGoFileExplicit(argv[2], "1"); + if (mode == "--no-file-explicit-empty") return CheckNoPlayGoFileExplicitInvalid(argv[2], "0,"); + if (mode == "--no-file-explicit-range") return CheckNoPlayGoFileExplicitInvalid(argv[2], "65536"); + if (mode == "--no-file-explicit-text") return CheckNoPlayGoFileExplicitInvalid(argv[2], "0,x"); if (mode == "--package-chunk-table" || mode == "--scenario-with-chunk-table") return CheckPackageChunkTable(argv[2]); + if (mode == "--package-chunk-table-explicit") return CheckPackageChunkTableExplicit(argv[2]); + if (mode == "--scenario-only-explicit") return CheckScenarioWithoutChunkTableExplicit(argv[2]); if (mode == "--scenario-only") return CheckScenarioWithoutChunkTable(argv[2]); if (mode == "--scenario-only-installed") return CheckScenarioWithoutChunkTableAnswered(argv[2], "installed"); if (mode == "--scenario-only-absent") return CheckScenarioWithoutChunkTableAnswered(argv[2], "absent"); @@ -374,7 +454,14 @@ int main(int argc, char** argv) { RunLayout(argv[0], "--no-file-installed", {}); RunLayout(argv[0], "--no-file-absent", {}); RunLayout(argv[0], "--no-file-unknown-answer", {}); + RunLayout(argv[0], "--no-file-explicit", {}); + RunLayout(argv[0], "--no-file-explicit-one", {}); + RunLayout(argv[0], "--no-file-explicit-empty", {}); + RunLayout(argv[0], "--no-file-explicit-range", {}); + RunLayout(argv[0], "--no-file-explicit-text", {}); RunLayout(argv[0], "--package-chunk-table", {{"playgo-chunk.dat", ChunkTableFile()}}); + RunLayout(argv[0], "--package-chunk-table-explicit", {{"playgo-chunk.dat", ChunkTableFile()}}); + RunLayout(argv[0], "--scenario-only-explicit", {{"playgo-scenario.json", "{}"}}); RunLayout(argv[0], "--package-chunk-table", {{"playgo-chunk.dat", ChunkTableFile("plgo")}}); RunLayout(argv[0], "--scenario-with-chunk-table", {{"playgo-chunk.dat", ChunkTableFile()}, {"playgo-scenario.json", "{}"}}); std::int64_t eta = -1; diff --git a/core/libs/tests/GuestPlayReady.cpp b/core/libs/tests/GuestPlayReady.cpp index 07892d95de..a90c9cb0d4 100644 --- a/core/libs/tests/GuestPlayReady.cpp +++ b/core/libs/tests/GuestPlayReady.cpp @@ -33,6 +33,7 @@ void CheckBufferInit() { } sceBufferInit(nullptr); Require(scePlayReadyBufferInit(nullptr) == InvalidArgument, "scePlayReadyBufferInit rejects a null buffer"); + Require(scePlayReadyBufferInit(reinterpret_cast(0x10)) == InvalidArgument, "scePlayReadyBufferInit rejects an unwritable buffer"); } void CheckInitParams() { @@ -63,6 +64,7 @@ void CheckVersion() { Require(std::memcmp(version, expected[index], sizeof(expected[index])) == 0, "scePlayReadyApiVersion writes the version"); Require(Filled(reinterpret_cast(version), 32, sizeof(version), 0xA5), "scePlayReadyApiVersion writes 32 bytes"); Require(scePlayReadyApiVersion(index, nullptr) == InvalidArgument, "scePlayReadyApiVersion rejects a null output"); + Require(scePlayReadyApiVersion(index, reinterpret_cast(0x10)) == InvalidArgument, "scePlayReadyApiVersion rejects an unwritable output"); } for (uint32_t index : {2u, 3u, 0x80000000u, 0xFFFFFFFFu}) { uint64_t version[4]; diff --git a/core/libs/tests/GuestPlayerReviewDialog.cpp b/core/libs/tests/GuestPlayerReviewDialog.cpp index 1f608a2e58..6d64f2bb15 100644 --- a/core/libs/tests/GuestPlayerReviewDialog.cpp +++ b/core/libs/tests/GuestPlayerReviewDialog.cpp @@ -12,6 +12,7 @@ constexpr int COMMON_DIALOG_STATUS_INITIALIZED = 1; 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_INVALID_ARGUMENT = static_cast(0x80B8000Au); constexpr int COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast(0x80B80005u); constexpr int COMMON_DIALOG_ERROR_ARG_NULL = static_cast(0x80B8000Du); @@ -76,6 +77,7 @@ int main() { Require(scePlayerReviewDialogGetStatus() == COMMON_DIALOG_STATUS_FINISHED); Require(scePlayerReviewDialogUpdateStatus() == COMMON_DIALOG_STATUS_FINISHED); Require(scePlayerReviewDialogGetResult(nullptr) == COMMON_DIALOG_ERROR_ARG_NULL); + Require(scePlayerReviewDialogGetResult(reinterpret_cast(0x10)) == COMMON_DIALOG_ERROR_INVALID_ARGUMENT); Require(scePlayerReviewDialogGetResult(reinterpret_cast(result)) == COMMON_DIALOG_RESULT_USER_CANCELED); Require(result[0] == -1 && result[1] == -1 && result[3] == -1); Require(result[2] == COMMON_DIALOG_RESULT_USER_CANCELED); diff --git a/core/libs/tests/GuestPlayerSelectionDialog.cpp b/core/libs/tests/GuestPlayerSelectionDialog.cpp index 80a87cfd4a..56e768ca4a 100644 --- a/core/libs/tests/GuestPlayerSelectionDialog.cpp +++ b/core/libs/tests/GuestPlayerSelectionDialog.cpp @@ -1,6 +1,7 @@ #include "prx/libScePlayerSelectionDialog/include/Api.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include +#include #include #include #include @@ -11,10 +12,14 @@ namespace { void Require(bool value) { if (!value) std::abort(); } +constexpr int kStatusNone = 0; +constexpr int kStatusInitialized = 1; constexpr int kStatusFinished = 3; constexpr int kErrNotInitialized = static_cast(0x80B80003); constexpr int kErrAlreadyInitialized = static_cast(0x80B80004); constexpr int kErrNotFinished = static_cast(0x80B80005); +constexpr int kErrInvalidArg = static_cast(0x80B8000A); +constexpr int kErrNotRunning = static_cast(0x80B8000B); constexpr int kErrArgNull = static_cast(0x80B8000D); constexpr int kResultUserCanceled = 1; @@ -35,6 +40,9 @@ int main() { Require(scePlayerSelectionDialogOpen(nullptr) == kErrArgNull); Require(scePlayerSelectionDialogParamInitialize(nullptr) == kErrArgNull); Require(scePlayerSelectionDialogParamInitialize(¶m) == 0); + errno = 71; + Require(scePlayerSelectionDialogParamInitialize(reinterpret_cast(0x10)) == kErrArgNull); + Require(errno == 71); Require(param.base.size == sizeof(param.base)); Require(param.base.magic == static_cast(reinterpret_cast(¶m)) - 1060003575u); Require(param.size == sizeof(param)); @@ -54,59 +62,165 @@ int main() { Require(scePlayerSelectionDialogClose() == kErrNotInitialized); Require(scePlayerSelectionDialogGetResult(nullptr) == kErrArgNull); Require(scePlayerSelectionDialogGetResult(&result) == kErrNotInitialized); - Require(scePlayerSelectionDialogGetStatus() == 0); - Require(scePlayerSelectionDialogUpdateStatus() == 0); + Require(scePlayerSelectionDialogGetStatus() == kStatusNone); + Require(scePlayerSelectionDialogUpdateStatus() == kStatusNone); Require(scePlayerSelectionDialogInitialize() == 0); Require(scePlayerSelectionDialogInitialize() == kErrAlreadyInitialized); - Require(scePlayerSelectionDialogGetStatus() == 1); - Require(scePlayerSelectionDialogUpdateStatus() == 1); + Require(scePlayerSelectionDialogGetStatus() == kStatusInitialized); + Require(scePlayerSelectionDialogUpdateStatus() == kStatusInitialized); Require(scePlayerSelectionDialogOpen(nullptr) == kErrArgNull); Require(scePlayerSelectionDialogGetResult(&result) == kErrNotFinished); - Require(scePlayerSelectionDialogOpen(¶m) == 0); - Require(scePlayerSelectionDialogGetStatus() == kStatusFinished); + + param.base.size = sizeof(param.base) - 1; + Require(scePlayerSelectionDialogOpen(¶m) == kErrInvalidArg); + param.base.size = sizeof(param.base) + 1; + Require(scePlayerSelectionDialogOpen(¶m) == kErrInvalidArg); + param.base.size = sizeof(param.base); + + const auto savedMagic = param.base.magic; + param.base.magic = 0; + Require(scePlayerSelectionDialogOpen(¶m) == kErrInvalidArg); + param.base.magic = savedMagic + 1; + Require(scePlayerSelectionDialogOpen(¶m) == kErrInvalidArg); + param.base.magic = savedMagic; + + param.base.reserved[0] = 1; + Require(scePlayerSelectionDialogOpen(¶m) == kErrInvalidArg); + param.base.reserved[0] = 0; + param.base.reserved[35] = 1; + Require(scePlayerSelectionDialogOpen(¶m) == kErrInvalidArg); + param.base.reserved[35] = 0; param.size = 0; - Require(scePlayerSelectionDialogOpen(¶m) == static_cast(0x80B8000Au)); + Require(scePlayerSelectionDialogOpen(¶m) == kErrInvalidArg); + param.size = sizeof(param) - 1; + Require(scePlayerSelectionDialogOpen(¶m) == kErrInvalidArg); param.size = sizeof(param); + + param.userId = -1; + Require(scePlayerSelectionDialogOpen(¶m) == kErrInvalidArg); + param.userId = 0; + Require(scePlayerSelectionDialogOpen(¶m) == kErrInvalidArg); + param.userId = 42; + Require(scePlayerSelectionDialogOpen(¶m) == kErrInvalidArg); + param.userId = 999; + Require(scePlayerSelectionDialogOpen(¶m) == kErrInvalidArg); + param.userId = USER_SERVICE_INITIAL_USER_ID; + param.maxPlayers = 257; - Require(scePlayerSelectionDialogOpen(¶m) == static_cast(0x80B8000Au)); + Require(scePlayerSelectionDialogOpen(¶m) == kErrInvalidArg); + param.maxPlayers = 256; + Require(scePlayerSelectionDialogOpen(¶m) == 0); + Require(scePlayerSelectionDialogGetStatus() == kStatusFinished); + Require(scePlayerSelectionDialogClose() == kErrNotRunning); + param.maxPlayers = 1; param.mode = 4; - Require(scePlayerSelectionDialogOpen(¶m) == static_cast(0x80B8000Au)); + Require(scePlayerSelectionDialogOpen(¶m) == kErrInvalidArg); + param.mode = 3; + Require(scePlayerSelectionDialogOpen(¶m) == 0); param.mode = 0; + + param.playerCount = 257; + Require(scePlayerSelectionDialogOpen(¶m) == kErrInvalidArg); param.playerCount = 1; - Require(scePlayerSelectionDialogOpen(¶m) == static_cast(0x80B8000Au)); - PlayerSelectionDialogPlayer player{}; - param.players = &player; - Require(scePlayerSelectionDialogOpen(¶m) == static_cast(0x80B8000Au)); - player.accountId = 1; - player.option0 = 1; - Require(scePlayerSelectionDialogOpen(¶m) == static_cast(0x80B8000Au)); + param.players = nullptr; + Require(scePlayerSelectionDialogOpen(¶m) == kErrInvalidArg); + + PlayerSelectionDialogPlayer singlePlayer{}; + param.players = &singlePlayer; + Require(scePlayerSelectionDialogOpen(¶m) == kErrInvalidArg); + singlePlayer.accountId = 1; + singlePlayer.option0 = 1; + Require(scePlayerSelectionDialogOpen(¶m) == kErrInvalidArg); param.maxPlayers = 2; - player.option1 = 2; - Require(scePlayerSelectionDialogOpen(¶m) == static_cast(0x80B8000Au)); - player.option1 = 1; - player.mode = 3; - Require(scePlayerSelectionDialogOpen(¶m) == static_cast(0x80B8000Au)); - player.mode = 2; - player.reserved[63] = 1; - Require(scePlayerSelectionDialogOpen(¶m) == static_cast(0x80B8000Au)); - player.reserved[63] = 0; + singlePlayer.option0 = 2; + Require(scePlayerSelectionDialogOpen(¶m) == kErrInvalidArg); + singlePlayer.option0 = 1; + singlePlayer.option1 = 2; + Require(scePlayerSelectionDialogOpen(¶m) == kErrInvalidArg); + singlePlayer.option1 = 1; + singlePlayer.mode = 3; + Require(scePlayerSelectionDialogOpen(¶m) == kErrInvalidArg); + singlePlayer.mode = 2; + singlePlayer.reserved[0] = 1; + Require(scePlayerSelectionDialogOpen(¶m) == kErrInvalidArg); + singlePlayer.reserved[0] = 0; + singlePlayer.reserved[63] = 1; + Require(scePlayerSelectionDialogOpen(¶m) == kErrInvalidArg); + singlePlayer.reserved[63] = 0; + param.reserved[0] = 1; + Require(scePlayerSelectionDialogOpen(¶m) == kErrInvalidArg); + param.reserved[0] = 0; param.reserved[63] = 1; - Require(scePlayerSelectionDialogOpen(¶m) == static_cast(0x80B8000Au)); + Require(scePlayerSelectionDialogOpen(¶m) == kErrInvalidArg); param.reserved[63] = 0; Require(scePlayerSelectionDialogOpen(¶m) == 0); Require(scePlayerSelectionDialogGetStatus() == kStatusFinished); Require(scePlayerSelectionDialogUpdateStatus() == kStatusFinished); + PlayerSelectionDialogPlayer multiPlayers[2]{}; + multiPlayers[0].accountId = 1; + multiPlayers[0].option0 = 0; + multiPlayers[0].option1 = 0; + multiPlayers[0].mode = 0; + multiPlayers[1].accountId = 2; + multiPlayers[1].option0 = 0; + multiPlayers[1].option1 = 0; + multiPlayers[1].mode = 0; + param.players = multiPlayers; + param.playerCount = 2; + param.maxPlayers = 2; + Require(scePlayerSelectionDialogOpen(¶m) == 0); + + multiPlayers[1].accountId = 0; + Require(scePlayerSelectionDialogOpen(¶m) == kErrInvalidArg); + multiPlayers[1].accountId = 2; + + multiPlayers[1].option0 = 2; + Require(scePlayerSelectionDialogOpen(¶m) == kErrInvalidArg); + multiPlayers[1].option0 = 0; + + multiPlayers[1].option1 = 2; + Require(scePlayerSelectionDialogOpen(¶m) == kErrInvalidArg); + multiPlayers[1].option1 = 0; + + multiPlayers[1].mode = 3; + Require(scePlayerSelectionDialogOpen(¶m) == kErrInvalidArg); + multiPlayers[1].mode = 0; + + multiPlayers[1].reserved[0] = 1; + Require(scePlayerSelectionDialogOpen(¶m) == kErrInvalidArg); + multiPlayers[1].reserved[0] = 0; + + multiPlayers[1].reserved[63] = 1; + Require(scePlayerSelectionDialogOpen(¶m) == kErrInvalidArg); + multiPlayers[1].reserved[63] = 0; + + param.maxPlayers = 1; + multiPlayers[0].option0 = 1; + Require(scePlayerSelectionDialogOpen(¶m) == kErrInvalidArg); + multiPlayers[0].option0 = 0; + multiPlayers[1].option0 = 1; + Require(scePlayerSelectionDialogOpen(¶m) == kErrInvalidArg); + multiPlayers[1].option0 = 0; + + param.maxPlayers = 2; + multiPlayers[0].option0 = 1; + multiPlayers[1].option0 = 1; + Require(scePlayerSelectionDialogOpen(¶m) == 0); + Require(scePlayerSelectionDialogGetResult(nullptr) == kErrArgNull); + errno = 71; + Require(scePlayerSelectionDialogGetResult(reinterpret_cast(0x10)) == kErrArgNull); + Require(errno == 71); Require(scePlayerSelectionDialogGetResult(&result) == kResultUserCanceled); Require(result.result == kResultUserCanceled); Require(result.option == 99 && result.accountIds == nullptr && result.count == 77 && result.padding == 55); - Require(scePlayerSelectionDialogClose() == static_cast(0x80B8000Bu)); + Require(scePlayerSelectionDialogClose() == kErrNotRunning); Require(scePlayerSelectionDialogTerminate() == 0); Require(scePlayerSelectionDialogTerminate() == kErrNotInitialized); Require(scePlayerSelectionDialogOpen(nullptr) == kErrArgNull); @@ -114,7 +228,15 @@ int main() { Require(scePlayerSelectionDialogClose() == kErrNotInitialized); Require(scePlayerSelectionDialogGetResult(nullptr) == kErrArgNull); Require(scePlayerSelectionDialogGetResult(&result) == kErrNotInitialized); - Require(scePlayerSelectionDialogGetStatus() == 0); - Require(scePlayerSelectionDialogUpdateStatus() == 0); + Require(scePlayerSelectionDialogGetStatus() == kStatusNone); + Require(scePlayerSelectionDialogUpdateStatus() == kStatusNone); + + Require(scePlayerSelectionDialogInitialize() == 0); + Require(scePlayerSelectionDialogInitialize() == kErrAlreadyInitialized); + Require(scePlayerSelectionDialogGetStatus() == kStatusInitialized); + Require(scePlayerSelectionDialogOpen(¶m) == 0); + Require(scePlayerSelectionDialogGetStatus() == kStatusFinished); + Require(scePlayerSelectionDialogGetResult(&result) == kResultUserCanceled); + Require(scePlayerSelectionDialogTerminate() == 0); Require(sceUserServiceTerminate() == 0); } diff --git a/core/libs/tests/GuestPoll.cpp b/core/libs/tests/GuestPoll.cpp index c00934f83f..be82e99f0a 100644 --- a/core/libs/tests/GuestPoll.cpp +++ b/core/libs/tests/GuestPoll.cpp @@ -327,10 +327,33 @@ static void PollsAHungUpSocketWithoutPollout() { } #endif +static void PollsAPipeClosedDuringTheWait() { + for (int attempt = 0; attempt < 200; ++attempt) { + int pair[2]{-1, -1}; + Require(pipe_nid_postfix(pair) == 0); + int result = 0; + short revents = 0; + std::atomic started{false}; + std::thread waiter([&] { + GuestPollDescriptor entry{pair[0], In, 0}; + started = true; + result = poll_nid_postfix(&entry, 1, 20); + revents = entry.revents; + }); + while (!started) std::this_thread::yield(); + std::this_thread::sleep_for(std::chrono::microseconds(1000 + (attempt % 20) * 100)); + Require(close_nid_postfix(pair[0]) == 0); + waiter.join(); + Require(close_nid_postfix(pair[1]) == 0); + Require(result == 0 || (result == 1 && (revents & Invalid) != 0)); + } +} + int main() { for (const int descriptor : {0, 1, 2}) StandardDescriptorFile(descriptor); PollsUrgentData(); PollsAStreamWhosePeerClosed(); + PollsAPipeClosedDuringTheWait(); #ifndef _WIN32 PollsAHungUpSocketWithoutPollout(); #endif diff --git a/core/libs/tests/GuestPosixExit.cpp b/core/libs/tests/GuestPosixExit.cpp new file mode 100644 index 0000000000..53e577a825 --- /dev/null +++ b/core/libs/tests/GuestPosixExit.cpp @@ -0,0 +1,80 @@ +#include "prx/libkernel/include/Api.hpp" +#include "prx/libkernel/Pthread/include/Cancel.hpp" +#include "SceTypes.hpp" + +#include +#include +#include + +namespace { + +void Require(bool value, const char* message) { + if (!value) { + std::fprintf(stderr, "%s\n", message); + std::abort(); + } +} + +struct Context { + PthreadKey key = 0; + int stored = 0; + int exitValue = 0; + bool exitWithValue = true; + std::atomic afterExit{false}; + std::atomic sequence{0}; + std::atomic cleanupOrder{0}; + std::atomic destructorOrder{0}; + std::atomic cleanupCalls{0}; + std::atomic destructorCalls{0}; + std::atomic destructorValue{nullptr}; +}; + +Context* g_context = nullptr; + +void APS5_VABI Cleanup(void* argument) { + auto& context = *static_cast(argument); + context.cleanupOrder = ++context.sequence; + ++context.cleanupCalls; +} + +void APS5_VABI Destroy(void* value) { + g_context->destructorValue = value; + g_context->destructorOrder = ++g_context->sequence; + ++g_context->destructorCalls; +} + +void* APS5_VABI Worker(void* argument) { + auto& context = *static_cast(argument); + Require(pthread_setspecific_nid_postfix(context.key, &context.stored) == 0, "worker setspecific returns 0"); + pthread_cleanup_push_nid_postfix(Cleanup, &context); + pthread_exit_nid_postfix(context.exitWithValue ? &context.exitValue : nullptr); + context.afterExit = true; + return argument; +} + +void Run(Context& context) { + g_context = &context; + Require(pthread_key_create_nid_postfix(&context.key, reinterpret_cast(Destroy)) == 0, "key create returns 0"); + Pthread thread = nullptr; + Require(pthread_create_nid_postfix(&thread, nullptr, Worker, &context) == 0, "create returns 0"); + void* result = &context; + Require(pthread_join_nid_postfix(thread, &result) == 0, "join returns 0"); + Require(!context.afterExit, "code after pthread_exit never runs"); + Require(context.destructorCalls == 1, "the key destructor runs once on pthread_exit"); + Require(context.cleanupCalls == 1, "the cleanup handler runs once on pthread_exit"); + Require(context.cleanupOrder < context.destructorOrder, "the cleanup handler runs before the key destructor"); + Require(context.destructorValue == &context.stored, "the key destructor receives the stored value"); + Require(result == (context.exitWithValue ? static_cast(&context.exitValue) : nullptr), "join returns the value passed to pthread_exit"); + Require(pthread_key_delete_nid_postfix(context.key) == 0, "key delete returns 0"); +} + +} + +int main() { + Context withValue; + Run(withValue); + Context withNull; + withNull.exitWithValue = false; + Run(withNull); + return 0; +} diff --git a/core/libs/tests/GuestPosixSem.cpp b/core/libs/tests/GuestPosixSem.cpp index aa3aea380f..6c66c1a240 100644 --- a/core/libs/tests/GuestPosixSem.cpp +++ b/core/libs/tests/GuestPosixSem.cpp @@ -115,6 +115,8 @@ int main(int argc, char** argv) { } return 0; } + *__error_nid_postfix() = 0; + Require(sem_post_nid_postfix(nullptr) == -1 && *__error_nid_postfix() == 22, "null semaphore pointer in sem_post must fail with EINVAL"); Require(sem_init_nid_postfix(&sem, 0, maximum) == 0, "maximum count must initialize"); Require(sem_getvalue_nid_postfix(&sem, &value) == 0 && value == static_cast(maximum), "maximum count must be readable"); *__error_nid_postfix() = 0; @@ -130,6 +132,11 @@ int main(int argc, char** argv) { Require(sem_getvalue_nid_postfix(&sem, &value) == 0 && value == static_cast(maximum), "repeated overflow changed count"); Require(sem_destroy_nid_postfix(&sem) == 0, "destroy must succeed"); Require(sem_init_nid_postfix(&sem, 0, 0) == 0, "zero count must initialize"); + *__error_nid_postfix() = 0; + Require(sem_trywait_nid_postfix(nullptr) == -1 && *__error_nid_postfix() == 22, "null semaphore pointer must report EINVAL"); + std::uintptr_t invalidSem = 0; + *__error_nid_postfix() = 0; + Require(sem_trywait_nid_postfix(&invalidSem) == -1 && *__error_nid_postfix() == 22, "uninitialized semaphore descriptor must report EINVAL"); Require(sem_trywait_nid_postfix(&sem) == -1 && *__error_nid_postfix() == 35, "empty semaphore must report EAGAIN"); Require(sem_post_nid_postfix(&sem) == 0, "normal post must succeed"); Require(sem_getvalue_nid_postfix(&sem, &value) == 0 && value == 1, "normal post must increment"); diff --git a/core/libs/tests/GuestPosixSpawnErrors.cpp b/core/libs/tests/GuestPosixSpawnErrors.cpp new file mode 100644 index 0000000000..771e1bd88e --- /dev/null +++ b/core/libs/tests/GuestPosixSpawnErrors.cpp @@ -0,0 +1,67 @@ +#include "prx/libSceLibcInternal/include/Api.hpp" +#include "prx/libkernel/include/Api.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static void Require(bool condition, int line) { + if (!condition) { + std::fprintf(stderr, "Posix spawn errors check failed at line %d\n", line); + std::exit(EXIT_FAILURE); + } +} +#define Check(value) Require((value), __LINE__) + +int main(int count, char** arguments) { + if (count == 2 && std::strcmp(arguments[1], "spawn-errors-child") == 0) return 43; + std::filesystem::create_directories("app0"); + { + std::ofstream image("app0/eboot.bin"); + image << "image"; + Check(image.good()); + } + std::filesystem::remove("app0/spawn-error-loop"); + std::filesystem::create_symlink("spawn-error-loop", "app0/spawn-error-loop"); + const std::array cases{ + std::pair{std::string("/app0/spawn-error-loop"), 62}, + std::pair{std::string("/app0/") + std::string(256, 'x'), 63}, + std::pair{std::string("/app0/eboot.bin"), 0} + }; + char program[] = "/app0/eboot.bin"; + char role[] = "spawn-errors-child"; + char* childArguments[] = {program, role, nullptr}; + bool passed = true; + for (auto spawn : std::array{&posix_spawn_nid_postfix, &posix_spawnp_nid_postfix}) { + for (const auto& [path, expected] : cases) { + PosixSpawnFileActions* actions = nullptr; + Check(posix_spawn_file_actions_init_nid_postfix(&actions) == 0); + Check(posix_spawn_file_actions_addopen_nid_postfix(&actions, 9, path.c_str(), 0, 0) == 0); + constexpr std::int32_t Sentinel = -12345; + std::int32_t process = Sentinel; + const int result = spawn(&process, program, &actions, nullptr, childArguments, nullptr); + Check(posix_spawn_file_actions_destroy_nid_postfix(&actions) == 0); + if (result == 0) { + Check(process > 0); + int status = -1; + Check(waitpid_nid_postfix(process, &status, 0) == process); + Check((status & 0x7f) == 0 && ((status >> 8) & 0xff) == 43); + } else { + Check(process == Sentinel); + } + if (result != expected) { + std::fprintf(stderr, "Spawn action %s: expected guest error %d, observed %d\n", path.c_str(), expected, result); + passed = false; + } + } + } + Check(std::filesystem::remove("app0/spawn-error-loop")); + Check(std::filesystem::remove("app0/eboot.bin")); + Check(std::filesystem::remove("app0")); + return passed ? EXIT_SUCCESS : EXIT_FAILURE; +} diff --git a/core/libs/tests/GuestPosixSpawnOptionalPid.cpp b/core/libs/tests/GuestPosixSpawnOptionalPid.cpp new file mode 100644 index 0000000000..9d616b3caa --- /dev/null +++ b/core/libs/tests/GuestPosixSpawnOptionalPid.cpp @@ -0,0 +1,47 @@ +#include "prx/libSceLibcInternal/include/Api.hpp" +#include "prx/libkernel/include/Api.hpp" +#include +#include +#include +#include +#include +#include + +static void Require(bool condition, int line) { + if (!condition) { + std::fprintf(stderr, "Posix spawn optional PID check failed at line %d\n", line); + std::exit(EXIT_FAILURE); + } +} +#define Check(value) Require((value), __LINE__) + +int main(int count, char** arguments) { +#ifdef _WIN32 + constexpr int ChildArgument = 2; +#else + constexpr int ChildArgument = 1; +#endif + if (count == ChildArgument + 1 && std::strcmp(arguments[ChildArgument], "spawn-optional-pid-child") == 0) return 43; + std::filesystem::create_directories("app0"); + { + std::ofstream image("app0/eboot.bin"); + image << "image"; + Check(image.good()); + } + char program[] = "/app0/eboot.bin"; + char role[] = "spawn-optional-pid-child"; + char* childArguments[] = {program, role, nullptr}; + const std::array spawnFunctions{&posix_spawn_nid_postfix, &posix_spawnp_nid_postfix}; + for (std::size_t index = 0; index < spawnFunctions.size(); ++index) { + const auto spawn = spawnFunctions[index]; + std::fprintf(stderr, "Spawn wrapper %zu: testing a null PID output\n", index); + Check(spawn(nullptr, program, nullptr, nullptr, childArguments, nullptr) == 0); + int status = -1; + Check(waitpid_nid_postfix(-1, &status, 0) > 0); + Check((status & 0x7f) == 0 && ((status >> 8) & 0xff) == 43); + Check(spawn(nullptr, "/app0/missing.bin", nullptr, nullptr, childArguments, nullptr) == 2); + Check(spawn(nullptr, program, nullptr, nullptr, nullptr, nullptr) == 14); + } + Check(std::filesystem::remove("app0/eboot.bin")); + Check(std::filesystem::remove("app0")); +} diff --git a/core/libs/tests/GuestPthreadAttr.cpp b/core/libs/tests/GuestPthreadAttr.cpp index b29f772084..770f317e89 100644 --- a/core/libs/tests/GuestPthreadAttr.cpp +++ b/core/libs/tests/GuestPthreadAttr.cpp @@ -2,6 +2,7 @@ #include "prx/libkernel/include/Api.hpp" #include "prx/libc/include/Api.hpp" #include "SceTypes.hpp" +#include #include #include #include @@ -53,15 +54,45 @@ static ReportedAttributes Query(Pthread thread) { static void CheckStackSizeLimit() { PthreadAttr attr = nullptr; + PthreadAttr unset = nullptr; + Require(scePthreadAttrSetstacksize(nullptr, MIN_STACK_SIZE) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrSetstacksize(&unset, MIN_STACK_SIZE) == SCE_KERNEL_ERROR_EINVAL); Require(scePthreadAttrInit(&attr) == SCE_OK); std::size_t stackSize = 0; Require(scePthreadAttrSetstacksize(&attr, MIN_STACK_SIZE) == SCE_OK); Require(scePthreadAttrGetstacksize(&attr, &stackSize) == SCE_OK && stackSize == MIN_STACK_SIZE); - Require(scePthreadAttrSetstacksize(&attr, MIN_STACK_SIZE - 1) == SCE_KERNEL_ERROR_EINVAL); - Require(scePthreadAttrSetstacksize(&attr, 0) == SCE_KERNEL_ERROR_EINVAL); + for (const std::size_t rejected : std::array{0, 1, 512, 4096, 8192, MIN_STACK_SIZE - 1}) { + Require(scePthreadAttrSetstacksize(&attr, rejected) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrGetstacksize(&attr, &stackSize) == SCE_OK && stackSize == MIN_STACK_SIZE); + } Require(pthread_attr_setstacksize_nid_postfix(&attr, MIN_STACK_SIZE - 1) == GUEST_EINVAL); Require(scePthreadAttrGetstacksize(&attr, &stackSize) == SCE_OK && stackSize == MIN_STACK_SIZE); Require(scePthreadAttrDestroy(&attr) == SCE_OK); + Require(scePthreadAttrSetstacksize(&attr, MIN_STACK_SIZE) == SCE_KERNEL_ERROR_EINVAL); +} + +static void CheckGuardSize() { + PthreadAttr attr = nullptr; + std::size_t guardSize = 0; + PthreadAttr unset = nullptr; + Require(scePthreadAttrGetguardsize(nullptr, &guardSize) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrGetguardsize(&unset, &guardSize) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrSetguardsize(nullptr, 4096) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrSetguardsize(&unset, 4096) == SCE_KERNEL_ERROR_EINVAL); + + Require(scePthreadAttrInit(&attr) == SCE_OK); + Require(scePthreadAttrGetguardsize(&attr, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrGetguardsize(&attr, &guardSize) == SCE_OK && guardSize == 4096); + + for (const std::size_t size : std::array{0, 4096, 8192, 16384, 65536}) { + Require(scePthreadAttrSetguardsize(&attr, size) == SCE_OK); + guardSize = 0; + Require(scePthreadAttrGetguardsize(&attr, &guardSize) == SCE_OK && guardSize == size); + } + + Require(scePthreadAttrDestroy(&attr) == SCE_OK); + Require(scePthreadAttrGetguardsize(&attr, &guardSize) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrSetguardsize(&attr, 4096) == SCE_KERNEL_ERROR_EINVAL); } static void CheckSoloSched() { @@ -126,6 +157,34 @@ static void CheckSchedArguments() { Require(scePthreadAttrDestroy(&attr) == SCE_OK); } +static void CheckSchedPolicyAndStackGetters() { + PthreadAttr unset = nullptr; + PthreadAttr destroyed = nullptr; + PthreadAttr live = nullptr; + Require(scePthreadAttrInit(&destroyed) == SCE_OK); + Require(scePthreadAttrDestroy(&destroyed) == SCE_OK); + Require(scePthreadAttrInit(&live) == SCE_OK); + for (PthreadAttr* invalid : {static_cast(nullptr), &unset, &destroyed}) { + int policy = -1; + KernelSchedParam param{-1}; + void* address = &policy; + std::size_t size = 7; + Require(pthread_attr_getschedpolicy_nid_postfix(invalid, &policy) == GUEST_EINVAL && policy == -1); + Require(pthread_attr_getschedparam_nid_postfix(invalid, ¶m) == GUEST_EINVAL && param.sched_priority == -1); + Require(pthread_attr_getstacksize_nid_postfix(invalid, &size) == GUEST_EINVAL && size == 7); + Require(pthread_attr_getstackaddr_nid_postfix(invalid, &address) == GUEST_EINVAL && address == &policy); + Require(pthread_attr_getstack_nid_postfix(invalid, &address, &size) == GUEST_EINVAL && address == &policy && size == 7); + } + int policy = -1; + void* address = &policy; + std::size_t size = 7; + Require(pthread_attr_getschedpolicy_nid_postfix(&live, nullptr) == GUEST_EINVAL); + Require(pthread_attr_getstackaddr_nid_postfix(&live, nullptr) == GUEST_EINVAL); + Require(pthread_attr_getstack_nid_postfix(&live, nullptr, &size) == GUEST_EINVAL && size == 7); + Require(pthread_attr_getstack_nid_postfix(&live, &address, nullptr) == GUEST_EINVAL && address == &policy); + Require(scePthreadAttrDestroy(&live) == SCE_OK); +} + static void* APS5_VABI Worker(void* arg) { static_cast*>(arg)->get(); return nullptr; @@ -134,18 +193,27 @@ static void* APS5_VABI Worker(void* arg) { static void CheckSchedulingAttributes() { int inherit = -1; int scope = -1; + PthreadAttr unset = nullptr; Require(pthread_attr_getinheritsched_nid_postfix(nullptr, &inherit) == GUEST_EINVAL); Require(pthread_attr_setinheritsched_nid_postfix(nullptr, EXPLICIT_SCHED) == GUEST_EINVAL); Require(pthread_attr_getscope_nid_postfix(nullptr, &scope) == GUEST_EINVAL); Require(pthread_attr_setscope_nid_postfix(nullptr, SCOPE_SYSTEM) == GUEST_EINVAL); + Require(scePthreadAttrGetinheritsched(nullptr, &inherit) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrGetinheritsched(&unset, &inherit) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrSetinheritsched(nullptr, EXPLICIT_SCHED) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrSetinheritsched(&unset, EXPLICIT_SCHED) == SCE_KERNEL_ERROR_EINVAL); PthreadAttr attr = nullptr; Require(pthread_attr_getinheritsched_nid_postfix(&attr, &inherit) == GUEST_EINVAL); Require(pthread_attr_setinheritsched_nid_postfix(&attr, EXPLICIT_SCHED) == GUEST_EINVAL); Require(pthread_attr_getscope_nid_postfix(&attr, &scope) == GUEST_EINVAL); Require(pthread_attr_setscope_nid_postfix(&attr, SCOPE_SYSTEM) == GUEST_EINVAL); + Require(scePthreadAttrGetinheritsched(&attr, &inherit) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrSetinheritsched(&attr, EXPLICIT_SCHED) == SCE_KERNEL_ERROR_EINVAL); Require(scePthreadAttrInit(&attr) == SCE_OK); + Require(scePthreadAttrGetinheritsched(&attr, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrGetinheritsched(&attr, &inherit) == SCE_OK && inherit == INHERIT_SCHED); Require(pthread_attr_getinheritsched_nid_postfix(&attr, &inherit) == 0 && inherit == INHERIT_SCHED); Require(pthread_attr_setinheritsched_nid_postfix(&attr, 1) == GUEST_ENOTSUP); Require(pthread_attr_setinheritsched_nid_postfix(&attr, -1) == GUEST_ENOTSUP); @@ -154,6 +222,19 @@ static void CheckSchedulingAttributes() { Require(pthread_attr_setinheritsched_nid_postfix(&attr, INHERIT_SCHED) == 0); Require(pthread_attr_getinheritsched_nid_postfix(&attr, &inherit) == 0 && inherit == INHERIT_SCHED); + Require(scePthreadAttrSetinheritsched(&attr, EXPLICIT_SCHED) == SCE_OK); + inherit = -1; + Require(scePthreadAttrGetinheritsched(&attr, &inherit) == SCE_OK && inherit == EXPLICIT_SCHED); + Require(scePthreadAttrSetinheritsched(&attr, INHERIT_SCHED) == SCE_OK); + inherit = -1; + Require(scePthreadAttrGetinheritsched(&attr, &inherit) == SCE_OK && inherit == INHERIT_SCHED); + + for (const int rejected : {-1, 1, 2, 3, 5, 8, 16, 100}) { + Require(scePthreadAttrSetinheritsched(&attr, rejected) == SCE_KERNEL_ERROR_EOPNOTSUPP); + inherit = -1; + Require(scePthreadAttrGetinheritsched(&attr, &inherit) == SCE_OK && inherit == INHERIT_SCHED); + } + Require(pthread_attr_getscope_nid_postfix(&attr, &scope) == 0 && scope == SCOPE_SYSTEM); Require(pthread_attr_setscope_nid_postfix(&attr, 1) == GUEST_EINVAL); Require(pthread_attr_setscope_nid_postfix(&attr, 3) == GUEST_EINVAL); @@ -172,6 +253,8 @@ static void CheckSchedulingAttributes() { Require(pthread_attr_setinheritsched_nid_postfix(&attr, EXPLICIT_SCHED) == GUEST_EINVAL); Require(pthread_attr_getscope_nid_postfix(&attr, &scope) == GUEST_EINVAL); Require(pthread_attr_setscope_nid_postfix(&attr, SCOPE_SYSTEM) == GUEST_EINVAL); + Require(scePthreadAttrGetinheritsched(&attr, &inherit) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrSetinheritsched(&attr, EXPLICIT_SCHED) == SCE_KERNEL_ERROR_EINVAL); } static void CheckThreadScope() { @@ -334,14 +417,27 @@ static void CheckGetprioOfTheCallingThread() { Require(scePthreadGetprio(self, &priority) == SCE_OK && priority == original); } +static void CheckSetStackSizeOnUnsetAttributes() { + PthreadAttr unset = nullptr; + PthreadAttr destroyed = nullptr; + Require(scePthreadAttrInit(&destroyed) == SCE_OK); + Require(scePthreadAttrDestroy(&destroyed) == SCE_OK); + Require(pthread_attr_setstacksize_nid_postfix(nullptr, STACK_SIZE) == GUEST_EINVAL); + Require(pthread_attr_setstacksize_nid_postfix(&unset, STACK_SIZE) == GUEST_EINVAL); + Require(pthread_attr_setstacksize_nid_postfix(&destroyed, STACK_SIZE) == GUEST_EINVAL); +} + int main() { CheckGetprioOfTheCallingThread(); CheckStackSizeLimit(); + CheckGuardSize(); CheckSoloSched(); CheckSchedArguments(); CheckSchedulingAttributes(); + CheckSchedPolicyAndStackGetters(); CheckThreadScope(); CheckEndedThreads(); + CheckSetStackSizeOnUnsetAttributes(); PthreadAttr attr = nullptr; Require(scePthreadAttrInit(&attr) == SCE_OK); Require(scePthreadAttrSetinheritsched(&attr, EXPLICIT_SCHED) == SCE_OK); diff --git a/core/libs/tests/GuestPthreadBarrier.cpp b/core/libs/tests/GuestPthreadBarrier.cpp index df85bc1cbb..597a37a7bf 100644 --- a/core/libs/tests/GuestPthreadBarrier.cpp +++ b/core/libs/tests/GuestPthreadBarrier.cpp @@ -101,10 +101,16 @@ int main() { Require(rejected); PthreadBarrierPrivate* invalid = nullptr; + Require(pthread_barrier_destroy_nid_postfix(nullptr) == GUEST_EINVAL); + Require(pthread_barrier_destroy_nid_postfix(&invalid) == GUEST_EINVAL); Require(pthread_barrier_init_nid_postfix(nullptr, &attr, 1) == GUEST_EINVAL); + Require(pthread_barrier_init_nid_postfix(nullptr, nullptr, 1) == GUEST_EINVAL); + Require(pthread_barrier_init_nid_postfix(nullptr, nullptr, 0) == GUEST_EINVAL); + Require(pthread_barrier_init_nid_postfix(&invalid, nullptr, 0) == GUEST_EINVAL && invalid == nullptr); Require(pthread_barrier_init_nid_postfix(&invalid, &attr, 0) == GUEST_EINVAL && invalid == nullptr); Require(pthread_barrier_init_nid_postfix(&invalid, &attr, 0x80000000u) == GUEST_EINVAL && invalid == nullptr); Require(pthread_barrier_init_nid_postfix(&invalid, &attr, 0xffffffffu) == GUEST_EINVAL && invalid == nullptr); + Require(pthread_barrier_destroy_nid_postfix(&invalid) == GUEST_EINVAL); Context withAttr; Require(pthread_barrier_init_nid_postfix(&withAttr.barrier, &attr, WAITERS) == 0 && withAttr.barrier != nullptr); @@ -133,8 +139,10 @@ int main() { } PthreadBarrierPrivate* single = nullptr; + Require(pthread_barrier_destroy_nid_postfix(nullptr) == GUEST_EINVAL); Require(pthread_barrierattr_init_nid_postfix(&attr) == 0); Require(pthread_barrier_init_nid_postfix(&single, &attr, 1) == 0); + Require(pthread_barrier_wait_nid_postfix(nullptr) == GUEST_EINVAL); Require(pthread_barrier_wait_nid_postfix(&single) == BARRIER_SERIAL_THREAD); Require(pthread_barrier_wait_nid_postfix(&single) == BARRIER_SERIAL_THREAD); Require(pthread_barrier_destroy_nid_postfix(&single) == 0); diff --git a/core/libs/tests/GuestPthreadCondattr.cpp b/core/libs/tests/GuestPthreadCondattr.cpp index d35bc52733..0ac21c203c 100644 --- a/core/libs/tests/GuestPthreadCondattr.cpp +++ b/core/libs/tests/GuestPthreadCondattr.cpp @@ -44,6 +44,8 @@ int main() { Require(scePthreadCondattrSetclock(&unset, Monotonic) == SceEinval); Require(scePthreadCondattrDestroy(nullptr) == SceEinval); Require(scePthreadCondattrDestroy(&unset) == SceEinval); + Require(pthread_condattr_destroy_nid_postfix(nullptr) == GuestEinval); + Require(pthread_condattr_destroy_nid_postfix(&unset) == GuestEinval); Require(pthread_condattr_setclock_nid_postfix(&unset, Monotonic) == GuestEinval); Require(pthread_condattr_setpshared_nid_postfix(nullptr, 0) == GuestEinval); Require(pthread_condattr_setpshared_nid_postfix(&unset, 0) == GuestEinval); @@ -70,6 +72,8 @@ int main() { CheckClock(&other, Monotonic); CheckClock(&attr, Monotonic); Require(pthread_condattr_destroy_nid_postfix(&other) == 0 && other == nullptr); + Require(pthread_condattr_destroy_nid_postfix(&other) == GuestEinval); + Require(pthread_condattr_destroy_nid_postfix(nullptr) == GuestEinval); Require(pthread_condattr_getclock_nid_postfix(&other, &clockId) == GuestEinval && clockId == -1); Require(scePthreadCondattrSetpshared(&attr, 1) == SceEinval); Require(scePthreadCondattrSetpshared(&attr, 0) == SCE_OK); @@ -81,7 +85,8 @@ int main() { Require(pthread_condattr_setclock_nid_postfix(&attr, clock) == GuestEinval); CheckClock(&attr, Monotonic); } - for (const int invalid : {1, 2, -1}) { + for (const int invalid : {1, 2, -1, 100, static_cast(0x7fffffff)}) { + Require(scePthreadCondattrSetpshared(&attr, invalid) == SceEinval); Require(pthread_condattr_setpshared_nid_postfix(&attr, invalid) == GuestEinval); sharing = -1; Require(pthread_condattr_getpshared_nid_postfix(&attr, &sharing) == 0 && sharing == 0); @@ -100,6 +105,9 @@ int main() { Require(scePthreadCondattrDestroy(&attr) == SCE_OK && attr == nullptr); Require(pthread_condattr_getclock_nid_postfix(&attr, &clockId) == GuestEinval && clockId == -1); Require(scePthreadCondattrDestroy(&attr) == SceEinval); + Require(scePthreadCondattrDestroy(nullptr) == SceEinval); + Require(pthread_condattr_destroy_nid_postfix(&attr) == GuestEinval); + Require(pthread_condattr_destroy_nid_postfix(nullptr) == GuestEinval); sharing = -1; Require(pthread_condattr_getpshared_nid_postfix(&attr, &sharing) == GuestEinval && sharing == -1); Require(pthread_condattr_setpshared_nid_postfix(&attr, 0) == GuestEinval); diff --git a/core/libs/tests/GuestPthreadMutexErrors.cpp b/core/libs/tests/GuestPthreadMutexErrors.cpp index 6a99ea4c97..587c57a449 100644 --- a/core/libs/tests/GuestPthreadMutexErrors.cpp +++ b/core/libs/tests/GuestPthreadMutexErrors.cpp @@ -120,14 +120,21 @@ int main() { for (int type : {1, 2, 3, 4}) Require(scePthreadMutexattrSettype(&attr, type) == SCE_OK); for (int protocol : {-1, 3}) Require(scePthreadMutexattrSetprotocol(&attr, protocol) == SCE_KERNEL_ERROR_EINVAL); for (int protocol : {0, 1, 2}) Require(scePthreadMutexattrSetprotocol(&attr, protocol) == SCE_OK); + for (int prioceiling : {-10, -1, 0, 255, 768, 1000}) Require(scePthreadMutexattrSetprioceiling(&attr, prioceiling) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadMutexattrSetprioceiling(&attr, 300) == SCE_OK); PthreadMutexattr unset = nullptr; Require(scePthreadMutexattrSettype(&unset, 1) == SCE_KERNEL_ERROR_EINVAL); Require(scePthreadMutexattrSetprotocol(&unset, 0) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadMutexattrSetprioceiling(&unset, 300) == SCE_KERNEL_ERROR_EINVAL); Require(scePthreadMutexattrDestroy(&unset) == SCE_KERNEL_ERROR_EINVAL); Require(scePthreadMutexattrSettype(nullptr, 1) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadMutexattrSetprioceiling(nullptr, 300) == SCE_KERNEL_ERROR_EINVAL); Require(scePthreadMutexattrDestroy(nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(Throws([] { pthread_mutex_destroy_nid_postfix(nullptr); })); + Require(Throws([] { scePthreadMutexDestroy(nullptr); })); + PthreadMutex mutex = nullptr; Require(scePthreadMutexInit(&mutex, &unset, "unset") == SCE_OK); Require(scePthreadMutexLock(&mutex) == SCE_OK); diff --git a/core/libs/tests/GuestPthreadMutexattr.cpp b/core/libs/tests/GuestPthreadMutexattr.cpp index 4568fc1464..675f5b8723 100644 --- a/core/libs/tests/GuestPthreadMutexattr.cpp +++ b/core/libs/tests/GuestPthreadMutexattr.cpp @@ -48,6 +48,8 @@ static void CheckTimedlockInvalidDeadline() { static void CheckType() { PthreadMutexattr attr = nullptr; + Require(pthread_mutexattr_destroy_nid_postfix(nullptr) == GUEST_EINVAL); + Require(pthread_mutexattr_destroy_nid_postfix(&attr) == GUEST_EINVAL); Require(pthread_mutexattr_init_nid_postfix(&attr) == 0); int type = -1; Require(pthread_mutexattr_gettype_nid_postfix(&attr, &type) == 0 && type == 1); @@ -55,10 +57,13 @@ static void CheckType() { Require(pthread_mutexattr_settype_nid_postfix(&attr, set) == 0); Require(pthread_mutexattr_gettype_nid_postfix(&attr, &type) == 0 && type == set); } + Require(pthread_mutexattr_settype_nid_postfix(nullptr, 1) == GUEST_EINVAL); Require(pthread_mutexattr_settype_nid_postfix(&attr, 5) == GUEST_EINVAL); Require(pthread_mutexattr_gettype_nid_postfix(&attr, &type) == 0 && type == 1); Require(Throws([&] { pthread_mutexattr_gettype_nid_postfix(&attr, nullptr); })); Require(pthread_mutexattr_destroy_nid_postfix(&attr) == 0); + Require(pthread_mutexattr_destroy_nid_postfix(nullptr) == GUEST_EINVAL); + Require(pthread_mutexattr_destroy_nid_postfix(&attr) == GUEST_EINVAL); type = -1; Require(pthread_mutexattr_gettype_nid_postfix(&attr, &type) == GUEST_EINVAL && type == -1); Require(pthread_mutexattr_gettype_nid_postfix(nullptr, &type) == GUEST_EINVAL && type == -1); @@ -125,6 +130,8 @@ static void CheckKind() { static void CheckSceAccessors() { PthreadMutexattr attr = nullptr; int value = -1; + Require(scePthreadMutexattrDestroy(nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadMutexattrDestroy(&attr) == SCE_KERNEL_ERROR_EINVAL); Require(scePthreadMutexattrGettype(nullptr, &value) == SCE_KERNEL_ERROR_EINVAL); Require(scePthreadMutexattrGettype(&attr, &value) == SCE_KERNEL_ERROR_EINVAL); Require(scePthreadMutexattrSetprotocol(&attr, PRIO_NONE) == SCE_KERNEL_ERROR_EINVAL); @@ -158,12 +165,16 @@ static void CheckSceAccessors() { Require(scePthreadMutexattrSetpshared(&attr, 0) == SCE_OK); Require(scePthreadMutexattrGetpshared(&attr, &value) == SCE_OK && value == 0); Require(scePthreadMutexattrDestroy(&attr) == SCE_OK); + Require(scePthreadMutexattrDestroy(nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadMutexattrDestroy(&attr) == SCE_KERNEL_ERROR_EINVAL); Require(scePthreadMutexattrGettype(&attr, &value) == SCE_KERNEL_ERROR_EINVAL); } static void CheckProcessSharing() { PthreadMutexattr attr = nullptr; int sharing = -1; + Require(pthread_mutexattr_destroy_nid_postfix(nullptr) == GUEST_EINVAL); + Require(pthread_mutexattr_destroy_nid_postfix(&attr) == GUEST_EINVAL); Require(pthread_mutexattr_getpshared_nid_postfix(nullptr, &sharing) == GUEST_EINVAL && sharing == -1); Require(pthread_mutexattr_getpshared_nid_postfix(&attr, &sharing) == GUEST_EINVAL && sharing == -1); Require(pthread_mutexattr_setpshared_nid_postfix(nullptr, 0) == GUEST_EINVAL); @@ -189,6 +200,8 @@ static void CheckProcessSharing() { Require(pthread_mutexattr_setpshared_nid_postfix(&attr, 0) == 0); Require(pthread_mutexattr_getpshared_nid_postfix(&attr, &sharing) == 0 && sharing == 0); Require(pthread_mutexattr_destroy_nid_postfix(&attr) == 0); + Require(pthread_mutexattr_destroy_nid_postfix(nullptr) == GUEST_EINVAL); + Require(pthread_mutexattr_destroy_nid_postfix(&attr) == GUEST_EINVAL); sharing = -1; Require(pthread_mutexattr_getpshared_nid_postfix(&attr, &sharing) == GUEST_EINVAL && sharing == -1); Require(pthread_mutexattr_setpshared_nid_postfix(&attr, 0) == GUEST_EINVAL); @@ -197,6 +210,8 @@ static void CheckProcessSharing() { static void CheckInvalidPriorityAttribute(PthreadMutexattr* attr) { int ceiling = -1; int protocol = -1; + Require(pthread_mutexattr_destroy_nid_postfix(attr) == GUEST_EINVAL); + Require(scePthreadMutexattrDestroy(attr) == SCE_KERNEL_ERROR_EINVAL); Require(pthread_mutexattr_setprotocol_nid_postfix(attr, PRIO_PROTECT) == GUEST_EINVAL); Require(pthread_mutexattr_setprotocol_nid_postfix(attr, -1) == GUEST_EINVAL); Require(pthread_mutexattr_getprotocol_nid_postfix(attr, &protocol) == GUEST_EINVAL && protocol == -1); @@ -206,12 +221,33 @@ static void CheckInvalidPriorityAttribute(PthreadMutexattr* attr) { Require(pthread_mutexattr_getprioceiling_nid_postfix(attr, nullptr) == GUEST_EINVAL); } +static void CheckDestroy() { + PthreadMutexattr attr = nullptr; + Require(pthread_mutexattr_destroy_nid_postfix(nullptr) == GUEST_EINVAL); + Require(pthread_mutexattr_destroy_nid_postfix(&attr) == GUEST_EINVAL); + Require(scePthreadMutexattrDestroy(nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadMutexattrDestroy(&attr) == SCE_KERNEL_ERROR_EINVAL); + Require(pthread_mutexattr_init_nid_postfix(&attr) == 0); + Require(pthread_mutexattr_destroy_nid_postfix(&attr) == 0); + Require(pthread_mutexattr_destroy_nid_postfix(nullptr) == GUEST_EINVAL); + Require(pthread_mutexattr_destroy_nid_postfix(&attr) == GUEST_EINVAL); + Require(scePthreadMutexattrDestroy(nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadMutexattrDestroy(&attr) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadMutexattrInit(&attr) == SCE_OK); + Require(scePthreadMutexattrDestroy(&attr) == SCE_OK); + Require(pthread_mutexattr_destroy_nid_postfix(nullptr) == GUEST_EINVAL); + Require(pthread_mutexattr_destroy_nid_postfix(&attr) == GUEST_EINVAL); + Require(scePthreadMutexattrDestroy(nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadMutexattrDestroy(&attr) == SCE_KERNEL_ERROR_EINVAL); +} + int main() { CheckType(); CheckKind(); CheckSceAccessors(); CheckProcessSharing(); CheckTimedlockInvalidDeadline(); + CheckDestroy(); PthreadMutexattr attr = nullptr; CheckInvalidPriorityAttribute(nullptr); CheckInvalidPriorityAttribute(&attr); diff --git a/core/libs/tests/GuestPthreadPosixCalls.cpp b/core/libs/tests/GuestPthreadPosixCalls.cpp index 91d08bad91..9ea4243501 100644 --- a/core/libs/tests/GuestPthreadPosixCalls.cpp +++ b/core/libs/tests/GuestPthreadPosixCalls.cpp @@ -65,6 +65,15 @@ int main() { Check(pthread_attr_destroy_nid_postfix(&attr) == 0); int afterDestroy = -1; Check(pthread_attr_getdetachstate_nid_postfix(&attr, &afterDestroy) == GuestEinval); + PthreadAttr unset = nullptr; + std::size_t guard = 7; + afterDestroy = -1; + for (PthreadAttr* invalid : {static_cast(nullptr), &unset, &attr}) { + Check(pthread_attr_getdetachstate_nid_postfix(invalid, &afterDestroy) == GuestEinval && afterDestroy == -1); + Check(pthread_attr_setdetachstate_nid_postfix(invalid, 0) == GuestEinval); + Check(pthread_attr_getguardsize_nid_postfix(invalid, &guard) == GuestEinval && guard == 7); + Check(pthread_attr_setguardsize_nid_postfix(invalid, 4096) == GuestEinval); + } Check(pthread_attr_init_nid_postfix(nullptr) == GuestEinval); Check(pthread_attr_destroy_nid_postfix(nullptr) == GuestEinval); diff --git a/core/libs/tests/GuestPthreadRename.cpp b/core/libs/tests/GuestPthreadRename.cpp index ccfa5ced39..f4dbb24202 100644 --- a/core/libs/tests/GuestPthreadRename.cpp +++ b/core/libs/tests/GuestPthreadRename.cpp @@ -7,8 +7,29 @@ #include #include +#ifdef _WIN32 +#include +#endif + static void Require(bool value) { if (!value) std::abort(); } +#ifdef _WIN32 +static void CheckDescription(const std::wstring& expected) { + PWSTR description = nullptr; + Require(SUCCEEDED(GetThreadDescription(GetCurrentThread(), &description)) && description != nullptr); + const std::wstring actual(description); + LocalFree(description); + if (actual != expected) { + std::fprintf(stderr, "thread description units:"); + for (const wchar_t unit : actual) std::fprintf(stderr, " %04x", static_cast(unit)); + std::fprintf(stderr, "; expected:"); + for (const wchar_t unit : expected) std::fprintf(stderr, " %04x", static_cast(unit)); + std::fprintf(stderr, "\n"); + std::abort(); + } +} +#endif + static void CheckStoredName(Pthread thread, const std::string& expected) { std::array buffer; buffer.fill(0xaa); @@ -45,6 +66,11 @@ static void CheckThreadRename(Pthread thread) { static void* APS5_VABI Worker(void*) { const Pthread self = scePthreadSelf(); Require(self != nullptr); +#ifdef _WIN32 + CheckDescription(L"Z\u00e4hler"); + Require(scePthreadRename(self, "Z\xc3\xa4hler-\xe6\x97\xa5") == 0); + CheckDescription(L"Z\u00e4hler-\u65e5"); +#endif CheckThreadRename(self); return nullptr; } @@ -54,6 +80,6 @@ int main() { Require(self != nullptr); CheckThreadRename(self); Pthread thread = nullptr; - Require(scePthreadCreate(&thread, nullptr, Worker, nullptr, "rename-test") == 0); + Require(scePthreadCreate(&thread, nullptr, Worker, nullptr, "Z\xc3\xa4hler") == 0); Require(scePthreadJoin(thread, nullptr) == 0); } diff --git a/core/libs/tests/GuestPthreadRwlock.cpp b/core/libs/tests/GuestPthreadRwlock.cpp index f57954b010..0d95ba0e13 100644 --- a/core/libs/tests/GuestPthreadRwlock.cpp +++ b/core/libs/tests/GuestPthreadRwlock.cpp @@ -157,7 +157,9 @@ static void CheckDestroyLifecycle() { Require(held == DestroyedRwlock); Require(scePthreadRwlockUnlock(&held) == SCE_KERNEL_ERROR_EINVAL); Require(scePthreadRwlockRdlock(&held) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadRwlockWrlock(&held) == SCE_KERNEL_ERROR_EINVAL); Require(scePthreadRwlockTryrdlock(&held) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadRwlockTrywrlock(&held) == SCE_KERNEL_ERROR_EINVAL); Require(scePthreadRwlockDestroy(&held) == SCE_KERNEL_ERROR_EINVAL); Require(pthread_rwlock_rdlock_nid_postfix(&held) == GUEST_EINVAL); Require(held == DestroyedRwlock); diff --git a/core/libs/tests/GuestQsort.cpp b/core/libs/tests/GuestQsort.cpp index 1218e3039a..6de50251d7 100644 --- a/core/libs/tests/GuestQsort.cpp +++ b/core/libs/tests/GuestQsort.cpp @@ -353,6 +353,25 @@ bool CheckMergesort() { return correct; } +std::uintptr_t heapsortBase = 0; +std::uintptr_t heapsortLimit = 0; +std::size_t heapsortStride = 0; +bool heapsortArgumentsValid = true; +int heapsortCompareCalls = 0; + +int APS5_VABI RecordingCompareKeys(const void* left, const void* right) { + ++heapsortCompareCalls; + const auto inArray = [](const void* candidate) { + const std::uintptr_t address = reinterpret_cast(candidate); + return address >= heapsortBase && address < heapsortLimit + && (address - heapsortBase) % heapsortStride == 0; + }; + const bool leftInArray = inArray(left); + const bool rightInArray = inArray(right); + if (!leftInArray && !rightInArray) heapsortArgumentsValid = false; + return CompareKeys(left, right); +} + bool CheckHeapsort() { bool correct = true; for (const std::size_t count : {0, 1, 2, 3, 7, 16, 33, 1000}) { @@ -365,6 +384,60 @@ bool CheckHeapsort() { correct &= before.key <= after.key; } } + for (const std::size_t count : {0, 1, 2, 3, 7, 16, 33, 1000}) { + std::vector elements; + for (std::size_t index = 0; index < count; ++index) elements.push_back({static_cast((index * 7919) % 11), static_cast(index)}); + std::vector counts(11); + for (const auto& element : elements) ++counts[element.key]; + correct &= heapsort_nid_postfix(elements.data(), count, sizeof(Element), CompareKeys) == 0; + for (std::size_t index = 1; index < count; ++index) correct &= elements[index - 1].key <= elements[index].key; + std::vector seen(count); + for (const auto& element : elements) { + if (element.key < 0 || element.key >= 11 || element.tag < 0 || static_cast(element.tag) >= count) { + correct = false; + continue; + } + --counts[element.key]; + correct &= !seen[element.tag] && element.key == (element.tag * 7919) % 11; + seen[element.tag] = true; + } + for (const int remaining : counts) correct &= remaining == 0; + } + { + unsigned char payload[8] = {5, 2, 4, 1, 3, 2, 5, 1}; + correct &= heapsort_nid_postfix(payload, 8, 1, CompareFirstByte) == 0; + const unsigned char expected[8] = {1, 1, 2, 2, 3, 4, 5, 5}; + correct &= std::memcmp(payload, expected, sizeof(payload)) == 0; + } + { + struct ThreeByte { unsigned char key; unsigned char tag[2]; }; + ThreeByte records[6] = {{4, {1,17}}, {1, {2,29}}, {4, {3,43}}, {2, {4,61}}, {0, {5,83}}, {3, {6,107}}}; + static_assert(sizeof(ThreeByte) == 3); + const auto original = std::to_array(records); + correct &= heapsort_nid_postfix(records, 6, sizeof(ThreeByte), CompareFirstByte) == 0; + for (std::size_t index = 1; index < 6; ++index) correct &= records[index - 1].key <= records[index].key; + std::array seen{}; + for (const auto& record : records) { + if (record.tag[0] < 1 || record.tag[0] > 6) { + correct = false; + continue; + } + const auto index = record.tag[0] - 1; + correct &= !seen[index] && record.key == original[index].key && record.tag[1] == original[index].tag[1]; + seen[index] = true; + } + } + { + std::vector elements; + for (int index = 0; index < 64; ++index) elements.push_back({(index * 11) % 7, index}); + heapsortBase = reinterpret_cast(elements.data()); + heapsortStride = sizeof(Element); + heapsortLimit = heapsortBase + elements.size() * heapsortStride; + heapsortArgumentsValid = true; + heapsortCompareCalls = 0; + correct &= heapsort_nid_postfix(elements.data(), elements.size(), sizeof(Element), RecordingCompareKeys) == 0 && heapsortArgumentsValid && heapsortCompareCalls > 0; + for (std::size_t index = 1; index < elements.size(); ++index) correct &= elements[index - 1].key <= elements[index].key; + } unsigned char bytes[3] = {3, 1, 2}; *__error_nid_postfix() = 0; correct &= heapsort_nid_postfix(nullptr, 0, 0, nullptr) == 0; @@ -372,6 +445,8 @@ bool CheckHeapsort() { correct &= heapsort_nid_postfix(&single, 1, 0, nullptr) == 0; correct &= heapsort_nid_postfix(bytes, 3, 0, CompareFirstByte) == -1 && *__error_nid_postfix() == 22 && bytes[0] == 3; correct &= heapsort_nid_postfix(nullptr, 0, 4, CompareKeys) == 0; + correct &= heapsort_nid_postfix(nullptr, 1, 4, CompareKeys) == 0; + correct &= heapsort_nid_postfix(nullptr, 1, 0, CompareKeys) == 0; if (!correct) std::fprintf(stderr, "heapsort failed\n"); return correct; } diff --git a/core/libs/tests/GuestRaiseException.cpp b/core/libs/tests/GuestRaiseException.cpp index deffa7dac0..5079190b40 100644 --- a/core/libs/tests/GuestRaiseException.cpp +++ b/core/libs/tests/GuestRaiseException.cpp @@ -3,6 +3,7 @@ #include "prx/libkernel/Semaphore/include/Semaphore.hpp" #include "prx/libkernel/Pthread/include/Rwlock.hpp" #include "prx/libkernel/Pthread/include/Mutex.hpp" +#include "prx/libkernel/Time/include/TimedWait.hpp" #include "SceTypes.hpp" #include #include @@ -272,6 +273,51 @@ static void* APS5_VABI Vectors(void* arg) { } #endif +#ifdef _WIN32 +static constexpr int DrainRounds = 10; +static std::atomic drainingRound{0}; +static std::uint32_t drainWord = 0; + +static void* APS5_VABI Churning(void* arg) { + auto& worker = *static_cast(arg); + worker.id = std::this_thread::get_id(); + const std::string source(64u << 20u, 'a'); + std::vector converted(64u << 20u); + std::vector moved(64u << 20u); + for (int round = 0; round < DrainRounds; ++round) { + worker.started.store(true); + while (drainingRound.load() == round) { + MultiByteToWideChar(CP_UTF8, 0, source.data(), static_cast(source.size()), converted.data(), static_cast(converted.size())); + std::memmove(moved.data(), source.data(), moved.size()); + std::this_thread::yield(); + } + Require(sceKernelSyncOnAddressWake(&drainWord, 1) >= 0); + } + return nullptr; +} + +static constexpr int ExitRounds = 10; +static std::atomic exitingRelease{false}; + +static void* APS5_VABI ChurnUntil(void* arg) { + auto& worker = *static_cast(arg); + worker.id = std::this_thread::get_id(); + const std::string source(64u << 20u, 'a'); + std::vector converted(64u << 20u); + worker.started.store(true); + while (!exitingRelease.load()) MultiByteToWideChar(CP_UTF8, 0, source.data(), static_cast(source.size()), converted.data(), static_cast(converted.size())); + return nullptr; +} + +static std::atomic postEntered{0}; +static std::atomic postHold{false}; + +static void DeliveryPostHook() { + postEntered.fetch_add(1); + while (postHold.load()) std::this_thread::yield(); +} +#endif + static std::atomic spinningId; static void* APS5_VABI Spinning(void* arg) { @@ -701,6 +747,81 @@ int main() { Require(scePthreadJoin(vectorsThread, nullptr) == 0); Require(std::memcmp(vectorPattern, vectorResult, VectorBytes) == 0); } + + Worker churning; + Pthread churningThread = nullptr; + TimedWait::SetDeliveryDrainPostHook(&DeliveryPostHook); + const int postsBeforeChurn = postEntered.load(); + Require(scePthreadCreate(&churningThread, nullptr, Churning, &churning, "churn then drain") == 0); + for (int round = 0; round < DrainRounds; ++round) { + while (!churning.started.load()) std::this_thread::yield(); + churning.started.store(false); + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + const int before = calls.load(); + Require(sceKernelRaiseException(churningThread, GUEST_SIGUSR1) == 0); + drainingRound.store(round + 1); + ExpectDelivery(before, churning.id); + } + Require(scePthreadJoin(churningThread, nullptr) == 0); + Require(postEntered.load() > postsBeforeChurn); + + for (int round = 0; round < ExitRounds; ++round) { + exitingRelease.store(false); + Worker exiting; + Pthread exitingThread = nullptr; + Require(scePthreadCreate(&exitingThread, nullptr, ChurnUntil, &exiting, "exit after churn") == 0); + while (!exiting.started.load()) std::this_thread::yield(); + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + const int result = sceKernelRaiseException(exitingThread, GUEST_SIGUSR1); + Require(result == 0 || result == SCE_KERNEL_ERROR_ESRCH); + exitingRelease.store(true); + Require(scePthreadJoin(exitingThread, nullptr) == 0); + Require(TimedWait::DeliveryDrainPending() == 0); + } + + exitingRelease.store(false); + postHold.store(true); + const int postsBeforeHeld = postEntered.load(); + Worker heldExit; + Pthread heldExitThread = nullptr; + Require(scePthreadCreate(&heldExitThread, nullptr, ChurnUntil, &heldExit, "exit while posted") == 0); + while (!heldExit.started.load()) std::this_thread::yield(); + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + std::atomic raiseResult{-1}; + bool posted = false; + for (int attempt = 0; attempt < 50 && !posted; ++attempt) { + raiseResult.store(-1); + std::thread raiser([&] { raiseResult.store(sceKernelRaiseException(heldExitThread, GUEST_SIGUSR1)); }); + const auto entered = std::chrono::steady_clock::now() + std::chrono::seconds(2); + while (postEntered.load() == postsBeforeHeld && raiseResult.load() == -1 && std::chrono::steady_clock::now() < entered) std::this_thread::yield(); + posted = postEntered.load() > postsBeforeHeld; + if (posted) { + exitingRelease.store(true); + Require(scePthreadJoin(heldExitThread, nullptr) == 0); + postHold.store(false); + } + raiser.join(); + } + Require(posted); + Require(raiseResult.load() == 0 || raiseResult.load() == SCE_KERNEL_ERROR_ESRCH); + const auto drained = std::chrono::steady_clock::now() + std::chrono::seconds(2); + while (TimedWait::DeliveryDrainPending() != 0 && std::chrono::steady_clock::now() < drained) std::this_thread::yield(); + Require(TimedWait::DeliveryDrainPending() == 0); + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + const int settledCalls = calls.load(); + drainingRound.store(0); + Worker settled; + Pthread settledThread = nullptr; + Require(scePthreadCreate(&settledThread, nullptr, Churning, &settled, "settled") == 0); + for (int round = 0; round < DrainRounds; ++round) { + while (!settled.started.load()) std::this_thread::yield(); + settled.started.store(false); + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + Require(sceKernelRaiseException(settledThread, GUEST_SIGUSR1) == 0); + drainingRound.store(round + 1); + ExpectDelivery(settledCalls + round, settled.id); + } + Require(scePthreadJoin(settledThread, nullptr) == 0); #endif SpinState spin; diff --git a/core/libs/tests/GuestReallocalign.cpp b/core/libs/tests/GuestReallocalign.cpp index ff924916d5..06cf0862ab 100644 --- a/core/libs/tests/GuestReallocalign.cpp +++ b/core/libs/tests/GuestReallocalign.cpp @@ -49,5 +49,21 @@ int main() { Require(empty != nullptr); free_nid_postfix(empty); + void* aligned[16]{}; + void* unaligned = nullptr; + for (int index = 0; index < 16 && unaligned == nullptr; ++index) { + void* candidate = reallocalign_nid_postfix(nullptr, 24, 64); + Require(candidate != nullptr && (reinterpret_cast(candidate) % 64) == 0); + if ((reinterpret_cast(candidate) % 256) != 0) unaligned = candidate; + else aligned[index] = candidate; + } + Require(unaligned != nullptr); + *__error_nid_postfix() = 1234; + void* moved = reallocalign_nid_postfix(unaligned, 0, 256); + Require(moved != nullptr && moved != unaligned && (reinterpret_cast(moved) % 256) == 0); + Require(malloc_usable_size_nid_postfix(moved) == 0 && *__error_nid_postfix() == 1234); + free_nid_postfix(moved); + for (void* block : aligned) free_nid_postfix(block); + return 0; } diff --git a/core/libs/tests/GuestRtc.cpp b/core/libs/tests/GuestRtc.cpp index 58c7cf5934..f18ebfc20d 100644 --- a/core/libs/tests/GuestRtc.cpp +++ b/core/libs/tests/GuestRtc.cpp @@ -90,6 +90,21 @@ int main(int argc, char** argv) { char text[64]; RtcTick tick{std::numeric_limits::max()}; Require(sceRtcFormatRFC3339(text, &tick, 1) == static_cast(0x80B50003)); + Require(sceRtcFormatRFC3339Precise(text, &tick, 60, 6) == static_cast(0x80B50003)); + const RtcTick year65536{2068084742400000000ull}, year65537{2068116364800000000ull}, year75535{2383622726400000000ull}, distant{4239646359910459728ull}; + Require(sceRtcFormatRFC3339(text, &year65536, 0) == static_cast(0x80B50008)); + Require(sceRtcFormatRFC2822(text, &year65536, 0) == static_cast(0x80B50008)); + Require(sceRtcFormatRFC3339Precise(text, &year65536, 0, 6) == static_cast(0x80B50008)); + Require(sceRtcFormatRFC3339(text, &year65537, 60) == 0 && std::strcmp(text, "0001-01-01T01:00:00.00+01:00") == 0); + Require(sceRtcFormatRFC2822(text, &year65537, 0) == 0 && std::strcmp(text, "Mon, 01 Jan 0001 00:00:00 +0000") == 0); + Require(sceRtcFormatRFC3339Precise(text, &year65537, 60, 6) == 0 && std::strcmp(text, "0001-01-01T01:00:00.000000+01:00") == 0); + Require(sceRtcFormatRFC3339(text, &year75535, -60) == 0 && std::strcmp(text, "9998-12-31T23:00:00.00-01:00") == 0); + Require(sceRtcFormatRFC2822(text, &year75535, 0) == 0 && std::strcmp(text, "Fri, 01 Jan 9999 00:00:00 +0000") == 0); + Require(sceRtcFormatRFC3339Precise(text, &year75535, -60, 6) == 0 && std::strcmp(text, "9998-12-31T23:00:00.000000-01:00") == 0); + Require(sceRtcFormatRFC2822(text, &distant, -821) == 0 && std::strcmp(text, "Mon, 17 Jan 3278 10:44:10 -1341") == 0); + const RtcTick westBound{3600000000ull}, westBelow{3599999999ull}; + Require(sceRtcFormatRFC3339Precise(text, &westBound, -60, 6) == 0 && std::strcmp(text, "0001-01-01T00:00:00.000000-01:00") == 0); + Require(sceRtcFormatRFC3339Precise(text, &westBelow, -60, 6) == static_cast(0x80B50003)); return 0; } EnterTitleDirectory("CUSA00000"); @@ -991,6 +1006,15 @@ int main(int argc, char** argv) { Require(sceRtcFormatRFC3339(ranges, &shifted, -1) == 0 && std::strcmp(ranges, "9999-12-31T23:59:00.00-00:01") == 0); Require(sceRtcInit() == 0); Require(sceRtcEnd() == 0); + Require(sceRtcGetCurrentTick(reinterpret_cast(0x10)) == invalidPointer); + Require(sceRtcGetCurrentNetworkTick(reinterpret_cast(0x10)) == invalidPointer); + Require(sceRtcGetCurrentClock(reinterpret_cast(0x10), 0) == invalidPointer); + Require(sceRtcGetCurrentClockLocalTime(reinterpret_cast(0x10)) == invalidPointer); + Require(sceRtcGetCurrentClockUtc(reinterpret_cast(0x10)) == invalidPointer); + Require(sceRtcGetCurrentRawNetworkTick(reinterpret_cast(0x10)) == invalidPointer); + Require(sceRtcGetCurrentAdNetworkTick(reinterpret_cast(0x10)) == invalidPointer); + Require(sceRtcGetCurrentDebugNetworkTick(reinterpret_cast(0x10)) == invalidPointer); + Require(sceRtcSetCurrentTick(reinterpret_cast(0x10)) == invalidPointer); for (const std::uint64_t value : {maxTick + 1, std::uint64_t{1} << 63, std::numeric_limits::max()}) { const RtcTick outOfRange{value}; Require(sceRtcSetCurrentTick(&outOfRange) == 0); diff --git a/core/libs/tests/GuestSanitizerReplaceTables.cpp b/core/libs/tests/GuestSanitizerReplaceTables.cpp new file mode 100644 index 0000000000..3b6ea582f0 --- /dev/null +++ b/core/libs/tests/GuestSanitizerReplaceTables.cpp @@ -0,0 +1,26 @@ +#include "prx/libkernel/include/Api.hpp" +#include "SceTypes.hpp" +#include +#include +#include + +static void Require(bool value) { + if (!value) std::abort(); +} + +template +static void RequireEmpty(const Table* table, std::uint64_t size) { + Require(table != nullptr); + Require(table->size == size); + const auto* entries = reinterpret_cast(table) + sizeof(table->size); + for (std::size_t index = 0; index < sizeof(Table) - sizeof(table->size); index++) Require(entries[index] == 0); +} + +int main() { + const auto* mallocTable = sceKernelGetSanitizerMallocReplaceExternal(); + RequireEmpty(mallocTable, 0x70); + Require(sceKernelGetSanitizerMallocReplaceExternal() == mallocTable); + const auto* newTable = sceKernelGetSanitizerNewReplaceExternal(); + RequireEmpty(newTable, 0x68); + Require(sceKernelGetSanitizerNewReplaceExternal() == newTable); +} diff --git a/core/libs/tests/GuestSaveDataDialog.cpp b/core/libs/tests/GuestSaveDataDialog.cpp index e3f2ef95bf..b1a4977232 100644 --- a/core/libs/tests/GuestSaveDataDialog.cpp +++ b/core/libs/tests/GuestSaveDataDialog.cpp @@ -34,6 +34,7 @@ int main() { Require(sceSaveDataDialogGetResult(&uninitResult) == SAVE_DATA_DIALOG_ERROR_NOT_FINISHED, "get result before init"); Require(sceSaveDataDialogProgressBarInc(0, 10) == SAVE_DATA_DIALOG_ERROR_NOT_RUNNING, "progress inc before init"); Require(sceSaveDataDialogProgressBarSetValue(0, 50) == SAVE_DATA_DIALOG_ERROR_NOT_RUNNING, "progress set value before init"); + Require(sceSaveDataDialogIsReadyToDisplay() == 1, "isReadyToDisplay before init"); Require(sceSaveDataDialogInitialize() == 0, "initialize"); Require(sceSaveDataDialogInitialize() == SAVE_DATA_DIALOG_ERROR_ALREADY_INITIALIZED, "double init"); @@ -129,4 +130,5 @@ int main() { Require(sceSaveDataDialogProgressBarInc(0, 10) == SAVE_DATA_DIALOG_ERROR_NOT_RUNNING, "progress inc after terminate"); Require(sceSaveDataDialogProgressBarSetValue(0, 50) == SAVE_DATA_DIALOG_ERROR_NOT_RUNNING, "progress set value after terminate"); + Require(sceSaveDataDialogIsReadyToDisplay() == 1, "isReadyToDisplay after terminate"); } diff --git a/core/libs/tests/GuestSaveDataEventCallback.cpp b/core/libs/tests/GuestSaveDataEventCallback.cpp new file mode 100644 index 0000000000..ea1d0f208a --- /dev/null +++ b/core/libs/tests/GuestSaveDataEventCallback.cpp @@ -0,0 +1,467 @@ +#include "prx/libSceSaveData/include/Api.hpp" +#include "prx/libSceSaveData.native/include/Api.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/include/Api.hpp" +#include "SceTypes.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr int SaveDataErrorParameter = -2137063424; +constexpr int SaveDataErrorNotInitialized = -2137063423; +constexpr int SaveDataErrorBusy = -2137063421; +constexpr int SaveDataErrorNotFound = -2137063416; +constexpr std::uint32_t MountReadWrite = 2; +constexpr std::uint32_t MountCreate2 = 32; +constexpr std::int32_t User = 7; + +int failures = 0; + +void Check(bool condition, const std::string& what) { + if (condition) return; + std::fprintf(stderr, "savedata event callback check failed: %s\n", what.c_str()); + ++failures; +} + +struct Call { + int callback; + SaveDataEvent event; + void* userdata; + std::thread::id thread; + int terminated; +}; + +std::mutex callsMutex; +std::condition_variable callsChanged; +std::vector calls; + +void Record(int callback, const SaveDataEvent* event, void* userdata, int terminated = 0) { + std::lock_guard lock(callsMutex); + calls.push_back({callback, *event, userdata, std::this_thread::get_id(), terminated}); + callsChanged.notify_all(); +} + +void APS5_VABI First(const SaveDataEvent* event, void* userdata) { Record(1, event, userdata); } +void APS5_VABI Second(const SaveDataEvent* event, void* userdata) { Record(2, event, userdata); } +void APS5_VABI Terminating(const SaveDataEvent* event, void* userdata) { Record(3, event, userdata, sceSaveDataTerminate()); } +void APS5_VABI Stale(const SaveDataEvent* event, void* userdata) { Record(4, event, userdata); } +void APS5_VABI Fence(const SaveDataEvent* event, void* userdata) { Record(5, event, userdata); } + +std::mutex holdMutex; +std::condition_variable holdChanged; +bool holding = false; +bool released = false; + +void APS5_VABI Holding(const SaveDataEvent*, void*) { + std::unique_lock lock(holdMutex); + holding = true; + holdChanged.notify_all(); + holdChanged.wait(lock, [] { return released; }); +} + +std::size_t CallCount() { + std::lock_guard lock(callsMutex); + return calls.size(); +} + +bool WaitForCalls(std::size_t count) { + std::unique_lock lock(callsMutex); + return callsChanged.wait_for(lock, std::chrono::seconds(5), [&] { return calls.size() >= count; }); +} + +Call CallAt(std::size_t index) { + std::lock_guard lock(callsMutex); + return calls.at(index); +} + +std::size_t CallsOf(int callback, std::size_t from) { + std::lock_guard lock(callsMutex); + std::size_t count = 0; + for (std::size_t index = from; index < calls.size(); ++index) count += calls[index].callback == callback ? 1u : 0u; + return count; +} + +bool WaitForCallback(int callback, std::size_t from) { + std::unique_lock lock(callsMutex); + return callsChanged.wait_for(lock, std::chrono::seconds(5), [&] { + for (std::size_t index = from; index < calls.size(); ++index) { + if (calls[index].callback == callback) return true; + } + return false; + }); +} + +class Watchdog { +public: + explicit Watchdog(const char* what) : what(what), thread([this] { + std::unique_lock lock(mutex); + if (!changed.wait_for(lock, std::chrono::seconds(10), [this] { return done; })) { + std::fprintf(stderr, "savedata event callback check failed: %s did not return within 10 s\n", this->what); + std::_Exit(1); + } + }) {} + + ~Watchdog() { + { + std::lock_guard lock(mutex); + done = true; + } + changed.notify_all(); + thread.join(); + } + + Watchdog(const Watchdog&) = delete; + Watchdog& operator=(const Watchdog&) = delete; + +private: + const char* what; + std::mutex mutex; + std::condition_variable changed; + bool done = false; + std::thread thread; +}; + +#ifdef __linux__ +std::size_t TaskCount() { + return static_cast(std::distance(std::filesystem::directory_iterator("/proc/self/task"), std::filesystem::directory_iterator{})); +} + +bool WaitForTaskCount(std::size_t count) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); + while (TaskCount() != count) { + if (std::chrono::steady_clock::now() > deadline) return false; + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + return true; +} +#endif + +void WriteText(const std::filesystem::path& path, const std::string& text) { + std::ofstream file(path, std::ios::binary | std::ios::trunc); + file << text; + Check(static_cast(file), "writes " + path.string()); +} + +SceSaveDataDirName DirName(const char* name) { + SceSaveDataDirName dirName{}; + std::strncpy(dirName.data, name, sizeof(dirName.data) - 1); + return dirName; +} + +SceSaveDataTitleId TitleId(const char* name) { + SceSaveDataTitleId titleId{}; + std::strncpy(titleId.data, name, sizeof(titleId.data) - 1); + return titleId; +} + +int BackUp(const char* name) { + const auto dirName = DirName(name); + const auto titleId = TitleId("PPSA00000"); + SaveDataBackup backup{}; + backup.user_id = User; + backup.title_id = &titleId; + backup.dir_name = &dirName; + return sceSaveDataBackup(&backup); +} + +SaveDataEvent Poll(const std::string& what) { + SaveDataEvent event{}; + Check(sceSaveDataGetEventResult(nullptr, &event) == 0, what + " is polled"); + return event; +} + +bool SameEvent(const SaveDataEvent& left, const SaveDataEvent& right) { + return left.type == right.type && left.error_code == right.error_code && left.user_id == right.user_id && std::strncmp(left.title_id.data, right.title_id.data, sizeof(left.title_id.data)) == 0 && std::strncmp(left.dir_name.data, right.dir_name.data, sizeof(left.dir_name.data)) == 0; +} + +void CheckDelivered(std::size_t index, int callback, void* userdata, std::uint32_t type, const char* dir, const std::string& what) { + const auto call = CallAt(index); + Check(call.callback == callback, what + " reaches callback " + std::to_string(callback) + ", got " + std::to_string(call.callback)); + Check(call.userdata == userdata, what + " passes the registered userdata"); + Check(call.event.type == type, what + " has type " + std::to_string(type) + ", got " + std::to_string(call.event.type)); + Check(call.event.error_code == 0, what + " has error 0, got " + std::to_string(call.event.error_code)); + Check(call.event.user_id == User, what + " names the user"); + Check(std::strncmp(call.event.title_id.data, "PPSA00000", sizeof(call.event.title_id.data)) == 0, what + " names the title"); + Check(std::strncmp(call.event.dir_name.data, dir, sizeof(call.event.dir_name.data)) == 0, what + " names the save " + dir); + Check(call.thread != std::this_thread::get_id(), what + " runs on another thread than the caller"); + const auto polled = Poll(what); + Check(SameEvent(polled, call.event), what + " is the event the poll returns"); +} + +void CheckSessionsWithoutRegistration() { +#ifdef __linux__ + const auto before = TaskCount(); +#endif + for (int session = 0; session < 2; ++session) { + const auto label = "session " + std::to_string(session) + " without a registration"; + Check(sceSaveDataInitialize3(nullptr) == 0, label + ": SaveData initializes"); +#ifdef __linux__ + Check(TaskCount() == before, label + ": Initialize3 starts no thread, " + std::to_string(before) + " threads before and " + std::to_string(TaskCount()) + " after"); +#endif + Check(sceSaveDataTerminate() == 0, label + ": SaveData terminates"); + } +} + +#ifdef __linux__ +void CheckDeliveryThreadStartsAtRegistration() { + const auto before = TaskCount(); + const auto counted = [&](const std::string& what) { return what + ", " + std::to_string(before) + " threads before and " + std::to_string(TaskCount()) + " now"; }; + int tag = 0; + Check(sceSaveDataInitialize3(nullptr) == 0, "SaveData initializes for the delivery thread check"); + Check(TaskCount() == before, counted("Initialize3 starts no delivery thread")); + Check(sceSaveDataRegisterEventCallback(nullptr, &tag) == SaveDataErrorParameter, "a null callback is PARAMETER in the delivery thread check"); + Check(TaskCount() == before, counted("a refused registration starts no delivery thread")); + Check(sceSaveDataRegisterEventCallback(First, &tag) == 0, "the first callback registers in the delivery thread check"); + Check(TaskCount() == before + 1, counted("the first registration starts one delivery thread")); + Check(sceSaveDataRegisterEventCallback(Second, &tag) == 0, "the second callback registers in the delivery thread check"); + Check(sceSaveDataUnregisterEventCallback() == 0, "the second callback unregisters in the delivery thread check"); + Check(sceSaveDataRegisterEventCallback(First, &tag) == 0, "the first callback registers again in the delivery thread check"); + Check(TaskCount() == before + 1, counted("a replacing registration and one after an unregister reuse the delivery thread")); + { + Watchdog watchdog("the terminate that joins the delivery thread"); + Check(sceSaveDataTerminate() == 0, "SaveData terminates after the delivery thread check"); + } + Check(WaitForTaskCount(before), counted("the final terminate joins the delivery thread within 5 s")); +} +#endif + +void CheckRegistration() { + int tag = 0; + Check(sceSaveDataRegisterEventCallback(First, &tag) == SaveDataErrorNotInitialized, "register before initialize is NOT_INITIALIZED"); + Check(sceSaveDataUnregisterEventCallback() == 0, "unregister before initialize succeeds"); + Check(sceSaveDataInitialize3(nullptr) == 0, "SaveData initializes"); + Check(sceSaveDataRegisterEventCallback(nullptr, &tag) == SaveDataErrorParameter, "a null callback is PARAMETER"); +} + +void CheckBackupDelivery(int* firstTag) { + Check(sceSaveDataRegisterEventCallback(First, firstTag) == 0, "the first callback registers"); + Check(BackUp("backed") == 0, "the backup succeeds"); + Check(WaitForCalls(1), "the backup's completion reaches the callback within 5 s"); + Check(CallCount() == 1, "the backup is delivered once"); + if (CallCount() >= 1) CheckDelivered(0, 1, firstTag, 2, "backed", "the backup event"); + SaveDataEvent none{}; + Check(sceSaveDataGetEventResult(nullptr, &none) == SaveDataErrorNotFound, "the backup posts one event to poll"); +} + +void CheckUmountWithBackupDelivery(int* firstTag) { + std::filesystem::create_directories("app0/sce_sys"); + WriteText("app0/sce_sys/param.json", "{\"titleId\":\"PPSA00000\",\"localizedParameters\":{\"en-US\":{\"titleName\":\"Example\"}}}"); + auto dirName = DirName("mounted"); + SaveDataMount3 mount{}; + mount.user_id = User; + mount.dir_name = &dirName; + mount.mount_mode = MountReadWrite | MountCreate2; + SaveDataMountResult mounted{}; + Check(sceSaveDataMount3(&mount, &mounted) == 0, "the save mounts"); + const auto before = CallCount(); + Check(sceSaveDataUmountWithBackup(&mounted.mount_point) == 0, "umountWithBackup succeeds"); + Check(WaitForCalls(before + 1), "umountWithBackup's completion reaches the callback within 5 s"); + Check(CallCount() == before + 1, "umountWithBackup is delivered once"); + if (CallCount() >= before + 1) CheckDelivered(before, 1, firstTag, 1, "mounted", "the umountWithBackup event"); +} + +void CheckUnregister(int* firstTag, int* secondTag) { + Check(sceSaveDataUnregisterEventCallback() == 0, "the first callback unregisters"); + const auto before = CallCount(); + Check(BackUp("unwatched") == 0, "a backup with no callback succeeds"); + const auto unwatched = Poll("the backup with no callback"); + Check(unwatched.type == 2 && std::strncmp(unwatched.dir_name.data, "unwatched", sizeof(unwatched.dir_name.data)) == 0, "the backup with no callback still posts its event"); + Check(sceSaveDataRegisterEventCallback(Second, secondTag) == 0, "the second callback registers"); + Check(BackUp("watched") == 0, "the second backup succeeds"); + Check(WaitForCalls(before + 1), "the second backup reaches the second callback within 5 s"); + Check(CallCount() == before + 1, "only the second backup is delivered, got " + std::to_string(CallCount() - before) + " calls"); + if (CallCount() >= before + 1) CheckDelivered(before, 2, secondTag, 2, "watched", "the event after the callback was unregistered"); + Check(sceSaveDataRegisterEventCallback(First, firstTag) == 0, "the first callback registers again"); + int replacedTag = 0; + Check(sceSaveDataRegisterEventCallback(Second, &replacedTag) == 0, "the second callback replaces the first"); + Check(BackUp("replaced") == 0, "the backup after the replacement succeeds"); + Check(WaitForCalls(before + 2), "the backup after the replacement is delivered within 5 s"); + Check(CallCount() == before + 2, "the backup after the replacement is delivered once"); + if (CallCount() >= before + 2) CheckDelivered(before + 1, 2, &replacedTag, 2, "replaced", "the event after the replacement"); +} + +void CheckTerminateFromCallback() { + const auto before = CallCount(); + int tag = 0; + { + Watchdog watchdog("a terminate from inside the callback"); + Check(sceSaveDataRegisterEventCallback(Terminating, &tag) == 0, "the terminating callback registers"); + Check(BackUp("terminating") == 0, "the backup for the terminating callback succeeds"); + Check(WaitForCalls(before + 1), "the terminating callback runs within 5 s"); + } + if (CallCount() >= before + 1) Check(CallAt(before).terminated == SaveDataErrorBusy, "a terminate from inside the callback is BUSY, got " + std::to_string(CallAt(before).terminated)); + Poll("the terminating callback's backup"); + Check(sceSaveDataUnregisterEventCallback() == 0, "the terminating callback unregisters"); +} + +void CheckIdleTerminateAndRestart(int* firstTag) { + { + Watchdog watchdog("an idle terminate"); + Check(sceSaveDataTerminate() == 0, "SaveData terminates with the delivery thread idle"); + } + Check(sceSaveDataRegisterEventCallback(First, firstTag) == SaveDataErrorNotInitialized, "register after terminate is NOT_INITIALIZED"); + Check(sceSaveDataInitialize3(nullptr) == 0, "SaveData initializes again"); + const auto before = CallCount(); + Check(BackUp("restarted") == 0, "a backup after the restart succeeds"); + Poll("the backup after the restart"); + Check(sceSaveDataRegisterEventCallback(First, firstTag) == 0, "the first callback registers after the restart"); + Check(BackUp("redelivered") == 0, "a backup with a callback after the restart succeeds"); + Check(WaitForCalls(before + 1), "the backup after the restart is delivered within 5 s"); + Check(CallCount() == before + 1, "terminate dropped the callback: the restart delivers only after registering again"); + if (CallCount() >= before + 1) CheckDelivered(before, 1, firstTag, 2, "redelivered", "the event after the restart"); + { + Watchdog watchdog("the final terminate"); + Check(sceSaveDataTerminate() == 0, "SaveData terminates again"); + } +} + +struct Race { + std::atomic start{false}; + int staleTag = 0; + int registered = 0; + int terminated = 0; +}; + +struct RaceOutcomes { + int notInitialized = 0; + int registered = 0; +}; + +void* APS5_VABI RaceRegister(void* argument) { + auto& race = *static_cast(argument); + while (!race.start.load(std::memory_order_acquire)) std::this_thread::yield(); + race.registered = sceSaveDataRegisterEventCallback(Stale, &race.staleTag); + return nullptr; +} + +void* APS5_VABI RaceTerminate(void* argument) { + auto& race = *static_cast(argument); + while (!race.start.load(std::memory_order_acquire)) std::this_thread::yield(); + race.terminated = sceSaveDataTerminate(); + return nullptr; +} + +Pthread Spawn(PthreadEntry entry, Race& race, const char* name) { + Pthread thread = nullptr; + if (scePthreadCreate(&thread, nullptr, entry, &race, name) != 0) { + std::fprintf(stderr, "savedata event callback check failed: scePthreadCreate failed for %s\n", name); + std::_Exit(1); + } + return thread; +} + +bool WaitForFinalTerminate() { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); + while (sceSaveDataDeleteTransactionResource(0) != SaveDataErrorNotInitialized) { + if (std::chrono::steady_clock::now() > deadline) return false; + std::this_thread::yield(); + } + return sceSaveDataTerminate() == SaveDataErrorNotInitialized; +} + +bool RaceRegisterWithFinalTerminate(int iteration, bool hold, RaceOutcomes& outcomes) { + const auto failuresBefore = failures; + const auto label = "iteration " + std::to_string(iteration) + (hold ? " with the delivery thread held" : " without a held delivery thread"); + Check(sceSaveDataInitialize3(nullptr) == 0, label + ": SaveData initializes"); + int holdTag = 0; + if (hold) { + { + std::lock_guard lock(holdMutex); + holding = false; + released = false; + } + Check(sceSaveDataRegisterEventCallback(Holding, &holdTag) == 0, label + ": the holding callback registers"); + Check(BackUp("held") == 0, label + ": the backup for the holding callback succeeds"); + std::unique_lock lock(holdMutex); + Check(holdChanged.wait_for(lock, std::chrono::seconds(5), [] { return holding; }), label + ": the holding callback runs within 5 s"); + } + Race race; + const auto registering = Spawn(RaceRegister, race, "SaveDataRaceRegister"); + const auto terminating = Spawn(RaceTerminate, race, "SaveDataRaceTerminate"); + race.start.store(true, std::memory_order_release); + if (hold) { + Check(WaitForFinalTerminate(), label + ": the racing terminate reaches zero initializations within 5 s"); + { + std::lock_guard lock(holdMutex); + released = true; + } + holdChanged.notify_all(); + } + Check(scePthreadJoin(registering, nullptr) == 0, label + ": the registering thread joins"); + Check(scePthreadJoin(terminating, nullptr) == 0, label + ": the terminating thread joins"); + Check(race.terminated == 0, label + ": the racing final terminate succeeds, got " + std::to_string(race.terminated)); + Check(race.registered == 0 || race.registered == SaveDataErrorNotInitialized, label + ": the racing registration is OK or NOT_INITIALIZED, got " + std::to_string(race.registered)); + if (race.registered == SaveDataErrorNotInitialized) ++outcomes.notInitialized; + if (race.registered == 0) ++outcomes.registered; + const auto before = CallCount(); + Check(sceSaveDataInitialize3(nullptr) == 0, label + ": SaveData initializes again"); + Check(BackUp("raced") == 0, label + ": the backup after the race succeeds"); + int fenceTag = 0; + Check(sceSaveDataRegisterEventCallback(Fence, &fenceTag) == 0, label + ": the fence callback registers"); + Check(BackUp("fenced") == 0, label + ": the fenced backup succeeds"); + Check(WaitForCallback(5, before), label + ": the fenced backup reaches the fence callback within 5 s"); + Check(CallsOf(4, before) == 0, label + ": the callback whose registration returned " + std::to_string(race.registered) + " beside the final terminate is called after Initialize3, " + std::to_string(CallsOf(4, before)) + " time(s)"); + Check(sceSaveDataUnregisterEventCallback() == 0, label + ": the fence callback unregisters"); + SaveDataEvent drained{}; + while (sceSaveDataGetEventResult(nullptr, &drained) == 0) { + } + Check(sceSaveDataTerminate() == 0, label + ": SaveData terminates after the race"); + return failures == failuresBefore; +} + +void CheckRegisterRacingFinalTerminate() { + constexpr int Iterations = 200; + RaceOutcomes held; + RaceOutcomes plain; + int ran = 0; + while (ran < Iterations) { + Watchdog watchdog("a registration racing the final terminate"); + const bool hold = ran % 2 == 0; + const bool passed = RaceRegisterWithFinalTerminate(ran, hold, hold ? held : plain); + ++ran; + if (!passed) break; + } + std::printf("register against the final terminate: %d of %d iterations; held delivery thread: %d NOT_INITIALIZED, %d OK; plain race: %d NOT_INITIALIZED, %d OK\n", ran, Iterations, held.notInitialized, held.registered, plain.notInitialized, plain.registered); +} + +} + +int main() { + const auto root = std::filesystem::temp_directory_path() / ("anyps5-savedata-event-callback-" + std::to_string(std::random_device{}())); + std::filesystem::create_directories(root); + const auto previous = std::filesystem::current_path(); + std::filesystem::current_path(root); + int firstTag = 0; + int secondTag = 0; + CheckSessionsWithoutRegistration(); +#ifdef __linux__ + CheckDeliveryThreadStartsAtRegistration(); +#endif + CheckRegistration(); + CheckBackupDelivery(&firstTag); + CheckUmountWithBackupDelivery(&firstTag); + CheckUnregister(&firstTag, &secondTag); + CheckTerminateFromCallback(); + CheckIdleTerminateAndRestart(&firstTag); + CheckRegisterRacingFinalTerminate(); + std::filesystem::current_path(previous); + std::error_code error; + std::filesystem::remove_all(root, error); + if (failures != 0) return 1; + std::printf("savedata event callback tests passed\n"); + return 0; +} diff --git a/core/libs/tests/GuestSaveDataMountStatus.cpp b/core/libs/tests/GuestSaveDataMountStatus.cpp index a4b1e81ae6..e9bed03241 100644 --- a/core/libs/tests/GuestSaveDataMountStatus.cpp +++ b/core/libs/tests/GuestSaveDataMountStatus.cpp @@ -7,12 +7,16 @@ #include #include #include +#include +#include +#include #include #include #include #include #ifdef _WIN32 #include +#include #endif namespace { @@ -25,6 +29,7 @@ constexpr int SaveDataErrorNotInitialized = -2137063423; constexpr int SaveDataErrorNotMounted = -2137063420; constexpr int SaveDataErrorBusy = -2137063421; constexpr int SaveDataErrorNotFound = -2137063416; +constexpr int SaveDataErrorInternal = -2137063413; constexpr int SaveDataErrorMountFull = -2137063412; int failures = 0; @@ -63,6 +68,17 @@ void CheckUnmountedCalls() { Check(sceSaveDataSetParam(nullptr, 1, buf, sizeof(buf)) == SaveDataErrorParameter, "setParam null point is PARAMETER"); Check(sceSaveDataSetParam(&point, 1, nullptr, sizeof(buf)) == SaveDataErrorParameter, "setParam null buf is PARAMETER"); Check(sceSaveDataSetParam(&point, 1, buf, sizeof(buf)) == SaveDataErrorNotMounted, "setParam unmounted point is NOT_MOUNTED"); + SaveDataMountPoint unterminatedPoint{}; + std::memset(unterminatedPoint.data, 'a', sizeof(unterminatedPoint.data)); + Check(sceSaveDataUmount2(0, &unterminatedPoint) == SaveDataErrorParameter, "umount2 unterminated point is PARAMETER"); + Check(sceSaveDataLoadIcon(&unterminatedPoint, &icon) == SaveDataErrorParameter, "loadIcon unterminated point is PARAMETER"); + Check(sceSaveDataSaveIcon(&unterminatedPoint, &icon) == SaveDataErrorParameter, "saveIcon unterminated point is PARAMETER"); + Check(sceSaveDataSaveIconByPath(&unterminatedPoint, "test.png") == SaveDataErrorParameter, "saveIconByPath unterminated point is PARAMETER"); + Check(sceSaveDataGetMountInfo(&unterminatedPoint, &info) == SaveDataErrorParameter, "getMountInfo unterminated point is PARAMETER"); + Check(sceSaveDataGetParam(&unterminatedPoint, 1, buf, sizeof(buf), &gotSize) == SaveDataErrorParameter, "getParam unterminated point is PARAMETER"); + Check(sceSaveDataSetParam(&unterminatedPoint, 1, buf, sizeof(buf)) == SaveDataErrorParameter, "setParam unterminated point is PARAMETER"); + unterminatedPoint.data[sizeof(unterminatedPoint.data) - 1] = '\0'; + Check(sceSaveDataUmount2(0, &unterminatedPoint) == SaveDataErrorNotMounted, "umount2 terminated unmounted point is NOT_MOUNTED"); Check(sceSaveDataBackup(nullptr) == SaveDataErrorParameter, "backup null backup is PARAMETER"); SaveDataBackup nullDirBackup{}; Check(sceSaveDataBackup(&nullDirBackup) == SaveDataErrorParameter, "backup null dir_name is PARAMETER"); @@ -89,6 +105,15 @@ void CheckUnmountedCalls() { Check(backupEvent.user_id == 0x10000000, "backup event user_id matches"); Check(std::strcmp(backupEvent.dir_name.data, "valid_backup") == 0, "backup event dir_name matches"); Check(sceSaveDataGetEventResult(nullptr, &emptyEvent) == SaveDataErrorNotFound, "event queue is empty after retrieving backup event"); + SceSaveDataDirName dirName{}; + std::memcpy(dirName.data, "fresh", sizeof("fresh")); + SaveDataCheckBackupData check{}; + check.dir_name = &dirName; + Check(sceSaveDataCheckBackupData(&check) == SaveDataErrorNotInitialized, "checkBackupData before initialize is NOT_INITIALIZED"); + SaveDataRestoreBackupData restore{}; + restore.dir_name = &dirName; + Check(sceSaveDataRestoreBackupData(&restore) == SaveDataErrorNotInitialized, "restoreBackupData before initialize is NOT_INITIALIZED"); + Check(sceSaveDataUmountWithBackup(&point) == SaveDataErrorNotInitialized, "umountWithBackup before initialize is NOT_INITIALIZED"); } void CheckMountInfoAndParam(const SaveDataMountPoint* point) { @@ -131,6 +156,15 @@ void CheckMountInfoAndParam(const SaveDataMountPoint* point) { Check(sceSaveDataSetParam(point, 1, newTitle, sizeof(newTitle)) == 0, "setParam on mounted save succeeds"); Check(sceSaveDataGetParam(point, 1, readTitle, sizeof(readTitle), &gotSize) == 0, "getParam reads back set title"); Check(std::strcmp(readTitle, newTitle) == 0, "readTitle matches newTitle"); + SaveDataMountPoint unterminatedMounted{}; + std::memset(unterminatedMounted.data, 'a', sizeof(unterminatedMounted.data)); + Check(sceSaveDataUmount2(0, &unterminatedMounted) == SaveDataErrorParameter, "umount2 unterminated point is PARAMETER on mounted save"); + Check(sceSaveDataLoadIcon(&unterminatedMounted, &icon) == SaveDataErrorParameter, "loadIcon unterminated point is PARAMETER on mounted save"); + Check(sceSaveDataSaveIcon(&unterminatedMounted, &icon) == SaveDataErrorParameter, "saveIcon unterminated point is PARAMETER on mounted save"); + Check(sceSaveDataSaveIconByPath(&unterminatedMounted, "icon.png") == SaveDataErrorParameter, "saveIconByPath unterminated point is PARAMETER on mounted save"); + Check(sceSaveDataGetMountInfo(&unterminatedMounted, &info) == SaveDataErrorParameter, "getMountInfo unterminated point is PARAMETER on mounted save"); + Check(sceSaveDataGetParam(&unterminatedMounted, 1, buf, sizeof(buf), &gotSize) == SaveDataErrorParameter, "getParam unterminated point is PARAMETER on mounted save"); + Check(sceSaveDataSetParam(&unterminatedMounted, 1, newTitle, sizeof(newTitle)) == SaveDataErrorParameter, "setParam unterminated point is PARAMETER on mounted save"); } void MountAndCheck(const char* name, std::uint32_t mode, std::uint32_t expectedStatus, const std::string& what) { @@ -263,6 +297,475 @@ void CheckInvalidNamesAreRejected() { MountAndCheck("after", MountReadWrite | MountCreate2, 1u, "CREATE2 after the rejected names"); } +void WriteText(const std::filesystem::path& path, const std::string& text) { + std::ofstream file(path, std::ios::binary | std::ios::trunc); + file << text; + Check(static_cast(file), "writes " + path.string()); +} + +std::string ReadText(const std::filesystem::path& path) { + std::ifstream file(path, std::ios::binary); + return std::string(std::istreambuf_iterator(file), std::istreambuf_iterator()); +} + +bool MountSave(SceSaveDataDirName* dirName, std::uint32_t mode, SaveDataMountResult* result) { + SaveDataMount3 mount{}; + mount.user_id = 7; + mount.dir_name = dirName; + mount.mount_mode = mode; + return sceSaveDataMount3(&mount, result) == 0; +} + +std::map Snapshot(const std::filesystem::path& save) { + std::map files; + for (const char* side : {".param", ".blocks"}) { + std::filesystem::path path = save; + path += side; + if (std::filesystem::exists(path)) files[side] = ReadText(path); + } + if (!std::filesystem::is_directory(save)) return files; + files["/"] = ""; + for (const auto& entry : std::filesystem::recursive_directory_iterator(save)) { + const std::string relative = std::filesystem::relative(entry.path(), save).generic_string(); + if (entry.is_directory()) { + files[relative + "/"] = ""; + } else { + files[relative] = ReadText(entry.path()); + } + } + return files; +} + +int UmountWithBackup(const std::string& name, const SaveDataMountPoint* point) { + Check(sceSaveDataUmountWithBackup(point) == 0, name + " unmounts with a backup"); + SaveDataEvent event{}; + Check(sceSaveDataGetEventResult(nullptr, &event) == 0, name + "'s backup posts an event"); + return event.error_code; +} + +int BackUp(SceSaveDataDirName* dirName, const std::string& data, const char* title) { + const std::string name = dirName->data; + SaveDataMountResult mounted{}; + if (!MountSave(dirName, MountReadWrite | MountCreate2, &mounted)) { + Check(false, name + " mounts to be backed up"); + return SaveDataErrorNotMounted; + } + WriteText(std::filesystem::path("_sd") / name / "data.bin", data); + Check(sceSaveDataSetParam(&mounted.mount_point, 1, title, std::strlen(title) + 1) == 0, name + " takes the title " + title); + return UmountWithBackup(name, &mounted.mount_point); +} + +int RemountWithBackup(SceSaveDataDirName* dirName) { + const std::string name = dirName->data; + SaveDataMountResult mounted{}; + if (!MountSave(dirName, MountReadWrite, &mounted)) { + Check(false, name + " mounts to be backed up again"); + return SaveDataErrorNotMounted; + } + return UmountWithBackup(name, &mounted.mount_point); +} + +void CheckBackups() { + const auto paramJson = ResolvePath_nid_no_patch("/app0/sce_sys/param.json"); + std::filesystem::create_directories(paramJson.parent_path()); + WriteText(paramJson, "{\"titleId\":\"PPSA00000\",\"localizedParameters\":{\"en-US\":{\"titleName\":\"Example\"}}}"); + SceSaveDataDirName kept{}; + std::memcpy(kept.data, "kept", sizeof("kept")); + SceSaveDataDirName fresh{}; + std::memcpy(fresh.data, "fresh", sizeof("fresh")); + SaveDataCheckBackupData check{}; + SaveDataRestoreBackupData restore{}; + Check(sceSaveDataCheckBackupData(nullptr) == SaveDataErrorParameter, "checkBackupData null block is PARAMETER"); + Check(sceSaveDataCheckBackupData(&check) == SaveDataErrorParameter, "checkBackupData null dir_name is PARAMETER"); + Check(sceSaveDataRestoreBackupData(nullptr) == SaveDataErrorParameter, "restoreBackupData null block is PARAMETER"); + Check(sceSaveDataRestoreBackupData(&restore) == SaveDataErrorParameter, "restoreBackupData null dir_name is PARAMETER"); + Check(sceSaveDataUmountWithBackup(nullptr) == SaveDataErrorParameter, "umountWithBackup null point is PARAMETER"); + check.dir_name = &fresh; + restore.dir_name = &fresh; + Check(sceSaveDataCheckBackupData(&check) == SaveDataErrorNotFound, "checkBackupData without a backup is NOT_FOUND"); + Check(sceSaveDataRestoreBackupData(&restore) == SaveDataErrorNotFound, "restoreBackupData without a backup is NOT_FOUND"); + + SaveDataMountResult mounted{}; + Check(MountSave(&kept, MountReadWrite | MountCreate2, &mounted), "the backed-up save mounts"); + const auto data = std::filesystem::path("_sd") / "kept" / "data.bin"; + WriteText(data, "first"); + const char backedUpTitle[] = "Backed Up"; + Check(sceSaveDataSetParam(&mounted.mount_point, 1, backedUpTitle, sizeof(backedUpTitle)) == 0, "the backed-up save takes a title"); + check.dir_name = &kept; + restore.dir_name = &kept; + Check(sceSaveDataCheckBackupData(&check) == SaveDataErrorBusy, "checkBackupData of a mounted save is BUSY"); + Check(sceSaveDataRestoreBackupData(&restore) == SaveDataErrorBusy, "restoreBackupData of a mounted save is BUSY"); + Check(sceSaveDataUmountWithBackup(&mounted.mount_point) == 0, "umountWithBackup succeeds"); + SaveDataMountInfo info{}; + Check(sceSaveDataGetMountInfo(&mounted.mount_point, &info) == SaveDataErrorNotMounted, "umountWithBackup unmounts the save"); + + SaveDataEvent event{}; + Check(sceSaveDataGetEventResult(nullptr, &event) == 0, "umountWithBackup posts an event"); + Check(event.type == 1, "the event is UMOUNT_BACKUP, got type " + std::to_string(event.type)); + Check(event.error_code == 0, "the backup succeeded, error " + std::to_string(event.error_code)); + Check(event.user_id == 7, "the event names the mounting user"); + Check(std::strncmp(event.title_id.data, "PPSA00000", sizeof(event.title_id.data)) == 0, "the event names the running title"); + Check(std::strncmp(event.dir_name.data, "kept", sizeof(event.dir_name.data)) == 0, "the event names the save"); + + SaveDataParam param{}; + check.param = ¶m; + Check(sceSaveDataCheckBackupData(&check) == 0, "checkBackupData finds the backup"); + Check(std::strcmp(param.title, backedUpTitle) == 0, "checkBackupData fills param from the backup"); + SceSaveDataTitleId running{}; + std::memcpy(running.data, "PPSA00000", sizeof("PPSA00000")); + check.title_id = &running; + Check(sceSaveDataCheckBackupData(&check) == 0, "checkBackupData with the running title's id finds the backup"); + + WriteText(data, "second"); + WriteText(std::filesystem::path("_sd") / "kept" / "added.bin", "added"); + Check(MountSave(&kept, MountReadWrite, &mounted), "the save mounts again after the backup"); + const char changedTitle[] = "Changed"; + Check(sceSaveDataSetParam(&mounted.mount_point, 1, changedTitle, sizeof(changedTitle)) == 0, "the title changes after the backup"); + Check(sceSaveDataUmount2(0, &mounted.mount_point) == 0, "the changed save unmounts"); + Check(sceSaveDataRestoreBackupData(&restore) == 0, "restoreBackupData succeeds"); + Check(ReadText(data) == "first", "restoreBackupData puts back a file changed after the backup"); + Check(!std::filesystem::exists(std::filesystem::path("_sd") / "kept" / "added.bin"), "restoreBackupData removes a file added after the backup"); + char title[128]{}; + Check(MountSave(&kept, MountReadWrite, &mounted), "the restored save mounts"); + Check(sceSaveDataGetParam(&mounted.mount_point, 1, title, sizeof(title), nullptr) == 0 && std::strcmp(title, backedUpTitle) == 0, "restoreBackupData puts back the backed-up title"); + Check(sceSaveDataUmount2(0, &mounted.mount_point) == 0, "the restored save unmounts"); + SaveDataDelete del{}; + del.dir_name = &kept; + Check(sceSaveDataDelete(&del) == 0, "the backed-up save is deleted"); + Check(sceSaveDataCheckBackupData(&check) == SaveDataErrorNotFound, "deleting a save deletes its backup"); +} + +const char* failingStep = nullptr; +int failingCall = 0; +int stepCalls = 0; + +bool FailingStep(const char* step, const char*) { + if (failingStep == nullptr || std::strcmp(step, failingStep) != 0) return true; + return ++stepCalls != failingCall; +} + +void FailStep(const char* step, int call) { + failingStep = step; + failingCall = call; + stepCalls = 0; + SaveDataSetFileStepHook_nid_no_patch(FailingStep); +} + +void ClearFailingStep() { + SaveDataSetFileStepHook_nid_no_patch(nullptr); + failingStep = nullptr; +} + +struct FailedStep { + const char* step; + int call; +}; + +bool LeavesCopies(const std::string& name) { + for (const char* root : {"_sd_backup_tmp", "_sd_backup_old", "_sd_restore_tmp", "_sd_restore_old"}) { + if (std::filesystem::exists(std::filesystem::path(root) / name)) return true; + } + return false; +} + +template +void CheckCorruptBackup(const std::string& label, Damage damage) { + SceSaveDataDirName name{}; + std::memcpy(name.data, "corrupt", sizeof("corrupt")); + Check(BackUp(&name, "backed up", "Corrupt") == 0, label + ": the save is backed up"); + WriteText(std::filesystem::path("_sd") / "corrupt" / "data.bin", "live"); + damage(std::filesystem::path("_sd_backup") / "corrupt"); + const auto live = std::filesystem::path("_sd") / "corrupt"; + const auto before = Snapshot(live); + SaveDataCheckBackupData check{}; + check.dir_name = &name; + Check(sceSaveDataCheckBackupData(&check) == SaveDataErrorInternal, label + ": checkBackupData without param is INTERNAL"); + SaveDataParam param; + std::memset(¶m, 0xA5, sizeof(param)); + const SaveDataParam untouched = param; + check.param = ¶m; + Check(sceSaveDataCheckBackupData(&check) == SaveDataErrorInternal, label + ": checkBackupData with param is INTERNAL"); + Check(std::memcmp(¶m, &untouched, sizeof(param)) == 0, label + ": checkBackupData leaves param as it was"); + SaveDataRestoreBackupData restore{}; + restore.dir_name = &name; + Check(sceSaveDataRestoreBackupData(&restore) == SaveDataErrorInternal, label + ": restoreBackupData is INTERNAL"); + Check(Snapshot(live) == before, label + ": restoreBackupData leaves the save byte for byte"); +} + +void CheckCorruptBackups() { + CheckCorruptBackup("a backup without its .param", [](const std::filesystem::path& entry) { std::filesystem::remove(entry / "save.param"); }); + CheckCorruptBackup("an empty backup directory", [](const std::filesystem::path& entry) { + std::filesystem::remove_all(entry); + std::filesystem::create_directory(entry); + }); + CheckCorruptBackup("a backup with a short .param", [](const std::filesystem::path& entry) { + const std::string bytes = ReadText(entry / "save.param"); + WriteText(entry / "save.param", bytes.substr(0, bytes.size() - 1)); + }); + CheckCorruptBackup("a backup with a long .param", [](const std::filesystem::path& entry) { WriteText(entry / "save.param", ReadText(entry / "save.param") + "x"); }); +} + +void CheckStagedRestore() { + SceSaveDataDirName name{}; + std::memcpy(name.data, "staged", sizeof("staged")); + const auto live = std::filesystem::path("_sd") / "staged"; + std::filesystem::create_directories(live / "sub"); + WriteText(live / "sub" / "deep.bin", "deep"); + WriteText(std::filesystem::path("_sd") / "staged.blocks", "96"); + Check(BackUp(&name, "backed up", "Staged") == 0, "the staged save is backed up"); + const auto backup = Snapshot(std::filesystem::path("_sd_backup") / "staged" / "save"); + WriteText(live / "data.bin", "live"); + WriteText(live / "added.bin", "added"); + WriteText(std::filesystem::path("_sd") / "staged.blocks", "128"); + const auto before = Snapshot(live); + SaveDataRestoreBackupData restore{}; + restore.dir_name = &name; + const FailedStep failures[] = {{"restore.copy", 1}, {"restore.copy", 3}, {"restore.copy", 6}, {"restore.aside", 1}, {"restore.aside", 2}, {"restore.place", 1}, {"restore.place", 3}}; + for (const FailedStep& failure : failures) { + const std::string what = std::string("a restore whose ") + failure.step + " step " + std::to_string(failure.call) + " fails"; + FailStep(failure.step, failure.call); + Check(sceSaveDataRestoreBackupData(&restore) == SaveDataErrorInternal, what + " is INTERNAL"); + ClearFailingStep(); + Check(Snapshot(live) == before, what + " leaves the save as it was"); + Check(!LeavesCopies("staged"), what + " leaves no staged or old copy"); + } + Check(sceSaveDataRestoreBackupData(&restore) == 0, "a restore with no failing step succeeds"); + Check(Snapshot(live) == backup, "the restored save matches its backup byte for byte"); + Check(!LeavesCopies("staged"), "a restore leaves no staged or old copy"); + std::filesystem::remove_all(live); + Check(sceSaveDataRestoreBackupData(&restore) == SaveDataErrorInternal, "a restore into a missing save directory is INTERNAL"); + SaveDataCheckBackupData check{}; + check.dir_name = &name; + Check(sceSaveDataCheckBackupData(&check) == 0, "the backup outlives a restore into a missing save directory"); +} + +void CheckBackupSwap() { + SceSaveDataDirName name{}; + std::memcpy(name.data, "rotated", sizeof("rotated")); + const auto backup = std::filesystem::path("_sd_backup") / "rotated" / "save"; + SaveDataParam param{}; + SaveDataCheckBackupData check{}; + check.dir_name = &name; + check.param = ¶m; + const char* titles[] = {"First", "Second", "Third"}; + for (int i = 0; i < 3; ++i) { + const std::string what = "backup " + std::to_string(i + 1) + " of three"; + const std::string data = "rotation " + std::to_string(i + 1); + Check(BackUp(&name, data, titles[i]) == 0, what + " succeeds"); + Check(ReadText(backup / "data.bin") == data, what + " holds the save it was made from"); + Check(sceSaveDataCheckBackupData(&check) == 0 && std::strcmp(param.title, titles[i]) == 0, what + " replaces the last one's param"); + Check(!LeavesCopies("rotated"), what + " leaves no staged or old copy"); + } + const auto previous = Snapshot(backup); + const FailedStep failures[] = {{"backup.aside", 1}, {"backup.place", 1}}; + for (const FailedStep& failure : failures) { + const std::string what = std::string("a backup whose ") + failure.step + " step fails"; + FailStep(failure.step, failure.call); + const int error = BackUp(&name, "unsaved", "Unsaved"); + ClearFailingStep(); + Check(error == SaveDataErrorInternal, what + " gives the event error INTERNAL, got " + std::to_string(error)); + Check(Snapshot(backup) == previous, what + " keeps the previous backup byte for byte"); + param = {}; + Check(sceSaveDataCheckBackupData(&check) == 0 && std::strcmp(param.title, "Third") == 0, what + " leaves the previous backup usable"); + Check(!LeavesCopies("rotated"), what + " leaves no staged or old copy"); + } + SaveDataRestoreBackupData restore{}; + restore.dir_name = &name; + Check(sceSaveDataRestoreBackupData(&restore) == 0, "the backup kept by the failed backups restores"); + Check(ReadText(std::filesystem::path("_sd") / "rotated" / "data.bin") == "rotation 3", "the restore puts back the third backup"); +} + +#ifdef _WIN32 +bool Junction(const std::filesystem::path& link, const std::filesystem::path& target) { + std::error_code error; + if (!std::filesystem::create_directory(link, error)) return false; + const auto printName = std::filesystem::absolute(target).make_preferred().wstring(); + const auto substituteName = L"\\??\\" + printName; + struct MountPoint { + ULONG tag; + USHORT dataLength; + USHORT reserved; + USHORT substituteOffset; + USHORT substituteLength; + USHORT printOffset; + USHORT printLength; + wchar_t names[2048]; + } data{}; + if (substituteName.size() + printName.size() + 2 > std::size(data.names)) return false; + data.tag = IO_REPARSE_TAG_MOUNT_POINT; + data.substituteLength = static_cast(substituteName.size() * sizeof(wchar_t)); + data.printOffset = static_cast((substituteName.size() + 1) * sizeof(wchar_t)); + data.printLength = static_cast(printName.size() * sizeof(wchar_t)); + data.dataLength = static_cast(8 + data.printOffset + data.printLength + sizeof(wchar_t)); + std::memcpy(data.names, substituteName.c_str(), data.substituteLength + sizeof(wchar_t)); + std::memcpy(reinterpret_cast(data.names) + data.printOffset, printName.c_str(), data.printLength + sizeof(wchar_t)); + const HANDLE handle = CreateFileW(link.c_str(), GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr); + bool set = false; + if (handle != INVALID_HANDLE_VALUE) { + DWORD returned = 0; + set = DeviceIoControl(handle, FSCTL_SET_REPARSE_POINT, &data, 8 + data.dataLength, nullptr, 0, &returned, nullptr) != 0; + CloseHandle(handle); + } + if (!set) RemoveDirectoryW(link.c_str()); + return set; +} +#endif + +bool LinkDirectory(const std::filesystem::path& link, const std::filesystem::path& target) { +#ifdef _WIN32 + return Junction(link, target); +#else + std::error_code error; + std::filesystem::create_directory_symlink(std::filesystem::absolute(target), link, error); + return !error; +#endif +} + +bool LinkFile(const std::filesystem::path& link, const std::filesystem::path& target) { +#ifdef _WIN32 + return CreateSymbolicLinkW(link.c_str(), std::filesystem::absolute(target).c_str(), SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) != 0; +#else + std::error_code error; + std::filesystem::create_symlink(std::filesystem::absolute(target), link, error); + return !error; +#endif +} + +void Unlink(const std::filesystem::path& link) { +#ifdef _WIN32 + const DWORD attributes = GetFileAttributesW(link.c_str()); + const bool removed = attributes != INVALID_FILE_ATTRIBUTES && ((attributes & FILE_ATTRIBUTE_DIRECTORY) != 0 ? RemoveDirectoryW(link.c_str()) != 0 : DeleteFileW(link.c_str()) != 0); +#else + std::error_code error; + const bool removed = std::filesystem::remove(link, error); +#endif + Check(removed, "the link " + link.string() + " is removed"); +} + +void CheckLinkedPaths() { + const auto outside = std::filesystem::absolute("outside"); + std::filesystem::create_directories(outside / "sentinel"); + WriteText(outside / "sentinel" / "keep.bin", "sentinel"); + std::filesystem::create_directories(outside / "old" / "escape"); + WriteText(outside / "old" / "escape" / "keep.bin", "sentinel"); + const auto sentinel = Snapshot(outside / "sentinel"); + const auto oldSentinel = Snapshot(outside / "old" / "escape"); + SceSaveDataDirName name{}; + std::memcpy(name.data, "escape", sizeof("escape")); + Check(BackUp(&name, "inside", "Inside") == 0, "the escape save is backed up"); + const auto backup = std::filesystem::path("_sd_backup") / "escape" / "save"; + const auto kept = Snapshot(backup); + const auto live = std::filesystem::path("_sd") / "escape"; + SaveDataRestoreBackupData restore{}; + restore.dir_name = &name; + const auto survives = [&](const std::string& what) { + Check(Snapshot(outside / "sentinel") == sentinel && Snapshot(outside / "old" / "escape") == oldSentinel, what + ": the outside directory and its files survive"); + Check(Snapshot(backup) == kept, what + ": the backup is kept"); + }; + const auto restoreRefused = [&](const std::string& what) { + Check(sceSaveDataRestoreBackupData(&restore) == SaveDataErrorInternal, what + ": restoreBackupData is INTERNAL"); + survives(what); + }; + const auto backupRefused = [&](const std::string& what) { + Check(RemountWithBackup(&name) == SaveDataErrorInternal, what + ": the backup gives the event error INTERNAL"); + survives(what); + }; + + std::filesystem::remove_all(live); + if (LinkDirectory(live, outside / "sentinel")) { + restoreRefused("a save directory that links outside"); + backupRefused("a save directory that links outside"); + Unlink(live); + } else { + std::fprintf(stderr, "skipped the linked save directory: the link could not be created\n"); + } + std::filesystem::create_directories(live); + WriteText(live / "data.bin", "inside"); + + if (LinkFile(live / "link.bin", outside / "sentinel" / "keep.bin")) { + restoreRefused("a save with a file that links outside"); + backupRefused("a save with a file that links outside"); + Unlink(live / "link.bin"); + } else { + std::fprintf(stderr, "skipped the linked file in a save: the link could not be created\n"); + } + const auto inside = Snapshot(live); + + for (const char* root : {"_sd_backup_old", "_sd_restore_old"}) { + std::error_code error; + std::filesystem::remove_all(root, error); + const std::string what = std::string("a ") + root + " root that links outside"; + if (!LinkDirectory(root, outside / "old")) { + std::fprintf(stderr, "skipped %s: the link could not be created\n", what.c_str()); + continue; + } + if (std::strcmp(root, "_sd_backup_old") == 0) { + backupRefused(what); + } else { + restoreRefused(what); + } + Unlink(root); + } + + Check(Snapshot(live) == inside, "the refused calls leave the save as it was"); + Check(sceSaveDataRestoreBackupData(&restore) == 0, "the save restores once its paths are plain again"); + Check(ReadText(live / "data.bin") == "inside", "the restore puts back the kept backup"); +} + +void CheckBackupGuestErrors() { + SceSaveDataDirName name{}; + std::memcpy(name.data, "errors", sizeof("errors")); + SaveDataParam param{}; + SaveDataCheckBackupData check{}; + check.dir_name = &name; + check.param = ¶m; + Check(BackUp(&name, "first", "First") == 0, "the first backup of a save succeeds"); + Check(BackUp(&name, "second", "Second") == 0, "a second sceSaveDataUmountWithBackup succeeds"); + Check(sceSaveDataCheckBackupData(&check) == 0 && std::strcmp(param.title, "Second") == 0, "the second backup replaces the first one's param"); + Check(ReadText(std::filesystem::path("_sd_backup") / "errors" / "save" / "data.bin") == "second", "the second backup replaces the first one's files"); + + SaveDataMountResult mounted{}; + Check(MountSave(&name, MountReadWrite, &mounted), "the save mounts for a backup whose copy fails"); + FailStep("backup.copy", 1); + Check(sceSaveDataUmountWithBackup(&mounted.mount_point) == 0, "umountWithBackup returns 0 when the backup copy fails"); + ClearFailingStep(); + SaveDataEvent event{}; + Check(sceSaveDataGetEventResult(nullptr, &event) == 0 && event.error_code == SaveDataErrorInternal, "a failed backup copy gives the event error INTERNAL, got " + std::to_string(event.error_code)); + SaveDataMountInfo info{}; + Check(sceSaveDataGetMountInfo(&mounted.mount_point, &info) == SaveDataErrorNotMounted, "a failed backup copy still unmounts the save"); + param = {}; + Check(sceSaveDataCheckBackupData(&check) == 0 && std::strcmp(param.title, "Second") == 0, "a failed backup copy keeps the previous backup"); + + SceSaveDataTitleId other{}; + std::memcpy(other.data, "PPSA99999", sizeof("PPSA99999")); + check.title_id = &other; + Check(sceSaveDataCheckBackupData(&check) == SaveDataErrorNotFound, "checkBackupData with another title's id is NOT_FOUND"); + SaveDataRestoreBackupData restore{}; + restore.dir_name = &name; + restore.title_id = &other; + Check(sceSaveDataRestoreBackupData(&restore) == SaveDataErrorNotFound, "restoreBackupData with another title's id is NOT_FOUND"); + + for (const char* invalid : {"..", "a/b"}) { + const std::string label = std::string("\"") + invalid + "\""; + SceSaveDataDirName bad{}; + std::memcpy(bad.data, invalid, std::strlen(invalid) + 1); + SaveDataCheckBackupData badCheck{}; + badCheck.dir_name = &bad; + Check(sceSaveDataCheckBackupData(&badCheck) == SaveDataErrorParameter, "checkBackupData of " + label + " is PARAMETER"); + SaveDataRestoreBackupData badRestore{}; + badRestore.dir_name = &bad; + Check(sceSaveDataRestoreBackupData(&badRestore) == SaveDataErrorParameter, "restoreBackupData of " + label + " is PARAMETER"); + SaveDataMount3 mount{}; + mount.dir_name = &bad; + mount.mount_mode = MountReadWrite | MountCreate2; + SaveDataMountResult result{}; + Check(sceSaveDataMount3(&mount, &result) == SaveDataErrorParameter, "a save named " + label + " cannot be mounted to be backed up"); + SaveDataMountPoint point{}; + std::memcpy(point.data, invalid, std::strlen(invalid) + 1); + Check(sceSaveDataUmountWithBackup(&point) == SaveDataErrorNotMounted, "umountWithBackup of the mount point " + label + " is NOT_MOUNTED"); + } +} + void CheckMountCapacityAndLifecycle() { Check(sceSaveDataAbort() == 0, "abort after init succeeds"); Check(sceSaveDataClearProgress() == 0, "clearProgress after init succeeds"); @@ -396,6 +899,12 @@ int main() { CheckFailedMountKeepsResult(); CheckEquivalentMounts(); CheckInvalidNamesAreRejected(); + CheckBackups(); + CheckCorruptBackups(); + CheckStagedRestore(); + CheckBackupSwap(); + CheckLinkedPaths(); + CheckBackupGuestErrors(); Check(sceSaveDataTerminate() == 0, "SaveData terminates"); Check(sceSaveDataAbort() == SaveDataErrorNotInitialized, "abort post-terminate returns NOT_INITIALIZED"); Check(sceSaveDataClearProgress() == SaveDataErrorNotInitialized, "clearProgress post-terminate returns NOT_INITIALIZED"); diff --git a/core/libs/tests/GuestSaveDataPs4.cpp b/core/libs/tests/GuestSaveDataPs4.cpp index 2aec1faa9f..fe933e11bc 100644 --- a/core/libs/tests/GuestSaveDataPs4.cpp +++ b/core/libs/tests/GuestSaveDataPs4.cpp @@ -4,12 +4,18 @@ #include "SceTypes.hpp" #include #include +#include #include #include #include #include #include #include +#ifdef _WIN32 +#include +#else +#include +#endif static_assert(offsetof(SaveDataMount2, dir_name) == 8 && offsetof(SaveDataMount2, blocks) == 16 && offsetof(SaveDataMount2, mount_mode) == 24 && sizeof(SaveDataMount2) == 64, "SaveDataMount2 has the PS4 layout"); @@ -150,6 +156,8 @@ void TestMount2() { void TestSaveDataMemoryV1() { Check(sceSaveDataSetupSaveDataMemory(7, 0x1000, nullptr) == SaveDataErrorNotInitialized, "v1 SetupSaveDataMemory before v1 Initialize"); + Check(sceSaveDataGetSaveDataMemory(7, nullptr, 0, 0) == SaveDataErrorNotInitialized, "v1 GetSaveDataMemory before v1 Initialize"); + Check(sceSaveDataSetSaveDataMemory(7, nullptr, 0, 0) == SaveDataErrorNotInitialized, "v1 SetSaveDataMemory before v1 Initialize"); Check(sceSaveDataInitialize(nullptr) == 0, "v1 Initialize"); Check(sceSaveDataSetupSaveDataMemory(7, 0x1000, nullptr) == 0, "v1 SetupSaveDataMemory"); char written[16] = "settings-v1"; @@ -157,6 +165,14 @@ void TestSaveDataMemoryV1() { 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"); + Check(sceSaveDataGetSaveDataMemory(7, read, sizeof(read), -1) == SaveDataErrorParameter, "v1 GetSaveDataMemory negative offset returns PARAMETER"); + Check(sceSaveDataSetSaveDataMemory(7, written, sizeof(written), -1) == SaveDataErrorParameter, "v1 SetSaveDataMemory negative offset returns PARAMETER"); + Check(sceSaveDataGetSaveDataMemory(7, read, sizeof(read), 0x1001) == SaveDataErrorParameter, "v1 GetSaveDataMemory offset beyond size returns PARAMETER"); + Check(sceSaveDataSetSaveDataMemory(7, written, sizeof(written), 0x1001) == SaveDataErrorParameter, "v1 SetSaveDataMemory offset beyond size returns PARAMETER"); + Check(sceSaveDataGetSaveDataMemory(7, nullptr, sizeof(read), 0x100) == SaveDataErrorParameter, "v1 GetSaveDataMemory null buffer returns PARAMETER"); + Check(sceSaveDataSetSaveDataMemory(7, nullptr, sizeof(written), 0x100) == SaveDataErrorParameter, "v1 SetSaveDataMemory null buffer returns PARAMETER"); + Check(sceSaveDataGetSaveDataMemory(7, nullptr, 0, 0x100) == 0, "v1 GetSaveDataMemory zero length is a no-op"); + Check(sceSaveDataSetSaveDataMemory(7, nullptr, 0, 0x100) == 0, "v1 SetSaveDataMemory zero length is a no-op"); std::ifstream slot("_sd_mem/u7/slot0.bin", std::ios::binary); slot.seekg(0x100); char stored[16] = {}; @@ -164,6 +180,25 @@ void TestSaveDataMemoryV1() { Check(sceSaveDataTerminate() == 0, "Terminate after v1 Initialize"); } +void TestInitialize2() { + using Initialize2 = int (APS5_VABI*)(const void*); +#ifdef _WIN32 + void* symbol = reinterpret_cast(GetProcAddress(GetModuleHandleA("libSceSaveData.native.prx"), "sceSaveDataInitialize2")); +#else + void* symbol = dlsym(RTLD_DEFAULT, "sceSaveDataInitialize2"); +#endif + if (!symbol) { + std::fprintf(stderr, "sceSaveDataInitialize2 is not exported by libSceSaveData.native\n"); + std::abort(); + } + const auto initialize2 = reinterpret_cast(symbol); + Check(sceSaveDataSetupSaveDataMemory(8, 0x1000, nullptr) == SaveDataErrorNotInitialized, "SetupSaveDataMemory before Initialize2"); + Check(initialize2(nullptr) == 0, "Initialize2"); + Check(sceSaveDataSetupSaveDataMemory(8, 0x1000, nullptr) == 0, "SetupSaveDataMemory after Initialize2"); + Check(sceSaveDataTerminate() == 0, "Terminate after Initialize2"); + Check(sceSaveDataSetupSaveDataMemory(8, 0x1000, nullptr) == SaveDataErrorNotInitialized, "SetupSaveDataMemory after the Initialize2 session ended"); +} + void TestMountPs4() { SaveDataMountResult result{}; Check(sceSaveDataMount(nullptr, &result) == SaveDataErrorParameter, "null mount is PARAMETER"); @@ -192,6 +227,7 @@ int main() { TestTransferringMountPs4(); TestDirNameSearchPs4(); TestSaveDataMemoryV1(); + TestInitialize2(); TestMountPs4(); TestMount2(); diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index a4b9b33a06..471bbe4eb9 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -788,6 +788,9 @@ static void CheckRejectedArguments() { Require(std::memcmp(mac.data, untouched.data(), untouched.size()) == 0); } Require(Failed(sceNetGetMacAddress(nullptr, 1), 22)); + Require(Failed(sceNetGetMacAddress(reinterpret_cast(std::uintptr_t{0x10}), 0), 14)); + Require(sceNetGetMacAddress(&mac, 0) == 0); + Require(std::memcmp(mac.data, "\x02\x50\x53\x35\x00\x01", sizeof(mac.data)) == 0); for (const int flags : {1, 0x10000, -1}) Require(Failed(sceNetResolverCreate("rejected-resolver", 0, flags), 22)); @@ -1175,7 +1178,9 @@ static void CheckIoctl() { Require(sceNetRecv(socket, buffer, sizeof(buffer), 0) == sizeof(payload)); Require(sceNetIoctl(socket, 0x4004667f, &readable) == 0 && readable == 0); Require(Failed(sceNetIoctl(socket, 0x8004667e, nullptr), 14)); + Require(Failed(sceNetIoctl(socket, 0x8004667e, reinterpret_cast(std::uintptr_t{0x10})), 14)); Require(Failed(sceNetIoctl(socket, 0x4004667f, nullptr), 14)); + Require(Failed(sceNetIoctl(socket, 0x4004667f, reinterpret_cast(std::uintptr_t{0x10})), 14)); Require(Failed(sceNetIoctl(socket, 123, &readable), 25)); Require(Failed(sceNetIoctl(socket, 123, nullptr), 25)); Require(sceNetSocketClose(socket) == 0); @@ -2805,6 +2810,7 @@ int main() { Require(sceNetResolverAbort(other, 0) == static_cast(0x80410109) && *sceNetErrnoLoc() == 9); Require(sceNetResolverAbort(-1, 1) == static_cast(0x80410109) && *sceNetErrnoLoc() == 9); Require(sceNetResolverGetError(resolver, nullptr) == static_cast(0x80410116) && *sceNetErrnoLoc() == 22); + Require(Failed(sceNetResolverGetError(resolver, reinterpret_cast(std::uintptr_t{0x10})), 14)); Require(sceNetClearDnsCache(0) == 0); ipv4.fill(0xA5); Require(sceNetResolverStartNtoa(resolver, "localhost", ipv4.data(), 5000000, 1, 0) == 0 && ipv4[0] == 127); diff --git a/core/libs/tests/GuestSceV2BinaryScaling.cpp b/core/libs/tests/GuestSceV2BinaryScaling.cpp new file mode 100644 index 0000000000..7e7383846f --- /dev/null +++ b/core/libs/tests/GuestSceV2BinaryScaling.cpp @@ -0,0 +1,299 @@ +#include "prx/libc/libc.hpp" +#include +#include +#include + +static void Require(bool value) { if (!value) std::abort(); } + +struct ScaleCase { std::uint64_t input; long long exponent; short code; std::uint64_t result; short extendedCode; std::uint64_t extendedResult; }; +struct FloatScaleCase { std::uint32_t input; long long exponent; short code; std::uint32_t result; }; +struct UnscaleCase { std::uint64_t input; short code; short exponent; std::uint64_t result; short extendedCode; short extendedExponent; std::uint64_t extendedResult; }; +struct FloatUnscaleCase { std::uint32_t input; short code; short exponent; std::uint32_t result; }; + +static const ScaleCase kScales[] = { + {0x0000000000000000ull, 0ll, 0, 0x0000000000000000ull, 0, 0x0000000000000000ull}, + {0x0000000000000000ull, 52ll, 0, 0x0000000000000000ull, 0, 0x0000000000000000ull}, + {0x0000000000000000ull, -1022ll, 0, 0x0000000000000000ull, 0, 0x0000000000000000ull}, + {0x0000000000000000ull, -1075ll, 0, 0x0000000000000000ull, 0, 0x0000000000000000ull}, + {0x0000000000000000ull, 2047ll, 0, 0x0000000000000000ull, 0, 0x0000000000000000ull}, + {0x0000000000000000ull, -9223372036854775807ll - 1, 0, 0x0000000000000000ull, 0, 0x0000000000000000ull}, + {0x8000000000000000ull, -1ll, 0, 0x8000000000000000ull, 0, 0x8000000000000000ull}, + {0x8000000000000000ull, 1023ll, 0, 0x8000000000000000ull, 0, 0x8000000000000000ull}, + {0x8000000000000000ull, -1074ll, 0, 0x8000000000000000ull, 0, 0x8000000000000000ull}, + {0x8000000000000000ull, 1024ll, 0, 0x8000000000000000ull, 0, 0x8000000000000000ull}, + {0x8000000000000000ull, 9223372036854775807ll, 0, 0x8000000000000000ull, 0, 0x8000000000000000ull}, + {0x0000000000000001ull, 1ll, -1, 0x0000000000000002ull, -1, 0x0000000000000002ull}, + {0x0000000000000001ull, -52ll, 0, 0x0000000000000000ull, 0, 0x0000000000000000ull}, + {0x0000000000000001ull, -1023ll, 0, 0x0000000000000000ull, 0, 0x0000000000000000ull}, + {0x0000000000000001ull, -1076ll, 0, 0x0000000000000000ull, 0, 0x0000000000000000ull}, + {0x0000000000000001ull, -2100ll, 0, 0x0000000000000000ull, 0, 0x0000000000000000ull}, + {0x8000000000000003ull, 0ll, -1, 0x8000000000000003ull, -1, 0x8000000000000003ull}, + {0x8000000000000003ull, 52ll, -1, 0x8028000000000000ull, -1, 0x8028000000000000ull}, + {0x8000000000000003ull, -1022ll, 0, 0x8000000000000000ull, 0, 0x8000000000000000ull}, + {0x8000000000000003ull, -1075ll, 0, 0x8000000000000000ull, 0, 0x8000000000000000ull}, + {0x8000000000000003ull, 2047ll, -1, 0xfcd8000000000000ull, -1, 0xfcd8000000000000ull}, + {0x8000000000000003ull, -9223372036854775807ll - 1, 0, 0x8000000000000000ull, 0, 0x8000000000000000ull}, + {0x000fffffffffffffull, -1ll, -1, 0x0008000000000000ull, -1, 0x0008000000000000ull}, + {0x000fffffffffffffull, 1023ll, -1, 0x3ffffffffffffffeull, -1, 0x3ffffffffffffffeull}, + {0x000fffffffffffffull, -1074ll, 0, 0x0000000000000000ull, 0, 0x0000000000000000ull}, + {0x000fffffffffffffull, 1024ll, -1, 0x400ffffffffffffeull, -1, 0x400ffffffffffffeull}, + {0x000fffffffffffffull, 9223372036854775807ll, 1, 0x7ff0000000000000ull, 1, 0x7ff0000000000000ull}, + {0x0010000000000000ull, 1ll, -1, 0x0020000000000000ull, -1, 0x0020000000000000ull}, + {0x0010000000000000ull, -52ll, -1, 0x0000000000000001ull, -1, 0x0000000000000001ull}, + {0x0010000000000000ull, -1023ll, 0, 0x0000000000000000ull, 0, 0x0000000000000000ull}, + {0x0010000000000000ull, -1076ll, 0, 0x0000000000000000ull, 0, 0x0000000000000000ull}, + {0x0010000000000000ull, -2100ll, 0, 0x0000000000000000ull, 0, 0x0000000000000000ull}, + {0x3ff0000000000000ull, 0ll, -1, 0x3ff0000000000000ull, -1, 0x3ff0000000000000ull}, + {0x3ff0000000000000ull, 52ll, -1, 0x4330000000000000ull, -1, 0x4330000000000000ull}, + {0x3ff0000000000000ull, -1022ll, -1, 0x0010000000000000ull, -1, 0x0010000000000000ull}, + {0x3ff0000000000000ull, -1075ll, 0, 0x0000000000000000ull, 0, 0x0000000000000000ull}, + {0x3ff0000000000000ull, 2047ll, 1, 0x7ff0000000000000ull, 1, 0x7ff0000000000000ull}, + {0x3ff0000000000000ull, -9223372036854775807ll - 1, 0, 0x0000000000000000ull, 0, 0x0000000000000000ull}, + {0xbff8000000000000ull, -1ll, -1, 0xbfe8000000000000ull, -1, 0xbfe8000000000000ull}, + {0xbff8000000000000ull, 1023ll, -1, 0xffe8000000000000ull, -1, 0xffe8000000000000ull}, + {0xbff8000000000000ull, -1074ll, -1, 0x8000000000000002ull, -1, 0x8000000000000002ull}, + {0xbff8000000000000ull, 1024ll, 1, 0xfff0000000000000ull, 1, 0xfff0000000000000ull}, + {0xbff8000000000000ull, 9223372036854775807ll, 1, 0xfff0000000000000ull, 1, 0xfff0000000000000ull}, + {0x3ff0000000000001ull, 1ll, -1, 0x4000000000000001ull, -1, 0x4000000000000001ull}, + {0x3ff0000000000001ull, -52ll, -1, 0x3cb0000000000001ull, -1, 0x3cb0000000000001ull}, + {0x3ff0000000000001ull, -1023ll, -1, 0x0008000000000000ull, -1, 0x0008000000000000ull}, + {0x3ff0000000000001ull, -1076ll, 0, 0x0000000000000000ull, 0, 0x0000000000000000ull}, + {0x3ff0000000000001ull, -2100ll, 0, 0x0000000000000000ull, 0, 0x0000000000000000ull}, + {0x3ff8000000000000ull, 0ll, -1, 0x3ff8000000000000ull, -1, 0x3ff8000000000000ull}, + {0x3ff8000000000000ull, 52ll, -1, 0x4338000000000000ull, -1, 0x4338000000000000ull}, + {0x3ff8000000000000ull, -1022ll, -1, 0x0018000000000000ull, -1, 0x0018000000000000ull}, + {0x3ff8000000000000ull, -1075ll, -1, 0x0000000000000001ull, -1, 0x0000000000000001ull}, + {0x3ff8000000000000ull, 2047ll, 1, 0x7ff0000000000000ull, 1, 0x7ff0000000000000ull}, + {0x3ff8000000000000ull, -9223372036854775807ll - 1, 0, 0x0000000000000000ull, 0, 0x0000000000000000ull}, + {0x3ff4000000000000ull, -1ll, -1, 0x3fe4000000000000ull, -1, 0x3fe4000000000000ull}, + {0x3ff4000000000000ull, 1023ll, -1, 0x7fe4000000000000ull, -1, 0x7fe4000000000000ull}, + {0x3ff4000000000000ull, -1074ll, -1, 0x0000000000000001ull, -1, 0x0000000000000001ull}, + {0x3ff4000000000000ull, 1024ll, 1, 0x7ff0000000000000ull, 1, 0x7ff0000000000000ull}, + {0x3ff4000000000000ull, 9223372036854775807ll, 1, 0x7ff0000000000000ull, 1, 0x7ff0000000000000ull}, + {0x3ffc000000000000ull, 1ll, -1, 0x400c000000000000ull, -1, 0x400c000000000000ull}, + {0x3ffc000000000000ull, -52ll, -1, 0x3cbc000000000000ull, -1, 0x3cbc000000000000ull}, + {0x3ffc000000000000ull, -1023ll, -1, 0x000e000000000000ull, -1, 0x000e000000000000ull}, + {0x3ffc000000000000ull, -1076ll, 0, 0x0000000000000000ull, 0, 0x0000000000000000ull}, + {0x3ffc000000000000ull, -2100ll, 0, 0x0000000000000000ull, 0, 0x0000000000000000ull}, + {0x7fefffffffffffffull, 0ll, -1, 0x7fefffffffffffffull, -1, 0x7fefffffffffffffull}, + {0x7fefffffffffffffull, 52ll, 1, 0x7ff0000000000000ull, 1, 0x7ff0000000000000ull}, + {0x7fefffffffffffffull, -1022ll, -1, 0x400fffffffffffffull, -1, 0x400fffffffffffffull}, + {0x7fefffffffffffffull, -1075ll, -1, 0x3cbfffffffffffffull, -1, 0x3cbfffffffffffffull}, + {0x7fefffffffffffffull, 2047ll, 1, 0x7ff0000000000000ull, 1, 0x7ff0000000000000ull}, + {0x7fefffffffffffffull, -9223372036854775807ll - 1, 0, 0x0000000000000000ull, 0, 0x0000000000000000ull}, + {0xffefffffffffffffull, -1ll, -1, 0xffdfffffffffffffull, -1, 0xffdfffffffffffffull}, + {0xffefffffffffffffull, 1023ll, 1, 0xfff0000000000000ull, 1, 0xfff0000000000000ull}, + {0xffefffffffffffffull, -1074ll, -1, 0xbccfffffffffffffull, -1, 0xbccfffffffffffffull}, + {0xffefffffffffffffull, 1024ll, 1, 0xfff0000000000000ull, 1, 0xfff0000000000000ull}, + {0xffefffffffffffffull, 9223372036854775807ll, 1, 0xfff0000000000000ull, 1, 0xfff0000000000000ull}, + {0x7ff0000000000000ull, 1ll, 1, 0x7ff0000000000000ull, 1, 0x7ff0000000000000ull}, + {0x7ff0000000000000ull, -52ll, 1, 0x7ff0000000000000ull, 1, 0x7ff0000000000000ull}, + {0x7ff0000000000000ull, -1023ll, 1, 0x7ff0000000000000ull, 1, 0x7ff0000000000000ull}, + {0x7ff0000000000000ull, -1076ll, 1, 0x7ff0000000000000ull, 1, 0x7ff0000000000000ull}, + {0x7ff0000000000000ull, -2100ll, 1, 0x7ff0000000000000ull, 1, 0x7ff0000000000000ull}, + {0xfff0000000000000ull, 0ll, 1, 0xfff0000000000000ull, 1, 0xfff0000000000000ull}, + {0xfff0000000000000ull, 52ll, 1, 0xfff0000000000000ull, 1, 0xfff0000000000000ull}, + {0xfff0000000000000ull, -1022ll, 1, 0xfff0000000000000ull, 1, 0xfff0000000000000ull}, + {0xfff0000000000000ull, -1075ll, 1, 0xfff0000000000000ull, 1, 0xfff0000000000000ull}, + {0xfff0000000000000ull, 2047ll, 1, 0xfff0000000000000ull, 1, 0xfff0000000000000ull}, + {0xfff0000000000000ull, -9223372036854775807ll - 1, 1, 0xfff0000000000000ull, 1, 0xfff0000000000000ull}, + {0x7ff8000000000000ull, -1ll, 2, 0x7ff8000000000000ull, 2, 0x7ff8000000000000ull}, + {0x7ff8000000000000ull, 1023ll, 2, 0x7ff8000000000000ull, 2, 0x7ff8000000000000ull}, + {0x7ff8000000000000ull, -1074ll, 2, 0x7ff8000000000000ull, 2, 0x7ff8000000000000ull}, + {0x7ff8000000000000ull, 1024ll, 2, 0x7ff8000000000000ull, 2, 0x7ff8000000000000ull}, + {0x7ff8000000000000ull, 9223372036854775807ll, 2, 0x7ff8000000000000ull, 2, 0x7ff8000000000000ull}, + {0x7ff0000000000001ull, 1ll, 2, 0x7ff0000000000001ull, 2, 0x7ff0000000000001ull}, + {0x7ff0000000000001ull, -52ll, 2, 0x7ff0000000000001ull, 2, 0x7ff0000000000001ull}, + {0x7ff0000000000001ull, -1023ll, 2, 0x7ff0000000000001ull, 2, 0x7ff0000000000001ull}, + {0x7ff0000000000001ull, -1076ll, 2, 0x7ff0000000000001ull, 2, 0x7ff0000000000001ull}, + {0x7ff0000000000001ull, -2100ll, 2, 0x7ff0000000000001ull, 2, 0x7ff0000000000001ull}, + {0x405edd2f1a9fbe77ull, 0ll, -1, 0x405edd2f1a9fbe77ull, -1, 0x405edd2f1a9fbe77ull}, + {0x405edd2f1a9fbe77ull, 52ll, -1, 0x439edd2f1a9fbe77ull, -1, 0x439edd2f1a9fbe77ull}, + {0x405edd2f1a9fbe77ull, -1022ll, -1, 0x007edd2f1a9fbe77ull, -1, 0x007edd2f1a9fbe77ull}, + {0x405edd2f1a9fbe77ull, -1075ll, -1, 0x000000000000003eull, -1, 0x000000000000003eull}, + {0x405edd2f1a9fbe77ull, 2047ll, 1, 0x7ff0000000000000ull, 1, 0x7ff0000000000000ull}, + {0x405edd2f1a9fbe77ull, -9223372036854775807ll - 1, 0, 0x0000000000000000ull, 0, 0x0000000000000000ull}, + {0x0008000000000000ull, -1ll, -1, 0x0004000000000000ull, -1, 0x0004000000000000ull}, + {0x0008000000000000ull, 1023ll, -1, 0x3ff0000000000000ull, -1, 0x3ff0000000000000ull}, + {0x0008000000000000ull, -1074ll, 0, 0x0000000000000000ull, 0, 0x0000000000000000ull}, + {0x0008000000000000ull, 1024ll, -1, 0x4000000000000000ull, -1, 0x4000000000000000ull}, + {0x0008000000000000ull, 9223372036854775807ll, 1, 0x7ff0000000000000ull, 1, 0x7ff0000000000000ull}, + {0x0000000000000400ull, 1ll, -1, 0x0000000000000800ull, -1, 0x0000000000000800ull}, + {0x0000000000000400ull, -52ll, 0, 0x0000000000000000ull, 0, 0x0000000000000000ull}, + {0x0000000000000400ull, -1023ll, 0, 0x0000000000000000ull, 0, 0x0000000000000000ull}, + {0x0000000000000400ull, -1076ll, 0, 0x0000000000000000ull, 0, 0x0000000000000000ull}, + {0x0000000000000400ull, -2100ll, 0, 0x0000000000000000ull, 0, 0x0000000000000000ull}, +}; + +static const FloatScaleCase kFloatScales[] = { + {0x00000000u, 0ll, 0, 0x00000000u}, + {0x00000000u, 23ll, 0, 0x00000000u}, + {0x00000000u, -127ll, 0, 0x00000000u}, + {0x00000000u, -151ll, 0, 0x00000000u}, + {0x00000000u, 9223372036854775807ll, 0, 0x00000000u}, + {0x80000000u, 1ll, 0, 0x80000000u}, + {0x80000000u, 127ll, 0, 0x80000000u}, + {0x80000000u, -149ll, 0, 0x80000000u}, + {0x80000000u, 255ll, 0, 0x80000000u}, + {0x80000000u, -9223372036854775807ll - 1, 0, 0x80000000u}, + {0x00000001u, 23ll, -1, 0x00800000u}, + {0x00000001u, -126ll, 0, 0x00000000u}, + {0x00000001u, -151ll, 0, 0x00000000u}, + {0x00000001u, -300ll, 0, 0x00000000u}, + {0x80000003u, 1ll, -1, 0x80000006u}, + {0x80000003u, -23ll, 0, 0x80000000u}, + {0x80000003u, -149ll, 0, 0x80000000u}, + {0x80000003u, 128ll, -1, 0xb5c00000u}, + {0x80000003u, -9223372036854775807ll - 1, 0, 0x80000000u}, + {0x007fffffu, -1ll, -1, 0x00400000u}, + {0x007fffffu, -126ll, 0, 0x00000000u}, + {0x007fffffu, -150ll, 0, 0x00000000u}, + {0x007fffffu, -300ll, 0, 0x00000000u}, + {0x00800000u, 0ll, -1, 0x00800000u}, + {0x00800000u, -23ll, -1, 0x00000001u}, + {0x00800000u, -127ll, 0, 0x00000000u}, + {0x00800000u, 128ll, -1, 0x40800000u}, + {0x00800000u, 9223372036854775807ll, 1, 0x7f800000u}, + {0x3f800000u, -1ll, -1, 0x3f000000u}, + {0x3f800000u, 127ll, -1, 0x7f000000u}, + {0x3f800000u, -150ll, 0, 0x00000000u}, + {0x3f800000u, 255ll, 1, 0x7f800000u}, + {0xbfc00000u, 0ll, -1, 0xbfc00000u}, + {0xbfc00000u, 23ll, -1, 0xcb400000u}, + {0xbfc00000u, -127ll, -1, 0x80600000u}, + {0xbfc00000u, -151ll, 0, 0x80000000u}, + {0xbfc00000u, 9223372036854775807ll, 1, 0xff800000u}, + {0x3f800001u, 1ll, -1, 0x40000001u}, + {0x3f800001u, 127ll, -1, 0x7f000001u}, + {0x3f800001u, -149ll, -1, 0x00000001u}, + {0x3f800001u, 255ll, 1, 0x7f800000u}, + {0x3f800001u, -9223372036854775807ll - 1, 0, 0x00000000u}, + {0x3fc00000u, 23ll, -1, 0x4b400000u}, + {0x3fc00000u, -126ll, -1, 0x00c00000u}, + {0x3fc00000u, -151ll, 0, 0x00000000u}, + {0x3fc00000u, -300ll, 0, 0x00000000u}, + {0x3fa00000u, 1ll, -1, 0x40200000u}, + {0x3fa00000u, -23ll, -1, 0x34200000u}, + {0x3fa00000u, -149ll, -1, 0x00000001u}, + {0x3fa00000u, 128ll, 1, 0x7f800000u}, + {0x3fa00000u, -9223372036854775807ll - 1, 0, 0x00000000u}, + {0x3fe00000u, -1ll, -1, 0x3f600000u}, + {0x3fe00000u, -126ll, -1, 0x00e00000u}, + {0x3fe00000u, -150ll, -1, 0x00000001u}, + {0x3fe00000u, -300ll, 0, 0x00000000u}, + {0x7f7fffffu, 0ll, -1, 0x7f7fffffu}, + {0x7f7fffffu, -23ll, -1, 0x73ffffffu}, + {0x7f7fffffu, -127ll, -1, 0x3fffffffu}, + {0x7f7fffffu, 128ll, 1, 0x7f800000u}, + {0x7f7fffffu, 9223372036854775807ll, 1, 0x7f800000u}, + {0xff7fffffu, -1ll, -1, 0xfeffffffu}, + {0xff7fffffu, 127ll, 1, 0xff800000u}, + {0xff7fffffu, -150ll, -1, 0xb47fffffu}, + {0xff7fffffu, 255ll, 1, 0xff800000u}, + {0x7f800000u, 0ll, 1, 0x7f800000u}, + {0x7f800000u, 23ll, 1, 0x7f800000u}, + {0x7f800000u, -127ll, 1, 0x7f800000u}, + {0x7f800000u, -151ll, 1, 0x7f800000u}, + {0x7f800000u, 9223372036854775807ll, 1, 0x7f800000u}, + {0xff800000u, 1ll, 1, 0xff800000u}, + {0xff800000u, 127ll, 1, 0xff800000u}, + {0xff800000u, -149ll, 1, 0xff800000u}, + {0xff800000u, 255ll, 1, 0xff800000u}, + {0xff800000u, -9223372036854775807ll - 1, 1, 0xff800000u}, + {0x7fc00000u, 23ll, 2, 0x7fc00000u}, + {0x7fc00000u, -126ll, 2, 0x7fc00000u}, + {0x7fc00000u, -151ll, 2, 0x7fc00000u}, + {0x7fc00000u, -300ll, 2, 0x7fc00000u}, + {0x7f800001u, 1ll, 2, 0x7f800001u}, + {0x7f800001u, -23ll, 2, 0x7f800001u}, + {0x7f800001u, -149ll, 2, 0x7f800001u}, + {0x7f800001u, 128ll, 2, 0x7f800001u}, + {0x7f800001u, -9223372036854775807ll - 1, 2, 0x7f800001u}, + {0x42f6e979u, -1ll, -1, 0x4276e979u}, + {0x42f6e979u, -126ll, -1, 0x03f6e979u}, + {0x42f6e979u, -150ll, -1, 0x0000003eu}, + {0x42f6e979u, -300ll, 0, 0x00000000u}, + {0x00400000u, 0ll, -1, 0x00400000u}, + {0x00400000u, -23ll, 0, 0x00000000u}, + {0x00400000u, -127ll, 0, 0x00000000u}, + {0x00400000u, 128ll, -1, 0x40000000u}, + {0x00400000u, 9223372036854775807ll, 1, 0x7f800000u}, + {0x00000400u, -1ll, -1, 0x00000200u}, + {0x00000400u, 127ll, -1, 0x39800000u}, + {0x00000400u, -150ll, 0, 0x00000000u}, + {0x00000400u, 255ll, -1, 0x79800000u}, +}; + +static const UnscaleCase kUnscales[] = { + {0x0000000000000000ull, 0, 0, 0x0000000000000000ull, 0, 0, 0x0000000000000000ull}, + {0x8000000000000000ull, 0, 0, 0x8000000000000000ull, 0, 0, 0x8000000000000000ull}, + {0x0000000000000001ull, -1, -1073, 0x3fe0000000000000ull, -1, -1073, 0x3fe0000000000000ull}, + {0x8000000000000003ull, -1, -1072, 0xbfe8000000000000ull, -1, -1072, 0xbfe8000000000000ull}, + {0x000fffffffffffffull, -1, -1022, 0x3feffffffffffffeull, -1, -1022, 0x3feffffffffffffeull}, + {0x0010000000000000ull, -1, -1021, 0x3fe0000000000000ull, -1, -1021, 0x3fe0000000000000ull}, + {0x3ff0000000000000ull, -1, 1, 0x3fe0000000000000ull, -1, 1, 0x3fe0000000000000ull}, + {0xbff8000000000000ull, -1, 1, 0xbfe8000000000000ull, -1, 1, 0xbfe8000000000000ull}, + {0x3ff0000000000001ull, -1, 1, 0x3fe0000000000001ull, -1, 1, 0x3fe0000000000001ull}, + {0x3ff8000000000000ull, -1, 1, 0x3fe8000000000000ull, -1, 1, 0x3fe8000000000000ull}, + {0x3ff4000000000000ull, -1, 1, 0x3fe4000000000000ull, -1, 1, 0x3fe4000000000000ull}, + {0x3ffc000000000000ull, -1, 1, 0x3fec000000000000ull, -1, 1, 0x3fec000000000000ull}, + {0x7fefffffffffffffull, -1, 1024, 0x3fefffffffffffffull, -1, 1024, 0x3fefffffffffffffull}, + {0xffefffffffffffffull, -1, 1024, 0xbfefffffffffffffull, -1, 1024, 0xbfefffffffffffffull}, + {0x7ff0000000000000ull, 1, 0, 0x7ff0000000000000ull, 1, 0, 0x7ff0000000000000ull}, + {0xfff0000000000000ull, 1, 0, 0xfff0000000000000ull, 1, 0, 0xfff0000000000000ull}, + {0x7ff8000000000000ull, 2, 0, 0x7ff8000000000000ull, 2, 0, 0x7ff8000000000000ull}, + {0x7ff0000000000001ull, 2, 0, 0x7ff0000000000001ull, 2, 0, 0x7ff0000000000001ull}, + {0x405edd2f1a9fbe77ull, -1, 7, 0x3feedd2f1a9fbe77ull, -1, 7, 0x3feedd2f1a9fbe77ull}, + {0x0008000000000000ull, -1, -1022, 0x3fe0000000000000ull, -1, -1022, 0x3fe0000000000000ull}, + {0x0000000000000400ull, -1, -1063, 0x3fe0000000000000ull, -1, -1063, 0x3fe0000000000000ull}, +}; + +static const FloatUnscaleCase kFloatUnscales[] = { + {0x00000000u, 0, 0, 0x00000000u}, + {0x80000000u, 0, 0, 0x80000000u}, + {0x00000001u, -1, -148, 0x3f000000u}, + {0x80000003u, -1, -147, 0xbf400000u}, + {0x007fffffu, -1, -126, 0x3f7ffffeu}, + {0x00800000u, -1, -125, 0x3f000000u}, + {0x3f800000u, -1, 1, 0x3f000000u}, + {0xbfc00000u, -1, 1, 0xbf400000u}, + {0x3f800001u, -1, 1, 0x3f000001u}, + {0x3fc00000u, -1, 1, 0x3f400000u}, + {0x3fa00000u, -1, 1, 0x3f200000u}, + {0x3fe00000u, -1, 1, 0x3f600000u}, + {0x7f7fffffu, -1, 128, 0x3f7fffffu}, + {0xff7fffffu, -1, 128, 0xbf7fffffu}, + {0x7f800000u, 1, 0, 0x7f800000u}, + {0xff800000u, 1, 0, 0xff800000u}, + {0x7fc00000u, 2, 0, 0x7fc00000u}, + {0x7f800001u, 2, 0, 0x7f800001u}, + {0x42f6e979u, -1, 7, 0x3f76e979u}, + {0x00400000u, -1, -126, 0x3f000000u}, + {0x00000400u, -1, -138, 0x3f000000u}, +}; + +int main() { + for (const auto& c : kScales) { + double value = std::bit_cast(c.input); + Require(_ZNSt8__sce_v27_DscaleEPdl_nid_postfix(&value, c.exponent) == c.code && std::bit_cast(value) == c.result); + value = std::bit_cast(c.input); + Require(_ZNSt8__sce_v28_LDscaleEPel_nid_postfix(&value, c.exponent) == c.extendedCode && std::bit_cast(value) == c.extendedResult); + } + for (const auto& c : kFloatScales) { + float value = std::bit_cast(c.input); + Require(_ZNSt8__sce_v28_FDscaleEPfl_nid_postfix(&value, c.exponent) == c.code && std::bit_cast(value) == c.result); + } + for (const auto& c : kUnscales) { + double value = std::bit_cast(c.input); + short exponent = 77; + Require(_ZNSt8__sce_v29_DunscaleEPsPd_nid_postfix(&exponent, &value) == c.code && exponent == c.exponent && std::bit_cast(value) == c.result); + value = std::bit_cast(c.input); + exponent = 77; + Require(_ZNSt8__sce_v210_LDunscaleEPsPe_nid_postfix(&exponent, &value) == c.extendedCode && exponent == c.extendedExponent && std::bit_cast(value) == c.extendedResult); + } + for (const auto& c : kFloatUnscales) { + float value = std::bit_cast(c.input); + short exponent = 77; + Require(_ZNSt8__sce_v210_FDunscaleEPsPf_nid_postfix(&exponent, &value) == c.code && exponent == c.exponent && std::bit_cast(value) == c.result); + } +} diff --git a/core/libs/tests/GuestSceV2FilesystemCalls.cpp b/core/libs/tests/GuestSceV2FilesystemCalls.cpp new file mode 100644 index 0000000000..261447acf1 --- /dev/null +++ b/core/libs/tests/GuestSceV2FilesystemCalls.cpp @@ -0,0 +1,193 @@ +#include "AppArchiveFixture.hpp" +#include "prx/libc/include/Api.hpp" +#include "prx/libkernel/include/Api.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr int TypeNotFound = -1; +constexpr int TypeRegular = 1; +constexpr int TypeDirectory = 2; +constexpr int NoSuchFile = 2; +constexpr int NotDirectory = 20; +constexpr int IsDirectory = 21; +constexpr int Invalid = 22; +constexpr int NameTooLong = 63; +constexpr int NoSystemCall = 78; +constexpr int NotSupported = 45; +constexpr std::uint32_t KernelNoSuchFile = 0x80020002u; +constexpr std::uint32_t KernelNotDirectory = 0x80020014u; + +void Require(bool condition, const char* message) { + if (condition) return; + std::fprintf(stderr, "sce v2 filesystem calls: %s\n", message); + std::abort(); +} + +void Write(const char* path, std::size_t bytes, unsigned seed) { + std::ofstream file(path, std::ios::binary | std::ios::trunc); + for (std::size_t index = 0; index < bytes; ++index) file.put(static_cast((index * 131 + seed) & 0xff)); +} + +std::string Read(const char* path) { + std::ifstream file(path, std::ios::binary); + return {std::istreambuf_iterator(file), {}}; +} + +FileStat Status(const char* path) { + FileStat status{}; + Require(stat_nid_postfix(path, &status) == 0, "stat of a test file"); + return status; +} + +void Prepare() { + std::error_code ignored; + for (const char* name : {"f", "g", "big", "d", "e", "ro", "new", "made", "copy", "renamed"}) { + std::filesystem::permissions(name, std::filesystem::perms::owner_all, std::filesystem::perm_options::add, ignored); + std::filesystem::remove_all(name, ignored); + } + Write("f", 7, 1); + Write("g", 3, 2); + Write("big", 200000, 4); + std::filesystem::create_directories("d/sub"); + Write("d/sub/h", 5, 5); + std::filesystem::create_directory("e"); + Write("ro", 4, 6); + std::filesystem::permissions("ro", std::filesystem::perms::owner_read | std::filesystem::perms::group_read | std::filesystem::perms::others_read); +} + +void TestStatus() { + Prepare(); + int perms = -1; + int error = -1; + Require(_ZNSt8__sce_v210filesystem6_LstatEPKcPNS0_5permsEPi_nid_postfix("f", &perms, &error) == TypeRegular && error == 0, "a regular file"); + Require(perms == (Status("f").st_mode & 0xfff), "the permission bits of a regular file"); + Require(_ZNSt8__sce_v210filesystem6_LstatEPKcPNS0_5permsEPi_nid_postfix("d", nullptr, nullptr) == TypeDirectory, "a directory without outputs"); + Require(_ZNSt8__sce_v210filesystem6_LstatEPKcPNS0_5permsE_nid_postfix("d/", &perms) == TypeDirectory && perms == (Status("d").st_mode & 0xfff), "a directory with a trailing slash"); + const struct { + const char* path; + int error; + } missing[] = {{"missing", NoSuchFile}, {"f/x", NotDirectory}, {"", NoSuchFile}}; + for (const auto& entry : missing) { + perms = 0x1234; + Require(_ZNSt8__sce_v210filesystem6_LstatEPKcPNS0_5permsEPi_nid_postfix(entry.path, &perms, &error) == TypeNotFound && error == entry.error && perms == 0x1234, "a path that is not found"); + Require(_ZNSt8__sce_v210filesystem6_LstatEPKcPNS0_5permsE_nid_postfix(entry.path, &perms) == TypeNotFound && perms == 0x1234, "a path that is not found without an error output"); + } + const std::string longName(300, 'n'); + Require(_ZNSt8__sce_v210filesystem6_LstatEPKcPNS0_5permsEPi_nid_postfix(longName.c_str(), &perms, &error) == TypeNotFound && error == NameTooLong, "a name that is too long"); +} + +void TestSizes() { + Prepare(); + int error = 0x5678; + Require(_ZNSt8__sce_v210filesystem10_File_sizeEPKcPi_nid_postfix("big", &error) == 200000 && error == 0x5678, "the size of a file leaves the error output alone"); + Require(_ZNSt8__sce_v210filesystem10_File_sizeEPKc_nid_postfix("f") == 7, "the size of a file without an error output"); + Require(_ZNSt8__sce_v210filesystem10_File_sizeEPKcPi_nid_postfix("missing", &error) == UINT64_MAX && static_cast(error) == KernelNoSuchFile, "a missing file reports the kernel error"); + Require(_ZNSt8__sce_v210filesystem10_File_sizeEPKcPi_nid_postfix("f/x", &error) == UINT64_MAX && static_cast(error) == KernelNotDirectory, "a file used as a directory reports the kernel error"); + Require(_ZNSt8__sce_v210filesystem10_File_sizeEPKcPi_nid_postfix("missing", nullptr) == UINT64_MAX, "a missing file without an error output"); + Require(_ZNSt8__sce_v210filesystem10_File_sizeEPKc_nid_postfix("missing") == UINT64_MAX, "a missing file through the short form"); +} + +void TestTimes() { + Prepare(); + const struct { + std::int64_t microseconds, seconds, nanoseconds; + } times[] = {{-1500001, -2, 499999000}, {1700000000123456, 1700000000, 123456000}, {999999, 0, 999999000}, {-1000000, -1, 0}, {-1, -1, 999999000}}; + for (const auto& time : times) { + Require(_ZNSt8__sce_v210filesystem20_Set_last_write_timeEPKcl_nid_postfix("f", time.microseconds), "set the write time of a file"); + std::int64_t seconds = 0; + std::int64_t nanoseconds = 0; + Require(_ZNSt8__sce_v210filesystem16_Last_write_timeEPKcPlS3__nid_postfix("f", &seconds, &nanoseconds) == 0, "read the write time of a file"); + Require(seconds == time.seconds && nanoseconds == time.nanoseconds, "the write time is floored to whole seconds and microseconds"); + Require(_ZNSt8__sce_v210filesystem16_Last_write_timeEPKc_nid_postfix("f") == time.seconds, "the write time in seconds"); + } + Require(_ZNSt8__sce_v210filesystem20_Set_last_write_timeEPKcl_nid_postfix("d", 1), "set the write time of a directory"); + Require(!_ZNSt8__sce_v210filesystem20_Set_last_write_timeEPKcl_nid_postfix("missing", 0), "a missing file has no write time to set"); + std::int64_t seconds = 0x1111; + std::int64_t nanoseconds = 0x2222; + Require(_ZNSt8__sce_v210filesystem16_Last_write_timeEPKcPlS3__nid_postfix("missing", &seconds, &nanoseconds) == NoSuchFile && seconds == 0x1111 && nanoseconds == 0x2222, "a missing file leaves the time outputs alone"); + Require(_ZNSt8__sce_v210filesystem16_Last_write_timeEPKc_nid_postfix("missing") == -1, "a missing file has no write time"); +} + +void TestDirectories() { + Prepare(); + Require(_ZNSt8__sce_v210filesystem9_Make_dirEPKcS2__nid_postfix("made", nullptr) == 1 && std::filesystem::is_directory("made"), "make a directory"); + Require(_ZNSt8__sce_v210filesystem9_Make_dirEPKcS2__nid_postfix("made", nullptr) == 0, "an existing directory is not made again"); + Require(_ZNSt8__sce_v210filesystem9_Make_dirEPKcS2__nid_postfix("f", nullptr) == 0, "an existing file is reported as existing"); + Require(_ZNSt8__sce_v210filesystem9_Make_dirEPKcS2__nid_postfix("missing/x", nullptr) == -NoSuchFile, "a missing parent"); + Require(_ZNSt8__sce_v210filesystem9_Make_dirEPKcS2__nid_postfix("f/x", nullptr) == -NotDirectory, "a file used as a parent"); + Require(_ZNSt8__sce_v210filesystem9_Make_dirEPKcS2__nid_postfix("new", "missing") == -NoSuchFile && !std::filesystem::exists("new"), "a missing template stops before making the directory"); + Require(_ZNSt8__sce_v210filesystem9_Make_dirEPKcS2__nid_postfix("new", "d") == 1 && std::filesystem::is_directory("new"), "make a directory from a template"); + const std::string longName(300, 'n'); + Require(_ZNSt8__sce_v210filesystem9_Make_dirEPKcS2__nid_postfix(longName.c_str(), nullptr) == -NameTooLong, "a name that is too long"); + Require(_ZNSt8__sce_v210filesystem11_Remove_dirEPKc_nid_postfix("made") && !std::filesystem::exists("made"), "remove an empty directory"); + Require(!_ZNSt8__sce_v210filesystem11_Remove_dirEPKc_nid_postfix("d") && std::filesystem::exists("d/sub/h"), "a directory that is not empty stays"); + Require(!_ZNSt8__sce_v210filesystem11_Remove_dirEPKc_nid_postfix("f") && std::filesystem::exists("f"), "a file is not removed as a directory"); + Require(!_ZNSt8__sce_v210filesystem11_Remove_dirEPKc_nid_postfix("missing"), "a missing directory"); +} + +void TestEntries() { + Prepare(); + Require(_ZNSt8__sce_v210filesystem7_UnlinkEPKc_nid_postfix("f/x") == NotDirectory, "unlink below a file"); + Require(_ZNSt8__sce_v210filesystem7_UnlinkEPKc_nid_postfix("f") == 0 && !std::filesystem::exists("f"), "unlink a file"); + Require(_ZNSt8__sce_v210filesystem7_UnlinkEPKc_nid_postfix("missing") == NoSuchFile, "unlink a missing file"); + Require(_ZNSt8__sce_v210filesystem7_RenameEPKcS2__nid_postfix("g", "renamed") == 0 && Read("renamed").size() == 3 && !std::filesystem::exists("g"), "rename a file"); + Require(_ZNSt8__sce_v210filesystem7_RenameEPKcS2__nid_postfix("missing", "new") == NoSuchFile, "rename a missing file"); + Require(_ZNSt8__sce_v210filesystem7_RenameEPKcS2__nid_postfix("renamed", "d") == IsDirectory && std::filesystem::exists("renamed"), "rename a file over a directory"); + Require(_ZNSt8__sce_v210filesystem7_RenameEPKcS2__nid_postfix("renamed", "missing/x") == NoSuchFile, "rename into a missing directory"); + Require(_ZNSt8__sce_v210filesystem7_ResizeEPKcm_nid_postfix("renamed", 100000) == 0 && Status("renamed").st_size == 100000, "grow a file"); + Require(_ZNSt8__sce_v210filesystem7_ResizeEPKcm_nid_postfix("renamed", 2) == 0 && Status("renamed").st_size == 2, "shrink a file"); + Require(_ZNSt8__sce_v210filesystem7_ResizeEPKcm_nid_postfix("renamed", 0x8000000000000000ull) == Invalid, "a size above the signed range"); + Require(_ZNSt8__sce_v210filesystem7_ResizeEPKcm_nid_postfix("d", 0) == IsDirectory, "resize a directory"); + Require(_ZNSt8__sce_v210filesystem7_ResizeEPKcm_nid_postfix("missing", 0) == NoSuchFile, "resize a missing file"); + Require(_ZNSt8__sce_v210filesystem6_ChmodEPKcNS0_5permsE_nid_postfix("renamed", 0444) == 0 && (Status("renamed").st_mode & 0222) == 0, "make a file read only"); + Require(_ZNSt8__sce_v210filesystem6_ChmodEPKcNS0_5permsE_nid_postfix("renamed", 0x1ffff) == 0 && (Status("renamed").st_mode & 0200) != 0, "only the low sixteen permission bits are passed"); + Require(_ZNSt8__sce_v210filesystem6_ChmodEPKcNS0_5permsE_nid_postfix("missing", 0444) == NoSuchFile, "change a missing file"); +} + +void TestCopy() { + Prepare(); + const auto contents = Read("big"); + Require(_ZNSt8__sce_v210filesystem10_Copy_fileEPKcS2__nid_postfix("big", "copy") == 0 && Read("copy") == contents, "copy a file larger than one block"); + Require(_ZNSt8__sce_v210filesystem10_Copy_fileEPKcS2__nid_postfix("g", "copy") == 0 && Read("copy") == Read("g"), "copy over a longer file truncates it"); + Require(_ZNSt8__sce_v210filesystem10_Copy_fileEPKcS2__nid_postfix("ro", "new") == 0 && Read("new") == Read("ro"), "copy a read-only file"); + Require((Status("new").st_mode & 07777) == (Status("ro").st_mode & 07777), "the copy takes the permissions of its source"); + Require(_ZNSt8__sce_v210filesystem10_Copy_fileEPKcS2__nid_postfix("missing", "made") == NoSuchFile && !std::filesystem::exists("made"), "a missing source creates nothing"); + Require(_ZNSt8__sce_v210filesystem10_Copy_fileEPKcS2__nid_postfix("f", "missing/x") == NoSuchFile, "a target in a missing directory"); + Require(_ZNSt8__sce_v210filesystem10_Copy_fileEPKcS2__nid_postfix("f", "d") == IsDirectory, "a directory target"); + Require(_ZNSt8__sce_v210filesystem10_Copy_fileEPKcS2__nid_postfix("d", "made") == IsDirectory, "a directory source"); +} + +void TestUnsupported() { + std::uint64_t space[4] = {1, 2, 3, 4}; + Require(_ZNSt8__sce_v210filesystem8_StatvfsEPKcRNS0_10space_infoE_nid_postfix("f", space) == NoSystemCall, "statvfs is not supported"); + Require(space[0] == UINT64_MAX && space[1] == UINT64_MAX && space[2] == UINT64_MAX && space[3] == 4, "statvfs reports unknown space and writes three fields"); + Require(_ZNSt8__sce_v210filesystem13_Current_set2EPKc_nid_postfix("/") == 0, "the root stays the current directory"); + for (const char* path : {"", ".", "d", "//", "/d", "f/"}) Require(_ZNSt8__sce_v210filesystem13_Current_set2EPKc_nid_postfix(path) == NotSupported, "any other current directory is not supported"); +} + +} + +int main() { + const auto directory = AppArchiveFixture::EnterFreshRoot("anyps5-sce-v2-filesystem-calls"); + TestStatus(); + TestSizes(); + TestTimes(); + TestDirectories(); + TestEntries(); + TestCopy(); + TestUnsupported(); + Prepare(); + std::error_code ignored; + std::filesystem::current_path(std::filesystem::temp_directory_path(), ignored); + std::filesystem::remove_all(directory, ignored); + return 0; +} diff --git a/core/libs/tests/GuestSceV2FloatHelpers.cpp b/core/libs/tests/GuestSceV2FloatHelpers.cpp new file mode 100644 index 0000000000..c1a121b8f1 --- /dev/null +++ b/core/libs/tests/GuestSceV2FloatHelpers.cpp @@ -0,0 +1,327 @@ +#include "prx/libc/libc.hpp" +#include +#include +#include + +static void Require(bool value) { if (!value) std::abort(); } + +struct TestCase { std::uint64_t input; short code; short extendedCode; }; +struct FloatTestCase { std::uint32_t input; short code; }; +struct IntCase { std::uint64_t input; short scale; short code; std::uint64_t result; short extendedCode; std::uint64_t extendedResult; }; +struct FloatIntCase { std::uint32_t input; short scale; short code; std::uint32_t result; }; +struct NormCase { std::uint64_t input; short code; std::uint64_t result; }; +struct FloatNormCase { std::uint32_t input; short code; std::uint32_t result; }; + +static const TestCase kTests[] = { + {0x0000000000000000ull, 0, 0}, + {0x0000000000000001ull, -2, -2}, + {0x3fefffffffffffffull, -1, -1}, + {0x3fe0000000000000ull, -1, -1}, + {0x3fe0000000000001ull, -1, -1}, + {0xc000000000000000ull, -1, -1}, + {0x4008000000000000ull, -1, -1}, + {0x4007ffffffffffffull, -1, -1}, + {0xc024000000000000ull, -1, -1}, + {0x4024000000000001ull, -1, -1}, + {0x3fb9999999999999ull, -1, -1}, + {0x81a56e1fc2f8f359ull, -1, -1}, + {0x01a56e1fc2f8f35aull, -1, -1}, + {0x7e37e43c8800759bull, -1, -1}, + {0x4086280000000000ull, -1, -1}, + {0x4086280000000001ull, -1, -1}, + {0xc0862e3d70a3d70aull, -1, -1}, + {0x4086300000000000ull, -1, -1}, + {0x40862fffffffffffull, -1, -1}, + {0xc08633c28f5c28f6ull, -1, -1}, + {0x408633d70a3d70a4ull, -1, -1}, + {0x408633d70a3d70a3ull, -1, -1}, + {0xc086380000000000ull, -1, -1}, + {0x4086380000000001ull, -1, -1}, + {0x40873fffffffffffull, -1, -1}, + {0x4087480000000000ull, -1, -1}, + {0x4087480000000001ull, -1, -1}, + {0xc087500000000000ull, -1, -1}, + {0x3e45798ee2308c3aull, -1, -1}, + {0x3e45798ee2308c3bull, -1, -1}, + {0xbc9cd2b297d889bcull, -1, -1}, + {0x4036000000000000ull, -1, -1}, + {0x4035ffffffffffffull, -1, -1}, + {0xc044000000000000ull, -1, -1}, + {0x4044000000000001ull, -1, -1}, + {0x4055ffffffffffffull, -1, -1}, + {0x4056400000000000ull, -1, -1}, + {0x4056400000000001ull, -1, -1}, + {0x408ff7ffffffffffull, -1, -1}, + {0x4090000000000000ull, -1, -1}, + {0x4090000000000001ull, -1, -1}, + {0xc090cc0000000000ull, -1, -1}, + {0x4202a05f20000000ull, -1, -1}, + {0x4202a05f1fffffffull, -1, -1}, + {0xd4b249ad2594c37dull, -1, -1}, + {0x54b249ad2594c37eull, -1, -1}, + {0x0010000000000000ull, -1, -1}, + {0x0010000000000001ull, -1, -1}, + {0xffefffffffffffffull, -1, -1}, + {0x3cb0000000000000ull, -1, -1}, + {0x3cafffffffffffffull, -1, -1}, + {0x7ff8000000000000ull, 2, 2}, + {0x7ff0000000000001ull, 2, 2}, + {0x4006000000000000ull, -1, -1}, + {0x405edd2f1a9fbe77ull, -1, -1}, + {0x800abcdef0123456ull, -2, -2}, +}; + +static const FloatTestCase kFloatTests[] = { + {0x00000000u, 0}, + {0x80000000u, 0}, + {0xbf800000u, -1}, + {0xbf000000u, -1}, + {0xc0000000u, -1}, + {0xc0400000u, -1}, + {0xc1200000u, -1}, + {0x3dcccccdu, -1}, + {0x7f800000u, 1}, + {0x44314000u, -1}, + {0x443171ecu, -1}, + {0x44318000u, -1}, + {0x44319e14u, -1}, + {0x44319eb8u, -1}, + {0xc4319eb8u, -1}, + {0xc431c000u, -1}, + {0xc43a0000u, -1}, + {0xc43a4000u, -1}, + {0xc43a8000u, -1}, + {0xb22bcc77u, -1}, + {0xa4e69595u, -1}, + {0x41b00000u, -1}, + {0x42200000u, -1}, + {0x42b00000u, -1}, + {0x42b20000u, -1}, + {0x447fc000u, -1}, + {0x44800000u, -1}, + {0x44866000u, -1}, + {0xc4866000u, -1}, + {0xd01502f9u, -1}, + {0xa5800000u, -1}, + {0xffc00000u, 2}, + {0x7fffffffu, 2}, + {0xc2b170a4u, -1}, + {0xc2b175c3u, -1}, + {0x42b2d1ecu, -1}, + {0x42b2d70au, -1}, + {0x42ce0000u, -1}, + {0x42d00000u, -1}, + {0x006ce3eeu, -2}, + {0x00000001u, -2}, + {0x7f7fc99eu, -1}, + {0xff7fc99eu, -1}, + {0x7fa00000u, 2}, + {0x40300000u, -1}, + {0x42f6e979u, -1}, + {0x00400000u, -2}, + {0x4b7fffffu, -1}, +}; + +static const IntCase kInts[] = { + {0x0000000000000000ull, 0, 0, 0x0000000000000000ull, 0, 0x0000000000000000ull}, + {0x8000000000000000ull, 1100, 0, 0x8000000000000000ull, 0, 0x8000000000000000ull}, + {0x3ff0000000000000ull, 3, 0, 0x3ff0000000000000ull, 0, 0x3ff0000000000000ull}, + {0x3fefffffffffffffull, 0, -1, 0x0000000000000000ull, -1, 0x0000000000000000ull}, + {0x3ff0000000000001ull, -32000, -1, 0x0000000000000000ull, -1, 0x0000000000000000ull}, + {0xbfe0000000000000ull, 52, 0, 0xbfe0000000000000ull, 0, 0xbfe0000000000000ull}, + {0x3fe0000000000001ull, 1, -1, 0x3fe0000000000000ull, -1, 0x3fe0000000000000ull}, + {0x4000000000000000ull, 32000, 0, 0x4000000000000000ull, 0, 0x4000000000000000ull}, + {0x3fffffffffffffffull, 53, 0, 0x3fffffffffffffffull, 0, 0x3fffffffffffffffull}, + {0x4008000000000000ull, -1, -1, 0x4000000000000000ull, -1, 0x4000000000000000ull}, + {0x4007ffffffffffffull, 0, -1, 0x4000000000000000ull, -1, 0x4000000000000000ull}, + {0x4008000000000001ull, 1100, 0, 0x4008000000000001ull, 0, 0x4008000000000001ull}, + {0xc024000000000000ull, 3, 0, 0xc024000000000000ull, 0, 0xc024000000000000ull}, + {0x4024000000000001ull, 1, -1, 0x4024000000000000ull, -1, 0x4024000000000000ull}, + {0x3fb999999999999aull, -32000, -1, 0x0000000000000000ull, -1, 0x0000000000000000ull}, + {0x3fb9999999999999ull, 52, -1, 0x3fb9999999999990ull, -1, 0x3fb9999999999990ull}, + {0x01a56e1fc2f8f359ull, -1, -1, 0x0000000000000000ull, -1, 0x0000000000000000ull}, + {0x81a56e1fc2f8f359ull, 32000, 0, 0x81a56e1fc2f8f359ull, 0, 0x81a56e1fc2f8f359ull}, + {0x01a56e1fc2f8f35aull, 53, -1, 0x0000000000000000ull, -1, 0x0000000000000000ull}, + {0xfe37e43c8800759cull, 3, 0, 0xfe37e43c8800759cull, 0, 0xfe37e43c8800759cull}, + {0x7e37e43c8800759dull, 0, 0, 0x7e37e43c8800759dull, 0, 0x7e37e43c8800759dull}, + {0x4086280000000000ull, 1100, 0, 0x4086280000000000ull, 0, 0x4086280000000000ull}, + {0x408627ffffffffffull, 52, 0, 0x408627ffffffffffull, 0, 0x408627ffffffffffull}, + {0x40862e3d70a3d70aull, 1, -1, 0x40862c0000000000ull, -1, 0x40862c0000000000ull}, + {0xc0862e3d70a3d70aull, -32000, -1, 0x8000000000000000ull, -1, 0x8000000000000000ull}, + {0x40862e3d70a3d70bull, 53, 0, 0x40862e3d70a3d70bull, 0, 0x40862e3d70a3d70bull}, + {0xc086300000000000ull, -1, 0, 0xc086300000000000ull, 0, 0xc086300000000000ull}, + {0x40862fffffffffffull, 32000, 0, 0x40862fffffffffffull, 0, 0x40862fffffffffffull}, + {0x408633c28f5c28f6ull, 1100, 0, 0x408633c28f5c28f6ull, 0, 0x408633c28f5c28f6ull}, + {0x408633c28f5c28f5ull, 3, -1, 0x4086330000000000ull, -1, 0x4086330000000000ull}, + {0x408633d70a3d70a4ull, 0, -1, 0x4086300000000000ull, -1, 0x4086300000000000ull}, + {0xc08633d70a3d70a4ull, 1100, 0, 0xc08633d70a3d70a4ull, 0, 0xc08633d70a3d70a4ull}, + {0x408633d70a3d70a5ull, 52, 0, 0x408633d70a3d70a5ull, 0, 0x408633d70a3d70a5ull}, + {0xc086380000000000ull, 1, 0, 0xc086380000000000ull, 0, 0xc086380000000000ull}, + {0x408637ffffffffffull, -32000, -1, 0x0000000000000000ull, -1, 0x0000000000000000ull}, + {0x4087400000000000ull, 53, 0, 0x4087400000000000ull, 0, 0x4087400000000000ull}, + {0x40873fffffffffffull, -1, -1, 0x4087300000000000ull, -1, 0x4087300000000000ull}, + {0x4087400000000001ull, 32000, 0, 0x4087400000000001ull, 0, 0x4087400000000001ull}, + {0xc087480000000000ull, 1100, 0, 0xc087480000000000ull, 0, 0xc087480000000000ull}, + {0x4087480000000001ull, 3, -1, 0x4087480000000000ull, -1, 0x4087480000000000ull}, + {0xc087500000000000ull, 0, 0, 0xc087500000000000ull, 0, 0xc087500000000000ull}, + {0x40874fffffffffffull, -32000, -1, 0x0000000000000000ull, -1, 0x0000000000000000ull}, + {0x3e45798ee2308c3aull, 52, -1, 0x3e45798ee0000000ull, -1, 0x3e45798ee0000000ull}, + {0x3e45798ee2308c39ull, 1, -1, 0x0000000000000000ull, -1, 0x0000000000000000ull}, + {0x3e45798ee2308c3bull, 32000, 0, 0x3e45798ee2308c3bull, 0, 0x3e45798ee2308c3bull}, + {0xbc9cd2b297d889bcull, 53, -1, 0x8000000000000000ull, -1, 0x8000000000000000ull}, + {0x3c9cd2b297d889bdull, -1, -1, 0x0000000000000000ull, -1, 0x0000000000000000ull}, + {0xc036000000000000ull, 0, 0, 0xc036000000000000ull, 0, 0xc036000000000000ull}, + {0x4035ffffffffffffull, 1100, 0, 0x4035ffffffffffffull, 0, 0x4035ffffffffffffull}, + {0x4044000000000000ull, 3, 0, 0x4044000000000000ull, 0, 0x4044000000000000ull}, + {0x4043ffffffffffffull, 1, -1, 0x4043c00000000000ull, -1, 0x4043c00000000000ull}, + {0x4044000000000001ull, -32000, -1, 0x0000000000000000ull, -1, 0x0000000000000000ull}, + {0xc056000000000000ull, 52, 0, 0xc056000000000000ull, 0, 0xc056000000000000ull}, + {0x4056000000000001ull, -1, -1, 0x4056000000000000ull, -1, 0x4056000000000000ull}, + {0x4056400000000000ull, 32000, 0, 0x4056400000000000ull, 0, 0x4056400000000000ull}, + {0x40563fffffffffffull, 53, 0, 0x40563fffffffffffull, 0, 0x40563fffffffffffull}, + {0x408ff80000000000ull, 3, 0, 0x408ff80000000000ull, 0, 0x408ff80000000000ull}, + {0x408ff7ffffffffffull, 0, -1, 0x408ff00000000000ull, -1, 0x408ff00000000000ull}, + {0x408ff80000000001ull, 1100, 0, 0x408ff80000000001ull, 0, 0x408ff80000000001ull}, + {0xc090000000000000ull, 3, 0, 0xc090000000000000ull, 0, 0xc090000000000000ull}, + {0x4090000000000001ull, 1, -1, 0x4090000000000000ull, -1, 0x4090000000000000ull}, + {0x4090cc0000000000ull, -32000, -1, 0x0000000000000000ull, -1, 0x0000000000000000ull}, + {0x4090cbffffffffffull, 52, 0, 0x4090cbffffffffffull, 0, 0x4090cbffffffffffull}, + {0x4202a05f20000000ull, -1, 0, 0x4202a05f20000000ull, 0, 0x4202a05f20000000ull}, + {0xc202a05f20000000ull, 32000, 0, 0xc202a05f20000000ull, 0, 0xc202a05f20000000ull}, + {0x4202a05f20000001ull, 53, 0, 0x4202a05f20000001ull, 0, 0x4202a05f20000001ull}, + {0xd4b249ad2594c37dull, 3, 0, 0xd4b249ad2594c37dull, 0, 0xd4b249ad2594c37dull}, + {0x54b249ad2594c37eull, 0, 0, 0x54b249ad2594c37eull, 0, 0x54b249ad2594c37eull}, + {0x8000000000000001ull, 1100, 0, 0x8000000000000001ull, 0, 0x8000000000000001ull}, + {0x0010000000000000ull, 52, -1, 0x0000000000000000ull, -1, 0x0000000000000000ull}, + {0x000fffffffffffffull, 1, -1, 0x0000000000000000ull, -1, 0x0000000000000000ull}, + {0x0010000000000001ull, -32000, 0, 0x0010000000000001ull, 0, 0x0010000000000001ull}, + {0xffefffffffffffffull, 53, 0, 0xffefffffffffffffull, 0, 0xffefffffffffffffull}, + {0x7ff0000000000000ull, -1, 1, 0x7ff0000000000000ull, 1, 0x7ff0000000000000ull}, + {0x3cb0000000000000ull, 32000, 0, 0x3cb0000000000000ull, 0, 0x3cb0000000000000ull}, + {0x3cafffffffffffffull, 1100, 0, 0x3cafffffffffffffull, 0, 0x3cafffffffffffffull}, + {0xfff0000000000000ull, 3, 1, 0xfff0000000000000ull, 1, 0xfff0000000000000ull}, + {0xfff8000000000000ull, 0, 2, 0xfff8000000000000ull, 2, 0xfff8000000000000ull}, + {0x7ff0000000000001ull, -32000, 2, 0x7ff0000000000001ull, 2, 0x7ff0000000000001ull}, + {0x7fffffffffffffffull, 52, 2, 0x7fffffffffffffffull, 2, 0x7fffffffffffffffull}, + {0xc006000000000000ull, 1, -1, 0xc004000000000000ull, -1, 0xc004000000000000ull}, + {0x405edd2f1a9fbe77ull, 32000, 0, 0x405edd2f1a9fbe77ull, 0, 0x405edd2f1a9fbe77ull}, + {0x0008000000000000ull, 53, -1, 0x0000000000000000ull, -1, 0x0000000000000000ull}, + {0x433fffffffffffffull, -1, -1, 0x433ffffffffffffeull, -1, 0x433ffffffffffffeull}, +}; + +static const FloatIntCase kFloatInts[] = { + {0x00000000u, 0, 0, 0x00000000u}, + {0x80000000u, 23, 0, 0x80000000u}, + {0x3f800000u, 32700, 0, 0x3f800000u}, + {0x3f000000u, 23, 0, 0x3f000000u}, + {0xbf000000u, 32700, 0, 0xbf000000u}, + {0xc0000000u, 3, 0, 0xc0000000u}, + {0x40400000u, 32700, 0, 0x40400000u}, + {0x41200000u, 3, 0, 0x41200000u}, + {0xc1200000u, -32700, -1, 0x80000000u}, + {0xbdcccccdu, 3, -1, 0x80000000u}, + {0x7f800000u, -32700, 1, 0x7f800000u}, + {0x44314000u, -1, -1, 0x44310000u}, + {0xc4314000u, -32700, -1, 0x80000000u}, + {0xc43171ecu, -1, -1, 0xc4310000u}, + {0x44318000u, 200, 0, 0x44318000u}, + {0x44319e14u, -1, -1, 0x44318000u}, + {0xc4319e14u, 200, 0, 0xc4319e14u}, + {0xc4319eb8u, 1, -1, 0xc4318000u}, + {0x4431c000u, 200, 0, 0x4431c000u}, + {0x443a0000u, 1, 0, 0x443a0000u}, + {0xc43a0000u, 200, 0, 0xc43a0000u}, + {0xc43a4000u, 1, 0, 0xc43a4000u}, + {0x443a8000u, 24, 0, 0x443a8000u}, + {0x322bcc77u, 1, -1, 0x00000000u}, + {0xb22bcc77u, 24, -1, 0x80000000u}, + {0xa4e69595u, 0, -1, 0x80000000u}, + {0x41b00000u, 24, 0, 0x41b00000u}, + {0x42200000u, 0, 0, 0x42200000u}, + {0xc2200000u, 23, 0, 0xc2200000u}, + {0xc2b00000u, 0, 0, 0xc2b00000u}, + {0x42b20000u, 23, 0, 0x42b20000u}, + {0xc2b20000u, 32700, 0, 0xc2b20000u}, + {0xc47fc000u, 23, 0, 0xc47fc000u}, + {0x44800000u, 32700, 0, 0x44800000u}, + {0x44866000u, 3, 0, 0x44866000u}, + {0xc4866000u, 32700, 0, 0xc4866000u}, + {0xd01502f9u, 3, 0, 0xd01502f9u}, + {0x25800000u, -32700, 0, 0x25800000u}, + {0x7fc00000u, 3, 2, 0x7fc00000u}, + {0xffc00000u, -32700, 2, 0xffc00000u}, + {0x7fffffffu, 3, 2, 0x7fffffffu}, + {0x42b170a4u, -32700, -1, 0x00000000u}, + {0x42b175c3u, -1, -1, 0x42b00000u}, + {0xc2b175c3u, -32700, -1, 0x80000000u}, + {0xc2b2d1ecu, -1, -1, 0xc2b00000u}, + {0x42b2d70au, 200, 0, 0x42b2d70au}, + {0x42ce0000u, -1, -1, 0x42cc0000u}, + {0xc2ce0000u, 200, 0, 0xc2ce0000u}, + {0xc2d00000u, 1, 0, 0xc2d00000u}, + {0x006ce3eeu, 200, 0, 0x006ce3eeu}, + {0x00000001u, 1, -1, 0x00000000u}, + {0x80000001u, 24, -1, 0x80000000u}, + {0xff7fc99eu, 1, 0, 0xff7fc99eu}, + {0x7f800001u, 24, 2, 0x7f800001u}, + {0x007fffffu, 0, -1, 0x00000000u}, + {0x40300000u, 24, 0, 0x40300000u}, + {0x42f6e979u, 0, -1, 0x42f60000u}, + {0x3e800000u, 23, 0, 0x3e800000u}, + {0x803456ffu, 0, -1, 0x80000000u}, + {0x4b7fffffu, 23, 0, 0x4b7fffffu}, +}; + +static const NormCase kNorms[] = { + {0x0000000000000001ull, -51, 0x0000000000000000ull}, + {0x0000000000000003ull, -50, 0x0008000000000000ull}, + {0x0008000000000000ull, 0, 0x0000000000000000ull}, + {0x000fffffffffffffull, 0, 0x000ffffffffffffeull}, + {0x8000000000000001ull, -51, 0x8000000000000000ull}, + {0x800abcdef0123456ull, 0, 0x800579bde02468acull}, + {0x0000123456789abcull, -7, 0x00023456789abc00ull}, + {0x0010000000000000ull, 1, 0x0000000000000000ull}, + {0x3ff0000000000000ull, 1, 0x0000000000000000ull}, +}; + +static const FloatNormCase kFloatNorms[] = { + {0x00000001u, -22, 0x00000000u}, + {0x00000003u, -21, 0x00400000u}, + {0x00400000u, 0, 0x00000000u}, + {0x007fffffu, 0, 0x007ffffeu}, + {0x80000001u, -22, 0x80000000u}, + {0x803456ffu, -1, 0x80515bfcu}, + {0x00012345u, -6, 0x0011a280u}, + {0x00800000u, 1, 0x00000000u}, + {0x3f800000u, 1, 0x00000000u}, +}; + +int main() { + for (const auto& c : kTests) { + double value = std::bit_cast(c.input); + Require(_ZNSt8__sce_v26_DtestEPd_nid_postfix(&value) == c.code && std::bit_cast(value) == c.input); + Require(_ZNSt8__sce_v27_LDtestEPe_nid_postfix(&value) == c.extendedCode && std::bit_cast(value) == c.input); + } + for (const auto& c : kFloatTests) { + float value = std::bit_cast(c.input); + Require(_ZNSt8__sce_v27_FDtestEPf_nid_postfix(&value) == c.code && std::bit_cast(value) == c.input); + } + for (const auto& c : kInts) { + double value = std::bit_cast(c.input); + Require(_ZNSt8__sce_v25_DintEPds_nid_postfix(&value, c.scale) == c.code && std::bit_cast(value) == c.result); + value = std::bit_cast(c.input); + Require(_ZNSt8__sce_v26_LDintEPes_nid_postfix(&value, c.scale) == c.extendedCode && std::bit_cast(value) == c.extendedResult); + } + for (const auto& c : kFloatInts) { + float value = std::bit_cast(c.input); + Require(_ZNSt8__sce_v26_FDintEPfs_nid_postfix(&value, c.scale) == c.code && std::bit_cast(value) == c.result); + } + for (const auto& c : kNorms) { + std::uint64_t words = c.input; + Require(_ZNSt8__sce_v26_DnormEPNS_5_DvalE_nid_postfix(reinterpret_cast(&words)) == c.code && words == c.result); + } + for (const auto& c : kFloatNorms) { + std::uint32_t words = c.input; + Require(_ZNSt8__sce_v27_FDnormEPNS_5_FvalE_nid_postfix(reinterpret_cast(&words)) == c.code && words == c.result); + } +} diff --git a/core/libs/tests/GuestSceV2Locale.cpp b/core/libs/tests/GuestSceV2Locale.cpp new file mode 100644 index 0000000000..5fbed784b2 --- /dev/null +++ b/core/libs/tests/GuestSceV2Locale.cpp @@ -0,0 +1,150 @@ +#include "prx/libc/include/ApplicationHeap.hpp" +#include "prx/libc/include/Api.hpp" +#include "prx/libc/include/GuestLocale.hpp" +#include +#include +#include +#include + +extern "C" { +extern std::uint64_t _ZNSt8__sce_v25ctypeIcE2idE_nid_postfix; +extern std::uint64_t _ZNSt8__sce_v25ctypeIwE2idE_nid_postfix; +extern std::uint64_t _ZNSt8__sce_v28numpunctIcE2idE_nid_postfix; +extern std::uint64_t _ZNSt8__sce_v27num_putIcNS_19ostreambuf_iteratorIcNS_11char_traitsIcEEEEE2idE_nid_postfix; +extern std::uint64_t _ZNSt8__sce_v27codecvtIwc9_MbstatetE2idE_nid_postfix; +extern std::int32_t _ZNSt8__sce_v26locale2id7_Id_cntE_nid_postfix; +extern GuestLocale::Implementation* _ZNSt8__sce_v221_sceLibcClassicLocaleE_nid_postfix; +extern unsigned char _ZNSt8__sce_v24coutE_nid_postfix[]; +extern unsigned char _ZNSt8__sce_v24cerrE_nid_postfix[]; +extern unsigned char _ZNSt8__sce_v25wcoutE_nid_postfix[]; +extern unsigned char _ZNSt8__sce_v25wcerrE_nid_postfix[]; +extern unsigned char _ZNSt8__sce_v23cinE_nid_postfix[]; +extern unsigned char _ZNSt8__sce_v24wcinE_nid_postfix[]; +} + +void* APS5_VABI TestHeapAllocate(std::size_t bytes) { return std::malloc(bytes); } +void APS5_VABI TestHeapFree(void* pointer) { std::free(pointer); } +void* APS5_VABI TestHeapCalloc(std::size_t count, std::size_t bytes) { return std::calloc(count, bytes); } +void* APS5_VABI TestHeapReallocate(void* pointer, std::size_t bytes) { return std::realloc(pointer, bytes); } +void* APS5_VABI TestHeapAlign(std::size_t, std::size_t bytes) { return std::malloc(bytes); } +void* APS5_VABI TestHeapRealign(void* pointer, std::size_t bytes, std::size_t) { return std::realloc(pointer, bytes); } +int APS5_VABI TestHeapPosixAlign(void** pointer, std::size_t, std::size_t bytes) { + *pointer = std::malloc(bytes); + return *pointer != nullptr ? 0 : -1; +} + +static void Require(bool value) { if (!value) std::abort(); } + +static int gEraseCalls = 0; +static bool gEraseSawStreamNodes = false; + +void APS5_VABI RecordErase(int, void* self, std::int32_t) { + ++gEraseCalls; + gEraseSawStreamNodes = reinterpret_cast(self)->streamNodes != nullptr; +} + +int main() { + std::array heapApi{}; + heapApi[0] = reinterpret_cast(&TestHeapAllocate); + heapApi[1] = reinterpret_cast(&TestHeapFree); + heapApi[2] = reinterpret_cast(&TestHeapCalloc); + heapApi[3] = reinterpret_cast(&TestHeapReallocate); + heapApi[4] = reinterpret_cast(&TestHeapAlign); + heapApi[5] = reinterpret_cast(&TestHeapRealign); + heapApi[6] = reinterpret_cast(&TestHeapPosixAlign); + ApplicationHeapRegister_nid_no_patch(heapApi.data()); + + Require(_ZNSt8__sce_v25ctypeIcE2idE_nid_postfix == 0); + Require(_ZNSt8__sce_v25ctypeIwE2idE_nid_postfix == 0); + Require(_ZNSt8__sce_v28numpunctIcE2idE_nid_postfix == 0); + Require(_ZNSt8__sce_v27num_putIcNS_19ostreambuf_iteratorIcNS_11char_traitsIcEEEEE2idE_nid_postfix == 0); + Require(_ZNSt8__sce_v27codecvtIwc9_MbstatetE2idE_nid_postfix == 0); + Require(_ZNSt8__sce_v26locale2id7_Id_cntE_nid_postfix == 0); + + unsigned char* streams[] { + _ZNSt8__sce_v24coutE_nid_postfix, + _ZNSt8__sce_v24cerrE_nid_postfix, + _ZNSt8__sce_v25wcoutE_nid_postfix, + _ZNSt8__sce_v25wcerrE_nid_postfix, + _ZNSt8__sce_v23cinE_nid_postfix, + _ZNSt8__sce_v24wcinE_nid_postfix, + }; + for (std::size_t index = 0; index < 6; ++index) { + Require(streams[index][0] == 0); + for (std::size_t other = 0; other < index; ++other) Require(streams[index] != streams[other]); + } + + auto* locale = _ZNSt8__sce_v26locale5_InitEb_nid_postfix(true); + Require(locale != nullptr); + Require(locale == _ZNSt8__sce_v221_sceLibcClassicLocaleE_nid_postfix); + Require(locale == _ZNSt8__sce_v26locale16_GetgloballocaleEv_nid_postfix()); + Require(_ZNSt8__sce_v26locale5_InitEb_nid_postfix(false) == locale); + + GuestLocale::Facet facet{}; + _ZNSt8__sce_v215_Facet_RegisterEPNS_11_Facet_baseE_nid_postfix(&facet); + + auto* copy = _ZNSt8__sce_v26locale7_Locimp11_New_LocimpERKS1__nid_postfix(locale); + Require(copy != nullptr && copy != locale); + Require(copy->facetCount == locale->facetCount); + + GuestLocale::Facet added{}; + _ZNSt8__sce_v26locale7_Locimp14_Locimp_AddfacEPS1_PNS0_5facetEm_nid_postfix(copy, &added, 41); + Require(copy->facetCount > 41); + Require(copy->facets[41] == &added); + + alignas(16) unsigned char classicStorage[sizeof(GuestLocale::SceV2IosBase)]{}; + auto* classic = reinterpret_cast(classicStorage); + classic->locale = &_ZNSt8__sce_v221_sceLibcClassicLocaleE_nid_postfix; + _ZNSt8__sce_v28ios_base7_AddstdEPS0__nid_postfix(classic); + Require(classic->standardStream >= 1 && classic->standardStream <= 8); + _ZNSt8__sce_v28ios_base14_Ios_base_dtorEPS0__nid_postfix(classic); + Require(classic->locale == nullptr); + + { + alignas(16) unsigned char storage[sizeof(GuestLocale::SceV2IosBase)]{}; + auto* stream = reinterpret_cast(storage); + auto* streamNode = static_cast(std::malloc(0x20)); + auto* callbackNode = static_cast(std::malloc(0x18)); + Require(streamNode != nullptr && callbackNode != nullptr); + std::memset(streamNode, 0, 0x20); + std::memset(callbackNode, 0, 0x18); + *reinterpret_cast(callbackNode + 0x10) = reinterpret_cast(&RecordErase); + *reinterpret_cast(callbackNode + 0x08) = 7; + stream->streamNodes = streamNode; + stream->callbackNodes = callbackNode; + _ZNSt8__sce_v28ios_base14_Ios_base_dtorEPS0__nid_postfix(stream); + Require(gEraseCalls == 1); + Require(gEraseSawStreamNodes); + Require(stream->streamNodes == nullptr && stream->callbackNodes == nullptr); + } + + copy->base.vtable->retain(©->base); + Require(copy->base.references == 2); + + GuestLocale::Implementation* firstSlot = copy; + GuestLocale::Implementation* secondSlot = copy; + auto** firstLocale = &firstSlot; + auto** secondLocale = &secondSlot; + + alignas(16) unsigned char firstStorage[sizeof(GuestLocale::SceV2IosBase)]{}; + alignas(16) unsigned char secondStorage[sizeof(GuestLocale::SceV2IosBase)]{}; + auto* first = reinterpret_cast(firstStorage); + auto* second = reinterpret_cast(secondStorage); + first->locale = firstLocale; + second->locale = secondLocale; + _ZNSt8__sce_v28ios_base7_AddstdEPS0__nid_postfix(first); + _ZNSt8__sce_v28ios_base7_AddstdEPS0__nid_postfix(second); + Require(first->standardStream >= 1 && first->standardStream <= 8); + Require(second->standardStream >= 1 && second->standardStream <= 8); + Require(first->standardStream != second->standardStream); + + _ZNSt8__sce_v28ios_base14_Ios_base_dtorEPS0__nid_postfix(first); + Require(first->locale == nullptr); + Require(copy->base.references == 1); + Require(copy->facets[41] == &added); + + _ZNSt8__sce_v28ios_base14_Ios_base_dtorEPS0__nid_postfix(second); + Require(second->locale == nullptr); + Require(copy->base.references == 0); + Require(copy->facets[41] == &added); +} diff --git a/core/libs/tests/GuestScreenShot.cpp b/core/libs/tests/GuestScreenShot.cpp index d947dd67e1..2773bb0347 100644 --- a/core/libs/tests/GuestScreenShot.cpp +++ b/core/libs/tests/GuestScreenShot.cpp @@ -66,6 +66,7 @@ void TestDisableState() { value = 7; Require(sceScreenShotIsDisabled(-1, &value) == ErrorArgument && value == 7, "id -1 is rejected"); Require(sceScreenShotIsDisabled(0, nullptr) == ErrorArgument, "null result is rejected"); + Require(sceScreenShotIsDisabled(0, reinterpret_cast(0x10)) == ErrorArgument, "invalid pointer is rejected"); Require(sceScreenShotDisableNotification() == 0 && sceScreenShotEnableNotification() == 0, "notification toggles"); Require(sceScreenShotIsDisabled(1, &value) == 0 && value == 0, "positive id 1 is accepted"); Require(sceScreenShotIsDisabled(2, &value) == 0 && value == 0, "positive id 2 is accepted"); diff --git a/core/libs/tests/GuestSelect.cpp b/core/libs/tests/GuestSelect.cpp index 1c1f48d9dd..e28074413b 100644 --- a/core/libs/tests/GuestSelect.cpp +++ b/core/libs/tests/GuestSelect.cpp @@ -3,6 +3,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "SceTypes.hpp" #include +#include #include #include #include @@ -109,9 +110,34 @@ static void Kqueues() { Require(close_nid_postfix(kq) == 0); } +static void ClosesDuringAWait() { + for (int attempt = 0; attempt < 200; ++attempt) { + int pair[2]{-1, -1}; + Require(pipe_nid_postfix(pair) == 0); + int result = 0; + int error = 0; + std::atomic started{false}; + std::thread waiter([&] { + GuestDescriptorSet read{}; + read.Add(pair[0]); + GuestTimeval limit{0, 20000}; + started = true; + result = select_nid_postfix(pair[0] + 1, read.words.data(), nullptr, nullptr, &limit); + error = *__error_nid_postfix(); + }); + while (!started) std::this_thread::yield(); + std::this_thread::sleep_for(std::chrono::microseconds(1000 + (attempt % 20) * 100)); + Require(close_nid_postfix(pair[0]) == 0); + waiter.join(); + Require(close_nid_postfix(pair[1]) == 0); + Require(result == 0 || (result == -1 && error == GuestEbadf)); + } +} + int main() { for (const int descriptor : {0, 1, 2}) StandardDescriptorFile(descriptor); Kqueues(); + ClosesDuringAWait(); int descriptors[2]{-1, -1}; Require(pipe_nid_postfix(descriptors) == 0); diff --git a/core/libs/tests/GuestSemGetvalueUninit.cpp b/core/libs/tests/GuestSemGetvalueUninit.cpp new file mode 100644 index 0000000000..29e0b8c76c --- /dev/null +++ b/core/libs/tests/GuestSemGetvalueUninit.cpp @@ -0,0 +1,49 @@ +#include "prx/libc/include/Api.hpp" +#include "prx/libkernel/include/Api.hpp" +#include "SceTypes.hpp" +#include +#include + +static void Require(bool condition) { + if (!condition) { + std::abort(); + } +} + +int main() { + std::uintptr_t sem = 0; + Require(sem_init_nid_postfix(&sem, 0, 1) == 0); + + *__error_nid_postfix() = 0; + Require(sem_getvalue_nid_postfix(&sem, nullptr) == -1); + Require(*__error_nid_postfix() == 22); + + int value = -1; + Require(sem_getvalue_nid_postfix(&sem, &value) == 0); + Require(value == 1); + + Require(sem_destroy_nid_postfix(&sem) == 0); + + std::uintptr_t uninitSem = 0; + *__error_nid_postfix() = 0; + Require(sem_getvalue_nid_postfix(&uninitSem, nullptr) == -1); + Require(*__error_nid_postfix() == 22); + + *__error_nid_postfix() = 0; + Require(sem_getvalue_nid_postfix(nullptr, nullptr) == -1); + Require(*__error_nid_postfix() == 22); + + std::intptr_t ksemId = -1; + Require(ksem_init_nid_postfix(&ksemId, 1) == 0); + + *__error_nid_postfix() = 0; + Require(ksem_getvalue_nid_postfix(ksemId, nullptr) == -1); + Require(*__error_nid_postfix() == 14); + + *__error_nid_postfix() = 0; + Require(ksem_getvalue_nid_postfix(-1, nullptr) == -1); + Require(*__error_nid_postfix() == 22); + + Require(ksem_destroy_nid_postfix(ksemId) == 0); + return 0; +} diff --git a/core/libs/tests/GuestSemaphore.cpp b/core/libs/tests/GuestSemaphore.cpp index 4ef7bdb093..dc09ddadd5 100644 --- a/core/libs/tests/GuestSemaphore.cpp +++ b/core/libs/tests/GuestSemaphore.cpp @@ -1,3 +1,4 @@ +#include "prx/libc/include/Api.hpp" #include "prx/libkernel/KernelErrors.hpp" #include "prx/libkernel/include/Api.hpp" #include "prx/libkernel/Semaphore/include/Semaphore.hpp" @@ -56,6 +57,8 @@ static void RequireInvalidArguments() { Require(sceKernelSignalSema(sem, 3) == SCE_OK); Require(sceKernelPollSema(sem, 3) == SCE_OK); Require(sceKernelDeleteSema(sem) == SCE_OK); + Require(ksem_wait_nid_postfix(-1) == -1 && *__error_nid_postfix() == 22); + Require(ksem_trywait_nid_postfix(-1) == -1 && *__error_nid_postfix() == 22); } int main() { @@ -113,9 +116,15 @@ int main() { remaining = 5000000; Require(sceKernelWaitSema(sem, 1, &remaining) == SCE_OK && remaining > 0 && remaining <= 5000000); + Require(sceKernelWaitSema(sem, 4, nullptr) == SCE_KERNEL_ERROR_EINVAL); + remaining = 1000; + Require(sceKernelWaitSema(sem, 4, &remaining) == SCE_KERNEL_ERROR_EINVAL && remaining == 1000); + Require(sceKernelSignalSema(sem, 1) == SCE_OK); Require(sceKernelWaitSema(sem, 1, nullptr) == SCE_OK); Require(sceKernelDeleteSema(sem) == SCE_OK); + Require(ksem_wait_nid_postfix(-1) == -1 && *__error_nid_postfix() == 22); + Require(ksem_trywait_nid_postfix(-1) == -1 && *__error_nid_postfix() == 22); RequireInvalidArguments(); } diff --git a/core/libs/tests/GuestShare.cpp b/core/libs/tests/GuestShare.cpp index 3bf336086b..476fa2a06b 100644 --- a/core/libs/tests/GuestShare.cpp +++ b/core/libs/tests/GuestShare.cpp @@ -75,10 +75,12 @@ int main() { Require(sceShareGetCurrentStatus(0, reinterpret_cast(status)) == invalidParam); Require(status[0] == 0x5a); Require(sceShareGetCurrentStatus(1, nullptr) == invalidParam); + Require(sceShareGetCurrentStatus(1, reinterpret_cast(0x10)) == invalidParam); std::uint32_t running[2]{0xffffffffu, 0x5a5a5a5au}; Require(sceShareGetRunningStatus(running) == 0); Require(running[0] == 0 && running[1] == 0x5a5a5a5au); Require(sceShareGetRunningStatus(nullptr) == invalidParam); + Require(sceShareGetRunningStatus(reinterpret_cast(0x10)) == invalidParam); Require(sceShareSetContentParamForApplicationTitle("title") == 0); Require(sceShareSetContentParamForApplicationTitle(nullptr) == invalidParam); Require(sceShareSetContentParamForApplicationTitle("") == invalidParam); diff --git a/core/libs/tests/GuestSignals.cpp b/core/libs/tests/GuestSignals.cpp index fe96c136d1..c16d63a1ff 100644 --- a/core/libs/tests/GuestSignals.cpp +++ b/core/libs/tests/GuestSignals.cpp @@ -318,7 +318,7 @@ int main() { Require(GuestAlarm_nid_no_patch(1) == 0); std::this_thread::sleep_for(std::chrono::milliseconds(1500)); Require(GuestAlarm_nid_no_patch(0) == 0); - for (int unmapped : {1, 13, 30, 31, 32, 128}) { + for (int unmapped : {1, 12, 30, 31, 32, 128}) { *__error_nid_postfix() = 0; Require(signal_nid_postfix(unmapped, ignore) == nullptr); Require(sigaction_nid_postfix(unmapped, nullptr, ¤t) == 0 && current.handler == 1 && current.flags == 0x2); diff --git a/core/libs/tests/GuestSocialScreenDialog.cpp b/core/libs/tests/GuestSocialScreenDialog.cpp index c86884d5f1..40138bb298 100644 --- a/core/libs/tests/GuestSocialScreenDialog.cpp +++ b/core/libs/tests/GuestSocialScreenDialog.cpp @@ -2,6 +2,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include #include +#include namespace { @@ -15,12 +16,14 @@ constexpr int kErrAlreadyInitialized = static_cast(0x80B80004); constexpr int kErrNotFinished = static_cast(0x80B80005); constexpr int kErrArgNull = static_cast(0x80B8000D); constexpr int kResultUserCanceled = 1; +constexpr std::int32_t kSentinel = static_cast(0x5a5a5a5a); } int main() { std::int64_t param = 0; - std::int32_t result[2] = {-1, -1}; + std::int32_t result[2]; + std::memset(result, 0x5a, sizeof(result)); Require(sceSocialScreenDialogTerminate() == kErrNotInitialized); Require(sceSocialScreenDialogOpen(nullptr) == kErrNotInitialized); @@ -37,7 +40,11 @@ int main() { Require(sceSocialScreenDialogUpdateStatus() == kStatusInitialized); Require(sceSocialScreenDialogOpen(nullptr) == kErrArgNull); + std::memset(result, 0x5a, sizeof(result)); + Require(sceSocialScreenDialogGetResult(nullptr) == kErrArgNull); Require(sceSocialScreenDialogGetResult(result) == kErrNotFinished); + Require(result[0] == kSentinel); + Require(result[1] == kSentinel); Require(sceSocialScreenDialogOpen(¶m) == 0); Require(sceSocialScreenDialogGetStatus() == kStatusFinished); @@ -46,7 +53,7 @@ int main() { Require(sceSocialScreenDialogGetResult(nullptr) == kErrArgNull); Require(sceSocialScreenDialogGetResult(result) == 0); Require(result[0] == kResultUserCanceled); - Require(result[1] == -1); + Require(result[1] == kSentinel); Require(sceSocialScreenDialogClose() == 0); Require(sceSocialScreenDialogTerminate() == 0); @@ -60,9 +67,10 @@ int main() { Require(sceSocialScreenDialogUpdateStatus() == kStatusInitialized); Require(sceSocialScreenDialogClose() == 0); Require(sceSocialScreenDialogUpdateStatus() == kStatusFinished); - result[0] = -1; + std::memset(result, 0x5a, sizeof(result)); Require(sceSocialScreenDialogGetResult(result) == 0); Require(result[0] == kResultUserCanceled); + Require(result[1] == kSentinel); Require(sceSocialScreenDialogTerminate() == 0); Require(sceSocialScreenDialogTerminate() == kErrNotInitialized); Require(sceSocialScreenDialogOpen(nullptr) == kErrNotInitialized); diff --git a/core/libs/tests/GuestSocketAddressLength.cpp b/core/libs/tests/GuestSocketAddressLength.cpp index 7c6f77cade..5a45abe4be 100644 --- a/core/libs/tests/GuestSocketAddressLength.cpp +++ b/core/libs/tests/GuestSocketAddressLength.cpp @@ -205,6 +205,72 @@ void LongMappedAddressesBindOnlyDualStackDatagramSockets() { Require(close_nid_postfix(datagram) == 0, "close failed"); } +std::array Inet(std::array host, std::uint16_t port) { + std::array address{16, 2, static_cast(port >> 8), static_cast(port)}; + std::memcpy(address.data() + 4, host.data(), host.size()); + return address; +} + +std::array Inet6(std::uint8_t scope, std::uint16_t port) { + std::array address{28, 28, static_cast(port >> 8), static_cast(port)}; + address[8] = 0xff; + address[9] = scope; + address[23] = 1; + return address; +} + +void StreamSocketsRefuseMulticastAddresses() { + constexpr int ErrorOperationNotSupported = 45; + constexpr int ErrorConnected = 56; + constexpr int GetFlags = 3; + constexpr int SetFlags = 4; + constexpr int NonBlockingFlag = 4; + const auto newStream = [](int family, bool nonblocking) { + const int descriptor = socket_nid_postfix(family, Stream, 0); + Require(descriptor >= 0, "socket failed"); + if (nonblocking) Require(fcntl_nid_postfix(descriptor, SetFlags, fcntl_nid_postfix(descriptor, GetFlags) | NonBlockingFlag) == 0, "fcntl(F_SETFL, O_NONBLOCK) failed"); + return descriptor; + }; + const auto group = Inet({239, 1, 2, 3}, 0); + const auto group6 = Inet6(0x0e, 0); + int descriptor = newStream(2, false); + Require(Fails(bind_nid_postfix(descriptor, group.data(), group.size()), ErrorAddressFamilyNotSupported), "tcp_usr_bind refuses an IPv4 multicast address, so bind to 239.1.2.3 must fail with EAFNOSUPPORT (47)"); + Require(close_nid_postfix(descriptor) == 0, "close failed"); + descriptor = newStream(28, false); + Require(Fails(bind_nid_postfix(descriptor, group6.data(), group6.size()), ErrorAddressFamilyNotSupported), "tcp6_usr_bind refuses an IPv6 multicast address, so bind to ff0e::1 must fail with EAFNOSUPPORT (47)"); + Require(close_nid_postfix(descriptor) == 0, "close failed"); + descriptor = socket_nid_postfix(2, Datagram, 0); + Require(descriptor >= 0, "socket failed"); + Require(!Fails(bind_nid_postfix(descriptor, group.data(), group.size()), ErrorAddressFamilyNotSupported), "the multicast check is in tcp_usr_bind only, so a datagram bind to 239.1.2.3 must not fail with EAFNOSUPPORT (47)"); + Require(close_nid_postfix(descriptor) == 0, "close failed"); + for (const bool nonblocking : {false, true}) { + const auto destination = Inet({224, 0, 0, 1}, 8080); + const auto destination6 = Inet6(0x02, 8080); + descriptor = newStream(2, nonblocking); + Require(Fails(connect_nid_postfix(descriptor, destination.data(), destination.size()), ErrorAddressFamilyNotSupported), "tcp_usr_connect refuses an IPv4 multicast address, so connect to 224.0.0.1:8080 must fail with EAFNOSUPPORT (47)"); + Require(close_nid_postfix(descriptor) == 0, "close failed"); + descriptor = newStream(28, nonblocking); + Require(Fails(connect_nid_postfix(descriptor, destination6.data(), destination6.size()), ErrorAddressFamilyNotSupported), "tcp6_usr_connect refuses an IPv6 multicast address, so connect to ff02::1 port 8080 must fail with EAFNOSUPPORT (47)"); + Require(close_nid_postfix(descriptor) == 0, "close failed"); + } + const auto portZero = Inet({224, 0, 0, 1}, 0); + descriptor = newStream(2, false); + Require(Fails(connect_nid_postfix(descriptor, portZero.data(), portZero.size()), ErrorAddressFamilyNotSupported), "tcp_usr_connect checks multicast before tcp_connect looks at the port, so connect to 224.0.0.1:0 must fail with EAFNOSUPPORT (47)"); + const auto loopback = Loopback(); + Require(bind_nid_postfix(descriptor, loopback.data(), loopback.size()) == 0, "bind to 127.0.0.1 failed"); + Require(Fails(bind_nid_postfix(descriptor, group.data(), group.size()), ErrorAddressFamilyNotSupported), "tcp_usr_bind checks multicast before in_pcbbind, so a bound socket binding 239.1.2.3 must fail with EAFNOSUPPORT (47), not EINVAL (22)"); + Require(listen_nid_postfix(descriptor, 1) == 0, "listen failed"); + const auto destination = Inet({224, 0, 0, 1}, 8080); + Require(Fails(connect_nid_postfix(descriptor, destination.data(), destination.size()), ErrorOperationNotSupported), "soconnect refuses a listening socket before tcp_usr_connect, so connect to 224.0.0.1 must fail with EOPNOTSUPP (45)"); + auto address = loopback; + std::uint32_t size = address.size(); + Require(getsockname_nid_postfix(descriptor, address.data(), &size) == 0, "getsockname failed"); + const int client = newStream(2, false); + Require(connect_nid_postfix(client, address.data(), address.size()) == 0, "connect to the loopback listener failed"); + Require(Fails(connect_nid_postfix(client, destination.data(), destination.size()), ErrorConnected), "soconnect refuses a connected stream socket before tcp_usr_connect, so connect to 224.0.0.1 must fail with EISCONN (56)"); + Require(close_nid_postfix(client) == 0 && close_nid_postfix(descriptor) == 0, "close failed"); +} + } int main() { @@ -219,5 +285,6 @@ int main() { LongAddressesAreInvalid(28, Stream); LongAddressesAreInvalid(28, Datagram); LongMappedAddressesBindOnlyDualStackDatagramSockets(); + StreamSocketsRefuseMulticastAddresses(); return 0; } diff --git a/core/libs/tests/GuestSocketOnNonSocket.cpp b/core/libs/tests/GuestSocketOnNonSocket.cpp index 49c9d54ea0..481e201061 100644 --- a/core/libs/tests/GuestSocketOnNonSocket.cpp +++ b/core/libs/tests/GuestSocketOnNonSocket.cpp @@ -9,6 +9,7 @@ namespace { constexpr int ErrorBadDescriptor = 9; +constexpr int ErrorInvalid = 22; constexpr int ErrorNotSocket = 38; void Require(bool condition, const char* what) { @@ -50,10 +51,51 @@ void ClosedAndInvalidDescriptorsAreBad() { Require(Fails(connect_nid_postfix(1 << 20, address.data(), address.size()), ErrorBadDescriptor), "connect on a descriptor that was never open must fail with EBADF (9)"); } +void AddressAndTransferCallsOnAPipeAreNotSocketCalls() { + int ends[2]; + Require(pipe_nid_postfix(ends) == 0, "pipe failed"); + std::array address{16, 2}; + std::uint32_t size = address.size(); + char buffer[4]; + Require(Fails(getpeername_nid_postfix(ends[0], address.data(), &size), ErrorNotSocket), "getpeername on a pipe must fail with ENOTSOCK (38)"); + Require(Fails(sendto_nid_postfix(ends[1], "x", 1, 0, nullptr, 0), ErrorNotSocket), "sendto on a pipe must fail with ENOTSOCK (38)"); + Require(Fails(recvfrom_nid_postfix(ends[0], buffer, sizeof(buffer), 0, nullptr, nullptr), ErrorNotSocket), "recvfrom on a pipe must fail with ENOTSOCK (38)"); + Require(close_nid_postfix(ends[0]) == 0 && close_nid_postfix(ends[1]) == 0, "close failed"); +} + +void SocketCallsOnMinusOneAreBad() { + std::array address{16, 2}; + std::uint32_t size = address.size(); + char buffer[4]; + int option = 0; + std::uint32_t optionSize = sizeof(option); + Require(Fails(accept_nid_postfix(-1, nullptr, nullptr), ErrorBadDescriptor), "accept on -1 must fail with EBADF (9)"); + Require(Fails(shutdown_nid_postfix(-1, 1), ErrorBadDescriptor), "shutdown on -1 must fail with EBADF (9)"); + Require(Fails(shutdown_nid_postfix(-1, 3), ErrorBadDescriptor), "shutdown on -1 must report EBADF (9) before an invalid how"); + Require(Fails(getsockname_nid_postfix(-1, address.data(), &size), ErrorBadDescriptor), "getsockname on -1 must fail with EBADF (9)"); + Require(Fails(getpeername_nid_postfix(-1, address.data(), &size), ErrorBadDescriptor), "getpeername on -1 must fail with EBADF (9)"); + Require(Fails(getsockopt_nid_postfix(-1, 0xffff, 4, &option, &optionSize), ErrorBadDescriptor), "getsockopt on -1 must fail with EBADF (9)"); + Require(Fails(send_nid_postfix(-1, "x", 1, 0), ErrorBadDescriptor), "send on -1 must fail with EBADF (9)"); + Require(Fails(recv_nid_postfix(-1, buffer, sizeof(buffer), 0), ErrorBadDescriptor), "recv on -1 must fail with EBADF (9)"); + Require(Fails(sendto_nid_postfix(-1, "x", 1, 0, nullptr, 0), ErrorBadDescriptor), "sendto on -1 must fail with EBADF (9)"); + Require(Fails(recvfrom_nid_postfix(-1, buffer, sizeof(buffer), 0, nullptr, nullptr), ErrorBadDescriptor), "recvfrom on -1 must fail with EBADF (9)"); +} + +void ShutdownRejectsAnInvalidHow() { + const int datagram = socket_nid_postfix(2, 2, 0); + Require(datagram >= 0, "socket failed"); + for (const int how : {-1, 3}) + Require(Fails(shutdown_nid_postfix(datagram, how), ErrorInvalid), "shutdown with an invalid how must fail with EINVAL (22)"); + Require(close_nid_postfix(datagram) == 0, "close failed"); +} + } int main() { CallsOnAnOpenPipeAreNotSocketCalls(); ClosedAndInvalidDescriptorsAreBad(); + AddressAndTransferCallsOnAPipeAreNotSocketCalls(); + SocketCallsOnMinusOneAreBad(); + ShutdownRejectsAnInvalidHow(); return 0; } diff --git a/core/libs/tests/GuestSocketOptions.cpp b/core/libs/tests/GuestSocketOptions.cpp index e33092f87d..c0b35651fa 100644 --- a/core/libs/tests/GuestSocketOptions.cpp +++ b/core/libs/tests/GuestSocketOptions.cpp @@ -7,6 +7,12 @@ #include #include +#ifdef _WIN32 +#include +#else +#include +#endif + namespace { constexpr int Stream = 1; @@ -306,6 +312,8 @@ void IpLevelFollowsThePs5() { const unsigned char byte = 9; Require(SetFails(datagram, 0, IpTimeToLive, &byte, 1, ErrorInvalid), "a one-byte IP_TTL must fail with EINVAL (22)"); Require(SetIp(datagram, IpTypeOfService, 0x1ff) && GetIp(datagram, IpTypeOfService) == 0xff, "IP_TOS keeps the low byte"); + Require(SetIp(datagram, IpTypeOfService, -1) && GetIp(datagram, IpTypeOfService) == 255, "IP_TOS of -1 reads back as 255"); + Require(SetFails(datagram, 0, IpTypeOfService, &byte, 1, ErrorInvalid), "a one-byte IP_TOS must fail with EINVAL (22)"); const unsigned char seven = 7; Require(setsockopt_nid_postfix(datagram, 0, IpMulticastTimeToLive, &seven, 1) == 0 && GetIp(datagram, IpMulticastTimeToLive) == 7, "a one-byte IP_MULTICAST_TTL is accepted"); const int tooLarge = 300; @@ -404,6 +412,95 @@ void UnknownRequestsFail() { } +constexpr int ErrorFault = 14; +constexpr int SoReceiveTimeout = 0x1006; +constexpr int SoNoSigpipe2 = 0x800; + +void* MapPage(bool readable) { +#ifdef _WIN32 + void* page = VirtualAlloc(nullptr, 4096, MEM_COMMIT | MEM_RESERVE, readable ? PAGE_READONLY : PAGE_NOACCESS); + Require(page != nullptr, "VirtualAlloc failed"); +#else + void* page = mmap(nullptr, 4096, readable ? PROT_READ : PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(page != MAP_FAILED, "mmap failed"); +#endif + return page; +} + +void UnmapPage(void* page) { +#ifdef _WIN32 + VirtualFree(page, 0, MEM_RELEASE); +#else + munmap(page, 4096); +#endif +} + +bool GetFails(int descriptor, int level, int option, void* value, std::uint32_t* size, int error) { + return getsockopt_nid_postfix(descriptor, level, option, value, size) == -1 && *__error_nid_postfix() == error; +} + +void BadPointersFail() { + void* unreadable = MapPage(false); + void* readOnly = MapPage(true); + const int stream = socket_nid_postfix(2, Stream, 0); + const int datagram = socket_nid_postfix(2, Datagram, 0); + Require(stream >= 0 && datagram >= 0, "socket failed"); + for (void* bad : {reinterpret_cast(0x10), unreadable}) { + Require(SetFails(stream, SolSocket, SoKeepAlive, bad, sizeof(int), ErrorFault), "setsockopt with an unreadable value must fail with EFAULT (14)"); + Require(GetInt(stream, SoKeepAlive) == 0, "a faulting setsockopt must not change the option"); + Require(SetFails(stream, SolSocket, SoLinger, bad, sizeof(Linger), ErrorFault), "SO_LINGER with an unreadable value must fail with EFAULT"); + Require(SetFails(stream, SolSocket, SoReceiveTimeout, bad, 16, ErrorFault), "SO_RCVTIMEO with an unreadable value must fail with EFAULT"); + Require(SetFails(stream, SolSocket, SoNoSigpipe2, bad, sizeof(int), ErrorFault), "SO_NOSIGPIPE with an unreadable value must fail with EFAULT"); + Require(SetFails(stream, SolSocket, SoSendBuffer, bad, sizeof(int), ErrorFault), "SO_SNDBUF with an unreadable value must fail with EFAULT"); + Require(SetFails(stream, 6, 1, bad, sizeof(int), ErrorFault), "TCP_NODELAY with an unreadable value must fail with EFAULT"); + Require(SetFails(stream, 0, 4, bad, sizeof(int), ErrorFault), "IP_TTL with an unreadable value must fail with EFAULT"); + Require(SetFails(datagram, 0, 12, bad, 8, ErrorFault), "IP_ADD_MEMBERSHIP with an unreadable value must fail with EFAULT"); + Require(SetFails(datagram, 0, 9, bad, 4, ErrorFault), "IP_MULTICAST_IF with an unreadable value must fail with EFAULT"); + Require(SetFails(datagram, 0, 9, bad, 12, ErrorFault), "IP_MULTICAST_IF with an unreadable ip_mreqn must fail with EFAULT"); + Require(SetFails(datagram, 0, 10, bad, 1, ErrorFault), "IP_MULTICAST_TTL with an unreadable byte must fail with EFAULT"); + Require(SetFails(datagram, 0, 10, bad, 4, ErrorFault), "IP_MULTICAST_TTL with an unreadable int must fail with EFAULT"); + Require(SetFails(stream, SolSocket, SoKeepAlive, bad, 2, ErrorInvalid), "a short length is rejected before the value is read"); + Require(SetFails(stream, SolSocket, 0x7777, bad, sizeof(int), ErrorNoProtocolOption), "an unknown option is rejected before the value is read"); + Require(SetFails(stream, 99, SoKeepAlive, bad, sizeof(int), ErrorInvalid), "an unknown level is rejected before the value is read"); + Require(SetFails(-1, SolSocket, SoKeepAlive, bad, sizeof(int), ErrorBadDescriptor), "the descriptor is checked before the value is read"); + } + const int on = 1; + Require(SetFails(stream, SolSocket, SoKeepAlive, &on, 0x80000000u, ErrorInvalid), "a negative length must fail with EINVAL (22)"); + + for (void* bad : {reinterpret_cast(0x10), unreadable, readOnly}) { + std::uint32_t size = sizeof(int); + Require(GetFails(stream, SolSocket, SoType, bad, &size, ErrorFault) && size == sizeof(int), "getsockopt into an unwritable value must fail with EFAULT and keep the length"); + Require(GetFails(stream, SolSocket, SoLinger, bad, &size, ErrorFault), "SO_LINGER into an unwritable value must fail with EFAULT"); + size = 16; + Require(GetFails(stream, SolSocket, SoReceiveTimeout, bad, &size, ErrorFault), "SO_RCVTIMEO into an unwritable value must fail with EFAULT"); + size = sizeof(int); + Require(GetFails(stream, 6, 1, bad, &size, ErrorFault), "TCP_NODELAY into an unwritable value must fail with EFAULT"); + Require(GetFails(stream, 0, 4, bad, &size, ErrorFault), "IP_TTL into an unwritable value must fail with EFAULT"); + Require(GetFails(datagram, 0, 9, bad, &size, ErrorFault), "IP_MULTICAST_IF into an unwritable value must fail with EFAULT"); + Require(GetFails(stream, SolSocket, 0x7777, bad, &size, ErrorNoProtocolOption), "an unknown option is rejected before the value is written"); + Require(GetFails(-1, SolSocket, SoType, bad, &size, ErrorBadDescriptor), "the descriptor is checked before the value is written"); + std::uint32_t zero = 0; + Require(getsockopt_nid_postfix(stream, SolSocket, SoType, bad, &zero) == 0 && zero == 0, "a zero length never touches the value"); + } + + for (std::uint32_t* bad : {reinterpret_cast(0x10), static_cast(unreadable), static_cast(nullptr)}) { + int value = 0; + Require(GetFails(stream, SolSocket, SoType, &value, bad, ErrorFault), "getsockopt with an unreadable length must fail with EFAULT"); + Require(GetFails(-1, SolSocket, SoType, &value, bad, ErrorFault), "the length is read before the descriptor is checked"); + Require(GetFails(stream, SolSocket, 0x7777, &value, bad, ErrorFault), "the length is read before the option is checked"); + } + for (std::uint32_t* bad : {reinterpret_cast(0x10), static_cast(unreadable), static_cast(readOnly), static_cast(nullptr)}) { + Require(GetFails(stream, SolSocket, SoType, nullptr, bad, ErrorFault), "the final length store failing must give EFAULT"); + Require(GetFails(stream, SolSocket, 0x7777, nullptr, bad, ErrorNoProtocolOption), "an option error is reported before the length store"); + } + int value = 0; + std::uint32_t size = sizeof(int); + Require(getsockopt_nid_postfix(stream, SolSocket, SoType, &value, &size) == 0 && value == Stream && size == sizeof(int), "valid getsockopt still works"); + Require(close_nid_postfix(stream) == 0 && close_nid_postfix(datagram) == 0, "close failed"); + UnmapPage(unreadable); + UnmapPage(readOnly); +} + int main(int argc, char** argv) { if (argc > 1 && std::strcmp(argv[1], "multicast") == 0) { MulticastOnTheHost(); @@ -417,6 +514,7 @@ int main(int argc, char** argv) { LengthsFollowFreeBsd(); BuffersFollowThePs5(); NullValues(); + BadPointersFail(); IpLevelFollowsThePs5(); MulticastMembership(); UnknownRequestsFail(); diff --git a/core/libs/tests/GuestSocketPair.cpp b/core/libs/tests/GuestSocketPair.cpp index b760914fab..7fdfc4da27 100644 --- a/core/libs/tests/GuestSocketPair.cpp +++ b/core/libs/tests/GuestSocketPair.cpp @@ -159,6 +159,11 @@ static void CheckReceiveMessageFaults() { message = {readOnly + 128, 0, readOnlyVector, 1, nullptr, 0, -1}; Require(Queue(pair[0], pair[1], "gh") && recvmsg_nid_postfix(pair[1], &message, 0) == 2); Require(std::memcmp(data, "gh", 2) == 0 && message.nameLength == 0); + message = {nullptr, 0, &good, 1, nullptr, 0, -1}; + for (const int flag : {1, 4, 0x100, 0x40000, -1, Peek | 1, 0x80 | 4}) { + *__error_nid_postfix() = 0; + Require(recvmsg_nid_postfix(pair[1], &message, flag) == -1 && *__error_nid_postfix() == 45); + } Require(close_nid_postfix(pair[0]) == 0 && close_nid_postfix(pair[1]) == 0); Require(sceKernelMprotect(memory, page * 3, 3) == 0 && sceKernelMunmap(reinterpret_cast(memory), page * 3) == 0); diff --git a/core/libs/tests/GuestSocketPortZero.cpp b/core/libs/tests/GuestSocketPortZero.cpp index f450f75277..513cf5c7af 100644 --- a/core/libs/tests/GuestSocketPortZero.cpp +++ b/core/libs/tests/GuestSocketPortZero.cpp @@ -88,10 +88,57 @@ static void CheckStream(int family) { Require(close_nid_postfix(client) == 0 && close_nid_postfix(listener) == 0, "close"); } +static void CheckUnspecified(int type) { + const int server = socket_nid_postfix(28, type, 0); + if (server < 0 && *__error_nid_postfix() == 47) return; + Require(server >= 0, "server"); + auto address = Loopback(28, 28); + const int bound = bind_nid_postfix(server, address.data(), 28); + if (bound < 0 && *__error_nid_postfix() == 49) { + Require(close_nid_postfix(server) == 0, "close"); + return; + } + Require(bound == 0, "bind server"); + if (type == 1) Require(listen_nid_postfix(server, 1) == 0, "listen"); + std::uint32_t length = address.size(); + Require(getsockname_nid_postfix(server, address.data(), &length) == 0, "server name"); + auto unspecified = address; + std::memset(unspecified.data() + 8, 0, 16); + const int client = socket_nid_postfix(28, type, 0); + Require(client >= 0, "client"); + Address any{}; + any[0] = 28; + any[1] = 28; + Require(connect_nid_postfix(client, any.data(), 28) == -1 && *__error_nid_postfix() == 49, + "connect to port 0 of the unspecified AF_INET6 address must fail with EADDRNOTAVAIL (49)"); + Require(connect_nid_postfix(client, unspecified.data(), 28) == 0, + "connect to the unspecified AF_INET6 address must reach ::1 on the same port"); + Address peer{}; + length = peer.size(); + Require(getpeername_nid_postfix(client, peer.data(), &length) == 0 && length == 28 && peer[1] == 28 + && std::memcmp(peer.data() + 2, address.data() + 2, 2) == 0 && std::memcmp(peer.data() + 8, address.data() + 8, 16) == 0, + "the peer of a connect to the unspecified AF_INET6 address must be ::1 on the same port"); + if (type == 1) { + const int accepted = accept_nid_postfix(server, nullptr, nullptr); + Require(accepted >= 0, "accept"); + Require(send_nid_postfix(client, "x", 1, 0) == 1, "send"); + char byte = 0; + Require(recv_nid_postfix(accepted, &byte, 1, 0) == 1 && byte == 'x', "receive on the accepted socket"); + Require(close_nid_postfix(accepted) == 0, "close"); + } else { + Require(send_nid_postfix(client, "y", 1, 0) == 1, "send"); + char byte = 0; + Require(recv_nid_postfix(server, &byte, 1, 0) == 1 && byte == 'y', "receive on ::1"); + } + Require(close_nid_postfix(client) == 0 && close_nid_postfix(server) == 0, "close"); +} + int main() { CheckDatagram(2); CheckDatagram(28); CheckStream(2); CheckStream(28); + CheckUnspecified(1); + CheckUnspecified(2); return 0; } diff --git a/core/libs/tests/GuestSpanProtected.cpp b/core/libs/tests/GuestSpanProtected.cpp new file mode 100644 index 0000000000..00b677dc72 --- /dev/null +++ b/core/libs/tests/GuestSpanProtected.cpp @@ -0,0 +1,92 @@ +#include "prx/libc/include/Api.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static void Require(bool condition) { + if (!condition) { + std::fputs("GuestSpanProtected check failed\n", stderr); + std::abort(); + } +} + +#ifdef _WIN32 +#include +#else +#include +#include +#endif + +class Buffer { + unsigned char* allocation; + std::size_t page; +public: + Buffer() { +#ifdef _WIN32 + SYSTEM_INFO info{}; + GetSystemInfo(&info); + page = info.dwPageSize; + allocation = static_cast(VirtualAlloc(nullptr, page * 3, MEM_RESERVE | MEM_COMMIT, PAGE_NOACCESS)); + Require(allocation != nullptr); + DWORD previous; + Require(VirtualProtect(allocation + page, page, PAGE_READWRITE, &previous) != 0); +#else + const long size = sysconf(_SC_PAGESIZE); + Require(size > 0); + page = static_cast(size); + allocation = static_cast(mmap(nullptr, page * 3, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)); + Require(allocation != MAP_FAILED); + Require(mprotect(allocation + page, page, PROT_READ | PROT_WRITE) == 0); +#endif + std::memset(allocation + page, 0xa5, page); + } + ~Buffer() { +#ifdef _WIN32 + Require(VirtualFree(allocation, 0, MEM_RELEASE) != 0); +#else + Require(munmap(allocation, page * 3) == 0); +#endif + } + Buffer(const Buffer&) = delete; + Buffer& operator=(const Buffer&) = delete; + unsigned char* At(std::size_t count) { + Require(count <= page - 32); + return allocation + page * 2 - (std::max)(count, std::size_t{1}); + } +}; + +int main() { + Buffer textBuffer, setBuffer; + for (unsigned selected = 0; selected < 256; ++selected) { + std::string members; + std::array present{}; + for (unsigned value = 1; value < 256; ++value) { + if ((value + selected) % 7 == 0 || value == selected) { + members.push_back(static_cast(value)); + present[value] = true; + } + } + if (selected == 0) { members.clear(); present.fill(false); } + auto* set = reinterpret_cast(setBuffer.At(members.size() + 1)); + std::memcpy(set, members.c_str(), members.size() + 1); + for (std::size_t length : std::array{0, 1, 7, 15, 16, 31, 32, 63, 127}) { + std::string input; + for (std::size_t i = 0; i < length; ++i) input.push_back(static_cast(1 + (i * 37 + selected) % 255)); + auto* text = reinterpret_cast(textBuffer.At(length + 1)); + std::memcpy(text, input.c_str(), length + 1); + std::size_t accepted = 0, rejected = 0; + while (accepted < length && present[static_cast(input[accepted])]) ++accepted; + while (rejected < length && !present[static_cast(input[rejected])]) ++rejected; + Require(strspn_nid_postfix(text, set) == accepted); + Require(strcspn_nid_postfix(text, set) == rejected); + Require(std::memcmp(text, input.c_str(), length + 1) == 0); + Require(std::memcmp(set, members.c_str(), members.size() + 1) == 0); + } + } +} diff --git a/core/libs/tests/GuestStrcmpBoundary.cpp b/core/libs/tests/GuestStrcmpBoundary.cpp new file mode 100644 index 0000000000..876031921a --- /dev/null +++ b/core/libs/tests/GuestStrcmpBoundary.cpp @@ -0,0 +1,99 @@ +#include "prx/libc/include/Api.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static void Require(bool condition) { + if (!condition) { + std::fputs("GuestStrcmpBoundary check failed\n", stderr); + std::abort(); + } +} + +#ifdef _WIN32 +#include +#else +#include +#include +#endif + +class Buffer { + unsigned char* allocation; + std::size_t page; +public: + Buffer() { +#ifdef _WIN32 + SYSTEM_INFO info{}; + GetSystemInfo(&info); + page = info.dwPageSize; + allocation = static_cast(VirtualAlloc(nullptr, page * 3, MEM_RESERVE | MEM_COMMIT, PAGE_NOACCESS)); + Require(allocation != nullptr); + DWORD previous; + Require(VirtualProtect(allocation + page, page, PAGE_READWRITE, &previous) != 0); +#else + const long size = sysconf(_SC_PAGESIZE); + Require(size > 0); + page = static_cast(size); + allocation = static_cast(mmap(nullptr, page * 3, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)); + Require(allocation != MAP_FAILED); + Require(mprotect(allocation + page, page, PROT_READ | PROT_WRITE) == 0); +#endif + std::memset(allocation + page, 0xa5, page); + } + ~Buffer() { +#ifdef _WIN32 + Require(VirtualFree(allocation, 0, MEM_RELEASE) != 0); +#else + Require(munmap(allocation, page * 3) == 0); +#endif + } + Buffer(const Buffer&) = delete; + Buffer& operator=(const Buffer&) = delete; + template + T* At(std::size_t count) { + Require(count <= (page - 32) / sizeof(T)); + return reinterpret_cast(allocation + page * 2 - (std::max)(count, std::size_t{1}) * sizeof(T)); + } +}; + +template +static void Prefix(const T* data) { + for (int index = 1; index <= 8; ++index) Require(data[-index] == static_cast(sizeof(T) == 1 ? 0xa5 : 0xa5a5)); +} + +static int Sign(int value) { + return (value > 0) - (value < 0); +} + +int main() { + Buffer first, second; + for (std::size_t count = 0; count <= 128; ++count) { + auto* left = first.At(count + 1); + auto* right = second.At(count + 1); + for (std::size_t index = 0; index < count; ++index) left[index] = static_cast(1 + (index * 73 + count) % 255); + left[count] = 0; + const std::vector saved(left, left + count + 1); + for (std::size_t position = 0; position <= count; ++position) { + for (unsigned char value : {0, 1, 127, 128, 255}) { + if (position == count && value != 0) continue; + std::copy(saved.begin(), saved.end(), right); + right[position] = value; + int expected = 0; + for (std::size_t index = 0; index <= count; ++index) { + if (left[index] != right[index]) { expected = static_cast(left[index]) - right[index]; break; } + if (left[index] == 0) break; + } + const std::vector other(right, right + count + 1); + Require(Sign(strcmp_nid_postfix(reinterpret_cast(left), reinterpret_cast(right))) == Sign(expected)); + Require(std::equal(saved.begin(), saved.end(), left)); + Require(std::equal(other.begin(), other.end(), right)); + } + } + } +} diff --git a/core/libs/tests/GuestStrcollBoundary.cpp b/core/libs/tests/GuestStrcollBoundary.cpp new file mode 100644 index 0000000000..b51eddef21 --- /dev/null +++ b/core/libs/tests/GuestStrcollBoundary.cpp @@ -0,0 +1,91 @@ +#include "prx/libc/include/Api.hpp" +#include +#include +#include +#include +#include +#include + +static void Require(bool condition) { + if (!condition) { + std::fputs("GuestStrcollBoundary check failed\n", stderr); + std::abort(); + } +} + +#ifdef _WIN32 +#include +#else +#include +#include +#endif + +class Buffer { + unsigned char* allocation; + std::size_t page; +public: + Buffer() { +#ifdef _WIN32 + SYSTEM_INFO info{}; + GetSystemInfo(&info); + page = info.dwPageSize; + allocation = static_cast(VirtualAlloc(nullptr, page * 3, MEM_RESERVE | MEM_COMMIT, PAGE_NOACCESS)); + Require(allocation != nullptr); + DWORD previous; + Require(VirtualProtect(allocation + page, page, PAGE_READWRITE, &previous) != 0); +#else + const long size = sysconf(_SC_PAGESIZE); + Require(size > 0); + page = static_cast(size); + allocation = static_cast(mmap(nullptr, page * 3, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)); + Require(allocation != MAP_FAILED); + Require(mprotect(allocation + page, page, PROT_READ | PROT_WRITE) == 0); +#endif + std::memset(allocation + page, 0xa5, page); + } + ~Buffer() { +#ifdef _WIN32 + Require(VirtualFree(allocation, 0, MEM_RELEASE) != 0); +#else + Require(munmap(allocation, page * 3) == 0); +#endif + } + Buffer(const Buffer&) = delete; + Buffer& operator=(const Buffer&) = delete; + template + T* At(std::size_t count) { + Require(count <= (page - 32) / sizeof(T)); + return reinterpret_cast(allocation + page * 2 - (std::max)(count, std::size_t{1}) * sizeof(T)); + } +}; + +static int Sign(int value) { + return (value > 0) - (value < 0); +} + +int main() { + Buffer first, second; + for (std::size_t count = 0; count <= 128; ++count) { + auto* left = first.At(count + 1); + auto* right = second.At(count + 1); + for (std::size_t index = 0; index < count; ++index) left[index] = static_cast(1 + (index * 73 + count) % 255); + left[count] = 0; + const std::vector saved(left, left + count + 1); + for (std::size_t position = 0; position <= count; ++position) { + for (unsigned char value : {0, 1, 127, 128, 255}) { + if (position == count && value != 0) continue; + std::copy(saved.begin(), saved.end(), right); + right[position] = value; + int expected = 0; + for (std::size_t index = 0; index <= count; ++index) { + if (left[index] != right[index]) { expected = static_cast(left[index]) - right[index]; break; } + if (left[index] == 0) break; + } + const std::vector other(right, right + count + 1); + Require(Sign(strcoll_nid_postfix(reinterpret_cast(left), reinterpret_cast(right))) == Sign(expected)); + Require(std::equal(saved.begin(), saved.end(), left)); + Require(std::equal(other.begin(), other.end(), right)); + } + } + } +} diff --git a/core/libs/tests/GuestStrcpyExtent.cpp b/core/libs/tests/GuestStrcpyExtent.cpp new file mode 100644 index 0000000000..c7527f9c58 --- /dev/null +++ b/core/libs/tests/GuestStrcpyExtent.cpp @@ -0,0 +1,84 @@ +#include "prx/libc/include/Api.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static void Require(bool condition) { + if (!condition) { + std::fputs("GuestStrcpyExtent check failed\n", stderr); + std::abort(); + } +} + +#ifdef _WIN32 +#include +#else +#include +#include +#endif + +class Buffer { + unsigned char* allocation; + std::size_t page; +public: + Buffer() { +#ifdef _WIN32 + SYSTEM_INFO info{}; + GetSystemInfo(&info); + page = info.dwPageSize; + allocation = static_cast(VirtualAlloc(nullptr, page * 3, MEM_RESERVE | MEM_COMMIT, PAGE_NOACCESS)); + Require(allocation != nullptr); + DWORD previous; + Require(VirtualProtect(allocation + page, page, PAGE_READWRITE, &previous) != 0); +#else + const long size = sysconf(_SC_PAGESIZE); + Require(size > 0); + page = static_cast(size); + allocation = static_cast(mmap(nullptr, page * 3, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)); + Require(allocation != MAP_FAILED); + Require(mprotect(allocation + page, page, PROT_READ | PROT_WRITE) == 0); +#endif + std::memset(allocation + page, 0xa5, page); + } + ~Buffer() { +#ifdef _WIN32 + Require(VirtualFree(allocation, 0, MEM_RELEASE) != 0); +#else + Require(munmap(allocation, page * 3) == 0); +#endif + } + Buffer(const Buffer&) = delete; + Buffer& operator=(const Buffer&) = delete; + template + T* At(std::size_t count) { + Require(count <= (page - 32) / sizeof(T)); + return reinterpret_cast(allocation + page * 2 - (std::max)(count, std::size_t{1}) * sizeof(T)); + } +}; + +template +static void Prefix(const T* data) { + for (int index = 1; index <= 8; ++index) Require(data[-index] == static_cast(sizeof(T) == 1 ? 0xa5 : 0xa5a5)); +} + +int main() { + Buffer input, output; + for (std::size_t count = 0; count <= 192; ++count) { + auto* source = input.At(count + 1); + auto* destination = output.At(count + 1); + for (std::size_t index = 0; index < count; ++index) source[index] = static_cast(1 + (index * 97 + count) % 255); + source[count] = 0; + const std::string saved(source, count + 1); + std::memset(destination - 16, 0xa5, count + 17); + Require(strcpy_nid_postfix(destination, source) == destination); + Require(std::equal(saved.begin(), saved.end(), destination)); + Require(std::equal(saved.begin(), saved.end(), source)); + Prefix(reinterpret_cast(destination)); + } +} diff --git a/core/libs/tests/GuestStrings.cpp b/core/libs/tests/GuestStrings.cpp index 10a7aaac13..f1b18a34e6 100644 --- a/core/libs/tests/GuestStrings.cpp +++ b/core/libs/tests/GuestStrings.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include static void Require(bool condition) { @@ -74,7 +75,7 @@ static void CheckStandardStrings() { const unsigned char same[6] = {1, 2, 3, 4, 5, 6}; Require(bcmp_nid_postfix(bytes, same, 6) == 0 && bcmp_nid_postfix(bytes, "zz", 0) == 0); bytes[5] = 9; - Require(bcmp_nid_postfix(bytes, same, 6) != 0 && bcmp_nid_postfix(bytes, same, 5) == 0); + Require(bcmp_nid_postfix(bytes, same, 6) == 1 && bcmp_nid_postfix(same, bytes, 6) == 1 && bcmp_nid_postfix(bytes, same, 5) == 0); bzero_nid_postfix(bytes + 1, 4); Require(bytes[0] == 1 && bytes[1] == 0 && bytes[4] == 0 && bytes[5] == 9); } @@ -440,6 +441,31 @@ int main() { end = stpcpy_nid_postfix(end, "cd"); Require(end == chained + 4 && std::strcmp(chained, "abcd") == 0); Require(stpcpy_nid_postfix(end, "") == end && chained[5] == 'z'); + char shortBuffer[8]; + std::memset(shortBuffer, 'z', sizeof(shortBuffer)); + char* shortEnd = stpncpy_nid_postfix(shortBuffer, "ab", 5); + Require(shortEnd == shortBuffer + 2); + Require(shortBuffer[0] == 'a' && shortBuffer[1] == 'b'); + Require(shortBuffer[2] == '\0' && shortBuffer[3] == '\0' && shortBuffer[4] == '\0'); + Require(shortBuffer[5] == 'z' && shortBuffer[6] == 'z' && shortBuffer[7] == 'z'); + char equalBuffer[8]; + std::memset(equalBuffer, 'z', sizeof(equalBuffer)); + char* equalEnd = stpncpy_nid_postfix(equalBuffer, "abcde", 5); + Require(equalEnd == equalBuffer + 5); + Require(std::memcmp(equalBuffer, "abcde", 5) == 0); + Require(equalBuffer[5] == 'z' && equalBuffer[6] == 'z' && equalBuffer[7] == 'z'); + char longerBuffer[8]; + std::memset(longerBuffer, 'z', sizeof(longerBuffer)); + char* longerEnd = stpncpy_nid_postfix(longerBuffer, "abcdefgh", 3); + Require(longerEnd == longerBuffer + 3); + Require(std::memcmp(longerBuffer, "abc", 3) == 0); + Require(longerBuffer[3] == 'z' && longerBuffer[4] == 'z' && longerBuffer[5] == 'z' && longerBuffer[6] == 'z' && longerBuffer[7] == 'z'); + char zeroBuffer[8]; + std::memset(zeroBuffer, 'z', sizeof(zeroBuffer)); + const char originalZero[8] = {'z', 'z', 'z', 'z', 'z', 'z', 'z', 'z'}; + char* zeroEnd = stpncpy_nid_postfix(zeroBuffer, "test", 0); + Require(zeroEnd == zeroBuffer); + Require(std::memcmp(zeroBuffer, originalZero, sizeof(zeroBuffer)) == 0); Require(strncat_nid_postfix(buffer, "xyz", 2) == buffer); Require(std::strcmp(buffer, "xy") == 0); char transformed[8] = "zzzzzzz"; @@ -475,6 +501,21 @@ int main() { Require(inetAton("1.2.3.4 trailing", "1.2.3.4")); Require(inetAton("1.2.3.4\n", "1.2.3.4")); Require(__inet_aton_nid_postfix("1.2.3.4", nullptr) == 1); + bool atonNullThrew = false; + try { + unsigned char address[4] = {0xA5, 0xA5, 0xA5, 0xA5}; + __inet_aton_nid_postfix(nullptr, address); + } catch (const std::invalid_argument&) { + atonNullThrew = true; + } + Require(atonNullThrew); + bool addrNullThrew = false; + try { + __inet_addr_nid_postfix(nullptr); + } catch (const std::invalid_argument&) { + addrNullThrew = true; + } + Require(addrNullThrew); for (const char* invalid : {"", " 1.2.3.4", "1.2.3.4.5", "256.1.1.1", "1.2.3.256", "1.2.65536", "08", "1..2", "a.b.c.d", "1.2.3.4x", "0x", "1.2.3.", "-1"}) { unsigned char address[4] = {0xA5, 0xA5, 0xA5, 0xA5}; diff --git a/core/libs/tests/GuestStrncmpBoundary.cpp b/core/libs/tests/GuestStrncmpBoundary.cpp new file mode 100644 index 0000000000..5960fd7099 --- /dev/null +++ b/core/libs/tests/GuestStrncmpBoundary.cpp @@ -0,0 +1,99 @@ +#include "prx/libc/include/Api.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static void Require(bool condition) { + if (!condition) { + std::fputs("GuestStrncmpBoundary check failed\n", stderr); + std::abort(); + } +} + +#ifdef _WIN32 +#include +#else +#include +#include +#endif + +class Buffer { + unsigned char* allocation; + std::size_t page; +public: + Buffer() { +#ifdef _WIN32 + SYSTEM_INFO info{}; + GetSystemInfo(&info); + page = info.dwPageSize; + allocation = static_cast(VirtualAlloc(nullptr, page * 3, MEM_RESERVE | MEM_COMMIT, PAGE_NOACCESS)); + Require(allocation != nullptr); + DWORD previous; + Require(VirtualProtect(allocation + page, page, PAGE_READWRITE, &previous) != 0); +#else + const long size = sysconf(_SC_PAGESIZE); + Require(size > 0); + page = static_cast(size); + allocation = static_cast(mmap(nullptr, page * 3, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)); + Require(allocation != MAP_FAILED); + Require(mprotect(allocation + page, page, PROT_READ | PROT_WRITE) == 0); +#endif + std::memset(allocation + page, 0xa5, page); + } + ~Buffer() { +#ifdef _WIN32 + Require(VirtualFree(allocation, 0, MEM_RELEASE) != 0); +#else + Require(munmap(allocation, page * 3) == 0); +#endif + } + Buffer(const Buffer&) = delete; + Buffer& operator=(const Buffer&) = delete; + template + T* At(std::size_t count) { + Require(count <= (page - 32) / sizeof(T)); + return reinterpret_cast(allocation + page * 2 - (std::max)(count, std::size_t{1}) * sizeof(T)); + } +}; + +template +static void Prefix(const T* data) { + for (int index = 1; index <= 8; ++index) Require(data[-index] == static_cast(sizeof(T) == 1 ? 0xa5 : 0xa5a5)); +} + +static int Sign(int value) { + return (value > 0) - (value < 0); +} + +int main() { + Buffer first, second; + for (std::size_t count = 0; count <= 128; ++count) { + auto* left = first.At(count); + auto* right = second.At(count); + const auto extent = (std::max)(count, std::size_t{1}); + for (int mode = 0; mode < 7; ++mode) { + for (std::size_t index = 0; index < extent; ++index) left[index] = right[index] = static_cast(1 + (index * 131 + count) % 255); + if (count != 0) { + const std::size_t position = mode % 3 == 0 ? 0 : mode % 3 == 1 ? count / 2 : count - 1; + if (mode < 3) { left[position] = 255; right[position] = 1; } + if (mode >= 3 && mode < 6) { left[position] = right[position] = 0; } + if (mode != 6 && position + 1 < count) { left[position + 1] = 1; right[position + 1] = 255; } + } + int expected = 0; + for (std::size_t index = 0; index < count; ++index) { + if (left[index] != right[index]) { expected = static_cast(left[index]) - right[index]; break; } + if (left[index] == 0) break; + } + const std::vector a(left, left + extent), b(right, right + extent); + Require(Sign(strncmp_nid_postfix(reinterpret_cast(left), reinterpret_cast(right), count)) == Sign(expected)); + Require(Sign(strncmp_nid_postfix(reinterpret_cast(right), reinterpret_cast(left), count)) == -Sign(expected)); + Require(std::equal(a.begin(), a.end(), left) && std::equal(b.begin(), b.end(), right)); + } + } +} diff --git a/core/libs/tests/GuestStrnstrBoundary.cpp b/core/libs/tests/GuestStrnstrBoundary.cpp new file mode 100644 index 0000000000..4455cc05b6 --- /dev/null +++ b/core/libs/tests/GuestStrnstrBoundary.cpp @@ -0,0 +1,100 @@ +#include "prx/libc/include/Api.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static void Require(bool condition) { + if (!condition) { + std::fputs("GuestStrnstrBoundary check failed\n", stderr); + std::abort(); + } +} + +#ifdef _WIN32 +#include +#else +#include +#include +#endif + +class Buffer { + unsigned char* allocation; + std::size_t page; +public: + Buffer() { +#ifdef _WIN32 + SYSTEM_INFO info{}; + GetSystemInfo(&info); + page = info.dwPageSize; + allocation = static_cast(VirtualAlloc(nullptr, page * 3, MEM_RESERVE | MEM_COMMIT, PAGE_NOACCESS)); + Require(allocation != nullptr); + DWORD previous; + Require(VirtualProtect(allocation + page, page, PAGE_READWRITE, &previous) != 0); +#else + const long size = sysconf(_SC_PAGESIZE); + Require(size > 0); + page = static_cast(size); + allocation = static_cast(mmap(nullptr, page * 3, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)); + Require(allocation != MAP_FAILED); + Require(mprotect(allocation + page, page, PROT_READ | PROT_WRITE) == 0); +#endif + std::memset(allocation + page, 0xa5, page); + } + ~Buffer() { +#ifdef _WIN32 + Require(VirtualFree(allocation, 0, MEM_RELEASE) != 0); +#else + Require(munmap(allocation, page * 3) == 0); +#endif + } + Buffer(const Buffer&) = delete; + Buffer& operator=(const Buffer&) = delete; + template + T* At(std::size_t count) { + Require(count <= (page - 32) / sizeof(T)); + return reinterpret_cast(allocation + page * 2 - (std::max)(count, std::size_t{1}) * sizeof(T)); + } +}; + +template +static void Prefix(const T* data) { + for (int index = 1; index <= 8; ++index) Require(data[-index] == static_cast(sizeof(T) == 1 ? 0xa5 : 0xa5a5)); +} + +int main() { + Buffer input, pattern; + for (std::size_t length = 0; length <= 128; ++length) { + auto* source = input.At(length); + const auto extent = (std::max)(length, std::size_t{1}); + for (std::size_t size = 0; size <= 17; ++size) { + auto* needle = pattern.At(size + 1); + std::fill(needle, needle + size, 'a'); + if (size) needle[size - 1] = 'b'; + needle[size] = 0; + const std::string wanted(needle, size); + for (int mode = 0; mode < 4; ++mode) { + std::fill(source, source + extent, 'a'); + if (size && size <= length) { + const auto position = mode == 0 ? 0 : length - size; + std::copy(wanted.begin(), wanted.end(), source + position); + if (mode == 2) source[position + size - 1] = 'c'; + } + if (mode == 3 && length) source[length / 2] = 0; + const std::string saved(source, extent); + const auto zero = saved.find(char{0}); + const auto available = (std::min)(length, zero == std::string::npos ? length : zero); + const auto found = saved.substr(0, available).find(wanted); + const auto* expected = found == std::string::npos ? nullptr : source + found; + Require(strnstr_nid_postfix(source, needle, length) == expected); + Require(std::equal(saved.begin(), saved.end(), source)); + Require(std::equal(wanted.begin(), wanted.end(), needle) && needle[size] == 0); + } + } + } +} diff --git a/core/libs/tests/GuestStrxfrmBoundary.cpp b/core/libs/tests/GuestStrxfrmBoundary.cpp new file mode 100644 index 0000000000..c68765430b --- /dev/null +++ b/core/libs/tests/GuestStrxfrmBoundary.cpp @@ -0,0 +1,88 @@ +#include "prx/libc/include/Api.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static void Require(bool condition) { + if (!condition) { + std::fputs("GuestStrxfrmBoundary check failed\n", stderr); + std::abort(); + } +} + +#ifdef _WIN32 +#include +#else +#include +#include +#endif + +class Buffer { + unsigned char* allocation; + std::size_t page; +public: + Buffer() { +#ifdef _WIN32 + SYSTEM_INFO info{}; + GetSystemInfo(&info); + page = info.dwPageSize; + allocation = static_cast(VirtualAlloc(nullptr, page * 3, MEM_RESERVE | MEM_COMMIT, PAGE_NOACCESS)); + Require(allocation != nullptr); + DWORD previous; + Require(VirtualProtect(allocation + page, page, PAGE_READWRITE, &previous) != 0); +#else + const long size = sysconf(_SC_PAGESIZE); + Require(size > 0); + page = static_cast(size); + allocation = static_cast(mmap(nullptr, page * 3, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)); + Require(allocation != MAP_FAILED); + Require(mprotect(allocation + page, page, PROT_READ | PROT_WRITE) == 0); +#endif + std::memset(allocation + page, 0xa5, page); + } + ~Buffer() { +#ifdef _WIN32 + Require(VirtualFree(allocation, 0, MEM_RELEASE) != 0); +#else + Require(munmap(allocation, page * 3) == 0); +#endif + } + Buffer(const Buffer&) = delete; + Buffer& operator=(const Buffer&) = delete; + template + T* At(std::size_t count) { + Require(count <= (page - 32) / sizeof(T)); + return reinterpret_cast(allocation + page * 2 - (std::max)(count, std::size_t{1}) * sizeof(T)); + } +}; + +template +static void Prefix(const T* data) { + for (int index = 1; index <= 8; ++index) Require(data[-index] == static_cast(sizeof(T) == 1 ? 0xa5 : 0xa5a5)); +} + +int main() { + Buffer input, output; + for (std::size_t length = 0; length <= 128; ++length) { + auto* source = input.At(length + 1); + for (std::size_t i = 0; i < length; ++i) source[i] = static_cast(1 + (i * 73 + length) % 255); + source[length] = 0; + const std::vector saved(source, source + length + 1); + for (std::size_t capacity : {std::size_t{0}, std::size_t{1}, length, length + 1, length + 7}) { + auto* destination = output.At(capacity); + const auto extent = (std::max)(capacity, std::size_t{1}); + std::memset(destination - 8, 0xa5, (extent + 8) * sizeof(char)); + Require(strxfrm_nid_postfix(destination, source, capacity) == length); + if (length < capacity) Require(std::equal(saved.begin(), saved.end(), destination)); + if (capacity == 0) Require(destination[0] == static_cast(static_cast(0xa5))); + Prefix(reinterpret_cast(destination)); + Require(std::equal(saved.begin(), saved.end(), source)); + } + } +} diff --git a/core/libs/tests/GuestSysUtilNotification.cpp b/core/libs/tests/GuestSysUtilNotification.cpp index aa60f22ae7..37afa71a77 100644 --- a/core/libs/tests/GuestSysUtilNotification.cpp +++ b/core/libs/tests/GuestSysUtilNotification.cpp @@ -90,6 +90,8 @@ int main() { Require(hello == 0, "a text notification was not sent"); Require(empty == 0, "a null message was not sent as an empty text"); Require(truncated == 0, "a long message was not sent"); + Require(sceSysUtilSendSystemNotificationWithText(0, reinterpret_cast(0x10)) == static_cast(0x8002000Eu), "an unreadable message pointer did not return EFAULT"); + Require(sceSysUtilSendSystemNotificationWithText(0, reinterpret_cast(~std::uintptr_t{0xfff})) == static_cast(0x8002000Eu), "an unreadable message pointer did not return EFAULT"); const std::string expected = "[notification] hello\n[notification] \n[notification] " + longText.substr(0, 179) + "\n"; if (output != expected) { std::fprintf(stderr, "notification output was:\n%s", output.c_str()); diff --git a/core/libs/tests/GuestSystemConfiguration.cpp b/core/libs/tests/GuestSystemConfiguration.cpp index 35db1a61ad..ae00f17297 100644 --- a/core/libs/tests/GuestSystemConfiguration.cpp +++ b/core/libs/tests/GuestSystemConfiguration.cpp @@ -352,11 +352,20 @@ static void CheckProcessListSysctl() { const int mib[] = {1, 14, 8}; std::size_t length = 0; Require(sysctl_nid_postfix(mib, 3, nullptr, &length, nullptr, 0) == 0); + const std::size_t size = length; + *__error_nid_postfix() = 13; + length = 0; + Require(sysctlbyname_nid_postfix("kern.proc.proc", nullptr, &length, nullptr, 0) == 0 && length == size && *__error_nid_postfix() == 13); unsigned char buffer[1100]; std::memset(buffer, 0xaa, sizeof(buffer)); length = sizeof(buffer); Require(sysctl_nid_postfix(mib, 3, buffer, &length, nullptr, 0) == 0 && length == 1088); + unsigned char named[1100]; + std::memset(named, 0xaa, sizeof(named)); + length = sizeof(named); + Require(sysctlbyname_nid_postfix("kern.proc.proc", named, &length, nullptr, 0) == 0 && length == 1088 && *__error_nid_postfix() == 13); + Require(std::memcmp(named, buffer, sizeof(buffer)) == 0); std::int32_t structSize = 0; std::int32_t pid = 0; std::memcpy(&structSize, buffer, sizeof(structSize)); @@ -377,6 +386,13 @@ static void CheckProcessListSysctl() { length = sizeof(buffer); Require(sysctl_nid_postfix(mib, 3, buffer, &length, buffer, sizeof(buffer)) == -1 && *__error_nid_postfix() == 1); + + *__error_nid_postfix() = 13; + length = 100; + Require(sysctlbyname_nid_postfix("kern.proc.proc", buffer, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 12 && length == 100); + *__error_nid_postfix() = 13; + length = sizeof(buffer); + Require(sysctlbyname_nid_postfix("kern.proc.proc", buffer, &length, buffer, sizeof(buffer)) == -1 && *__error_nid_postfix() == 1); } struct SystemString { int mib[2]; @@ -775,15 +791,16 @@ static void CheckOvercommitSysctl() { length = 2; Require(sysctlbyname_nid_postfix("vm.overcommit", bytes, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 12); Require(length == 2 && bytes[2] == 0xaa && bytes[3] == 0xaa); - bool threw = false; + int written = 1; + value = -1; length = sizeof(value); - try { - sysctlbyname_nid_postfix("vm.overcommit", &value, &length, &value, sizeof(value)); - - } catch (const std::runtime_error&) { - threw = true; - } - Require(threw); + *__error_nid_postfix() = 13; + Require(sysctlbyname_nid_postfix("vm.overcommit", &value, &length, &written, sizeof(written)) == -1 && *__error_nid_postfix() == 1); + Require(value == -1 && length == sizeof(value)); + Require(sysctlbyname_nid_postfix("vm.overcommit", nullptr, nullptr, &written, sizeof(written)) == -1 && *__error_nid_postfix() == 1); + Require(sysctlbyname_nid_postfix("vm.overcommit", nullptr, nullptr, &written, 0) == -1 && *__error_nid_postfix() == 1); + Require(sysctlbyname_nid_postfix("vm.overcommit", nullptr, nullptr, reinterpret_cast(0x10), 1) == -1 && *__error_nid_postfix() == 1); + Require(sysctlbyname_nid_postfix("vm.overcommit", &value, &length, nullptr, 0) == 0 && value == 0); } static void CheckSdkVersionSysctl() { *__error_nid_postfix() = 13; @@ -970,6 +987,7 @@ static void CheckNameToMib() { {"kern.boottime", {1, 21}, 2}, {"kern.rng_pseudo", {1, 50}, 2}, {"kern.proc.pathname", {1, 14, 12}, 3}, + {"kern.proc.proc", {1, 14, 8}, 3}, }; *__error_nid_postfix() = 13; for (const auto& entry : expected) { diff --git a/core/libs/tests/GuestSystemGesture.cpp b/core/libs/tests/GuestSystemGesture.cpp index 09829b0cad..6b41eb618e 100644 --- a/core/libs/tests/GuestSystemGesture.cpp +++ b/core/libs/tests/GuestSystemGesture.cpp @@ -169,6 +169,9 @@ static void TestPrimitiveTouchEvents(std::int32_t handle, int invalidArgument, i Require(UpdateWithReports(handle + 1, &pair, 1) == invalidHandle); Require(sceSystemGestureUpdatePrimitiveTouchRecognizer(handle, nullptr) == invalidArgument); + SystemGestureTouchPadData withoutBuffer{}; + withoutBuffer.report_number = 1; + Require(sceSystemGestureUpdatePrimitiveTouchRecognizer(handle, &withoutBuffer) == invalidArgument); Require(sceSystemGestureGetPrimitiveTouchEventsCount(handle) == 2); const std::array sequence{TouchReport(7000, {{3, 110, 200}}), TouchReport(8000, {{3, 130, 210}})}; @@ -193,6 +196,58 @@ static void TestPrimitiveTouchEvents(std::int32_t handle, int invalidArgument, i Require(sceSystemGestureGetPrimitiveTouchEventsCount(handle) == 1); } +static void TestCreateTouchRecognizer(std::int32_t handle, int invalidArgument, int invalidHandle) { + constexpr std::int32_t gestureType = 5; + const SystemGestureRectangle area{1.0f, 2.0f, 3.0f, 4.0f, {}}; + SystemGestureTouchRecognizer recognizer{}; + std::memset(&recognizer, 0x5a, sizeof(recognizer)); + SystemGestureTouchRecognizer untouched = recognizer; + Require(sceSystemGestureCreateTouchRecognizer(handle + 1, &recognizer, gestureType, &area, nullptr) == invalidHandle); + Require(std::memcmp(&recognizer, &untouched, sizeof(recognizer)) == 0); + Require(sceSystemGestureCreateTouchRecognizer(handle, nullptr, gestureType, &area, nullptr) == invalidArgument); + Require(sceSystemGestureCreateTouchRecognizer(handle + 1, nullptr, gestureType, &area, nullptr) == invalidHandle); + Require(std::memcmp(&recognizer, &untouched, sizeof(recognizer)) == 0); + Require(sceSystemGestureGetTouchEventsCount(handle, &recognizer) == 0); + Require(sceSystemGestureGetTouchEventsCount(handle + 1, &recognizer) == invalidHandle); +} + +static void TestNotInitialized(std::int32_t handle) { + constexpr int notInitialized = static_cast(0x80890003); + SystemGestureTouchRecognizer recognizer{}; + SystemGestureRectangle rectangle{}; + SystemGestureTouchEvent event{}; + SystemGesturePrimitiveTouchEvent primitive{}; + SystemGestureTouchRecognizerInformation information{}; + SystemGestureTouchPadData input{}; + std::uint32_t count = 7; + Require(sceSystemGestureFinalizePrimitiveTouchRecognizer() == 0); + Require(sceSystemGestureOpen(0, nullptr) == notInitialized); + Require(sceSystemGestureOpen(1, nullptr) == notInitialized); + Require(sceSystemGestureClose(handle) == notInitialized); + Require(sceSystemGestureCreateTouchRecognizer(handle, &recognizer, 0, &rectangle, nullptr) == notInitialized); + Require(sceSystemGestureAppendTouchRecognizer(handle, &recognizer) == notInitialized); + Require(sceSystemGestureRemoveTouchRecognizer(handle, &recognizer) == notInitialized); + Require(sceSystemGestureResetTouchRecognizer(handle, &recognizer) == notInitialized); + Require(sceSystemGestureUpdateTouchRecognizer(handle, &recognizer) == notInitialized); + Require(sceSystemGestureUpdateTouchRecognizerRectangle(handle, &recognizer, &rectangle) == notInitialized); + Require(sceSystemGestureUpdateAllTouchRecognizer(handle) == notInitialized); + Require(sceSystemGestureResetPrimitiveTouchRecognizer(handle) == notInitialized); + Require(sceSystemGestureUpdatePrimitiveTouchRecognizer(handle, &input) == notInitialized); + Require(sceSystemGestureUpdatePrimitiveTouchRecognizer(0, nullptr) == notInitialized); + Require(sceSystemGestureGetPrimitiveTouchEventByIndex(handle, 0, &primitive) == notInitialized); + Require(sceSystemGestureGetPrimitiveTouchEventByPrimitiveID(handle, 0, &primitive) == notInitialized); + Require(sceSystemGestureGetPrimitiveTouchEvents(handle, &primitive, 1, &count) == notInitialized); + Require(count == 7); + Require(sceSystemGestureGetPrimitiveTouchEventsCount(handle) == notInitialized); + Require(sceSystemGestureGetTouchEventByEventID(handle, &recognizer, 0, &event) == notInitialized); + Require(sceSystemGestureGetTouchEventByIndex(handle, &recognizer, 0, &event) == notInitialized); + Require(sceSystemGestureGetTouchEvents(handle, &recognizer, &event, 1, &count) == notInitialized); + Require(count == 7); + Require(sceSystemGestureGetTouchEventsCount(handle, &recognizer) == notInitialized); + Require(sceSystemGestureGetTouchRecognizerInformation(handle, &recognizer, &information) == notInitialized); + Require(sceSystemGestureInitializePrimitiveTouchRecognizer() == 0); +} + int main() { constexpr int invalidArgument = static_cast(0x80890001); constexpr int invalidHandle = static_cast(0x80890006); @@ -263,6 +318,7 @@ int main() { Require(sceSystemGestureDebugGetVersion() == 0x6a00); TestTouchRecognizerInformation(handle, invalidArgument, invalidHandle); TestPrimitiveTouchEvents(handle, invalidArgument, invalidHandle, indexOutOfArray); + TestCreateTouchRecognizer(handle, invalidArgument, invalidHandle); std::array otherHandles{}; for (auto& otherHandle : otherHandles) { otherHandle = sceSystemGestureOpen(0, nullptr); @@ -278,6 +334,7 @@ int main() { Require(sceSystemGestureGetPrimitiveTouchEventsCount(otherHandles[0]) == invalidHandle); Require(sceSystemGestureClose(replacement) == 0); for (std::size_t index = 1; index < otherHandles.size(); ++index) Require(sceSystemGestureClose(otherHandles[index]) == 0); + TestNotInitialized(handle); Require(sceSystemGestureClose(handle) == 0); Require(sceSystemGestureFinalizePrimitiveTouchRecognizer() == 0); Require(sceSystemGestureOpen(0, nullptr) == static_cast(0x80890003)); diff --git a/core/libs/tests/GuestSystemService.cpp b/core/libs/tests/GuestSystemService.cpp index 6a7e4a671f..bef25b3b82 100644 --- a/core/libs/tests/GuestSystemService.cpp +++ b/core/libs/tests/GuestSystemService.cpp @@ -10,6 +10,8 @@ #include #include #include +#include +#include namespace { @@ -117,6 +119,57 @@ void TestPlayerDialogParam() { Require(sceSystemServiceLaunchPlayerDialog(buffer) == SYSTEM_SERVICE_OK); } +void TestSuspendConfirmationDialog() { + const int runningAppId = sceSystemServiceGetAppIdOfRunningBigApp(); + Require(sceShellCoreUtilGetSuspendConfirmationDialogFlag(runningAppId)); + Require(sceSystemServiceDisableSuspendConfirmationDialog() == SYSTEM_SERVICE_OK); + Require(!sceShellCoreUtilGetSuspendConfirmationDialogFlag(runningAppId)); + Require(sceSystemServiceDisableSuspendConfirmationDialog() == SYSTEM_SERVICE_OK); + Require(!sceShellCoreUtilGetSuspendConfirmationDialogFlag(runningAppId)); + Require(sceSystemServiceEnableSuspendConfirmationDialog() == SYSTEM_SERVICE_OK); + Require(sceShellCoreUtilGetSuspendConfirmationDialogFlag(runningAppId)); + std::vector threads; + for (unsigned index = 0; index < 4; ++index) { + threads.emplace_back([index] { + for (unsigned round = 0; round < 1000; ++round) { + const int result = ((index + round) & 1u) ? sceSystemServiceEnableSuspendConfirmationDialog() : sceSystemServiceDisableSuspendConfirmationDialog(); + Require(result == SYSTEM_SERVICE_OK); + } + }); + } + for (std::thread& thread : threads) thread.join(); + Require(sceSystemServiceEnableSuspendConfirmationDialog() == SYSTEM_SERVICE_OK); + Require(sceShellCoreUtilGetSuspendConfirmationDialogFlag(runningAppId)); +} + +void TestGpuLoadEmulationMode() { + const int runningAppId = sceSystemServiceGetAppIdOfRunningBigApp(); + Require(sceSystemServiceGetGpuLoadEmulationMode() == 1); + Require(sceShellCoreUtilGetGpuLoadEmulationMode() == 1); + Require(sceSystemServiceSetGpuLoadEmulationMode(0) == SYSTEM_SERVICE_OK); + Require(sceSystemServiceGetGpuLoadEmulationMode() == 0); + Require(sceShellCoreUtilGetGpuLoadEmulationMode() == 0); + Require(sceShellCoreUtilGetGpuLoadEmulationModeByAppId(runningAppId) == 0); + Require(sceShellCoreUtilSetGpuLoadEmulationMode(1) == SYSTEM_SERVICE_OK); + Require(sceSystemServiceGetGpuLoadEmulationMode() == 1); + Require(sceShellCoreUtilSetGpuLoadEmulationModeByAppId(runningAppId, 0) == SYSTEM_SERVICE_OK); + Require(sceSystemServiceGetGpuLoadEmulationMode() == 0); + Require(sceShellCoreUtilGetGpuLoadEmulationModeByAppId(runningAppId) == 0); + Require(sceSystemServiceGetGpuLoadEmulationMode() == 0); + std::vector threads; + for (unsigned index = 0; index < 4; ++index) { + threads.emplace_back([index] { + for (unsigned round = 0; round < 1000; ++round) { + Require(sceSystemServiceSetGpuLoadEmulationMode((index + round) & 1u) == SYSTEM_SERVICE_OK); + const int mode = sceSystemServiceGetGpuLoadEmulationMode(); + Require(mode == 0 || mode == 1); + } + }); + } + for (std::thread& thread : threads) thread.join(); + Require(sceSystemServiceSetGpuLoadEmulationMode(1) == SYSTEM_SERVICE_OK); +} + } int main() { @@ -218,4 +271,6 @@ int main() { TestRunningAppState(); TestRemoteSharePlayCpuBudget(); TestPlayerDialogParam(); + TestGpuLoadEmulationMode(); + TestSuspendConfirmationDialog(); } diff --git a/core/libs/tests/GuestSystemServiceLanguageAuto.cpp b/core/libs/tests/GuestSystemServiceLanguageAuto.cpp new file mode 100644 index 0000000000..0889d916ec --- /dev/null +++ b/core/libs/tests/GuestSystemServiceLanguageAuto.cpp @@ -0,0 +1,95 @@ +#include "prx/libSceSystemService/include/SystemService.hpp" +#include "SceTypes.hpp" +#include "prx/libSceSystemService/SystemService.hpp" +#include +#include +#include +#include +#include +#include + +namespace { + +void Require(bool value, const char* what) { + if (value) return; + std::fprintf(stderr, "check failed: %s\n", what); + std::abort(); +} + +void SetVariable(const char* name, std::string_view value) { + if (value == "-") Require(::unsetenv(name) == 0, name); + else Require(::setenv(name, std::string(value).c_str(), 1) == 0, name); +} + +int Query(int paramId) { + int value = -1; + Require(sceSystemServiceParamGetInt(paramId, &value) == SYSTEM_SERVICE_OK, "query"); + return value; +} + +bool Rejected(const char* value, std::string& message) { + SetVariable("ANYPS5_LANGUAGE", value); + int language = -1; + try { + static_cast(sceSystemServiceParamGetInt(SYSTEM_SERVICE_PARAM_ID_LANG, &language)); + } catch (const std::runtime_error& error) { + message = error.what(); + return language == -1; + } + return false; +} + +std::string ReadAll(std::FILE* file) { + std::string text; + std::rewind(file); + for (int c = std::fgetc(file); c != EOF; c = std::fgetc(file)) text += static_cast(c); + return text; +} + +int CountNotices(const std::string& text) { + int count = 0; + for (std::size_t at = text.find("ANYPS5_LANGUAGE=auto:"); at != std::string::npos; at = text.find("ANYPS5_LANGUAGE=auto:", at + 1)) ++count; + return count; +} + +} + +int main(int argc, char** argv) { + Require(argc == 8, "arguments: expected LC_ALL LC_MESSAGES LANG LANGUAGE spelling notices"); + const int expected = std::atoi(argv[1]); + const int expectedNotices = std::atoi(argv[7]); + std::FILE* captured = std::tmpfile(); + Require(captured != nullptr && ::dup2(::fileno(captured), 2) == 2, "capture stderr"); + SetVariable("LC_ALL", argv[2]); + SetVariable("LC_MESSAGES", argv[3]); + SetVariable("LANG", argv[4]); + SetVariable("LANGUAGE", argv[5]); + SetVariable("ANYPS5_LANGUAGE", argv[6]); + Require(Query(SYSTEM_SERVICE_PARAM_ID_LANG) == expected, "parameter 1"); + Require(Query(SYSTEM_SERVICE_PARAM_ID_400) == expected, "parameter 400"); + Require(SystemServiceConsoleLanguage("test") == expected, "SystemServiceConsoleLanguage"); + SetVariable("LC_ALL", "ja_JP.UTF-8"); + SetVariable("LC_MESSAGES", "ja_JP.UTF-8"); + SetVariable("LANG", "ja_JP.UTF-8"); + SetVariable("LANGUAGE", "ja"); + Require(Query(SYSTEM_SERVICE_PARAM_ID_LANG) == expected, "the host locale is resolved once"); + Require(SystemServiceConsoleLanguage("test") == expected, "the host locale is resolved once for every caller"); + Require(CountNotices(ReadAll(captured)) == expectedNotices, "number of notices"); + SetVariable("ANYPS5_LANGUAGE", "17"); + Require(Query(SYSTEM_SERVICE_PARAM_ID_LANG) == 17, "an integer after auto"); + SetVariable("ANYPS5_LANGUAGE", "0"); + Require(Query(SYSTEM_SERVICE_PARAM_ID_400) == 0, "integer 0 after auto"); + SetVariable("ANYPS5_LANGUAGE", argv[6]); + Require(Query(SYSTEM_SERVICE_PARAM_ID_LANG) == expected, "auto again gives the same answer"); + SetVariable("ANYPS5_LANGUAGE", ""); + Require(Query(SYSTEM_SERVICE_PARAM_ID_LANG) == SYSTEM_SERVICE_PARAM_LANG_ENGLISH_US, "empty after auto"); + SetVariable("ANYPS5_LANGUAGE", "-"); + Require(Query(SYSTEM_SERVICE_PARAM_ID_LANG) == SYSTEM_SERVICE_PARAM_LANG_ENGLISH_US, "unset after auto"); + for (const char* value : {"auto ", " auto", "autoo", "aut", "a", "au to", "auto\n", "automatic", "-auto", "+auto", "31", "-1", "21a", "0x15", "arabic"}) { + std::string message; + Require(Rejected(value, message), value); + Require(message.find("ANYPS5_LANGUAGE") != std::string::npos, "the error names the variable"); + Require(message.find("0 to 30 or auto") != std::string::npos, "the error lists auto"); + } + Require(CountNotices(ReadAll(captured)) == expectedNotices, "no notice after the first resolution"); +} diff --git a/core/libs/tests/GuestTerminal.cpp b/core/libs/tests/GuestTerminal.cpp index 8fabd1454f..df9a0bb2ce 100644 --- a/core/libs/tests/GuestTerminal.cpp +++ b/core/libs/tests/GuestTerminal.cpp @@ -48,13 +48,15 @@ static void RequireControlFails(int descriptor, int error) { } } -static void RequireProcessGroupFails(int descriptor, int error) { +static void RequireProcessGroupAndSessionFail(int descriptor, int error) { *__error_nid_postfix() = 0; Require(Fails(tcgetpgrp_nid_postfix(descriptor), error)); for (const int group : {0, 1}) { *__error_nid_postfix() = 0; Require(Fails(tcsetpgrp_nid_postfix(descriptor, group), error)); } + *__error_nid_postfix() = 0; + Require(Fails(tcgetsid_nid_postfix(descriptor), error)); } template static bool Throws(TCall call) { @@ -86,10 +88,10 @@ int main() { const int file = open_nid_postfix(name.c_str(), 0x202, 0644); Require(file >= 0); RequireNotTerminal(file); - RequireProcessGroupFails(file, NotTerminal); + RequireProcessGroupAndSessionFail(file, NotTerminal); for (const int stream : {0, 1, 2}) { RequireNotTerminal(stream); - RequireProcessGroupFails(stream, NotTerminal); + RequireProcessGroupAndSessionFail(stream, NotTerminal); } int ends[2] = {-1, -1}; @@ -98,6 +100,8 @@ int main() { RequireNotTerminal(end); Require(Throws([end] { tcgetpgrp_nid_postfix(end); })); Require(Throws([end] { tcsetpgrp_nid_postfix(end, 0); })); + *__error_nid_postfix() = 0; + Require(Fails(tcgetsid_nid_postfix(end), NotTerminal)); } const int socket = socket_nid_postfix(2, 2, 0); @@ -110,14 +114,14 @@ int main() { Require(std::memcmp(probe, untouched, sizeof(probe)) == 0); Require(Fails(tcsetattr_nid_postfix(socket, Flush, probe), AddressNotAvailable)); RequireControlFails(socket, AddressNotAvailable); - RequireProcessGroupFails(socket, AddressNotAvailable); + RequireProcessGroupAndSessionFail(socket, AddressNotAvailable); int pair[2] = {-1, -1}; Require(socketpair_nid_postfix(1, 1, 0, pair) == 0); for (const int end : pair) { Require(Fails(tcgetattr_nid_postfix(end, probe), OperationNotSupported)); RequireControlFails(end, OperationNotSupported); - RequireProcessGroupFails(end, OperationNotSupported); + RequireProcessGroupAndSessionFail(end, OperationNotSupported); } Require(close_nid_postfix(pair[0]) == 0 && close_nid_postfix(pair[1]) == 0); @@ -126,7 +130,7 @@ int main() { const int directory = open_nid_postfix(directoryName.c_str(), 0x00020000, 0); Require(directory >= 0); RequireNotTerminal(directory); - RequireProcessGroupFails(directory, NotTerminal); + RequireProcessGroupAndSessionFail(directory, NotTerminal); Require(close_nid_postfix(directory) == 0); RequireControlFails(directory, BadDescriptor); std::filesystem::remove(directoryName); @@ -151,21 +155,21 @@ int main() { } } RequireControlFails(-1, BadDescriptor); - RequireProcessGroupFails(-1, BadDescriptor); + RequireProcessGroupAndSessionFail(-1, BadDescriptor); Require(close_nid_postfix(socket) == 0); Require(Fails(tcgetattr_nid_postfix(socket, attributes), BadDescriptor)); Require(Fails(tcsetattr_nid_postfix(socket, Now, attributes), BadDescriptor)); RequireControlFails(socket, BadDescriptor); - RequireProcessGroupFails(socket, BadDescriptor); + RequireProcessGroupAndSessionFail(socket, BadDescriptor); Require(close_nid_postfix(ends[0]) == 0 && close_nid_postfix(ends[1]) == 0); RequireControlFails(ends[0], BadDescriptor); - RequireProcessGroupFails(ends[0], BadDescriptor); + RequireProcessGroupAndSessionFail(ends[0], BadDescriptor); Require(close_nid_postfix(file) == 0); Require(Fails(tcgetattr_nid_postfix(file, attributes), BadDescriptor)); Require(Fails(tcsetattr_nid_postfix(file, Flush, attributes), BadDescriptor)); RequireControlFails(file, BadDescriptor); - RequireProcessGroupFails(file, BadDescriptor); + RequireProcessGroupAndSessionFail(file, BadDescriptor); std::filesystem::remove(name); return 0; } diff --git a/core/libs/tests/GuestTimingsafeBoundary.cpp b/core/libs/tests/GuestTimingsafeBoundary.cpp new file mode 100644 index 0000000000..58e8330d06 --- /dev/null +++ b/core/libs/tests/GuestTimingsafeBoundary.cpp @@ -0,0 +1,88 @@ +#include "prx/libc/include/Api.hpp" +#include +#include +#include +#include +#include +#include +#include + +static void Require(bool condition) { + if (!condition) { + std::fputs("GuestTimingsafeBoundary check failed\n", stderr); + std::abort(); + } +} + +#ifdef _WIN32 +#include +#else +#include +#include +#endif + +class Buffer { + unsigned char* allocation; + std::size_t page; +public: + Buffer() { +#ifdef _WIN32 + SYSTEM_INFO info{}; + GetSystemInfo(&info); + page = info.dwPageSize; + allocation = static_cast(VirtualAlloc(nullptr, page * 3, MEM_RESERVE | MEM_COMMIT, PAGE_NOACCESS)); + Require(allocation != nullptr); + DWORD previous; + Require(VirtualProtect(allocation + page, page, PAGE_READWRITE, &previous) != 0); +#else + const long size = sysconf(_SC_PAGESIZE); + Require(size > 0); + page = static_cast(size); + allocation = static_cast(mmap(nullptr, page * 3, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)); + Require(allocation != MAP_FAILED); + Require(mprotect(allocation + page, page, PROT_READ | PROT_WRITE) == 0); +#endif + std::memset(allocation + page, 0xa5, page); + } + ~Buffer() { +#ifdef _WIN32 + Require(VirtualFree(allocation, 0, MEM_RELEASE) != 0); +#else + Require(munmap(allocation, page * 3) == 0); +#endif + } + Buffer(const Buffer&) = delete; + Buffer& operator=(const Buffer&) = delete; + template + T* At(std::size_t count) { + Require(count <= (page - 32) / sizeof(T)); + return reinterpret_cast(allocation + page * 2 - (std::max)(count, std::size_t{1}) * sizeof(T)); + } +}; + +static int Sign(int value) { + return (value > 0) - (value < 0); +} + +int main() { + Buffer first, second; + for (std::size_t length = 0; length <= 128; ++length) { + auto* left = first.At(length); + auto* right = second.At(length); + const auto extent = (std::max)(length, std::size_t{1}); + for (int mode = 0; mode < 4; ++mode) { + for (std::size_t i = 0; i < extent; ++i) left[i] = right[i] = static_cast(i * 131 + length); + if (length && mode < 3) { + const auto position = mode == 0 ? 0 : mode == 1 ? length / 2 : length - 1; + left[position] = 0xff; right[position] = 1; + if (position + 1 < length) { left[position + 1] = 1; right[position + 1] = 0xff; } + } + int expected = 0; + for (std::size_t i = 0; i < length; ++i) if (left[i] != right[i]) { expected = left[i] > right[i] ? 1 : -1; break; } + const std::vector a(left, left + extent), b(right, right + extent); + Require(Sign(timingsafe_memcmp_nid_postfix(left, right, length)) == expected); + Require(Sign(timingsafe_memcmp_nid_postfix(right, left, length)) == -expected); + Require(std::equal(a.begin(), a.end(), left) && std::equal(b.begin(), b.end(), right)); + } + } +} diff --git a/core/libs/tests/GuestUserContext.cpp b/core/libs/tests/GuestUserContext.cpp index 46fb68c1d5..e87831a5b7 100644 --- a/core/libs/tests/GuestUserContext.cpp +++ b/core/libs/tests/GuestUserContext.cpp @@ -118,6 +118,7 @@ asm(".text\n" namespace { +constexpr int GuestEfault = 14; constexpr int GuestEinval = 22; constexpr int GuestSigBlock = 1; constexpr int GuestSigSetmask = 3; @@ -218,7 +219,24 @@ void CheckErrors() { Require(getcontext_nid_postfix(nullptr) == -1 && *__error_nid_postfix() == GuestEinval, "getcontext rejects a null context"); *__error_nid_postfix() = 0; Require(setcontext_nid_postfix(nullptr) == -1 && *__error_nid_postfix() == GuestEinval, "setcontext rejects a null context"); + *__error_nid_postfix() = 0; + Require(getcontext_nid_postfix(reinterpret_cast(0x10)) == -1 && *__error_nid_postfix() == GuestEfault, "getcontext rejects an invalid destination pointer"); + *__error_nid_postfix() = 0; + Require(setcontext_nid_postfix(reinterpret_cast(0x10)) == -1 && *__error_nid_postfix() == GuestEfault, "setcontext rejects an invalid source pointer"); + *__error_nid_postfix() = 0; + Require(SigreturnContext_nid_no_patch(nullptr) == -1 && *__error_nid_postfix() == GuestEfault, "sigreturn rejects a null context"); + *__error_nid_postfix() = 0; + Require(SigreturnContext_nid_no_patch(reinterpret_cast(0x10)) == -1 && *__error_nid_postfix() == GuestEfault, "sigreturn rejects an invalid context pointer"); + *__error_nid_postfix() = 0; + Require(swapcontext_nid_postfix(reinterpret_cast(0x10), nullptr) == -1 && *__error_nid_postfix() == GuestEinval, "swapcontext rejects a null next pointer"); UserUcontext context{}; + Require(getcontext_nid_postfix(&context) == 0, "capture a valid context"); + *__error_nid_postfix() = 0; + Require(swapcontext_nid_postfix(reinterpret_cast(0x10), &context) == -1 && *__error_nid_postfix() == GuestEfault, "swapcontext rejects an invalid saved pointer"); + UserUcontext savedContext{}; + *__error_nid_postfix() = 0; + Require(swapcontext_nid_postfix(&savedContext, reinterpret_cast(0x10)) == -1 && *__error_nid_postfix() == GuestEfault, "swapcontext rejects an invalid next pointer"); + Require(savedContext.mcontext.len == sizeof(UserMcontext) && savedContext.mcontext.rip != 0, "swapcontext writes saved context before failing next"); Require(getcontext_nid_postfix(&context) == 0, "capture a context to corrupt"); context.mcontext.len = 0; *__error_nid_postfix() = 0; diff --git a/core/libs/tests/GuestUserService.cpp b/core/libs/tests/GuestUserService.cpp index 39de9a2f70..2281482617 100644 --- a/core/libs/tests/GuestUserService.cpp +++ b/core/libs/tests/GuestUserService.cpp @@ -9,6 +9,8 @@ namespace { +constexpr int SCE_USER_SERVICE_ERROR_INVALID_USER_ID = USER_SERVICE_ERROR_UNKNOWN_USER; + void Require(bool value) { if (!value) std::abort(); } void SetLanguage(const char* value) { @@ -53,6 +55,17 @@ int main() { int color = -1; Require(sceUserServiceGetUserColor(USER_SERVICE_INITIAL_USER_ID, nullptr) == USER_SERVICE_ERROR_INVALID_ARGUMENT); Require(sceUserServiceGetUserColor(123, &color) == USER_SERVICE_ERROR_INVALID_ARGUMENT); + Require(sceUserServiceGetUserColor(0, &color) == USER_SERVICE_ERROR_INVALID_ARGUMENT); + Require(sceUserServiceGetUserColor(USER_SERVICE_USER_ID_INVALID, &color) == USER_SERVICE_ERROR_INVALID_ARGUMENT); + Require(sceUserServiceGetUserColor(USER_SERVICE_INITIAL_USER_ID - 1, &color) == USER_SERVICE_ERROR_INVALID_ARGUMENT); + Require(sceUserServiceGetUserColor(USER_SERVICE_INITIAL_USER_ID + USER_SERVICE_MAX_USERS, &color) == USER_SERVICE_ERROR_INVALID_ARGUMENT); + Require(sceUserServiceGetUserColor(0x20000000, &color) == USER_SERVICE_ERROR_INVALID_ARGUMENT); + Require(sceUserServiceGetUserColor(0, nullptr) == USER_SERVICE_ERROR_INVALID_ARGUMENT); + Require(sceUserServiceGetUserColor(USER_SERVICE_USER_ID_INVALID, nullptr) == USER_SERVICE_ERROR_INVALID_ARGUMENT); + Require(sceUserServiceGetUserColor(USER_SERVICE_INITIAL_USER_ID - 1, nullptr) == USER_SERVICE_ERROR_INVALID_ARGUMENT); + Require(sceUserServiceGetUserColor(USER_SERVICE_INITIAL_USER_ID + USER_SERVICE_MAX_USERS, nullptr) == USER_SERVICE_ERROR_INVALID_ARGUMENT); + Require(sceUserServiceGetUserColor(0x20000000, nullptr) == USER_SERVICE_ERROR_INVALID_ARGUMENT); + Require(color == -1); Require(sceUserServiceGetUserColor(USER_SERVICE_INITIAL_USER_ID, &color) == USER_SERVICE_OK); SetLanguage(nullptr); @@ -95,6 +108,16 @@ int main() { Require(sceUserServiceGetKeyboardType(123, &untouched) == USER_SERVICE_ERROR_UNKNOWN_USER && untouched == 0x5A5A); Require(sceUserServiceGetJapaneseInputType(USER_SERVICE_INITIAL_USER_ID, &untouched) == USER_SERVICE_OK && untouched == 0); } +#if defined(__linux__) + for (const char* name : {"LC_MESSAGES", "LANG", "LANGUAGE"}) Require(::unsetenv(name) == 0); + Require(::setenv("LC_ALL", "de_DE.UTF-8", 1) == 0); + SetLanguage("auto"); + std::int32_t german = 0x5A5A; + Require(sceUserServiceGetKeyboardType(USER_SERVICE_INITIAL_USER_ID, &german) == USER_SERVICE_OK && german == keyboardTypeByLanguage[4]); + Require(::setenv("LC_ALL", "ja_JP.UTF-8", 1) == 0); + german = 0x5A5A; + Require(sceUserServiceGetKeyboardType(USER_SERVICE_INITIAL_USER_ID, &german) == USER_SERVICE_OK && german == keyboardTypeByLanguage[4]); +#endif SetLanguage(nullptr); Require(color == 0); @@ -102,7 +125,7 @@ int main() { Require(sceUserServiceGetNpAccountId(USER_SERVICE_INITIAL_USER_ID, nullptr) == USER_SERVICE_ERROR_INVALID_ARGUMENT); Require(sceUserServiceGetNpAccountId(123, &account_id) == USER_SERVICE_ERROR_UNKNOWN_USER); Require(account_id == 0xdeadbeefu); - Require(sceUserServiceGetNpAccountId(123, nullptr) == USER_SERVICE_ERROR_UNKNOWN_USER); + Require(sceUserServiceGetNpAccountId(123, nullptr) == USER_SERVICE_ERROR_INVALID_ARGUMENT); Require(sceUserServiceGetNpAccountId(USER_SERVICE_INITIAL_USER_ID, &account_id) == USER_SERVICE_OK); Require(account_id == 0); @@ -120,7 +143,7 @@ int main() { account_id = 0xdeadbeefu; Require(sceUserServiceGetNpAccountId(user_id, &account_id) == USER_SERVICE_ERROR_UNKNOWN_USER); Require(account_id == 0xdeadbeefu); - Require(sceUserServiceGetNpAccountId(user_id, nullptr) == USER_SERVICE_ERROR_UNKNOWN_USER); + Require(sceUserServiceGetNpAccountId(user_id, nullptr) == USER_SERVICE_ERROR_INVALID_ARGUMENT); } Require(sceUserServiceGetAgeLevel(USER_SERVICE_INITIAL_USER_ID, &age_level) == USER_SERVICE_OK); Require(age_level == 0xffffffffu); @@ -180,6 +203,10 @@ int main() { char name[32]{}; Require(sceUserServiceGetUserName(USER_SERVICE_INITIAL_USER_ID, nullptr, sizeof(name)) == USER_SERVICE_ERROR_INVALID_ARGUMENT); Require(sceUserServiceGetUserName(123, name, sizeof(name)) == USER_SERVICE_ERROR_UNKNOWN_USER); + Require(sceUserServiceGetUserName(0x10000004, name, sizeof(name)) == SCE_USER_SERVICE_ERROR_INVALID_USER_ID); + Require(sceUserServiceGetUserName(static_cast(0xFFFFFFFFu), name, sizeof(name)) == SCE_USER_SERVICE_ERROR_INVALID_USER_ID); + Require(sceUserServiceGetUserName(USER_SERVICE_INITIAL_USER_ID + USER_SERVICE_MAX_USERS, name, sizeof(name)) == USER_SERVICE_ERROR_UNKNOWN_USER); + Require(sceUserServiceGetUserName(USER_SERVICE_USER_ID_INVALID, name, sizeof(name)) == USER_SERVICE_ERROR_UNKNOWN_USER); Require(sceUserServiceGetUserName(USER_SERVICE_INITIAL_USER_ID, name, 5) == USER_SERVICE_ERROR_BUFFER_TOO_SHORT); Require(sceUserServiceGetUserName(USER_SERVICE_INITIAL_USER_ID, name, sizeof(name)) == USER_SERVICE_OK); Require(std::strcmp(name, "Player") == 0); @@ -206,7 +233,7 @@ int main() { account_id = 0xdeadbeefu; Require(sceUserServiceGetNpAccountId(USER_SERVICE_INITIAL_USER_ID + 1, &account_id) == USER_SERVICE_ERROR_UNKNOWN_USER); Require(account_id == 0xdeadbeefu); - Require(sceUserServiceGetNpAccountId(USER_SERVICE_INITIAL_USER_ID + 1, nullptr) == USER_SERVICE_ERROR_UNKNOWN_USER); + Require(sceUserServiceGetNpAccountId(USER_SERVICE_INITIAL_USER_ID + 1, nullptr) == USER_SERVICE_ERROR_INVALID_ARGUMENT); for (const Setting& setting : settings) { std::int32_t value = 0x5a5a5a5a; Require(setting.get(USER_SERVICE_INITIAL_USER_ID + 1, &value) == USER_SERVICE_ERROR_NOT_LOGGED_IN); @@ -257,7 +284,7 @@ int main() { account_id = 0xdeadbeefu; Require(sceUserServiceGetNpAccountId(USER_SERVICE_INITIAL_USER_ID + 1, &account_id) == USER_SERVICE_ERROR_UNKNOWN_USER); Require(account_id == 0xdeadbeefu); - Require(sceUserServiceGetNpAccountId(USER_SERVICE_INITIAL_USER_ID + 1, nullptr) == USER_SERVICE_ERROR_UNKNOWN_USER); + Require(sceUserServiceGetNpAccountId(USER_SERVICE_INITIAL_USER_ID + 1, nullptr) == USER_SERVICE_ERROR_INVALID_ARGUMENT); for (const Setting& setting : settings) { std::int32_t value = 0x5a5a5a5a; Require(setting.get(USER_SERVICE_INITIAL_USER_ID + 1, &value) == USER_SERVICE_ERROR_NOT_LOGGED_IN); diff --git a/core/libs/tests/GuestUserServiceUserKind.cpp b/core/libs/tests/GuestUserServiceUserKind.cpp new file mode 100644 index 0000000000..1332473d4b --- /dev/null +++ b/core/libs/tests/GuestUserServiceUserKind.cpp @@ -0,0 +1,55 @@ +#include "SceTypes.hpp" +#include "prx/libSceUserService/UserService.hpp" +#include +#include +#include + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +std::int32_t Id(std::uint32_t value) { + std::int32_t id = 0; + std::memcpy(&id, &value, sizeof(id)); + return id; +} + +void CheckGuestUser() { + const std::uint32_t guests[] = {0x20000000u, 0x20000001u, 0x2abcdef0u, 0x2fffffffu, 0x30000000u, 0x3fffffffu}; + const std::uint32_t others[] = {0u, 0x10000000u, 0x10000003u, 0x1fffffffu, 0x40000000u, 0x60000000u, + 0xa0000000u, 0xe0000000u, 0xffffffffu}; + for (const std::uint32_t id : guests) Require(sceUserServiceIsGuestUser(Id(id)) == 1); + for (const std::uint32_t id : others) Require(sceUserServiceIsGuestUser(Id(id)) == 0); + Require(sceUserServiceIsGuestUser(USER_SERVICE_INITIAL_USER_ID) == 0); + Require(sceUserServiceIsGuestUser(USER_SERVICE_USER_ID_INVALID) == 0); +} + +void CheckSharePlayClientUser() { + const std::uint32_t clients[] = {0x30000000u, 0x30000001u, 0x3abcdef0u, 0x3fffffffu}; + const std::uint32_t others[] = {0u, 0x10000000u, 0x20000000u, 0x2fffffffu, 0x40000000u, 0x70000000u, + 0xb0000000u, 0xf0000000u, 0xffffffffu}; + for (const std::uint32_t id : clients) Require(sceUserServiceIsSharePlayClientUser(Id(id)) == 1); + for (const std::uint32_t id : others) Require(sceUserServiceIsSharePlayClientUser(Id(id)) == 0); + Require(sceUserServiceIsSharePlayClientUser(USER_SERVICE_INITIAL_USER_ID) == 0); +} + +void CheckSpartacusEnabled() { + const std::int32_t ids[] = {USER_SERVICE_INITIAL_USER_ID, USER_SERVICE_USER_ID_INVALID, 0, 123, Id(0x20000000u)}; + for (const std::int32_t id : ids) { + Require(sceUserServiceGetIsSpartacusEnabled(id, nullptr) == USER_SERVICE_ERROR_INVALID_ARGUMENT); + unsigned char bytes[2] = {0x5a, 0x5a}; + bool* enabled = reinterpret_cast(bytes); + Require(sceUserServiceGetIsSpartacusEnabled(id, enabled) == USER_SERVICE_OK); + Require(bytes[0] == 1); + Require(bytes[1] == 0x5a); + } +} + +} + +int main() { + CheckGuestUser(); + CheckSharePlayClientUser(); + CheckSpartacusEnabled(); + return 0; +} diff --git a/core/libs/tests/GuestVdecCore.cpp b/core/libs/tests/GuestVdecCore.cpp index e62fbd0248..a7ab8641e5 100644 --- a/core/libs/tests/GuestVdecCore.cpp +++ b/core/libs/tests/GuestVdecCore.cpp @@ -1,10 +1,14 @@ #include "prx/libSceVdecCore/include/Api.hpp" #include "prx/libSceVdecCore/include/Decoder.hpp" +#include "H264Fixture.hpp" +#include #include +#include #include #include #include +#include #include #ifdef _WIN32 @@ -17,6 +21,15 @@ namespace { constexpr int InvalidArgument = static_cast(0x80C00001); constexpr int InvalidCodecType = static_cast(0x80C00003); +constexpr int NoPicture = static_cast(0x80C00010); +constexpr int DecodeRunning = static_cast(0x80C00018); +constexpr std::int64_t PtsInvalid = -1; +constexpr std::uint64_t SlotSize = 0x10000; +constexpr std::uint32_t OutputPitch = 64; +constexpr std::array VisiblePictureHashes{ + 0x5871fdc85dfa274eull, 0x26ce4afb974cffc4ull, 0xd2e739633733d5baull, + 0x54feb3d290e028fdull, 0xfa1342a4ba1819c8ull, 0x870125dd7f079a4cull, +}; constexpr std::uint32_t CodecAvc = 0; constexpr std::uint32_t CodecHevc = 4; constexpr std::uint32_t DecoderVdec2 = 2; @@ -174,6 +187,322 @@ void CheckMapMemoryBlock() { Require(sceVdecCoreDeleteDecoder(decoder) == 0, "delete the mapped decoder"); } +void* CreateAvc(std::int32_t width) { + const VdecCoreConfigInfo config = Config(width, static_cast(H264Fixture::Height), 256, 0); + void* decoder = nullptr; + Require(sceVdecCoreCreateDecoder(&config, nullptr, nullptr, &decoder) == 0 && decoder != nullptr, "create an AVC decoder"); + return decoder; +} + +const std::vector>& Units() { + static const std::vector> units = H264Fixture::accessUnits(H264Fixture::Stream.data(), H264Fixture::Stream.size(), false); + Require(units.size() == H264Fixture::PictureHashes.size(), "the fixture's access units"); + return units; +} + +void Feed(void* decoder, std::size_t unit, std::int64_t pts, std::int64_t dts, std::uint64_t userData) { + const auto& data = Units()[unit]; + const VdecCoreInputData input{data.data(), data.size(), pts, dts, userData, 0, 0}; + std::array status{}; + Require(sceVdecCoreSetDecodeInput(decoder, &input, nullptr, status.data()) == 0, "set an access unit as decode input"); + Require(status[0] == 1 && status[1] == 1, "the input status of an accepted access unit"); +} + +void FeedAll(void* decoder, std::int64_t ptsBase, std::int64_t dtsBase, std::uint64_t userBase) { + for (std::size_t unit = 0; unit < Units().size(); ++unit) { + const auto offset = static_cast(unit); + Feed(decoder, unit, ptsBase == PtsInvalid ? PtsInvalid : ptsBase + offset, dtsBase == PtsInvalid ? PtsInvalid : dtsBase + offset, userBase + unit); + } +} + +std::uint32_t Flush(void* decoder) { + std::uint32_t count = 0xffffffffu; + Require(sceVdecCoreFlushDecodeOutput(decoder, &count, nullptr) == 0, "flush the decode output"); + return count; +} + +VdecCoreOutputInfo Output(void* decoder, VdecCoreFrameBuffer* frameBuffer) { + VdecCoreOutputInfo output{}; + Require(sceVdecCoreGetDecodeOutput(decoder, frameBuffer, 0, &output, nullptr) == 0, "get a decoded picture"); + return output; +} + +std::uint64_t VisibleHash(const std::uint8_t* buffer, std::uint32_t pitch, std::uint32_t frameHeight) { + std::uint64_t hash = 0xcbf29ce484222325ull; + const auto mix = [&](const std::uint8_t* row) { + for (std::uint32_t column = 0; column < H264Fixture::Width; ++column) hash = (hash ^ row[column]) * 0x100000001b3ull; + }; + for (std::uint32_t row = 0; row < H264Fixture::Height; ++row) mix(buffer + static_cast(row) * pitch); + const auto* chroma = buffer + static_cast(pitch) * frameHeight; + for (std::uint32_t row = 0; row < H264Fixture::Height / 2; ++row) mix(chroma + static_cast(row) * pitch); + return hash; +} + +bool ExtendsLastRows(const std::uint8_t* buffer, std::uint32_t pitch, std::uint32_t frameHeight) { + const auto* chroma = buffer + static_cast(pitch) * frameHeight; + const auto* lastLuma = buffer + static_cast(pitch) * (H264Fixture::Height - 1); + const auto* lastChroma = chroma + static_cast(pitch) * (H264Fixture::Height / 2 - 1); + for (std::uint32_t row = H264Fixture::Height; row < frameHeight; ++row) { + if (std::memcmp(buffer + static_cast(row) * pitch, lastLuma, H264Fixture::Width) != 0) return false; + } + for (std::uint32_t row = H264Fixture::Height / 2; row < frameHeight / 2; ++row) { + if (std::memcmp(chroma + static_cast(row) * pitch, lastChroma, H264Fixture::Width) != 0) return false; + } + return true; +} + +const VdecCoreHwPictureInfo& HwInfo(const VdecCoreOutputInfo& output, std::uint64_t allocatedSize) { + return *reinterpret_cast(static_cast(output.frameBuffer) + allocatedSize - sizeof(VdecCoreHwPictureInfo)); +} + +void RequirePicture(const VdecCoreOutputInfo& output, std::size_t picture, std::uint64_t allocatedSize, std::uint64_t pts, std::uint64_t userData, const char* message) { + const std::uint32_t height = H264Fixture::CodedHeight; + auto* buffer = static_cast(output.frameBuffer); + Require(output.pictureCount == 1 && buffer != nullptr && output.frameFormat == 0 && output.reserved0 == 0, message); + Require(output.frameWidth == H264Fixture::Width && output.framePitch == OutputPitch && output.frameHeight == height && output.framePitchInBytes == OutputPitch, message); + Require(output.lumaBuffer == buffer && output.chromaBuffer == buffer + static_cast(OutputPitch) * height, message); + const VdecCorePicture& first = output.pictures[0]; + Require(first.flags == 0 && first.reserved0 == 0 && first.pictureInfo == nullptr && first.frameBuffer == buffer && first.frameBufferSize == OutputPitch * height * 3 / 2 && first.framePitch == OutputPitch && first.reserved1 == 0, message); + Require(output.pictures[1].frameBuffer == nullptr, message); + Require(first.pts == pts && first.userData == userData, message); + Require(output.aspectRatio == 1 && output.sarWidth == 1 && output.videoFormat == 5 && output.colourPrimaries == 1 && output.transferCharacteristics == 1 && output.videoFullRangeFlag == 0 && output.colourDescriptionPresentFlag == 1, message); + Require(VisibleHash(buffer, output.framePitch, output.frameHeight) == VisiblePictureHashes[picture], message); + Require(ExtendsLastRows(buffer, output.framePitch, output.frameHeight), message); + const VdecCoreHwPictureInfo& info = HwInfo(output, allocatedSize); + Require(info.codecType == 0 && info.userData == userData && info.pts == pts && info.dts == first.dts && info.valid == 1, message); + Require(info.width == H264Fixture::Width && info.height == height && info.cropLeft == 0 && info.cropRight == 0 && info.cropTop == 0 && info.cropBottom == (height - H264Fixture::Height) / 2, message); + Require(info.sarWidth == 1 && info.sarHeight == 1 && info.flags == 0x03, message); +} + +void CheckAccessUnitLength() { + const std::uint64_t limit = VdecCoreMaxAccessUnitSize_nid_no_patch(); + Require(limit > 0 && limit < 0x7fffffffull, "the largest access unit leaves room for the packet padding in an int"); + Require(VdecCoreAccessUnitLength_nid_no_patch(0) < 0, "an empty access unit has no length"); + Require(VdecCoreAccessUnitLength_nid_no_patch(1) == 1, "a one-byte access unit keeps its length"); + Require(VdecCoreAccessUnitLength_nid_no_patch(limit) == static_cast(limit), "the largest access unit keeps its length"); + Require(VdecCoreAccessUnitLength_nid_no_patch(limit + 1) < 0, "an access unit one past the packet limit has no length"); + Require(VdecCoreAccessUnitLength_nid_no_patch(0x7fffffffull) < 0, "an access unit of the largest int has no length"); + Require(VdecCoreAccessUnitLength_nid_no_patch((1ull << 32) + 16) < 0, "an access unit whose low 32 bits look small has no length"); + Require(VdecCoreAccessUnitLength_nid_no_patch(~0ull) < 0, "the largest 64-bit size has no length"); +} + +void CheckDecodeArguments() { + void* decoder = CreateAvc(static_cast(H264Fixture::Width)); + const auto& unit = Units()[0]; + VdecCoreInputData input{unit.data(), unit.size(), 0, 0, 0, 0, 0}; + std::array status{7, 7}; + Require(sceVdecCoreSetDecodeInput(nullptr, &input, nullptr, status.data()) == InvalidArgument, "set input without a decoder"); + Require(sceVdecCoreSetDecodeInput(decoder, nullptr, nullptr, status.data()) == InvalidArgument, "set input without input data"); + Require(status[0] == 7 && status[1] == 7, "an input refused for a missing decoder or input leaves the status alone"); + input.auData = nullptr; + Require(sceVdecCoreSetDecodeInput(decoder, &input, nullptr, status.data()) == InvalidArgument, "set input without access unit data"); + Require(status[0] == 1 && status[1] == 1, "the status is written before the access unit is checked"); + status = {7, 7}; + input.auData = unit.data(); + input.auSize = 0; + Require(sceVdecCoreSetDecodeInput(decoder, &input, nullptr, status.data()) == InvalidArgument, "set input with an empty access unit"); + Require(status[0] == 1 && status[1] == 1, "the status is written before the access unit size is checked"); + const std::uint64_t limit = VdecCoreMaxAccessUnitSize_nid_no_patch(); + const std::array oversized{1ull << 31, limit + 1, (1ull << 32) + unit.size(), ~0ull}; + for (const auto size : oversized) { + status = {7, 7}; + input.auSize = size; + Require(sceVdecCoreSetDecodeInput(decoder, &input, nullptr, status.data()) == InvalidArgument, "set input with an access unit past the packet limit"); + Require(status[0] == 1 && status[1] == 1, "the status is written before an oversized access unit is refused"); + } + input.auSize = unit.size(); + Require(sceVdecCoreSetDecodeInput(decoder, &input, nullptr, nullptr) == 0, "set input without a status"); + + std::uint32_t count = 7; + std::uint64_t pointers[2] = {7, 7}; + VdecCoreOutputInfo output{}; + VdecCoreFrameBuffer frameBuffer{}; + Require(sceVdecCoreResetDecoder(nullptr, nullptr) == InvalidArgument, "reset without a decoder"); + Require(sceVdecCoreSyncDecode(nullptr, &count) == InvalidArgument, "sync without a decoder"); + Require(sceVdecCoreSyncDecodeWptr(nullptr, &count, &pointers[0], &pointers[1]) == InvalidArgument, "write-pointer sync without a decoder"); + Require(sceVdecCoreTrySyncDecode(nullptr, &count) == InvalidArgument, "try-sync without a decoder"); + Require(sceVdecCoreTrySyncDecodeWptr(nullptr, &count, &pointers[0], &pointers[1]) == InvalidArgument, "write-pointer try-sync without a decoder"); + Require(sceVdecCoreGetDecodeOutput(nullptr, &frameBuffer, 0, &output, nullptr) == InvalidArgument, "get output without a decoder"); + Require(sceVdecCoreGetDecodeOutput(decoder, &frameBuffer, 0, nullptr, nullptr) == InvalidArgument, "get output without output info"); + Require(sceVdecCoreFlushDecodeOutput(nullptr, &count, nullptr) == InvalidArgument, "flush without a decoder"); + Require(count == 7 && pointers[0] == 7 && pointers[1] == 7, "refused calls leave their outputs alone"); + + Require(sceVdecCoreSyncDecode(decoder, nullptr) == 0, "sync without a count"); + Require(sceVdecCoreFlushDecodeOutput(decoder, nullptr, nullptr) == 0, "flush without a count"); + std::uint32_t held = 7; + Require(sceVdecCoreTrySyncDecode(decoder, nullptr) == 0 && sceVdecCoreTrySyncDecode(decoder, &held) == 0 && held == 1, "try-sync after a flush counts the held picture"); + Require(sceVdecCoreResetDecoder(decoder, nullptr) == 0, "reset"); + Require(sceVdecCoreDeleteDecoder(decoder) == 0, "delete the argument decoder"); +} + +void CheckEmptyQueue() { + void* decoder = CreateAvc(100); + std::uint32_t count = 7; + Require(sceVdecCoreTrySyncDecode(decoder, &count) == 0 && count == 0, "try-sync on an idle decoder"); + count = 7; + Require(sceVdecCoreSyncDecode(decoder, &count) == 0 && count == 0, "sync on an idle decoder"); + VdecCoreOutputInfo output; + std::memset(&output, 0xab, sizeof(output)); + Require(sceVdecCoreGetDecodeOutput(decoder, nullptr, 0, &output, nullptr) == NoPicture, "get output from an empty queue"); + Require(output.frameWidth == 100 && output.framePitch == 128 && output.frameHeight == H264Fixture::Height, "an empty queue reports the maximum width, its 64-byte pitch and the maximum height"); + Require(output.pictureCount == 0 && output.frameBuffer == nullptr && output.frameFormat == 0 && output.reserved0 == 0, "an empty queue reports no picture"); + Require(output.pictures[0].frameBuffer == nullptr && output.pictures[1].frameBuffer == nullptr && output.lumaBuffer == nullptr && output.chromaBuffer == nullptr, "an empty queue reports no buffers"); + Require(output.pictures[0].pts == 0xababababababababull && output.aspectRatio == 0xababababu && output.colourDescriptionPresentFlag == 0xababababu, "an empty queue leaves the other fields alone"); + Require(sceVdecCoreDeleteDecoder(decoder) == 0, "delete the idle decoder"); +} + +void CheckDecodeIntoSlots() { + void* decoder = CreateAvc(static_cast(H264Fixture::Width)); + std::vector memory(SlotSize * 4); + const VdecCoreMemoryBlock block{memory.data(), memory.size(), 256, 0}; + Require(sceVdecCoreMapMemoryBlock(decoder, &block, 1) == 0, "map four frame buffer slots"); + const std::vector slots = Slots(decoder); + Require(slots.size() == 4, "four slots"); + + std::size_t pictures = 0; + std::vector dts; + const auto take = [&](std::uint32_t available) { + for (std::uint32_t index = 0; index < available; ++index) { + const VdecCoreOutputInfo output = Output(decoder, nullptr); + Require(pictures < H264Fixture::PictureHashes.size(), "more pictures than access units"); + Require(output.frameBuffer == slots[pictures % slots.size()], "pictures go to the mapped slots in turn"); + const auto unit = H264Fixture::DisplayOrderUnits[pictures]; + RequirePicture(output, pictures, SlotSize, 1000 + unit, 3000 + unit, "a picture decoded into a slot"); + dts.push_back(output.pictures[0].dts); + ++pictures; + } + }; + for (std::size_t unit = 0; unit < Units().size(); ++unit) { + Feed(decoder, unit, 1000 + static_cast(unit), 2000 + static_cast(unit), 3000 + unit); + std::uint32_t tried = 0xffffffffu; + const int result = sceVdecCoreTrySyncDecode(decoder, &tried); + Require(result == 0 || (result == DecodeRunning && tried == 0), "try-sync during a decode"); + std::uint32_t count = 0xffffffffu; + Require(sceVdecCoreSyncDecode(decoder, &count) == 0, "sync after an access unit"); + take(count); + } + take(Flush(decoder)); + Require(pictures == H264Fixture::PictureHashes.size(), "every access unit gives a picture"); + for (const auto value : dts) Require(value >= 2000 && value < 2000 + Units().size(), "a picture's dts is one of the inputs'"); + VdecCoreOutputInfo output{}; + Require(sceVdecCoreGetDecodeOutput(decoder, nullptr, 0, &output, nullptr) == NoPicture, "no picture after the flushed ones"); + Require(sceVdecCoreDeleteDecoder(decoder) == 0, "delete the slot decoder"); +} + +void CheckInvalidPts() { + void* decoder = CreateAvc(static_cast(H264Fixture::Width)); + FeedAll(decoder, PtsInvalid, PtsInvalid, 5000); + Require(Flush(decoder) == H264Fixture::PictureHashes.size(), "a flush holds every picture"); + for (std::size_t picture = 0; picture < H264Fixture::PictureHashes.size(); ++picture) { + const VdecCoreOutputInfo output = Output(decoder, nullptr); + RequirePicture(output, picture, SlotSize, ~std::uint64_t{0}, 5000 + H264Fixture::DisplayOrderUnits[picture], "a picture without a pts"); + Require(output.pictures[0].dts == ~std::uint64_t{0}, "a picture without a dts takes the last input's invalid dts"); + } + Require(sceVdecCoreDeleteDecoder(decoder) == 0, "delete the invalid pts decoder"); +} + +void CheckDestinations() { + void* decoder = CreateAvc(static_cast(H264Fixture::Width)); + FeedAll(decoder, 1000, 2000, 3000); + Require(Flush(decoder) == H264Fixture::PictureHashes.size(), "a flush holds every picture for the fallback"); + const VdecCoreOutputInfo fallback = Output(decoder, nullptr); + RequirePicture(fallback, 0, SlotSize, 1000, 3000, "a picture in the fallback buffer"); + const VdecCoreOutputInfo again = Output(decoder, nullptr); + Require(again.frameBuffer == fallback.frameBuffer, "the fallback buffer is reused"); + RequirePicture(again, 1, SlotSize, 1000 + H264Fixture::DisplayOrderUnits[1], 3000 + H264Fixture::DisplayOrderUnits[1], "a second picture in the fallback buffer"); + + std::vector slotMemory(SlotSize); + const VdecCoreMemoryBlock block{slotMemory.data(), slotMemory.size(), 256, 0}; + Require(sceVdecCoreMapMemoryBlock(decoder, &block, 1) == 0, "map one slot"); + std::vector caller(0x2000); + VdecCoreFrameBuffer frameBuffer{caller.data(), caller.size()}; + const VdecCoreOutputInfo supplied = Output(decoder, &frameBuffer); + Require(supplied.frameBuffer == caller.data(), "a caller frame buffer comes before the slots"); + RequirePicture(supplied, 2, caller.size(), 1000 + H264Fixture::DisplayOrderUnits[2], 3000 + H264Fixture::DisplayOrderUnits[2], "a picture in the caller's frame buffer"); + VdecCoreFrameBuffer unaddressed{nullptr, caller.size()}; + const VdecCoreOutputInfo slot = Output(decoder, &unaddressed); + Require(slot.frameBuffer == slotMemory.data(), "a frame buffer without an address leaves the picture to the slots"); + RequirePicture(slot, 3, SlotSize, 1000 + H264Fixture::DisplayOrderUnits[3], 3000 + H264Fixture::DisplayOrderUnits[3], "a picture in a slot keeps the slot's size, not the caller's"); + Require(sceVdecCoreDeleteDecoder(decoder) == 0, "delete the destination decoder"); +} + +void CheckOutputStaysInItsDestination() { + void* decoder = CreateAvc(static_cast(H264Fixture::Width)); + FeedAll(decoder, 1000, 2000, 3000); + Require(Flush(decoder) == H264Fixture::PictureHashes.size(), "a flush holds every picture for the destination bounds"); + constexpr std::uint8_t Guard = 0x5a; + std::vector memory(SlotSize * 2, Guard); + const VdecCoreMemoryBlock block{memory.data(), SlotSize, 256, 0}; + Require(sceVdecCoreMapMemoryBlock(decoder, &block, 1) == 0, "map one slot followed by guard bytes"); + VdecCoreFrameBuffer larger{nullptr, SlotSize * 2}; + const VdecCoreOutputInfo slot = Output(decoder, &larger); + Require(slot.frameBuffer == memory.data(), "a frame buffer with no address and a larger size goes to the slot"); + RequirePicture(slot, 0, SlotSize, 1000, 3000, "a picture in a slot under a larger caller size"); + Require(std::all_of(memory.begin() + static_cast(SlotSize), memory.end(), [](std::uint8_t byte) { return byte == Guard; }), "a picture in a slot under a larger caller size wrote past the slot"); + const std::uint32_t pictureSize = OutputPitch * H264Fixture::CodedHeight * 3 / 2; + std::vector tight(pictureSize + 0x200, Guard); + VdecCoreFrameBuffer small{tight.data(), tight.size()}; + const VdecCoreOutputInfo caller = Output(decoder, &small); + Require(caller.frameBuffer == tight.data(), "a caller frame buffer with room for the picture only"); + const auto unit = H264Fixture::DisplayOrderUnits[1]; + Require(caller.pictures[0].pts == 1000 + unit && caller.pictures[0].userData == 3000 + unit, "the picture in a caller frame buffer with room for the picture only"); + Require(VisibleHash(tight.data(), caller.framePitch, caller.frameHeight) == VisiblePictureHashes[1] && ExtendsLastRows(tight.data(), caller.framePitch, caller.frameHeight), "picture information too large for the room after a picture overwrote the picture"); + Require(std::all_of(tight.begin() + static_cast(pictureSize), tight.end(), [](std::uint8_t byte) { return byte == Guard; }), "picture information was written into a caller frame buffer with no room for it after the picture"); + Require(sceVdecCoreDeleteDecoder(decoder) == 0, "delete the bounds decoder"); +} + +void CheckFlushResetAndSync() { + void* decoder = CreateAvc(static_cast(H264Fixture::Width)); + std::vector memory(SlotSize * 3); + const VdecCoreMemoryBlock block{memory.data(), memory.size(), 256, 0}; + Require(sceVdecCoreMapMemoryBlock(decoder, &block, 1) == 0, "map three slots"); + const std::vector slots = Slots(decoder); + + FeedAll(decoder, 1000, 2000, 3000); + const std::uint32_t held = Flush(decoder); + Require(held == H264Fixture::PictureHashes.size(), "a flush returns every held picture"); + std::uint32_t count = 0; + Require(sceVdecCoreSyncDecode(decoder, &count) == 0 && count == held, "sync after a flush counts the held pictures"); + std::uint32_t tried = 0; + Require(sceVdecCoreTrySyncDecode(decoder, &tried) == 0 && tried == held, "try-sync after a sync counts the held pictures"); + std::uint64_t pointers[2] = {7, 7}; + std::uint32_t forwarded = 0; + Require(sceVdecCoreSyncDecodeWptr(decoder, &forwarded, &pointers[0], &pointers[1]) == 0 && forwarded == held, "the write-pointer sync forwards to the sync"); + forwarded = 0; + Require(sceVdecCoreTrySyncDecodeWptr(decoder, &forwarded, &pointers[0], &pointers[1]) == 0 && forwarded == held, "the write-pointer try-sync forwards to the try-sync"); + Require(pointers[0] == 7 && pointers[1] == 7, "the write-pointer variants leave the pointers alone"); + + Require(Output(decoder, nullptr).frameBuffer == slots[0], "the first picture goes to the first slot"); + Require(Output(decoder, nullptr).frameBuffer == slots[1], "the second picture goes to the second slot"); + + FeedAll(decoder, 1000, 2000, 3000); + Require(sceVdecCoreResetDecoder(decoder, nullptr) == 0, "reset with queued input"); + count = 7; + Require(sceVdecCoreSyncDecode(decoder, &count) == 0 && count == 0, "a reset drops the queued input and its pictures"); + tried = 7; + Require(sceVdecCoreTrySyncDecode(decoder, &tried) == 0 && tried == 0, "try-sync after a reset"); + VdecCoreOutputInfo output{}; + Require(sceVdecCoreGetDecodeOutput(decoder, nullptr, 0, &output, nullptr) == NoPicture, "no picture after a reset"); + Require(Flush(decoder) == 0, "a flush after a reset holds nothing"); + + FeedAll(decoder, 1000, 2000, 3000); + Require(Flush(decoder) == H264Fixture::PictureHashes.size(), "the decoder decodes again after a reset"); + const VdecCoreOutputInfo restarted = Output(decoder, nullptr); + Require(restarted.frameBuffer == slots[0], "a reset starts the slots again"); + RequirePicture(restarted, 0, SlotSize, 1000, 3000, "the first picture after a reset"); + Require(sceVdecCoreDeleteDecoder(decoder) == 0, "delete the flushed decoder"); +} + +void CheckDeleteWithQueuedInput() { + void* decoder = CreateAvc(static_cast(H264Fixture::Width)); + for (int round = 0; round < 8; ++round) FeedAll(decoder, 1000, 2000, 3000); + Require(sceVdecCoreDeleteDecoder(decoder) == 0, "delete a decoder with queued input"); + void* held = CreateAvc(static_cast(H264Fixture::Width)); + FeedAll(held, 1000, 2000, 3000); + Require(Flush(held) == H264Fixture::PictureHashes.size(), "a flush before the delete holds every picture"); + Require(sceVdecCoreDeleteDecoder(held) == 0, "delete a decoder with held pictures"); +} + } int main() { @@ -183,5 +512,14 @@ int main() { CheckComputeResourceInfo(); CheckCreateAndDelete(); CheckMapMemoryBlock(); + CheckAccessUnitLength(); + CheckDecodeArguments(); + CheckEmptyQueue(); + CheckDecodeIntoSlots(); + CheckInvalidPts(); + CheckDestinations(); + CheckOutputStaysInItsDestination(); + CheckFlushResetAndSync(); + CheckDeleteWithQueuedInput(); return 0; } diff --git a/core/libs/tests/GuestVideodec2HevcAliases.cpp b/core/libs/tests/GuestVideodec2HevcAliases.cpp new file mode 100644 index 0000000000..2d13a8be37 --- /dev/null +++ b/core/libs/tests/GuestVideodec2HevcAliases.cpp @@ -0,0 +1,81 @@ +#include "prx/libSceVideodec2/include/Api.hpp" +#include "H264Fixture.hpp" +#include "HevcFixture.hpp" +#include +#include +#include +#include +#include +#include + +using namespace SceVideodec2; + +static void Require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +static auto Fields(const DecoderMemoryInfo& memory) { + return std::tie(memory.thisSize, memory.cpuMemorySize, memory.cpuMemory, memory.gpuMemorySize, memory.gpuMemory, + memory.cpuGpuMemorySize, memory.cpuGpuMemory, memory.maxFrameBufferSize, memory.frameBufferAlignment); +} + +template +static std::string Failure(TAction action) { + try { + action(); + } catch (const std::runtime_error& error) { + return error.what(); + } + throw std::runtime_error("invalid argument did not throw"); +} + +int main() { + try { + const DecoderConfigInfo config{sizeof(DecoderConfigInfo), 1, 974921, 1, 0, H264Fixture::Width, H264Fixture::Height, 4, 1}; + DecoderMemoryInfo generic{sizeof(DecoderMemoryInfo)}, alias{sizeof(DecoderMemoryInfo)}; + const auto expected = sceVideodec2QueryDecoderMemoryInfo_nid_postfix(&config, &generic); + Require(sceVideodec2QueryHevcDecoderMemoryInfo(&config, &alias) == expected && expected == 0, "memory query result differs"); + Require(Fields(alias) == Fields(generic), "memory query fields differ"); + Require(Failure([&] { sceVideodec2QueryHevcDecoderMemoryInfo(nullptr, &alias); }) == + Failure([&] { sceVideodec2QueryDecoderMemoryInfo_nid_postfix(nullptr, &generic); }), "query argument error differs"); + std::uint64_t handle = 0, reference = 0; + Require(Failure([&] { sceVideodec2CreateHevcDecoder(&config, nullptr, &handle); }) == + Failure([&] { sceVideodec2CreateDecoder_nid_postfix(&config, nullptr, &reference); }), "create argument error differs"); + Require(sceVideodec2CreateHevcDecoder(&config, &alias, &handle) == 0 && handle != 0, "alias creation failed"); + Require(sceVideodec2CreateDecoder_nid_postfix(&config, &generic, &reference) == 0, "generic creation failed"); + std::vector storage; + const auto buffer = HevcFixture::AlignedFrameStorage(storage, alias.maxFrameBufferSize, alias.frameBufferAlignment); + FrameBuffer frame{sizeof(FrameBuffer), buffer.data(), buffer.size(), false}; + OutputInfo output{sizeof(OutputInfo)}; + const InputData empty{sizeof(InputData), nullptr, 0, 0, 0, 0}; + const auto error = sceVideodec2Decode_nid_postfix(reference, &empty, &frame, &output); + Require(error != 0 && sceVideodec2Decode_nid_postfix(handle, &empty, &frame, &output) == error, "decoder error code differs"); + Require(sceVideodec2DeleteDecoder_nid_postfix(reference) == 0, "generic deletion failed"); + std::size_t pictures = 0; + const auto take = [&] { + Require(frame.isAccepted == output.isValid, "frame acceptance differs"); + if (!output.isValid) return false; + Require(pictures < HevcFixture::HevcHashes.size(), "too many pictures"); + Require(output.codecType == config.codecType && !output.isErrorFrame && output.frameWidth == H264Fixture::Width && + output.frameHeight == H264Fixture::Height && output.framePitch == 256 && output.frameBuffer == buffer.data(), "picture geometry differs"); + Require(H264Fixture::hashNv12(buffer.data(), output.framePitch, output.frameHeight) == HevcFixture::HevcHashes[pictures], "picture hash differs"); + ++pictures; + return true; + }; + const auto units = HevcFixture::hevcUnits(HevcFixture::HevcStream.data(), HevcFixture::HevcStream.size()); + for (const auto& unit : units) { + const InputData input{sizeof(InputData), unit.data(), unit.size(), 0, 0, 0}; + Require(sceVideodec2Decode_nid_postfix(handle, &input, &frame, &output) == 0, "decode failed"); + take(); + } + do { + Require(sceVideodec2Flush_nid_postfix(handle, &frame, &output) == 0, "flush failed"); + } while (take()); + Require(pictures == HevcFixture::HevcHashes.size(), "missing pictures"); + Require(sceVideodec2DeleteDecoder_nid_postfix(handle) == 0, "alias deletion failed"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/libs/tests/GuestVideodec2Unimplemented.cpp b/core/libs/tests/GuestVideodec2Unimplemented.cpp index 090fdf8327..2cdb779066 100644 --- a/core/libs/tests/GuestVideodec2Unimplemented.cpp +++ b/core/libs/tests/GuestVideodec2Unimplemented.cpp @@ -29,7 +29,5 @@ int main() { passed = ReportsNotImplemented("sceVideodec2PresetDecoderConfigInfoHevc", [] { return sceVideodec2PresetDecoderConfigInfoHevc(); }) && passed; passed = ReportsNotImplemented("sceVideodec2PresetDecoderConfigInfoVp9", [] { return sceVideodec2PresetDecoderConfigInfoVp9(); }) && passed; passed = ReportsNotImplemented("sceVideodec2CreateDecoderBid", [] { return sceVideodec2CreateDecoderBid(); }) && passed; - passed = ReportsNotImplemented("sceVideodec2CreateHevcDecoder", [] { return sceVideodec2CreateHevcDecoder(); }) && passed; - passed = ReportsNotImplemented("sceVideodec2QueryHevcDecoderMemoryInfo", [] { return sceVideodec2QueryHevcDecoderMemoryInfo(); }) && passed; return passed ? 0 : 1; } diff --git a/core/libs/tests/GuestWcscmpBoundary.cpp b/core/libs/tests/GuestWcscmpBoundary.cpp new file mode 100644 index 0000000000..156b9b92cf --- /dev/null +++ b/core/libs/tests/GuestWcscmpBoundary.cpp @@ -0,0 +1,94 @@ +#include "prx/libc/include/Api.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static void Require(bool condition) { + if (!condition) { + std::fputs("GuestWcscmpBoundary check failed\n", stderr); + std::abort(); + } +} + +#ifdef _WIN32 +#include +#else +#include +#include +#endif + +class Buffer { + unsigned char* allocation; + std::size_t page; +public: + Buffer() { +#ifdef _WIN32 + SYSTEM_INFO info{}; + GetSystemInfo(&info); + page = info.dwPageSize; + allocation = static_cast(VirtualAlloc(nullptr, page * 3, MEM_RESERVE | MEM_COMMIT, PAGE_NOACCESS)); + Require(allocation != nullptr); + DWORD previous; + Require(VirtualProtect(allocation + page, page, PAGE_READWRITE, &previous) != 0); +#else + const long size = sysconf(_SC_PAGESIZE); + Require(size > 0); + page = static_cast(size); + allocation = static_cast(mmap(nullptr, page * 3, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)); + Require(allocation != MAP_FAILED); + Require(mprotect(allocation + page, page, PROT_READ | PROT_WRITE) == 0); +#endif + std::memset(allocation + page, 0xa5, page); + } + ~Buffer() { +#ifdef _WIN32 + Require(VirtualFree(allocation, 0, MEM_RELEASE) != 0); +#else + Require(munmap(allocation, page * 3) == 0); +#endif + } + Buffer(const Buffer&) = delete; + Buffer& operator=(const Buffer&) = delete; + template + T* At(std::size_t count) { + Require(count <= (page - 32) / sizeof(T)); + return reinterpret_cast(allocation + page * 2 - (std::max)(count, std::size_t{1}) * sizeof(T)); + } +}; + +static int Sign(int value) { + return (value > 0) - (value < 0); +} + +int main() { + Buffer first, second; + for (std::size_t length = 0; length <= 128; ++length) { + auto* left = first.At(length + 1); + auto* right = second.At(length + 1); + for (std::size_t i = 0; i < length; ++i) left[i] = static_cast(0x100 + i * 257); + left[length] = 0; + const std::u16string saved(left, length + 1); + for (std::size_t position = 0; position <= length; ++position) { + for (char16_t value : {0, 1, 0xff, 0x100, 0x8000, 0xd800, 0xffff}) { + if (position == length && value != 0) continue; + std::copy(saved.begin(), saved.end(), right); + right[position] = value; + int expected = 0; + for (std::size_t i = 0; i <= length; ++i) { + if (left[i] != right[i]) { expected = left[i] > right[i] ? 1 : -1; break; } + if (left[i] == 0) break; + } + const std::u16string other(right, length + 1); + Require(Sign(wcscmp_nid_postfix(left, right)) == expected); + Require(Sign(wcscmp_nid_postfix(right, left)) == -expected); + Require(std::equal(saved.begin(), saved.end(), left) && std::equal(other.begin(), other.end(), right)); + } + } + } +} diff --git a/core/libs/tests/GuestWcslenBoundary.cpp b/core/libs/tests/GuestWcslenBoundary.cpp new file mode 100644 index 0000000000..af78a54e6b --- /dev/null +++ b/core/libs/tests/GuestWcslenBoundary.cpp @@ -0,0 +1,75 @@ +#include "prx/libc/include/Api.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +static void Require(bool condition) { + if (!condition) { + std::fputs("GuestWcslenBoundary check failed\n", stderr); + std::abort(); + } +} + +#ifdef _WIN32 +#include +#else +#include +#include +#endif + +class Buffer { + unsigned char* allocation; + std::size_t page; +public: + Buffer() { +#ifdef _WIN32 + SYSTEM_INFO info{}; + GetSystemInfo(&info); + page = info.dwPageSize; + allocation = static_cast(VirtualAlloc(nullptr, page * 3, MEM_RESERVE | MEM_COMMIT, PAGE_NOACCESS)); + Require(allocation != nullptr); + DWORD previous; + Require(VirtualProtect(allocation + page, page, PAGE_READWRITE, &previous) != 0); +#else + const long size = sysconf(_SC_PAGESIZE); + Require(size > 0); + page = static_cast(size); + allocation = static_cast(mmap(nullptr, page * 3, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)); + Require(allocation != MAP_FAILED); + Require(mprotect(allocation + page, page, PROT_READ | PROT_WRITE) == 0); +#endif + std::memset(allocation + page, 0xa5, page); + } + ~Buffer() { +#ifdef _WIN32 + Require(VirtualFree(allocation, 0, MEM_RELEASE) != 0); +#else + Require(munmap(allocation, page * 3) == 0); +#endif + } + Buffer(const Buffer&) = delete; + Buffer& operator=(const Buffer&) = delete; + template + T* At(std::size_t count) { + Require(count <= (page - 32) / sizeof(T)); + return reinterpret_cast(allocation + page * 2 - (std::max)(count, std::size_t{1}) * sizeof(T)); + } +}; + +int main() { + Buffer input; + const std::array values{0x100, 1, 0x8000, 0xd800, 0xdc00, 0xffff}; + for (std::size_t length = 0; length <= 256; ++length) { + auto* source = input.At(length + 1); + for (std::size_t i = 0; i < length; ++i) source[i] = values[(i + length) % values.size()]; + source[length] = 0; + const std::u16string saved(source, length + 1); + Require(wcslen_nid_postfix(source) == length); + Require(std::equal(saved.begin(), saved.end(), source)); + } +} diff --git a/core/libs/tests/GuestWcsncatExtent.cpp b/core/libs/tests/GuestWcsncatExtent.cpp new file mode 100644 index 0000000000..a4d00e44ea --- /dev/null +++ b/core/libs/tests/GuestWcsncatExtent.cpp @@ -0,0 +1,93 @@ +#include "prx/libc/include/Api.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static void Require(bool condition) { + if (!condition) { + std::fputs("GuestWcsncatExtent check failed\n", stderr); + std::abort(); + } +} + +#ifdef _WIN32 +#include +#else +#include +#include +#endif + +class Buffer { + unsigned char* allocation; + std::size_t page; +public: + Buffer() { +#ifdef _WIN32 + SYSTEM_INFO info{}; + GetSystemInfo(&info); + page = info.dwPageSize; + allocation = static_cast(VirtualAlloc(nullptr, page * 3, MEM_RESERVE | MEM_COMMIT, PAGE_NOACCESS)); + Require(allocation != nullptr); + DWORD previous; + Require(VirtualProtect(allocation + page, page, PAGE_READWRITE, &previous) != 0); +#else + const long size = sysconf(_SC_PAGESIZE); + Require(size > 0); + page = static_cast(size); + allocation = static_cast(mmap(nullptr, page * 3, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)); + Require(allocation != MAP_FAILED); + Require(mprotect(allocation + page, page, PROT_READ | PROT_WRITE) == 0); +#endif + std::memset(allocation + page, 0xa5, page); + } + ~Buffer() { +#ifdef _WIN32 + Require(VirtualFree(allocation, 0, MEM_RELEASE) != 0); +#else + Require(munmap(allocation, page * 3) == 0); +#endif + } + Buffer(const Buffer&) = delete; + Buffer& operator=(const Buffer&) = delete; + template + T* At(std::size_t count) { + Require(count <= (page - 32) / sizeof(T)); + return reinterpret_cast(allocation + page * 2 - (std::max)(count, std::size_t{1}) * sizeof(T)); + } +}; + +template +static void Prefix(const T* data) { + for (int index = 1; index <= 8; ++index) Require(data[-index] == static_cast(sizeof(T) == 1 ? 0xa5 : 0xa5a5)); +} + +int main() { + Buffer input, output; + for (std::size_t limit = 0; limit <= 128; ++limit) { + for (std::size_t length : {std::size_t{0}, std::size_t{1}, limit, limit + 3}) { + auto* source = input.At(length + 1); + for (std::size_t i = 0; i < length; ++i) source[i] = static_cast(0x100 + i * 257); + source[length] = 0; + const std::u16string saved(source, length + 1); + const auto copied = (std::min)(length, limit); + for (std::size_t prefix : {0u, 1u, 15u, 31u}) { + auto* destination = output.At(prefix + copied + 1); + std::memset(destination - 8, 0xa5, (prefix + copied + 9) * sizeof(char16_t)); + const std::u16string start(prefix, 0xd800); + std::copy(start.begin(), start.end(), destination); + destination[prefix] = 0; + const auto expected = start + saved.substr(0, copied) + std::u16string(1, 0); + Require(wcsncat_nid_postfix(destination, source, limit) == destination); + Require(std::equal(expected.begin(), expected.end(), destination)); + Require(std::equal(saved.begin(), saved.end(), source)); + Prefix(destination); + } + } + } +} diff --git a/core/libs/tests/GuestWideStreamScan.cpp b/core/libs/tests/GuestWideStreamScan.cpp index 1f4f1971d0..b2cbc20865 100644 --- a/core/libs/tests/GuestWideStreamScan.cpp +++ b/core/libs/tests/GuestWideStreamScan.cpp @@ -2,10 +2,12 @@ #include "prx/libc/include/FileStream.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include "tests/TemporaryFile.hpp" +#include #include #include #include #include +#include #include extern "C" { @@ -55,6 +57,27 @@ static int APS5_VABI ScanInputSized(const char16_t* format, ...) { return result; } +static int APS5_VABI ScanNarrow(FileStream* stream, const char* format, ...) { + APS5_VA_BEGIN(format); + const int result = vfscanf_nid_postfix(stream, format, reinterpret_cast(args)); + APS5_VA_END(); + return result; +} + +static int APS5_VABI ScanNarrowSized(FileStream* stream, const char* format, ...) { + APS5_VA_BEGIN(format); + const int result = vfscanf_s_nid_postfix(stream, format, reinterpret_cast(args)); + APS5_VA_END(); + return result; +} + +static int APS5_VABI ScanNarrowInput(const char* format, ...) { + APS5_VA_BEGIN(format); + const int result = vscanf_nid_postfix(format, reinterpret_cast(args)); + APS5_VA_END(); + return result; +} + static FileStream* Open(const char* text) { auto* stream = new FileStream(TemporaryFile::Open()); Require(std::fputs(text, stream->GetHandle()) >= 0, "seed the stream"); @@ -67,6 +90,61 @@ static void Close(FileStream* stream) { delete stream; } +static void CheckNarrowFormatWideFields() { + std::int32_t value = 0; + std::int64_t count = -1; + char16_t word[8]{}; + char16_t letter = u'!'; + char narrow[8]{}; + + FileStream* stream = Open(" zlu ku 7"); + Require(fscanf_nid_postfix(stream, "%ls %lc", word, &letter) == 2 && std::u16string(word) == u"zlu" && letter == u'k', "fscanf %ls and %lc"); + Require(ScanNarrow(stream, "%S%ln %d", word, &count, &value) == 2 && std::u16string(word) == u"u" && count == 1 && value == 7, "vfscanf %S with %ln counting bytes"); + Require(fgetc_nid_postfix(stream) == EOF, "the whole input is consumed"); + Close(stream); + + stream = Open("12 ab cd"); + Require(fscanf_nid_postfix(stream, "%d %ls %7s", &value, word, narrow) == 3 && value == 12 && std::u16string(word) == u"ab" && std::strcmp(narrow, "cd") == 0, "wide and narrow fields share the stream"); + Close(stream); + + stream = Open("ab\xc3\xa9t rest"); + Require(fscanf_nid_postfix(stream, "%l[a-c]", word) == 1 && std::u16string(word) == u"ab", "a scanset stops at a byte outside it"); + *__error_nid_postfix() = 0; + Require(fscanf_nid_postfix(stream, "%l[^ ] %7s", word, narrow) == 0 && *__error_nid_postfix() == 86, "a byte after a UTF-8 lead byte fails the field"); + Require(fgetc_nid_postfix(stream) == 0xc3 && fgetc_nid_postfix(stream) == 0xa9, "the whole invalid multibyte character stays in the stream"); + Close(stream); + + char16_t three[5] = {u'!', u'!', u'!', u'!', u'!'}; + stream = Open("xyzw"); + Require(fscanf_nid_postfix(stream, "%3lc", three) == 1 && three[0] == u'x' && three[1] == u'y' && three[2] == u'z' && three[3] == u'!', "%3lc stores three units without a terminator"); + Require(fgetc_nid_postfix(stream) == 'w', "the character after %3lc stays in the stream"); + Close(stream); + + stream = Open("xyz"); + Require(fscanf_nid_postfix(stream, "%l[a-c]", word) == 0, "a scanset matching nothing"); + Require(fgetc_nid_postfix(stream) == 'x', "the rejected character stays in the stream"); + Close(stream); + + stream = Open(" "); + Require(fscanf_nid_postfix(stream, "%ls", word) == EOF, "only white space before %ls"); + Close(stream); + + stream = Open("hello world"); + char16_t fits[6]{}; + char16_t small[4] = {u'!', u'!', u'!', u'!'}; + Require(fscanf_s_nid_postfix(stream, "%ls", fits, 6u) == 1 && std::u16string(fits) == u"hello", "fscanf_s %ls that fits"); + Require(ScanNarrowSized(stream, "%ls", small, 4u) == 0 && small[0] == 0, "vfscanf_s rejects a too small buffer"); + Close(stream); + + FileStream* const standardInput = __stdinp_nid_postfix; + stream = Open("ab c"); + __stdinp_nid_postfix = stream; + Require(scanf_nid_postfix("%ls", word) == 1 && std::u16string(word) == u"ab", "scanf %ls reads the guest standard input"); + Require(ScanNarrowInput(" %lc", &letter) == 1 && letter == u'c', "vscanf %lc reads the guest standard input"); + __stdinp_nid_postfix = standardInput; + Close(stream); +} + static void CheckConsumption() { std::int32_t value = -1; std::int64_t count = -1; @@ -162,9 +240,73 @@ static void CheckStandardInput() { Close(stream); } +static void CheckNarrowWideFailurePosition() { + struct Case { + const char* input; + const char* format; + int result; + long position; + int next; + int error; + int count; + std::array output; + const char* remaining; + }; + const Case cases[]{ + {"\xc3\xa9", "%ls%n", 0, 0, 195, 86, -77, {0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5}, "\xc3\xa9"}, + {"\x61\xc3\xa9", "%ls%n", 0, 1, 195, 86, -77, {0x61, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5}, "\xc3\xa9"}, + {"\x61\x62\x80", "%ls%n", 0, 2, 128, 86, -77, {0x61, 0x62, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5}, "\x80"}, + {"\xc3\x20\x61", "%ls%n", -1, 1, 32, 97, -77, {0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5}, "\x20\x61"}, + {"\xc3", "%ls%n", -1, 1, -1, 97, -77, {0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5}, ""}, + {"\x80", "%ls%n", -1, 0, 128, 86, -77, {0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5}, "\x80"}, + {"\x61\x80", "%ls%n", 0, 1, 128, 86, -77, {0x61, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5}, "\x80"}, + {"\xc3\xa9", "%1ls%n", -1, 1, 169, 97, -77, {0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5}, "\xa9"}, + {"\x61\xc3\xa9", "%2ls%n", -1, 2, 169, 97, -77, {0x61, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5}, "\xa9"}, + {"\x61\xc3\xa9", "%3ls%n", 0, 1, 195, 86, -77, {0x61, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5}, "\xc3\xa9"}, + {"\xc3\xa9", "%2lc%n", 0, 0, 195, 86, -77, {0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5}, "\xc3\xa9"}, + {"\x61\xc3\xa9", "%l[^ ]%n", 0, 1, 195, 86, -77, {0x61, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5}, "\xc3\xa9"}, + {"\xc3\xa9", "%S%n", 0, 0, 195, 86, -77, {0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5}, "\xc3\xa9"}, + {"\x61\xc3\xa9", "%lS%n", 0, 1, 195, 86, -77, {0x61, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5}, "\xc3\xa9"}, + {"\xff\x7a", "%ls%n", -1, 1, 122, 86, -77, {0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5}, "\x7a"}, + {"\x61\xff\x7a", "%ls%n", 0, 2, 122, 86, -77, {0x61, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5}, "\x7a"}, + {"\xc3\xff\x7a", "%ls%n", 0, 1, 195, 86, -77, {0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5}, "\xc3\x7a"}, + {"\xc3\x80\x7a", "%ls%n", 0, 0, 195, 86, -77, {0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5}, "\xc3\x80\x7a"}, + {"\xc3\xc3\x7a", "%ls%n", 0, 0, 195, 86, -77, {0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5}, "\xc3\xc3\x7a"}, + {"\xc3\xff\x7a", "%2lc%n", 0, 1, 195, 86, -77, {0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5}, "\xc3\x7a"}, + {"\xc3\xff\x7a", "%3ls%n", 0, 1, 195, 86, -77, {0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5}, "\xc3\x7a"}, + {"\xc3\xff\x7a", "%l[^ ]%n", 0, 1, 195, 86, -77, {0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5}, "\xc3\x7a"}, + {"\xff\x7a", "%S%n", -1, 1, 122, 86, -77, {0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5}, "\x7a"}, + {"\xc3\xff\x7a", "%lS%n", 0, 1, 195, 86, -77, {0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5, 0xa5a5}, "\xc3\x7a"}, + }; + for (const auto& item : cases) { + for (const bool secure : {false, true}) { + for (const bool variadic : {false, true}) { + auto* stream = Open(item.input); + std::array output; + output.fill(0xa5a5); + int count = -77; + *__error_nid_postfix() = 97; + const int result = secure ? (variadic ? ScanNarrowSized(stream, item.format, output.data(), 8u, &count) : fscanf_s_nid_postfix(stream, item.format, output.data(), 8u, &count)) : (variadic ? ScanNarrow(stream, item.format, output.data(), &count) : fscanf_nid_postfix(stream, item.format, output.data(), &count)); + Require(result == item.result && count == item.count && output == item.output, "wide field failure keeps measured result, count and stored units"); + Require(*__error_nid_postfix() == item.error, "wide field failure keeps measured errno"); + Require(ftell_nid_postfix(stream) == item.position, "wide field failure keeps the measured stream position"); + const int next = fgetc_nid_postfix(stream); + Require(next == item.next, "wide field failure leaves the measured next input byte"); + std::string remaining; + if (next != EOF) remaining += static_cast(next); + for (int byte; (byte = fgetc_nid_postfix(stream)) != EOF;) remaining += static_cast(byte); + Require(remaining == item.remaining, "wide field failure keeps the measured remaining input"); + Close(stream); + } + } + } +} + int main() { + CheckNarrowWideFailurePosition(); CheckConsumption(); CheckWideText(); CheckStandardInput(); + CheckNarrowFormatWideFields(); return 0; } diff --git a/core/libs/tests/GuestWideTokenBoundary.cpp b/core/libs/tests/GuestWideTokenBoundary.cpp new file mode 100644 index 0000000000..0b826277fb --- /dev/null +++ b/core/libs/tests/GuestWideTokenBoundary.cpp @@ -0,0 +1,105 @@ +#include "prx/libc/include/Api.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static void Require(bool condition) { + if (!condition) { + std::fputs("GuestWideTokenBoundary check failed\n", stderr); + std::abort(); + } +} + +#ifdef _WIN32 +#include +#else +#include +#include +#endif + +class Buffer { + unsigned char* allocation; + std::size_t page; +public: + Buffer() { +#ifdef _WIN32 + SYSTEM_INFO info{}; + GetSystemInfo(&info); + page = info.dwPageSize; + allocation = static_cast(VirtualAlloc(nullptr, page * 3, MEM_RESERVE | MEM_COMMIT, PAGE_NOACCESS)); + Require(allocation != nullptr); + DWORD previous; + Require(VirtualProtect(allocation + page, page, PAGE_READWRITE, &previous) != 0); +#else + const long size = sysconf(_SC_PAGESIZE); + Require(size > 0); + page = static_cast(size); + allocation = static_cast(mmap(nullptr, page * 3, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)); + Require(allocation != MAP_FAILED); + Require(mprotect(allocation + page, page, PROT_READ | PROT_WRITE) == 0); +#endif + std::memset(allocation + page, 0xa5, page); + } + ~Buffer() { +#ifdef _WIN32 + Require(VirtualFree(allocation, 0, MEM_RELEASE) != 0); +#else + Require(munmap(allocation, page * 3) == 0); +#endif + } + Buffer(const Buffer&) = delete; + Buffer& operator=(const Buffer&) = delete; + template + T* At(std::size_t count) { + Require(count <= (page - 32) / sizeof(T)); + return reinterpret_cast(allocation + page * 2 - (std::max)(count, std::size_t{1}) * sizeof(T)); + } +}; + +template +static void Prefix(const T* data) { + for (int index = 1; index <= 8; ++index) Require(data[-index] == static_cast(sizeof(T) == 1 ? 0xa5 : 0xa5a5)); +} + +int main() { + Buffer input, delimiters, other; + auto* set = delimiters.At(3); + set[0] = 0x100; set[1] = 0xd800; set[2] = 0; + const std::u16string separator(set, 2); + for (std::size_t length = 0; length <= 128; ++length) { + auto* source = input.At(length + 1); + const std::array values{0x100, 1, 0x8000, 0xd800, 0xffff, 0x100, 0xdc00}; + for (std::size_t i = 0; i < length; ++i) source[i] = values[(i + length) % values.size()]; + source[length] = 0; + const std::u16string saved(source, length + 1); + std::vector expected; + std::u16string field; + for (std::size_t i = 0; i < length; ++i) { + if (separator.find(saved[i]) != std::u16string::npos) { + if (!field.empty()) { expected.push_back(field); field.clear(); } + } else field.push_back(saved[i]); + } + if (!field.empty()) expected.push_back(field); + auto* alternate = other.At(4); + alternate[0] = 7; alternate[1] = 0x100; alternate[2] = 9; alternate[3] = 0; + char16_t* state = nullptr; + char16_t* otherState = nullptr; + Require(wcstok_nid_postfix(alternate, set, &otherState) == alternate && alternate[0] == 7 && alternate[1] == 0); + for (std::size_t i = 0; i < expected.size(); ++i) { + auto* token = wcstok_nid_postfix(i == 0 ? source : nullptr, set, &state); + Require(token != nullptr && std::u16string(token) == expected[i]); + } + Require(wcstok_nid_postfix(expected.empty() ? source : nullptr, set, &state) == nullptr); + Require(wcstok_nid_postfix(nullptr, set, &state) == nullptr); + auto* last = wcstok_nid_postfix(nullptr, set, &otherState); + Require(last == alternate + 2 && last[0] == 9 && last[1] == 0); + for (std::size_t i = 0; i <= length; ++i) if (separator.find(saved[i]) == std::u16string::npos) Require(source[i] == saved[i]); + Require(set[0] == 0x100 && set[1] == 0xd800 && set[2] == 0); + } +} diff --git a/core/libs/tests/GuestWmemchrBoundary.cpp b/core/libs/tests/GuestWmemchrBoundary.cpp new file mode 100644 index 0000000000..889e712372 --- /dev/null +++ b/core/libs/tests/GuestWmemchrBoundary.cpp @@ -0,0 +1,85 @@ +#include "prx/libc/include/Api.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static void Require(bool condition) { + if (!condition) { + std::fputs("GuestWmemchrBoundary check failed\n", stderr); + std::abort(); + } +} + +#ifdef _WIN32 +#include +#else +#include +#include +#endif + +class Buffer { + unsigned char* allocation; + std::size_t page; +public: + Buffer() { +#ifdef _WIN32 + SYSTEM_INFO info{}; + GetSystemInfo(&info); + page = info.dwPageSize; + allocation = static_cast(VirtualAlloc(nullptr, page * 3, MEM_RESERVE | MEM_COMMIT, PAGE_NOACCESS)); + Require(allocation != nullptr); + DWORD previous; + Require(VirtualProtect(allocation + page, page, PAGE_READWRITE, &previous) != 0); +#else + const long size = sysconf(_SC_PAGESIZE); + Require(size > 0); + page = static_cast(size); + allocation = static_cast(mmap(nullptr, page * 3, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)); + Require(allocation != MAP_FAILED); + Require(mprotect(allocation + page, page, PROT_READ | PROT_WRITE) == 0); +#endif + std::memset(allocation + page, 0xa5, page); + } + ~Buffer() { +#ifdef _WIN32 + Require(VirtualFree(allocation, 0, MEM_RELEASE) != 0); +#else + Require(munmap(allocation, page * 3) == 0); +#endif + } + Buffer(const Buffer&) = delete; + Buffer& operator=(const Buffer&) = delete; + template + T* At(std::size_t count) { + Require(count <= (page - 32) / sizeof(T)); + return reinterpret_cast(allocation + page * 2 - (std::max)(count, std::size_t{1}) * sizeof(T)); + } +}; + +template +static void Prefix(const T* data) { + for (int index = 1; index <= 8; ++index) Require(data[-index] == static_cast(sizeof(T) == 1 ? 0xa5 : 0xa5a5)); +} + +int main() { + Buffer input; + const std::array values{0, 1, 0xff, 0x100, 0x8000, 0xd800, 0xffff}; + for (std::size_t length = 0; length <= 192; ++length) { + auto* source = input.At(length); + const auto extent = (std::max)(length, std::size_t{1}); + for (std::size_t i = 0; i < extent; ++i) source[i] = values[(i + length) % values.size()]; + const std::vector saved(source, source + extent); + for (char16_t target : {0, 1, 0xff, 0x100, 0x8000, 0xd800, 0xdc00, 0xffff}) { + const auto found = std::find(saved.begin(), saved.begin() + length, target); + const auto* expected = found == saved.begin() + length ? nullptr : source + (found - saved.begin()); + Require(wmemchr_nid_postfix(source, target, length) == expected); + Require(std::equal(saved.begin(), saved.end(), source)); + } + } +} diff --git a/core/libs/tests/ResolverRaw.cpp b/core/libs/tests/ResolverRaw.cpp index 0411d8b5b4..73a91266fe 100644 --- a/core/libs/tests/ResolverRaw.cpp +++ b/core/libs/tests/ResolverRaw.cpp @@ -58,6 +58,15 @@ int main() { Address("::1", 28, protocol, flags); } } + for (int flags : {2, 6}) { + GuestResolver::AddressInfo scoped{}; + scoped.flags = flags; + scoped.socketType = 3; + GuestResolver::AddressInfo* named = nullptr; + Require(getaddrinfo_nid_postfix("fe80::1%1", nullptr, &scoped, &named) == 0 && named, "scoped raw address resolution failed"); + Require(named->canonicalName && std::strcmp(named->canonicalName, "fe80::1%1") == 0, "scoped numeric canonical name differs"); + freeaddrinfo_nid_postfix(named); + } GuestResolver::AddressInfo hints{}; hints.flags = 4; hints.socketType = 3; diff --git a/core/libs/tests/SystemServiceLanguageLocale.cpp b/core/libs/tests/SystemServiceLanguageLocale.cpp new file mode 100644 index 0000000000..257556e9cd --- /dev/null +++ b/core/libs/tests/SystemServiceLanguageLocale.cpp @@ -0,0 +1,307 @@ +#include "prx/libSceSystemService/LanguageLocale.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void Require(bool value, std::string_view what) { + if (value) return; + std::fprintf(stderr, "check failed: %.*s\n", static_cast(what.size()), what.data()); + std::abort(); +} + +struct ParsedTag { + std::string_view text; + std::string_view language; + std::string_view script; + std::string_view region; +}; + +void RequireBcp47Parsing() { + constexpr ParsedTag accepted[] = { + {"en", "en", "", ""}, + {"en-US", "en", "", "US"}, + {"EN-us", "en", "", "US"}, + {"pt-BR", "pt", "", "BR"}, + {"es-419", "es", "", "419"}, + {"zh-Hant-TW", "zh", "Hant", "TW"}, + {"zh-hans-cn", "zh", "Hans", "CN"}, + {"ZH-HANT", "zh", "Hant", ""}, + {"sr-Latn-RS", "sr", "Latn", "RS"}, + {"fil", "fil", "", ""}, + {"en-US-u-ca-gregory", "en", "", "US"}, + {"de-DE-1996", "de", "", "DE"}, + {"ca-ES-valencia", "ca", "", "ES"}, + {"en-x-private", "en", "", ""}, + {"en-Latn-US-x-a", "en", "Latn", "US"}, + }; + for (const auto& expected : accepted) { + SystemServiceLocale::Tag tag; + Require(SystemServiceLocale::ParseTag(expected.text, tag), expected.text); + Require(tag.language == expected.language, expected.text); + Require(tag.script == expected.script, expected.text); + Require(tag.region == expected.region, expected.text); + } + for (std::string_view rejected : {"", "-", "en-", "-en", "en--US", "e", "english", "e1", "12", "en_US", "en-US.UTF-8", "en US", "en-U$", "en-abcdefghi", "C", "x-foo"}) { + SystemServiceLocale::Tag tag{"keep", "keep", "keep"}; + const bool parsed = SystemServiceLocale::ParseTag(rejected, tag); + Require(!parsed, rejected); + Require(tag.language == "keep" && tag.script == "keep" && tag.region == "keep", rejected); + } +} + +void RequirePosixNormalisation() { + constexpr std::pair names[] = { + {"en_US.UTF-8", "en-US"}, + {"en_GB.UTF-8@euro", "en-GB"}, + {"de_DE.ISO-8859-15@euro", "de-DE"}, + {"pt_BR", "pt-BR"}, + {"zh_TW", "zh-TW"}, + {"zh_Hant_TW", "zh-Hant-TW"}, + {"ca_ES@valencia", "ca-ES"}, + {"sr_RS@latin", "sr-Latn-RS"}, + {"sr@latin", "sr-Latn"}, + {"sr_Latn_RS@latin", "sr-Latn-RS"}, + {"uz_UZ@Cyrillic", "uz-Cyrl-UZ"}, + {"fr", "fr"}, + {"en-US", "en-US"}, + {"EN_us.utf8", "EN-us"}, + {"C", ""}, + {"POSIX", ""}, + {"c", ""}, + {"posix", ""}, + {"C.UTF-8", ""}, + {"C.utf8@euro", ""}, + {"", ""}, + {"@latin", ""}, + {".UTF-8", ""}, + }; + for (const auto& [name, expected] : names) { + Require(SystemServiceLocale::NormalizePosixLocale(name) == expected, name); + } +} + +void RequireEitherForm() { + constexpr ParsedTag locales[] = { + {"en_US.UTF-8", "en", "", "US"}, + {"en-US", "en", "", "US"}, + {"zh_Hant_TW.UTF-8", "zh", "Hant", "TW"}, + {"zh-Hans-CN", "zh", "Hans", "CN"}, + {"sr_RS@latin", "sr", "Latn", "RS"}, + {"pt_BR.utf8", "pt", "", "BR"}, + {"es_419", "es", "", "419"}, + {"fr", "fr", "", ""}, + }; + for (const auto& expected : locales) { + SystemServiceLocale::Tag tag; + Require(SystemServiceLocale::ParseLocale(expected.text, tag), expected.text); + Require(tag.language == expected.language && tag.script == expected.script && tag.region == expected.region, expected.text); + } + for (std::string_view none : {"", "C", "POSIX", "C.UTF-8", "@latin", "garbage!", "english", "e", "en__US", "_US", "123"}) { + SystemServiceLocale::Tag tag{"keep", "keep", "keep"}; + Require(!SystemServiceLocale::ParseLocale(none, tag), none); + Require(tag.language == "keep" && tag.script == "keep" && tag.region == "keep", none); + } +} + +struct LocaleLanguage { + std::string_view locale; + int id; +}; + +int LanguageOf(std::string_view locale) { + SystemServiceLocale::Tag tag; + if (!SystemServiceLocale::ParseLocale(locale, tag)) return SystemServiceLocale::NoConsoleLanguage; + return SystemServiceLocale::ConsoleLanguageForTag(tag); +} + +void RequireLanguages(const LocaleLanguage* first, const LocaleLanguage* last) { + for (const LocaleLanguage* entry = first; entry != last; ++entry) Require(LanguageOf(entry->locale) == entry->id, entry->locale); +} + +template +void RequireLanguages(const LocaleLanguage (&entries)[N]) { + RequireLanguages(entries, entries + N); +} + +void RequireLatinEuropeanRows() { + using namespace SystemServiceLocale; + constexpr LocaleLanguage entries[] = { + {"en", EnglishUs}, {"en-US", EnglishUs}, {"en_US.UTF-8", EnglishUs}, {"EN-us", EnglishUs}, {"en-CA", EnglishUs}, {"en-IN", EnglishUs}, + {"en-GB", EnglishUk}, {"en_GB.UTF-8@euro", EnglishUk}, {"en-IE", EnglishUk}, {"en-AU", EnglishUk}, {"en-NZ", EnglishUk}, {"en-Latn-GB", EnglishUk}, + {"fr", French}, {"fr-FR", French}, {"fr_BE.UTF-8", French}, {"fr-CH", French}, {"fr-CA", FrenchCanada}, {"fr_CA.UTF-8", FrenchCanada}, + {"de", German}, {"de-DE", German}, {"de_AT.UTF-8", German}, {"de-CH", German}, + {"it", Italian}, {"it-IT", Italian}, {"it_CH", Italian}, + {"nl", Dutch}, {"nl-NL", Dutch}, {"nl_BE.UTF-8", Dutch}, + {"ro", Romanian}, {"ro-RO", Romanian}, {"mo", Romanian}, + }; + RequireLanguages(entries); +} + +void RequireIberianRows() { + using namespace SystemServiceLocale; + constexpr LocaleLanguage entries[] = { + {"es", Spanish}, {"es-ES", Spanish}, {"es_ES.UTF-8@euro", Spanish}, {"ES-es", Spanish}, {"es-GQ", Spanish}, {"es-Latn-ES", Spanish}, + {"es-419", SpanishLatinAmerica}, {"es_MX.UTF-8", SpanishLatinAmerica}, {"es-AR", SpanishLatinAmerica}, {"es-CO", SpanishLatinAmerica}, + {"es_CL", SpanishLatinAmerica}, {"es-PE", SpanishLatinAmerica}, {"es-VE", SpanishLatinAmerica}, {"es-US", SpanishLatinAmerica}, + {"es_UY.utf8", SpanishLatinAmerica}, {"es-PR", SpanishLatinAmerica}, {"es-CU", SpanishLatinAmerica}, {"es-DO", SpanishLatinAmerica}, + {"pt", PortugueseBrazil}, {"pt-BR", PortugueseBrazil}, {"pt_BR.UTF-8", PortugueseBrazil}, {"PT-br", PortugueseBrazil}, + {"pt-PT", PortuguesePortugal}, {"pt_PT.UTF-8", PortuguesePortugal}, {"pt-AO", PortuguesePortugal}, {"pt-MZ", PortuguesePortugal}, + {"pt-CV", PortuguesePortugal}, {"pt-TL", PortuguesePortugal}, + }; + RequireLanguages(entries); + constexpr const char* latinAmerica[] = {"MX", "AR", "BO", "CL", "CO", "CR", "CU", "DO", "EC", "GT", "HN", "NI", "PA", "PE", "PR", "PY", "SV", "US", "UY", "VE", "419"}; + for (const char* region : latinAmerica) Require(LanguageOf(std::string("es-") + region) == SpanishLatinAmerica, region); +} + +void RequireChineseRows() { + using namespace SystemServiceLocale; + constexpr LocaleLanguage entries[] = { + {"zh", ChineseSimplified}, {"zh-CN", ChineseSimplified}, {"zh_CN.UTF-8", ChineseSimplified}, {"zh-SG", ChineseSimplified}, {"zh_SG.utf8", ChineseSimplified}, + {"zh-Hans", ChineseSimplified}, {"zh-Hans-CN", ChineseSimplified}, {"zh-hans-sg", ChineseSimplified}, {"zh-Latn", ChineseSimplified}, {"zh-MY", ChineseSimplified}, + {"zh-Hant", ChineseTraditional}, {"zh-Hant-TW", ChineseTraditional}, {"zh_Hant_TW.UTF-8", ChineseTraditional}, {"zh-TW", ChineseTraditional}, {"zh_TW.UTF-8", ChineseTraditional}, + {"zh-HK", ChineseTraditional}, {"zh_HK.UTF-8", ChineseTraditional}, {"zh-MO", ChineseTraditional}, {"zh-Hant-HK", ChineseTraditional}, {"zh-Hant-MO", ChineseTraditional}, + {"zh-Hans-TW", ChineseSimplified}, {"zh-Hans-HK", ChineseSimplified}, {"zh-Hant-CN", ChineseTraditional}, + {"cmn", ChineseSimplified}, {"cmn-Hans-CN", ChineseSimplified}, {"cmn-Hant", ChineseTraditional}, {"cmn-TW", ChineseTraditional}, + {"yue", ChineseTraditional}, {"yue-HK", ChineseTraditional}, {"yue-Hans-CN", ChineseSimplified}, + }; + RequireLanguages(entries); +} + +void RequireJapaneseKoreanRows() { + using namespace SystemServiceLocale; + constexpr LocaleLanguage entries[] = { + {"ja", Japanese}, {"ja-JP", Japanese}, {"ja_JP.UTF-8", Japanese}, {"JA-jp", Japanese}, {"ja-Jpan-JP", Japanese}, {"ja_JP.eucJP", Japanese}, + {"ko", Korean}, {"ko-KR", Korean}, {"ko_KR.UTF-8", Korean}, {"ko-KP", Korean}, {"ko-Kore-KR", Korean}, + }; + RequireLanguages(entries); +} + +void RequireSlavicNordicRows() { + using namespace SystemServiceLocale; + constexpr LocaleLanguage entries[] = { + {"ru", Russian}, {"ru-RU", Russian}, {"ru_RU.UTF-8", Russian}, {"ru-UA", Russian}, {"ru-BY", Russian}, + {"uk", Ukrainian}, {"uk-UA", Ukrainian}, {"uk_UA.UTF-8", Ukrainian}, + {"pl", Polish}, {"pl-PL", Polish}, {"pl_PL.UTF-8", Polish}, + {"cs", Czech}, {"cs-CZ", Czech}, {"cs_CZ.UTF-8", Czech}, + {"fi", Finnish}, {"fi-FI", Finnish}, {"fi_FI.UTF-8@euro", Finnish}, + {"sv", Swedish}, {"sv-SE", Swedish}, {"sv_FI.UTF-8", Swedish}, + {"da", Danish}, {"da-DK", Danish}, {"da_DK.UTF-8", Danish}, + {"no", Norwegian}, {"no-NO", Norwegian}, {"nb", Norwegian}, {"nb-NO", Norwegian}, {"nb_NO.UTF-8", Norwegian}, {"nn", Norwegian}, {"nn-NO", Norwegian}, {"nn_NO.UTF-8", Norwegian}, + }; + RequireLanguages(entries); + for (std::string_view unmapped : {"lt-LT", "lv-LV", "et-EE", "sk-SK", "sl-SI", "bg-BG", "sr-RS", "sr_RS@latin", "hr-HR", "be-BY", "is-IS"}) { + Require(LanguageOf(unmapped) == NoConsoleLanguage, unmapped); + } +} + +void RequireOtherRows() { + using namespace SystemServiceLocale; + constexpr LocaleLanguage entries[] = { + {"ar", Arabic}, {"ar-AE", Arabic}, {"ar-SA", Arabic}, {"ar_EG.UTF-8", Arabic}, {"ar-MA", Arabic}, {"ar-001", Arabic}, + {"th", Thai}, {"th-TH", Thai}, {"th_TH.UTF-8", Thai}, + {"vi", Vietnamese}, {"vi-VN", Vietnamese}, {"vi_VN.utf8", Vietnamese}, + {"id", Indonesian}, {"id-ID", Indonesian}, {"id_ID.UTF-8", Indonesian}, {"in", Indonesian}, {"in_ID", Indonesian}, + {"tr", Turkish}, {"tr-TR", Turkish}, {"tr_TR.UTF-8", Turkish}, {"tr-CY", Turkish}, + {"el", Greek}, {"el-GR", Greek}, {"el_GR.UTF-8", Greek}, {"el-CY", Greek}, + {"hu", Hungarian}, {"hu-HU", Hungarian}, {"hu_HU.UTF-8", Hungarian}, + }; + RequireLanguages(entries); +} + +void RequireFallbackRules() { + using namespace SystemServiceLocale; + constexpr const char* perLanguage[31] = { + "ja-JP", "en-US", "fr-FR", "es-ES", "de-DE", "it-IT", "nl-NL", "pt-PT", "ru-RU", "ko-KR", "zh-Hant", "zh-Hans", "fi-FI", "sv-SE", "da-DK", "no-NO", + "pl-PL", "pt-BR", "en-GB", "tr-TR", "es-419", "ar-AE", "fr-CA", "cs-CZ", "hu-HU", "el-GR", "ro-RO", "th-TH", "vi-VN", "id-ID", "uk-UA"}; + for (int id = 0; id <= 30; ++id) { + const Match match = MatchLocale(perLanguage[id]); + Require(match.recognised && match.id == id, perLanguage[id]); + } + for (std::string_view unrecognised : {"", "C", "POSIX", "C.UTF-8", "c", "garbage", "xx", "xx-YY", "x-foo", "en_", "_US", "lt-LT", "sw-KE", "he-IL", "hi-IN", "sr-Latn-RS", "ca-ES", "eu", "gl-ES", "qaa", "en--US", "12345", "\t"}) { + const Match match = MatchLocale(unrecognised); + Require(!match.recognised && match.id == EnglishUs, unrecognised); + } + Require(MatchLocale("en-US").recognised && MatchLocale("en-ZZ").recognised && MatchLocale("en-ZZ").id == EnglishUs, "en-ZZ"); + Require(MatchLocale("fr-ZZ").recognised && MatchLocale("fr-ZZ").id == French, "fr-ZZ"); + Require(MatchLocale("zh-ZZ").recognised && MatchLocale("zh-ZZ").id == ChineseSimplified, "zh-ZZ"); + Require(MatchLocale("pt-ZZ").recognised && MatchLocale("pt-ZZ").id == PortugueseBrazil, "pt-ZZ"); + Require(MatchLocale("es-ZZ").recognised && MatchLocale("es-ZZ").id == Spanish, "es-ZZ"); + Require(MatchLocale("de-Cyrl-RU").recognised && MatchLocale("de-Cyrl-RU").id == German, "de-Cyrl-RU"); +} + +std::vector Posix(const std::map& variables) { + return SystemServiceLocale::PosixHostLocales([&variables](const char* name) -> const char* { + const auto found = variables.find(name); + return found == variables.end() ? nullptr : found->second.c_str(); + }); +} + +void RequirePosixSource() { + using Locales = std::vector; + Require(Posix({}).empty(), "nothing set"); + Require(Posix({{"LANG", ""}, {"LC_ALL", ""}}).empty(), "all empty"); + Require(Posix({{"LANG", "de_DE.UTF-8"}}) == Locales{"de_DE.UTF-8"}, "LANG alone"); + Require(Posix({{"LC_MESSAGES", "fr_FR.UTF-8"}, {"LANG", "de_DE.UTF-8"}}) == Locales{"fr_FR.UTF-8"}, "LC_MESSAGES over LANG"); + Require(Posix({{"LC_ALL", "it_IT.UTF-8"}, {"LC_MESSAGES", "fr_FR.UTF-8"}, {"LANG", "de_DE.UTF-8"}}) == Locales{"it_IT.UTF-8"}, "LC_ALL over the rest"); + Require(Posix({{"LC_ALL", ""}, {"LC_MESSAGES", ""}, {"LANG", "nl_NL"}}) == Locales{"nl_NL"}, "empty values are skipped"); + Require(Posix({{"LC_ALL", ""}, {"LC_MESSAGES", "es_ES"}, {"LANG", "nl_NL"}}) == Locales{"es_ES"}, "empty LC_ALL is skipped"); + Require(Posix({{"LC_CTYPE", "ja_JP.UTF-8"}, {"LC_TIME", "ko_KR"}}).empty(), "other categories are not read"); + Require(Posix({{"LANG", "de_DE.UTF-8"}, {"LANGUAGE", "es:fr"}}) == Locales{"es", "fr", "de_DE.UTF-8"}, "LANGUAGE precedes a real locale"); + Require(Posix({{"LANG", "de_DE.UTF-8"}, {"LANGUAGE", ":pt_BR::en:"}}) == Locales{"pt_BR", "en", "de_DE.UTF-8"}, "empty LANGUAGE entries are skipped"); + Require(Posix({{"LANG", "de_DE.UTF-8"}, {"LANGUAGE", ""}}) == Locales{"de_DE.UTF-8"}, "empty LANGUAGE"); + Require(Posix({{"LANG", "C"}, {"LANGUAGE", "es"}}) == Locales{"C"}, "LANGUAGE is ignored under C"); + Require(Posix({{"LC_ALL", "POSIX"}, {"LANG", "de_DE"}, {"LANGUAGE", "es"}}) == Locales{"POSIX"}, "LANGUAGE is ignored under POSIX"); + Require(Posix({{"LANG", "C.UTF-8"}, {"LANGUAGE", "es"}}) == Locales{"C.UTF-8"}, "LANGUAGE is ignored under C.UTF-8"); + Require(Posix({{"LANGUAGE", "es"}}).empty(), "LANGUAGE alone is ignored"); + Require(Posix({{"LC_ALL", "C"}, {"LANG", "de_DE"}}) == Locales{"C"}, "LC_ALL=C overrides LANG"); +} + +int Resolve(const std::vector& locales, std::vector& notices) { + return SystemServiceLocale::ResolveHostLanguage([&locales] { return locales; }, [¬ices](const std::string& line) { notices.push_back(line); }); +} + +void RequireHostResolution() { + using namespace SystemServiceLocale; + std::vector notices; + Require(Resolve({"de_DE.UTF-8"}, notices) == German && notices.empty(), "one recognised locale"); + Require(Resolve({"pt_BR"}, notices) == PortugueseBrazil && notices.empty(), "pt_BR"); + Require(Resolve({"lt", "uk", "fr"}, notices) == Ukrainian && notices.empty(), "first recognised entry wins"); + Require(Resolve({"es", "fr", "de_DE.UTF-8"}, notices) == Spanish && notices.empty(), "LANGUAGE order"); + Require(Resolve({"en-GB", "fr"}, notices) == EnglishUk && notices.empty(), "first entry before later ones"); + Require(Resolve({"zh-Hant-TW"}, notices) == ChineseTraditional && notices.empty(), "zh-Hant-TW"); + Require(Resolve({"lt_LT.UTF-8"}, notices) == EnglishUs && notices.size() == 1, "unlisted language"); + Require(notices[0] == "ANYPS5_LANGUAGE=auto: the host locale \"lt_LT.UTF-8\" has no console language, using English (United States), language number 1", notices[0]); + notices.clear(); + Require(Resolve({"C"}, notices) == EnglishUs && notices.size() == 1, "C"); + Require(notices[0].find("\"C\"") != std::string::npos && notices[0].find("English (United States)") != std::string::npos, notices[0]); + notices.clear(); + Require(Resolve({"lt", "lv"}, notices) == EnglishUs && notices.size() == 1 && notices[0].find("\"lt\"") != std::string::npos, "first unrecognised entry is named"); + notices.clear(); + Require(Resolve({}, notices) == EnglishUs && notices.size() == 1, "no locale"); + Require(notices[0] == "ANYPS5_LANGUAGE=auto: the host reports no locale, using English (United States), language number 1", notices[0]); + notices.clear(); + Require(Resolve({"garbage!", "ja_JP"}, notices) == Japanese && notices.empty(), "malformed entry is skipped"); +} + +} + +int main() { + RequireBcp47Parsing(); + RequirePosixNormalisation(); + RequireEitherForm(); + RequireLatinEuropeanRows(); + RequireIberianRows(); + RequireChineseRows(); + RequireJapaneseKoreanRows(); + RequireSlavicNordicRows(); + RequireOtherRows(); + RequireFallbackRules(); + RequirePosixSource(); + RequireHostResolution(); +} diff --git a/core/libs/tests/TextureFormatLookup.cpp b/core/libs/tests/TextureFormatLookup.cpp index da6cbdc1d0..3f92ea1b5c 100644 --- a/core/libs/tests/TextureFormatLookup.cpp +++ b/core/libs/tests/TextureFormatLookup.cpp @@ -15,12 +15,27 @@ int main() { if (FindGuestColorTargetFormat(VK_FORMAT_B8G8R8A8_UNORM, 4) != 56) std::abort(); if (FindGuestColorTargetFormat(VK_FORMAT_B8G8R8A8_SNORM, 4) != 57) std::abort(); if (FindGuestColorTargetFormat(VK_FORMAT_B8G8R8A8_SRGB, 4) != 130) std::abort(); + if (FindGuestColorTargetFormat(VK_FORMAT_B8G8R8A8_SRGB, 4) != FindGuestTextureFormat(VK_FORMAT_R8G8B8A8_SRGB, 4)) std::abort(); if (FindGuestColorTargetFormat(VK_FORMAT_A2R10G10B10_UNORM_PACK32, 4) != 50) std::abort(); + if (FindGuestColorTargetFormat(VK_FORMAT_A2R10G10B10_UNORM_PACK32, 4) != FindGuestTextureFormat(VK_FORMAT_A2B10G10R10_UNORM_PACK32, 4)) std::abort(); if (FindGuestColorTargetFormat(VK_FORMAT_R8G8B8A8_UNORM, 4) != 56) std::abort(); if (FindGuestColorTargetFormat(VK_FORMAT_R16G16B16A16_SFLOAT, 8) != 71) std::abort(); if (FindGuestColorTargetFormat(VK_FORMAT_B8G8R8A8_UNORM, 8)) std::abort(); if (FindGuestColorTargetFormat(VK_FORMAT_B8G8R8A8_SNORM, 8)) std::abort(); if (FindGuestColorTargetFormat(VK_FORMAT_B8G8R8A8_SRGB, 1)) std::abort(); + if (FindGuestColorTargetFormat(VK_FORMAT_B8G8R8A8_SRGB, 2)) std::abort(); + if (FindGuestColorTargetFormat(VK_FORMAT_B8G8R8A8_SRGB, 8)) std::abort(); + if (FindGuestColorTargetFormat(VK_FORMAT_A2R10G10B10_UNORM_PACK32, 1)) std::abort(); + if (FindGuestColorTargetFormat(VK_FORMAT_A2R10G10B10_UNORM_PACK32, 2)) std::abort(); + if (FindGuestColorTargetFormat(VK_FORMAT_A2R10G10B10_UNORM_PACK32, 8)) std::abort(); if (FindGuestColorTargetFormat(VK_FORMAT_BC1_RGBA_UNORM_BLOCK, 8)) std::abort(); if (FindGuestColorTargetFormat(VK_FORMAT_UNDEFINED, 4)) std::abort(); + for (auto uncompressed : {1u, 7u, 14u, 22u, 50u, 56u, 71u, 128u, 130u}) { + if (IsBlockCompressed(uncompressed)) std::abort(); + if (BlockWidth(uncompressed) != 1u || BlockHeight(uncompressed) != 1u) std::abort(); + } + for (auto compressed : {169u, 170u, 171u, 172u, 173u, 174u, 175u, 176u, 177u, 178u, 179u, 180u, 181u, 182u}) { + if (!IsBlockCompressed(compressed)) std::abort(); + if (BlockWidth(compressed) != 4u || BlockHeight(compressed) != 4u) std::abort(); + } } diff --git a/core/libs/tests/WindowsCrashThreadName.cpp b/core/libs/tests/WindowsCrashThreadName.cpp new file mode 100644 index 0000000000..e98974576a --- /dev/null +++ b/core/libs/tests/WindowsCrashThreadName.cpp @@ -0,0 +1,42 @@ +#include +#include +#include +#include + +int main(int argc, char** argv) { + if (argc != 2) { + std::fprintf(stderr, "usage: %s \n", argv[0]); + return 2; + } + SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX); + SetUnhandledExceptionFilter([](EXCEPTION_POINTERS*) -> LONG { + TerminateProcess(GetCurrentProcess(), 3); + return EXCEPTION_EXECUTE_HANDLER; + }); + if (LoadLibraryExA(argv[1], nullptr, LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_DEFAULT_DIRS) == nullptr) { + std::fprintf(stderr, "%s: LoadLibraryEx failed with error %lu\n", argv[1], GetLastError()); + return 1; + } + HANDLE worker = CreateThread(nullptr, 0, [](LPVOID) -> DWORD { + Sleep(INFINITE); + return 0; + }, nullptr, 0, nullptr); + if (worker == nullptr) { + std::fprintf(stderr, "CreateThread failed with error %lu\n", GetLastError()); + return 1; + } + const std::wstring mainName = std::wstring(126, L'a') + L'Z' + std::wstring(70, L'b'); + const std::wstring workerName = std::wstring(126, L'c') + L'Y' + std::wstring(70, L'd'); + const HRESULT mainResult = SetThreadDescription(GetCurrentThread(), mainName.c_str()); + const HRESULT workerResult = SetThreadDescription(worker, workerName.c_str()); + if (FAILED(mainResult) || FAILED(workerResult)) { + std::fprintf(stderr, "SetThreadDescription failed: 0x%08lx 0x%08lx\n", static_cast(mainResult), static_cast(workerResult)); + return 1; + } + std::fputs("faulting\n", stderr); + std::fflush(stderr); + const std::uintptr_t unmapped = 8; + asm volatile("movq (%0), %%rax" : : "r"(unmapped) : "rax", "memory"); + std::fputs("the read of address 8 did not fault\n", stderr); + return 1; +} diff --git a/core/libs/tests/WindowsIntegerOverflowReport.cpp b/core/libs/tests/WindowsIntegerOverflowReport.cpp new file mode 100644 index 0000000000..fc780ff73e --- /dev/null +++ b/core/libs/tests/WindowsIntegerOverflowReport.cpp @@ -0,0 +1,33 @@ +#include +#include +#include + +int main(int argc, char** argv) { + if (argc != 2) { + std::fprintf(stderr, "usage: %s \n", argv[0]); + return 2; + } + SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX); + SetUnhandledExceptionFilter([](EXCEPTION_POINTERS*) -> LONG { + TerminateProcess(GetCurrentProcess(), 3); + return EXCEPTION_EXECUTE_HANDLER; + }); + if (LoadLibraryExA(argv[1], nullptr, LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_DEFAULT_DIRS) == nullptr) { + std::fprintf(stderr, "%s: LoadLibraryEx failed with error %lu\n", argv[1], GetLastError()); + return 1; + } + volatile int dividend = INT_MIN; + volatile int divisor = -1; + int quotient = dividend; + const int by = divisor; + std::fputs("dividing\n", stderr); + std::fflush(stderr); + asm volatile( + "cltd\n\t" + "idivl %1" + : "+a"(quotient) + : "r"(by) + : "rdx", "cc"); + std::fprintf(stderr, "the division did not fault: %d\n", quotient); + return 1; +} diff --git a/core/libs/tests/edge_watch/CMakeLists.txt b/core/libs/tests/edge_watch/CMakeLists.txt new file mode 100644 index 0000000000..db48881dc6 --- /dev/null +++ b/core/libs/tests/edge_watch/CMakeLists.txt @@ -0,0 +1,21 @@ +cmake_minimum_required(VERSION 3.20) + +if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR) + project(EdgeWatchTests LANGUAGES CXX) + enable_testing() + set(ANYPS5_TEST_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/tests") +endif() + +find_package(Threads REQUIRED) +add_executable(edge_watch_tests ../EdgeWatch.cpp ../../prx/libkernel/Equeue/src/EdgeWatch.cpp) +target_include_directories(edge_watch_tests PRIVATE ../.. ../../prx/libc/include) +target_compile_features(edge_watch_tests PRIVATE cxx_std_20) +target_link_libraries(edge_watch_tests PRIVATE Threads::Threads) +set_target_properties(edge_watch_tests PROPERTIES + CXX_EXTENSIONS OFF + RUNTIME_OUTPUT_DIRECTORY "$<1:${ANYPS5_TEST_OUTPUT_DIRECTORY}>") + +foreach(mode socket pipe write lifecycle errors) + add_test(NAME edge_watch_${mode} COMMAND edge_watch_tests ${mode}) + set_tests_properties(edge_watch_${mode} PROPERTIES TIMEOUT 30) +endforeach() diff --git a/core/libs/tests/test_public_headers.py b/core/libs/tests/test_public_headers.py index 6e99eb6ec8..f2d50c635e 100644 --- a/core/libs/tests/test_public_headers.py +++ b/core/libs/tests/test_public_headers.py @@ -20,7 +20,7 @@ DATA_DECLARATION = re.compile( r"^[ \t]*extern\s+(?:const\s+)?[A-Za-z_][\w:<>]*(?:\s+(?:const|volatile))?\s+\**\s*(\w+)\s*(?:\[[^;\]\n]*\])?\s*;", re.M ) INCLUDE = re.compile(r'^\s*#\s*include\s*"([^"]+)"', re.M) -TYPE_TOKEN = re.compile(r"\bnamespace\s*([\w:]*)\s*\{|\bextern\s*\{|\b(?:struct|union|class)\s+(\w+)\s*([;{])|\busing\s+(\w+)\s*=|[{}]") +TYPE_TOKEN = re.compile(r"\bnamespace\s*([\w:]*)\s*\{|\bextern\s*\{|\b(?:struct|union|class)\s+(\w+)\s*(?:final\s*)?(?::(?!:)[^;{]*)?([;{])|\busing\s+(\w+)\s*=|[{}]") @cache diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 6ede0aab5f..67acbb953a 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -98,6 +98,11 @@ if(WIN32) endif() if(BUILD_TESTING) + add_test_executable(console_progress_tests ${CMAKE_CURRENT_SOURCE_DIR}/cli/tests/ConsoleProgressTests.cpp) + target_include_directories(console_progress_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/cli/include ${CMAKE_CURRENT_SOURCE_DIR}/domain/include) + add_test(NAME console_progress COMMAND console_progress_tests) + set_tests_properties(console_progress PROPERTIES TIMEOUT 20) + add_test_executable(guest_load_ranges_tests ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/GuestLoadRangesTests.cpp) target_link_libraries(guest_load_ranges_tests PRIVATE relinker_core) add_test(NAME guest_load_ranges COMMAND guest_load_ranges_tests) @@ -106,6 +111,11 @@ if(BUILD_TESTING) add_test_executable(andn_lowering_tests ${CMAKE_CURRENT_SOURCE_DIR}/codegen/tests/AndnLoweringTests.cpp) target_link_libraries(andn_lowering_tests PRIVATE relinker_core) add_test(NAME andn_lowering COMMAND andn_lowering_tests) + + add_test_executable(import_module_tests ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/ImportModuleTests.cpp) + target_link_libraries(import_module_tests PRIVATE relinker_core) + add_test(NAME import_module COMMAND import_module_tests) + set_tests_properties(andn_lowering PROPERTIES TIMEOUT 20) add_test_executable(bls_lowering_tests ${CMAKE_CURRENT_SOURCE_DIR}/codegen/tests/BlsLoweringTests.cpp) @@ -723,6 +733,10 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_linux_entry_argv.py $) add_test(NAME linux_fixed_base COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_linux_fixed_base.py $) + add_test(NAME linux_main_stack_entry COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_linux_main_stack_entry.py + $ $) + add_asan_test_options(linux_main_stack_entry verify_asan_link_order=0) add_test(NAME linux_program_header_order COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_linux_program_header_order.py $) add_test(NAME linux_input_program_headers COMMAND ${Python3_EXECUTABLE} @@ -831,6 +845,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_absolute_symbols.py $) add_test(NAME guest_module_identity COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_module_identity.py $) + add_test(NAME guest_symbolic_modules COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_symbolic_modules.py $) add_test(NAME linux_guest_zero_address_exports COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_linux_guest_zero_address_exports.py $ $) diff --git a/core/relinker/cli/include/Cli.hpp b/core/relinker/cli/include/Cli.hpp index 23087c2ff4..f078c6c90c 100644 --- a/core/relinker/cli/include/Cli.hpp +++ b/core/relinker/cli/include/Cli.hpp @@ -22,6 +22,7 @@ struct Args { bool toWindows = false; bool toMacos = false; bool lazyBinding = false; + bool omitUnavailableDependencies = false; bool autorun = false; bool dryRun = false; bool windowsDiagnostics = false; diff --git a/core/relinker/cli/include/ConsoleProgress.hpp b/core/relinker/cli/include/ConsoleProgress.hpp index a05bf67795..5136137850 100644 --- a/core/relinker/cli/include/ConsoleProgress.hpp +++ b/core/relinker/cli/include/ConsoleProgress.hpp @@ -18,7 +18,7 @@ public: void IncludeFile(std::string_view path) { const auto separator = path.find_last_of("/\\"); const auto name = path.substr(separator == std::string_view::npos ? 0 : separator + 1); - nameWidth = std::max(nameWidth, textWidth(name)); + nameWidth = std::max(nameWidth, textWidth(displayName(name))); } void Report(const Domain::ProgressEvent& event) override { @@ -36,7 +36,7 @@ public: default: throw std::logic_error("Unknown progress stage"); } const auto separator = event.Input.find_last_of("/\\"); - const auto name = event.Input.substr(separator == std::string::npos ? 0 : separator + 1); + const auto name = displayName(event.Input.substr(separator == std::string::npos ? 0 : separator + 1)); if (event.Stage == Domain::ProgressStage::Executable) { if (event.Completed == 0) Message("Converting " + name + "\n"); else Finish(); @@ -92,6 +92,19 @@ public: } private: + static std::string displayName(std::string_view name) { + constexpr char hex[] = "0123456789ABCDEF"; + std::string result; + for (const unsigned char character : name) { + if (character < 0x20 || character == 0x7f) { + result += "\\x"; + result += hex[character >> 4]; + result += hex[character & 15]; + } else result += static_cast(character); + } + return result; + } + static std::size_t textWidth(std::string_view text) { return static_cast(std::count_if(text.begin(), text.end(), [](unsigned char character) { return (character & 0xc0) != 0x80; })); } diff --git a/core/relinker/cli/src/Autorun.cpp b/core/relinker/cli/src/Autorun.cpp index 95f8214038..0647c38018 100644 --- a/core/relinker/cli/src/Autorun.cpp +++ b/core/relinker/cli/src/Autorun.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -71,8 +72,11 @@ int Autorun(const std::string& absPath, bool toWindows) { std::cout << "\nRaw exit code: " << rawCode << "; Unpacked: " << exitCode << '\n'; #endif - std::cout << "\nPress Enter to exit...\n"; - std::cin.ignore((std::numeric_limits::max)(), '\n'); + const char* noPause = std::getenv("ANYPS5_AUTORUN_NO_PAUSE"); + if (!noPause || std::string_view(noPause) != "1") { + std::cout << "\nPress Enter to exit...\n"; + std::cin.ignore((std::numeric_limits::max)(), '\n'); + } return exitCode; } diff --git a/core/relinker/cli/src/CliArgs.cpp b/core/relinker/cli/src/CliArgs.cpp index d0b3f3d6ca..15886225fa 100644 --- a/core/relinker/cli/src/CliArgs.cpp +++ b/core/relinker/cli/src/CliArgs.cpp @@ -25,6 +25,8 @@ const char* Usage() { " --to-rosetta Convert as --to-intel and lower what Rosetta lacks too. Implies --to-intel.\n" " --frame-red-zone Windows: give leaf functions a frame so an exception on a guest thread can unwind.\n" " --rpath System library search path. Default: $ORIGIN/libs.\n" + " --omit-unavailable-dependencies Linux: leave out a module dependency that no module provides, no import uses\n" + " and no run path directory holds at conversion time.\n" " --registry Write .registry.json beside the output.\n" " --autorun Run the output after conversion and print its exit code.\n" " --dry-run Convert in memory and list planned outputs without writing them.\n" @@ -70,7 +72,7 @@ constexpr std::string_view kVerifyRejectedOptions[] = { "--dry-run", "--windows", "--macos", "--windows-gui", "--windows-diagnostics", "--windows-fixed-base", "--trace-imports", "--linux-fixed-base", "--macos-fixed-base", "--to-intel", "--to-rosetta", "--frame-red-zone", "--skip-syscall-check", "--trap-sdk-syscall-stubs", "unused-filter=", - "--rpath", "--registry", "--lazy-binding", "--autorun", "--patches", "--no-patches", + "--rpath", "--registry", "--omit-unavailable-dependencies", "--lazy-binding", "--autorun", "--patches", "--no-patches", }; constexpr std::string_view kModuleDiscoveryOptions[] = {"--sce-module-path", "--module-dir", "--exclude-sce-module", "--skip-sce-module"}; @@ -200,6 +202,8 @@ Args ParseArgs(int argc, char* argv[]) { args.toMacos = true; } else if (arg == "--lazy-binding") { args.lazyBinding = true; + } else if (arg == "--omit-unavailable-dependencies") { + args.omitUnavailableDependencies = true; } else if (arg == "--autorun") { args.autorun = true; } else if (arg == "--dry-run") { diff --git a/core/relinker/cli/tests/AutorunExitTests.cpp b/core/relinker/cli/tests/AutorunExitTests.cpp index 798fb129db..9bf9f3b6c6 100644 --- a/core/relinker/cli/tests/AutorunExitTests.cpp +++ b/core/relinker/cli/tests/AutorunExitTests.cpp @@ -1,6 +1,8 @@ #include #include #include +#include +#include #include #include #ifndef _WIN32 @@ -19,6 +21,26 @@ namespace { +void SetNoPause(const char* value) { +#ifdef _WIN32 + if (_putenv_s("ANYPS5_AUTORUN_NO_PAUSE", value ? value : "") != 0) throw std::runtime_error("Cannot set autorun environment"); +#else + if ((value ? setenv("ANYPS5_AUTORUN_NO_PAUSE", value, 1) : unsetenv("ANYPS5_AUTORUN_NO_PAUSE")) != 0) + throw std::runtime_error("Cannot set autorun environment"); +#endif +} + +class EnvironmentGuard { +public: + EnvironmentGuard() { + if (const auto* value = std::getenv("ANYPS5_AUTORUN_NO_PAUSE")) saved = value; + SetNoPause(nullptr); + } + ~EnvironmentGuard() { SetNoPause(saved ? saved->c_str() : nullptr); } +private: + std::optional saved; +}; + std::filesystem::path ExecutablePath(const char* argument) { #ifdef _WIN32 static_cast(argument); @@ -55,6 +77,7 @@ int main(int, char* argv[]) { #endif }; #endif + const EnvironmentGuard environment; std::istringstream input("\n\n\n"); auto* savedInput = std::cin.rdbuf(input.rdbuf()); int result = 0; @@ -66,6 +89,28 @@ int main(int, char* argv[]) { std::filesystem::create_symlink(executable, child); const auto changed = changeTime(executable); #endif + for (const char* value : {"1", "0", "yes", "", "10", "1 "}) { + SetNoPause(value); + std::istringstream pending("\nremaining"); + auto* originalInput = std::cin.rdbuf(pending.rdbuf()); + std::ostringstream output; + auto* originalOutput = std::cout.rdbuf(output.rdbuf()); + int code; + try { + code = Cli::Autorun(child.string(), true); + } catch (...) { + std::cin.rdbuf(originalInput); + std::cout.rdbuf(originalOutput); + throw; + } + std::cin.rdbuf(originalInput); + std::cout.rdbuf(originalOutput); + const bool skipped = std::string(value) == "1"; + if (code != 42 || (pending.tellg() == 0) != skipped || + (output.str().find("Press Enter") == std::string::npos) != skipped) + throw std::runtime_error("Autorun pause option changed the exit code, input position or prompt"); + } + SetNoPause(nullptr); std::vector suffixes{""}; #ifndef _WIN32 suffixes.insert(suffixes.end(), {" $HOME", " $(printf substituted)", " `printf substituted`", diff --git a/core/relinker/cli/tests/CliArgsTests.cpp b/core/relinker/cli/tests/CliArgsTests.cpp index 0a36a9a148..a998c86fd0 100644 --- a/core/relinker/cli/tests/CliArgsTests.cpp +++ b/core/relinker/cli/tests/CliArgsTests.cpp @@ -118,6 +118,11 @@ void rpath() { require(args.runPath == "/custom/libs", "--rpath value was not captured"); } +void omitUnavailableDependencies() { + require(!parse({"input.elf", "output.elf"}).omitUnavailableDependencies, "dependencies were left out by default"); + require(parse({"--omit-unavailable-dependencies", "input.elf", "output.elf"}).omitUnavailableDependencies, "--omit-unavailable-dependencies was ignored"); +} + void rpathConsumesNextTokenVerbatim() { const auto args = parse({"--rpath", "--windows", "input.elf", "output.elf"}); require(args.runPath == "--windows", "--rpath did not consume the following token as its value"); @@ -395,6 +400,7 @@ int main() { singleDashIsPositional(); optionTerminator(); rpath(); + omitUnavailableDependencies(); rpathConsumesNextTokenVerbatim(); rpathWithoutValue(); excludeSceModule(); diff --git a/core/relinker/cli/tests/ConsoleProgressTests.cpp b/core/relinker/cli/tests/ConsoleProgressTests.cpp new file mode 100644 index 0000000000..70ecd907b6 --- /dev/null +++ b/core/relinker/cli/tests/ConsoleProgressTests.cpp @@ -0,0 +1,41 @@ +#include +#include +#include +#include + +int main() { + try { + std::ostringstream stream; + Cli::ConsoleProgress progress(stream); + const std::string name = std::string("title") + '\x1b' + "[2J" + '\n' + '\r' + '\t' + '\x7f' + ".prx"; + progress.IncludeFile(name); + progress.IncludeFile("中文.prx"); + progress.Report({Domain::ProgressStage::Executable, "folder/" + name, 0, 1}); + const auto initial = stream.str(); + if (initial != "Converting title\\x1B[2J\\x0A\\x0D\\x09\\x7F.prx\n") + throw std::runtime_error("Executable name was not escaped literally"); + stream.str(""); + progress.Report({Domain::ProgressStage::Modules, name, 1, 1}); + progress.Report({Domain::ProgressStage::Modules, "中文.prx", 1, 1}); + progress.Finish(); + const auto output = stream.str(); + if (output.find('\x1b') != std::string::npos || output.find('\t') != std::string::npos || output.find('\x7f') != std::string::npos) + throw std::runtime_error("Progress output contains filename controls"); + if (output.find("title\\x1B[2J\\x0A\\x0D\\x09\\x7F.prx | 100% (1/1)") == std::string::npos) + throw std::runtime_error("Escaped name or completion was lost"); + if (output.find("中文.prx" + std::string(26, ' ') + " | 100% (1/1)") == std::string::npos) + throw std::runtime_error("Escaped width or UTF-8 name was lost"); + for (unsigned byte = 0; byte < 128; ++byte) { + if (byte == '/' || byte == '\\') continue; + std::ostringstream single; + Cli::ConsoleProgress item(single); + item.Report({Domain::ProgressStage::Executable, std::string(1, static_cast(byte)), 0, 1}); + if ((byte < 32 || byte == 127) && single.str().find("\\x") == std::string::npos) + throw std::runtime_error("ASCII control was not escaped"); + } + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } + return 0; +} diff --git a/core/relinker/domain/include/domain/ImportModule.hpp b/core/relinker/domain/include/domain/ImportModule.hpp index 18b018f770..2e25f56c3c 100644 --- a/core/relinker/domain/include/domain/ImportModule.hpp +++ b/core/relinker/domain/include/domain/ImportModule.hpp @@ -58,13 +58,19 @@ inline std::string ImportModule(const std::string& symbol, const std::map BuildEntryStub( std::uint64_t stubVaddr, - std::uint64_t realEntryVaddr + std::uint64_t realEntryVaddr, + std::optional mainStackEntrySlotVaddr ) const override; }; diff --git a/core/relinker/elfpatcher/include/elfpatcher/general/IEntryStubBuilder.hpp b/core/relinker/elfpatcher/include/elfpatcher/general/IEntryStubBuilder.hpp index 6a4799e93a..391ddb635a 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/general/IEntryStubBuilder.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/general/IEntryStubBuilder.hpp @@ -2,6 +2,7 @@ #define ELFPATCHER_DOMAIN_IENTRYSTUBBUILDER_HPP #include +#include #include namespace Elfpatcher { @@ -12,7 +13,8 @@ public: virtual std::vector BuildEntryStub( std::uint64_t stubVaddr, - std::uint64_t realEntryVaddr + std::uint64_t realEntryVaddr, + std::optional mainStackEntrySlotVaddr ) const = 0; }; diff --git a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeFormat.hpp b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeFormat.hpp index c9436e4915..fa3c5dc370 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeFormat.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeFormat.hpp @@ -54,6 +54,7 @@ struct PeImport { std::string Library; bool Weak = false; bool Function = false; + bool Lazy = false; }; struct PeRelocations { diff --git a/core/relinker/elfpatcher/src/general/EntryStubBuilder.cpp b/core/relinker/elfpatcher/src/general/EntryStubBuilder.cpp index bf5924d9a4..e92b99aa0e 100644 --- a/core/relinker/elfpatcher/src/general/EntryStubBuilder.cpp +++ b/core/relinker/elfpatcher/src/general/EntryStubBuilder.cpp @@ -15,26 +15,34 @@ void _appendBytes(std::vector& s, const std::uint8_t* bytes, std:: std::vector EntryStubBuilder::BuildEntryStub( const std::uint64_t stubVaddr, - const std::uint64_t realEntryVaddr + const std::uint64_t realEntryVaddr, + const std::optional mainStackEntrySlotVaddr ) const { std::vector s; + const auto appendRel32 = [&](const std::uint64_t target) { + const std::uint64_t nextVaddr = stubVaddr + s.size() + sizeof(std::int32_t); + std::int64_t displacement = 0; + if (!SignedDistance(target, nextVaddr, displacement) || displacement < std::numeric_limits::min() || displacement > std::numeric_limits::max()) + throw Domain::RelinkerException("Entry stub call exceeds rel32 range"); + const auto rel32 = static_cast(static_cast(displacement)); + for (std::size_t shift = 0; shift < 32; shift += 8) + s.push_back(static_cast((rel32 >> shift) & 0xff)); + }; _appendBytes(s, kStubOpMovRdiRsp, sizeof(kStubOpMovRdiRsp)); _appendBytes(s, kStubOpAndRsp0xf0, sizeof(kStubOpAndRsp0xf0)); _appendBytes(s, kStubOpPushZero, sizeof(kStubOpPushZero)); _appendBytes(s, kStubOpPushZero, sizeof(kStubOpPushZero)); _appendBytes(s, kStubOpMovRbpRsp, sizeof(kStubOpMovRbpRsp)); - _appendBytes(s, kStubOpXorRsiRsi, sizeof(kStubOpXorRsiRsi)); - const std::uint64_t callInsnVaddr = stubVaddr + s.size(); - const std::uint64_t callNextVaddr = callInsnVaddr + kStubCallInstructionSize; - std::int64_t displacement = 0; - if (!SignedDistance(realEntryVaddr, callNextVaddr, displacement) || displacement < std::numeric_limits::min() || displacement > std::numeric_limits::max()) - throw Domain::RelinkerException("Entry stub call exceeds rel32 range"); - const auto rel32 = static_cast(displacement); - s.push_back(kStubOpCallRel32); - s.push_back(static_cast(rel32 & 0xff)); - s.push_back(static_cast((rel32 >> 8) & 0xff)); - s.push_back(static_cast((rel32 >> 16) & 0xff)); - s.push_back(static_cast((rel32 >> 24) & 0xff)); + if (mainStackEntrySlotVaddr) { + _appendBytes(s, kStubOpLeaRsiRip, sizeof(kStubOpLeaRsiRip)); + appendRel32(realEntryVaddr); + _appendBytes(s, kStubOpCallRipIndirect, sizeof(kStubOpCallRipIndirect)); + appendRel32(*mainStackEntrySlotVaddr); + } else { + _appendBytes(s, kStubOpXorRsiRsi, sizeof(kStubOpXorRsiRsi)); + s.push_back(kStubOpCallRel32); + appendRel32(realEntryVaddr); + } _appendBytes(s, kStubOpUd2, sizeof(kStubOpUd2)); return s; } diff --git a/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp b/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp index 8b69963705..c52b905dca 100644 --- a/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp +++ b/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp @@ -297,7 +297,7 @@ Domain::SysVDynamicSection ImportsByNid(Domain::SysVDynamicSection dynSection) { 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 TracePrologue(const std::uint64_t vaddr, const std::uint64_t pages, 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) { code.insert(code.end(), opcode); @@ -306,14 +306,20 @@ std::vector TracePrologue(const std::uint64_t vaddr, const std::ui throw Domain::RelinkerException("Import trace table exceeds rel32 range"); Io::AppendU32(code, static_cast(displacement)); }; - rip({0x48, 0x8d, 0x1d}, table); + rip({0x48, 0x8d, 0x1d}, pages); + rip({0x4c, 0x8d, 0x25}, table); + const auto pageLoop = code.size(); + code.insert(code.end(), {0x4c, 0x39, 0xe3, 0x73, 0x00}); + const auto pageExit = code.size(); + code.insert(code.end(), {0x48, 0x8b, 0x3b, 0x48, 0x01, 0xdf, 0xbe, 0x00, 0x10, 0x00, 0x00, 0x8b, 0x53, 0x08, 0xb8, 0x0a, 0x00, 0x00, 0x00, 0x0f, 0x05, 0x48, 0x83, 0xc3, 0x10, 0xeb}); + code.push_back(static_cast(static_cast(static_cast(pageLoop) - static_cast(code.size() + 1)))); + code[pageExit - 1] = static_cast(code.size() - pageExit); rip({0x4c, 0x8d, 0x25}, tableEnd); const auto loop = code.size(); code.insert(code.end(), {0x4c, 0x39, 0xe3, 0x73, 0x00}); const auto exit = code.size(); code.insert(code.end(), {0x48, 0x8b, 0x03, 0x48, 0x8b, 0x3c, 0x03, 0x48, 0x8b, 0x73, 0x08, 0x48, 0x01, 0xde, 0x48, 0x8b, 0x53, 0x10, 0x48, 0x01, 0xda}); rip({0xff, 0x15}, function); - code.insert(code.end(), {0x48, 0x8b, 0x3b, 0x48, 0x01, 0xdf, 0x48, 0x81, 0xe7, 0x00, 0xf0, 0xff, 0xff, 0xbe, 0x00, 0x10, 0x00, 0x00, 0xba, 0x07, 0x00, 0x00, 0x00, 0x50, 0xb8, 0x0a, 0x00, 0x00, 0x00, 0x0f, 0x05, 0x58}); code.insert(code.end(), {0x48, 0x8b, 0x0b, 0x48, 0x89, 0x04, 0x0b, 0x48, 0x83, 0xc3, 0x18, 0xeb}); code.push_back(static_cast(static_cast(static_cast(loop) - static_cast(code.size() + 1)))); code[exit - 1] = static_cast(code.size() - exit); @@ -333,14 +339,13 @@ void RebaseProgramHeaders(std::vector& buf, const std::uint64_t ta if (type == PT_NULL || type == PT_GNU_STACK) continue; const auto address = Rebased(base, Io::ReadU64(buf, header + kPhdrVaddrOffset), "Program header address"); - const auto physical = Rebased(base, Io::ReadU64(buf, header + kPhdrPaddrOffset), "Program header physical address"); if (type == PT_LOAD) { RequireBaseAlignment(base, Io::ReadU64(buf, header + kPhdrAlignOffset)); if (Io::ReadU64(buf, header + kPhdrMemSizeOffset) > kLinuxUserAddressEnd - address) throw Domain::RelinkerException("PT_LOAD at " + Hex(address) + " ends outside the Linux user address space"); } Io::WriteU64(buf, header + kPhdrVaddrOffset, address); - Io::WriteU64(buf, header + kPhdrPaddrOffset, physical); + Io::WriteU64(buf, header + kPhdrPaddrOffset, address); } } @@ -463,9 +468,31 @@ std::vector LinuxElfPatcher::Patch( b.push_back(0); }; + const std::uint64_t extraBlockOff = static_cast(buf.size()); + const std::uint64_t extraBlockVaddr = _programHeaderLayoutBuilder->ComputeExtraBlockVaddr(originalHeaders, extraBlockOff); + + const auto vaddrOfExtraBlockOffset = [extraBlockOff, extraBlockVaddr](std::uint64_t fileOffset) -> std::uint64_t { + if (fileOffset < extraBlockOff) + throw Domain::RelinkerException("Extra block member offset lies before the extra block"); + return extraBlockVaddr + (fileOffset - extraBlockOff); + }; + + const auto& neededLibraries = dynSection.NeededLibraries; + const bool mainStackEntry = std::find(neededLibraries.begin(), neededLibraries.end(), kGuestMainStackEntryLibrary) != neededLibraries.end(); + std::optional mainStackSlotOff; + if (mainStackEntry) { + alignBuf(buf, sizeof(std::uint64_t)); + mainStackSlotOff = static_cast(buf.size()); + buf.resize(buf.size() + sizeof(std::uint64_t), 0); + } + const auto dynStrOff = static_cast(buf.size()); for (std::uint8_t b : dynSection.DynStrData) buf.push_back(b); + const std::uint64_t mainStackNameOff = static_cast(buf.size()) - dynStrOff; + if (mainStackEntry) + for (char c : kGuestMainStackEntrySymbol) + buf.push_back(static_cast(c)); const std::uint64_t runPathStrOff = static_cast(buf.size()) - dynStrOff; for (char c : runPath) buf.push_back(static_cast(c)); @@ -490,6 +517,17 @@ std::vector LinuxElfPatcher::Patch( Io::AppendU64(symbols, 0); Io::AppendU64(symbols, 0); } + std::uint64_t mainStackSymbol = 0; + if (mainStackEntry) { + RequireWholeEntries(symbols, kSymEntrySize, "Dynamic symbol table"); + if (symbols.empty()) + symbols.resize(kSymEntrySize, 0); + mainStackSymbol = symbols.size() / kSymEntrySize; + const auto mainStackSymbolOff = symbols.size(); + symbols.resize(symbols.size() + kSymEntrySize, 0); + Io::WriteU32(symbols, mainStackSymbolOff, static_cast(mainStackNameOff)); + symbols[mainStackSymbolOff + 4] = static_cast((STB_GLOBAL << 4) | STT_FUNC); + } for (std::uint8_t b : symbols) buf.push_back(b); alignBuf(buf, kDynSymAlignment); @@ -511,6 +549,12 @@ std::vector LinuxElfPatcher::Patch( Io::AppendU64(relocations, (static_cast(traceSymbol) << 32) | R_X86_64_GLOB_DAT); Io::AppendU64(relocations, 0); } + if (mainStackSlotOff) { + const auto mainStackRelocationOff = relocations.size(); + relocations.resize(relocations.size() + kRelaEntrySize, 0); + Io::WriteU64(relocations, mainStackRelocationOff, address(vaddrOfExtraBlockOffset(*mainStackSlotOff))); + Io::WriteU64(relocations, mainStackRelocationOff + 8, (mainStackSymbol << 32) | R_X86_64_GLOB_DAT); + } for (std::uint8_t b : relocations) buf.push_back(b); alignBuf(buf, kRelaAlignment); @@ -521,15 +565,6 @@ std::vector LinuxElfPatcher::Patch( buf.push_back(b); alignBuf(buf, kRelaPltAlignment); - const std::uint64_t extraBlockOff = dynStrOff; - const std::uint64_t extraBlockVaddr = _programHeaderLayoutBuilder->ComputeExtraBlockVaddr(originalHeaders, extraBlockOff); - - const auto vaddrOfExtraBlockOffset = [extraBlockOff, extraBlockVaddr](std::uint64_t fileOffset) -> std::uint64_t { - if (fileOffset < extraBlockOff) - throw Domain::RelinkerException("Extra block member offset lies before the extra block"); - return extraBlockVaddr + (fileOffset - extraBlockOff); - }; - std::vector dynSegBuf; for (std::uint8_t b : dynSection.DynamicSegmentData) dynSegBuf.push_back(b); @@ -560,8 +595,10 @@ std::vector LinuxElfPatcher::Patch( buf.push_back(b); std::uint64_t traceFunctionVaddr = 0; + std::uint64_t tracePagesVaddr = 0; std::uint64_t traceTableVaddr = 0; std::uint64_t traceTableEndVaddr = 0; + std::map tracePages; std::vector traceEntries; if (_traceImports) { alignBuf(buf, 8); @@ -569,6 +606,16 @@ std::vector LinuxElfPatcher::Patch( traceFunctionVaddr = vaddrOfExtraBlockOffset(functionOff); Io::AppendU64(buf, 0); Io::WriteU64(buf, static_cast(relaOff + traceRelocation), address(traceFunctionVaddr)); + tracePagesVaddr = vaddrOfExtraBlockOffset(buf.size()); + for (const auto& import : traced) { + tracePages.emplace(import.Slot & ~(kDefaultLoadAlignment - 1), 0); + tracePages.emplace((import.Slot + kPltGotEntryBytes - 1) & ~(kDefaultLoadAlignment - 1), 0); + } + for (auto& [page, offset] : tracePages) { + offset = buf.size(); + buf.resize(buf.size() + 16); + Io::WriteU64(buf, offset, page - vaddrOfExtraBlockOffset(offset)); + } traceTableVaddr = vaddrOfExtraBlockOffset(buf.size()); for (std::size_t index = 0; index < traced.size(); ++index) { traceEntries.push_back(buf.size()); @@ -598,11 +645,14 @@ std::vector LinuxElfPatcher::Patch( const auto stubOff = static_cast(buf.size()); const auto stubVaddr = vaddrOfExtraBlockOffset(stubOff); if (_traceImports) { - for (std::uint8_t b : TracePrologue(stubVaddr, traceTableVaddr, traceTableEndVaddr, traceFunctionVaddr)) + for (std::uint8_t b : TracePrologue(stubVaddr, tracePagesVaddr, traceTableVaddr, traceTableEndVaddr, traceFunctionVaddr)) buf.push_back(b); } const auto entryStubVaddr = vaddrOfExtraBlockOffset(buf.size()); - for (std::uint8_t b : _entryStubBuilder->BuildEntryStub(address(entryStubVaddr), address(realEntryVaddr))) + std::optional mainStackSlotVaddr; + if (mainStackSlotOff) + mainStackSlotVaddr = address(vaddrOfExtraBlockOffset(*mainStackSlotOff)); + for (std::uint8_t b : _entryStubBuilder->BuildEntryStub(address(entryStubVaddr), address(realEntryVaddr), mainStackSlotVaddr)) buf.push_back(b); const auto stubSize = static_cast(buf.size()) - stubOff; _byteWriter->WriteU64(buf, kEhdrEntryOffset, address(stubVaddr)); @@ -654,6 +704,33 @@ std::vector LinuxElfPatcher::Patch( if (_fixedBase) RebaseProgramHeaders(buf, tableOff, phEntSize, writtenPh, base); + std::uint64_t relroStart = 0; + std::uint64_t relroEnd = 0; + for (std::uint16_t index = 0; index < writtenPh; ++index) { + const auto header = static_cast(tableOff + static_cast(index) * phEntSize); + if (Io::ReadU32(buf, header + kPhdrTypeOffset) != PT_GNU_RELRO) + continue; + const auto start = Io::ReadU64(buf, header + kPhdrVaddrOffset); + const auto size = Io::ReadU64(buf, header + kPhdrMemSizeOffset); + relroStart = start & ~(kDefaultLoadAlignment - 1); + relroEnd = (start + size) & ~(kDefaultLoadAlignment - 1); + } + for (const auto& [page, offset] : tracePages) { + std::uint32_t flags = 0; + for (std::uint16_t index = 0; index < writtenPh; ++index) { + const auto header = static_cast(tableOff + static_cast(index) * phEntSize); + if (Io::ReadU32(buf, header + kPhdrTypeOffset) != PT_LOAD) + continue; + const auto start = Io::ReadU64(buf, header + kPhdrVaddrOffset); + const auto size = Io::ReadU64(buf, header + kPhdrMemSizeOffset); + if (size != 0 && address(page) >= (start & ~(kDefaultLoadAlignment - 1)) && address(page) < start + size) + flags = Io::ReadU32(buf, header + kPhdrFlagsOffset); + } + if (address(page) >= relroStart && address(page) < relroEnd) + flags &= ~PF_X; + Io::WriteU32(buf, offset + 8, 3 | ((flags & PF_X) != 0 ? 4 : 0)); + } + SectionHeaderTableRequest sectionRequest{}; sectionRequest.DynStrOffset = dynStrOff; sectionRequest.DynStrSize = dynStrSize; diff --git a/core/relinker/elfpatcher/src/macos/MacOsImage.cpp b/core/relinker/elfpatcher/src/macos/MacOsImage.cpp index b2545510c1..fd1abf4d68 100644 --- a/core/relinker/elfpatcher/src/macos/MacOsImage.cpp +++ b/core/relinker/elfpatcher/src/macos/MacOsImage.cpp @@ -265,6 +265,7 @@ void AppendPathCommand(std::vector& out, const std::uint32_t cmd, struct GsTrap { std::uint32_t Rva; std::size_t Length; + std::optional Entry; }; struct TlsAccess { @@ -291,7 +292,8 @@ struct TlsScan { TlsScan ScanTls(const std::vector& source, const std::vector& headers, const Windows::WindowsLoadImage& image, const std::function& message) { TlsScan scan; const Codegen::X64InstructionDecoder decoder; - const auto instructions = Relinker::CodeInstructionCollector().Collect(source, headers, {}, nullptr, message); + std::set entryRoots; + const auto instructions = Relinker::CodeInstructionCollector().Collect(source, headers, {}, &entryRoots, message); for (const auto& header : headers) { if (header.Type == 7) { if (scan.Tls != nullptr) throw Domain::RelinkerException("Multiple ELF TLS segments"); @@ -299,7 +301,7 @@ TlsScan ScanTls(const std::vector& source, const std::vector& source, const std::vector(header.Offset + *entry - header.MappedAddress) : std::nullopt}); + } continue; } const auto position = info.OpcodeOffset; @@ -335,6 +339,9 @@ TlsScan ScanTls(const std::vector& source, const std::vector WriteMacOsImage(MacOsImageInput& input) { const auto target = tls.BranchTargets.upper_bound(trap.Rva); if (target != tls.BranchTargets.end() && *target < trap.Rva + trap.Length) throw Domain::RelinkerException("Branch enters a guest GS instruction", *target); + if (trap.Entry) + throw Domain::RelinkerException("GS trap would overwrite an executable entry point", *trap.Entry); if (input.Trampolines != nullptr) { for (const auto& site : *input.Trampolines) { const auto siteRva = image.GetRva(site.Address, site.Length); diff --git a/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp index d9be16b069..a8cc3bb237 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp @@ -498,6 +498,34 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con }; code.Emit({0x53, 0x55, 0x56, 0x57, 0x41, 0x54, 0x48, 0x83, 0xec, 0x60}); + code.Emit({0x31, 0xc9}); + code.Rip({0x48, 0x8d, 0x15}, programPath); + code.Emit({0x41, 0xb8}); + code.U32(PathCapacity); + call("GetModuleFileNameW"); + requireNonzero(0, 0xc000000du); + code.Emit({0x3d}); + code.U32(PathCapacity); + const auto pathFits = code.Branch({0x0f, 0x82}); + fail(1, 0xc0000106u); + code.PatchBranch(pathFits, code.GetRva()); + code.Emit({0x89, 0xc1}); + code.Rip({0x48, 0x8d, 0x35}, programPath); + code.Rip({0x48, 0x8d, 0x3d}, modulePath); + code.Emit({0xfc, 0x66, 0xf3, 0xa5, 0x49, 0x89, 0xfc}); + code.Rip({0x48, 0x8d, 0x1d}, modulePath); + const auto findSeparator = code.GetRva(); + code.Emit({0x49, 0x39, 0xdc}); + const auto hasDirectory = code.Branch({0x0f, 0x85}); + fail(2, 0xc000000du); + code.PatchBranch(hasDirectory, code.GetRva()); + code.Emit({0x49, 0x83, 0xec, 0x02, 0x66, 0x41, 0x83, 0x3c, 0x24, 0x5c}); + code.Rip({0x0f, 0x85}, findSeparator); + code.Emit({0x49, 0x83, 0xc4, 0x02}); + code.Emit({0x66, 0x41, 0xc7, 0x04, 0x24, 0x00, 0x00}); + logMissingHandle(0xfffffff5u); + logMissingHandle(0xfffffff4u); + code.Rip({0x48, 0x8d, 0x0d}, parentVariable); code.Rip({0x48, 0x8d, 0x15}, parentText); code.Emit({0x41, 0xb8, 17, 0, 0, 0}); @@ -541,33 +569,6 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con requireNonzero(parentError, 1); code.PatchBranch(noParent, code.GetRva()); - code.Emit({0x31, 0xc9}); - code.Rip({0x48, 0x8d, 0x15}, programPath); - code.Emit({0x41, 0xb8}); - code.U32(PathCapacity); - call("GetModuleFileNameW"); - requireNonzero(0, 0xc000000du); - code.Emit({0x3d}); - code.U32(PathCapacity); - const auto pathFits = code.Branch({0x0f, 0x82}); - fail(1, 0xc0000106u); - code.PatchBranch(pathFits, code.GetRva()); - code.Emit({0x89, 0xc1}); - code.Rip({0x48, 0x8d, 0x35}, programPath); - code.Rip({0x48, 0x8d, 0x3d}, modulePath); - code.Emit({0xfc, 0x66, 0xf3, 0xa5, 0x49, 0x89, 0xfc}); - code.Rip({0x48, 0x8d, 0x1d}, modulePath); - const auto findSeparator = code.GetRva(); - code.Emit({0x49, 0x39, 0xdc}); - const auto hasDirectory = code.Branch({0x0f, 0x85}); - fail(2, 0xc000000du); - code.PatchBranch(hasDirectory, code.GetRva()); - code.Emit({0x49, 0x83, 0xec, 0x02, 0x66, 0x41, 0x83, 0x3c, 0x24, 0x5c}); - code.Rip({0x0f, 0x85}, findSeparator); - code.Emit({0x49, 0x83, 0xc4, 0x02}); - code.Emit({0x66, 0x41, 0xc7, 0x04, 0x24, 0x00, 0x00}); - logMissingHandle(0xfffffff5u); - logMissingHandle(0xfffffff4u); code.Rip({0x48, 0x8d, 0x0d}, modulePath); call("SetCurrentDirectoryW"); requireNonzero(directoryError, 0xc000000du); @@ -756,7 +757,7 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con code.PatchBranch(weakDone, code.GetRva()); continue; } - if (lazyBinding) { + if (lazyBinding && imports[index].Lazy) { lazyUnresolvedImports.push_back(index); const auto skipGotWrite = code.Branch({0xe9}); patchResolved(); diff --git a/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp b/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp index 7a3794848e..ca987359d7 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp @@ -15,7 +15,6 @@ namespace Elfpatcher { std::vector GuestModuleWriter::WriteWindows(const Relinker::GuestImage& guest, Domain::GuestRuntime& runtime, const std::function& message) const { using namespace Windows; - if (guest.Symbolic) throw Domain::RelinkerException("DF_SYMBOLIC guest modules are not supported by the Windows writer"); WindowsLoadImage image(guest.Bytes, guest.Headers, false); std::vector relocations; std::vector> tlsModules; diff --git a/core/relinker/elfpatcher/src/windows/WindowsIconResourceBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsIconResourceBuilder.cpp index 4a547373bd..a075e442a7 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsIconResourceBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsIconResourceBuilder.cpp @@ -81,7 +81,7 @@ PeDirectory WindowsIconResourceBuilder::Build(const std::vector& p throw Domain::RelinkerException("Invalid Windows resource section RVA", nextRva); if (std::any_of(sections.begin(), sections.end(), [](const auto& section) { return section.Name == ".rsrc"; })) throw Domain::RelinkerException("Duplicate Windows resource section"); - if (!sections.empty() && nextRva < AlignRva(static_cast(sections.back().Rva) + sections.back().Data.size())) + if (!sections.empty() && nextRva < AlignRva(static_cast(sections.back().Rva) + sections.back().MemorySize())) throw Domain::RelinkerException("Windows resource section overlaps preceding section", nextRva); if (png.size() > 0x7fffffffu - 184) throw Domain::RelinkerException("Invalid Windows icon size"); validatePng(png, iconPath); diff --git a/core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp index 566dc2f13e..3512669e70 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp @@ -79,6 +79,7 @@ PeRelocations WindowsRelocationBuilder::Apply(WindowsLoadImage& image, const Dom result.Imports.push_back({name, rva, addend}); result.Imports.back().Weak = (dynamicSection.DynSymData[symbolOffset + 4] >> 4) == STB_WEAK; result.Imports.back().Function = type == 7 || (type == 6 && dynamicSection.FunctionImports.contains(target)); + result.Imports.back().Lazy = type == 7 || (dynamicSection.DynSymData[symbolOffset + 4] & 15) == STT_FUNC; if (const auto module = dynamicSection.ImportModules.find(target); module != dynamicSection.ImportModules.end()) result.Imports.back().Library = module->second; } diff --git a/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp index 722584997a..a7f117644f 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp @@ -134,7 +134,7 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co if (header.Type != 1 || (header.Flags & 1) == 0) continue; std::uint64_t decodedEnd = 0; - for (auto instruction = instructions.lower_bound(header.MappedAddress); instruction != instructions.end() && *instruction - header.MappedAddress < header.FileSize; ++instruction) { + for (auto instruction = std::lower_bound(instructions.begin(), instructions.end(), header.MappedAddress); instruction != instructions.end() && *instruction - header.MappedAddress < header.FileSize; ++instruction) { if (scanned % 4096 == 0) report(7000 + 2000 * scanned / std::max(instructions.size(), 1)); ++scanned; const auto offset = *instruction - header.MappedAddress; diff --git a/core/relinker/elfpatcher/tests/EntryStubTests.cpp b/core/relinker/elfpatcher/tests/EntryStubTests.cpp index 282e8ff25c..80a4e7761b 100644 --- a/core/relinker/elfpatcher/tests/EntryStubTests.cpp +++ b/core/relinker/elfpatcher/tests/EntryStubTests.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -23,10 +24,33 @@ bool bytesAt(const std::vector& stub, std::size_t& offset, const s return true; } +void stubEntersThroughTheMainStackSlot() { + constexpr std::uint64_t stubVaddr = 0xb32db98; + constexpr std::uint64_t entryVaddr = 0x80; + constexpr std::uint64_t slotVaddr = 0xb32d000; + const auto stub = Elfpatcher::EntryStubBuilder().BuildEntryStub(stubVaddr, entryVaddr, slotVaddr); + std::size_t offset = 0; + const auto ripTarget = [&](const std::vector& opcode, const std::string& what) { + require(bytesAt(stub, offset, opcode), "the stub does not " + what); + std::int32_t rel32 = 0; + require(offset + sizeof(rel32) <= stub.size(), "the stub ends inside a displacement"); + std::memcpy(&rel32, stub.data() + offset, sizeof(rel32)); + offset += sizeof(rel32); + return stubVaddr + offset + static_cast(rel32); + }; + require(bytesAt(stub, offset, {0x48, 0x89, 0xe7}), "the stub does not pass the initial stack pointer in rdi"); + require(bytesAt(stub, offset, {0x48, 0x83, 0xe4, 0xf0}), "the stub does not align the stack to 16 bytes"); + require(bytesAt(stub, offset, {0x6a, 0x00, 0x6a, 0x00}), "the stub does not push a zero frame record"); + require(bytesAt(stub, offset, {0x48, 0x89, 0xe5}), "the stub does not point rbp at the zero frame record"); + require(ripTarget({0x48, 0x8d, 0x35}, "load the entry point into rsi") == entryVaddr, "the stub passes the wrong entry point"); + require(ripTarget({0xff, 0x15}, "call through the main stack entry slot") == slotVaddr, "the stub calls through the wrong slot"); + require(bytesAt(stub, offset, {0x0f, 0x0b}) && offset == stub.size(), "the stub does not end with ud2 after the call"); +} + void stubTerminatesTheFrameChain() { constexpr std::uint64_t stubVaddr = 0xb32db98; constexpr std::uint64_t entryVaddr = 0x80; - const auto stub = Elfpatcher::EntryStubBuilder().BuildEntryStub(stubVaddr, entryVaddr); + const auto stub = Elfpatcher::EntryStubBuilder().BuildEntryStub(stubVaddr, entryVaddr, std::nullopt); std::size_t offset = 0; require(bytesAt(stub, offset, {0x48, 0x89, 0xe7}), "the stub does not pass the initial stack pointer in rdi"); require(bytesAt(stub, offset, {0x48, 0x83, 0xe4, 0xf0}), "the stub does not align the stack to 16 bytes"); @@ -118,6 +142,16 @@ void windowsStubRejectsAnOverlongAllocatorPath() { throw std::runtime_error("an allocator path past the startup buffer was accepted"); } +void windowsStubWaitsForTheParentAfterOpeningTheLog(const std::string& runPath) { + const auto nativeImports = Elfpatcher::Windows::WindowsImportBuilder().Build(0x1000); + const auto stub = windowsStub(nativeImports, runPath, {"libc.prx"}); + const auto log = ripReference(stub.Code, {0xff, 0x15}, nativeImports.Functions.at("CreateFileW"), 0); + require(log != std::string::npos, runPath + ": the stub does not open anyps5.log"); + const auto parent = ripReference(stub.Code, {0xff, 0x15}, nativeImports.Functions.at("GetEnvironmentVariableW"), 0); + require(parent != std::string::npos, runPath + ": the stub does not read ANYPS5_LOADEXEC_PARENT"); + require(log < parent, runPath + ": the stub reads ANYPS5_LOADEXEC_PARENT before anyps5.log is opened"); +} + void windowsLibSceAmprObjectImportsFallBackToTheLibcProvider() { using namespace Elfpatcher::Windows; const auto nativeImports = WindowsImportBuilder().Build(0x1000); @@ -164,7 +198,7 @@ void windowsKernelVariantDependenciesLoadCanonicalKernel() { void stubRejectsAnEntryBeyondTheWrapBoundary() { bool threw = false; try { - static_cast(Elfpatcher::EntryStubBuilder().BuildEntryStub(0x1000, 0xFFFFFFFFFFFFF000)); + static_cast(Elfpatcher::EntryStubBuilder().BuildEntryStub(0x1000, 0xFFFFFFFFFFFFF000, std::nullopt)); } catch (const Domain::RelinkerException& error) { threw = std::string(error.what()).find("rel32") != std::string::npos; } @@ -175,6 +209,7 @@ void stubRejectsAnEntryBeyondTheWrapBoundary() { int main() { try { + stubEntersThroughTheMainStackSlot(); stubTerminatesTheFrameChain(); stubRejectsAnEntryBeyondTheWrapBoundary(); windowsStubLoadsTheAllocatorFirst("C:/Games/Title/libs", u"C:\\Games\\Title\\libs\\", {0x48, 0x8d, 0x0d}); @@ -182,6 +217,8 @@ int main() { windowsStubLoadsTheAllocatorOnRequest("C:/Games/Title/libs", u"C:\\Games\\Title\\libs\\", {0x48, 0x8d, 0x0d}); windowsStubLoadsTheAllocatorOnRequest("$ORIGIN/libs", u"libs\\", {0x48, 0x8d, 0x35}); windowsStubRejectsAnOverlongAllocatorPath(); + windowsStubWaitsForTheParentAfterOpeningTheLog("C:/Games/Title/libs"); + windowsStubWaitsForTheParentAfterOpeningTheLog("$ORIGIN/libs"); windowsLibSceAmprObjectImportsFallBackToTheLibcProvider(); windowsKernelVariantImportsSearchCanonicalKernelAfterTheRequestedVariant(); windowsKernelVariantDependenciesLoadCanonicalKernel(); diff --git a/core/relinker/elfpatcher/tests/GuestModuleArgsHost.cpp b/core/relinker/elfpatcher/tests/GuestModuleArgsHost.cpp index b99a198578..607dfc221f 100644 --- a/core/relinker/elfpatcher/tests/GuestModuleArgsHost.cpp +++ b/core/relinker/elfpatcher/tests/GuestModuleArgsHost.cpp @@ -61,3 +61,8 @@ extern "C" void __aps5_set_module_init_result_nid_no_patch(int result) { std::snprintf(line, sizeof(line), "result=%d\n", result); Write(line); } + +extern "C" [[noreturn]] void APS5_VABI EnterGuestMainStack_nid_no_patch(void* arguments, void (APS5_VABI* entry)(void*)) { + entry(arguments); + Fail("The guest entry point returned"); +} diff --git a/core/relinker/elfpatcher/tests/WindowsIconResourceTests.cpp b/core/relinker/elfpatcher/tests/WindowsIconResourceTests.cpp index a3813723aa..4dd0bc4aac 100644 --- a/core/relinker/elfpatcher/tests/WindowsIconResourceTests.cpp +++ b/core/relinker/elfpatcher/tests/WindowsIconResourceTests.cpp @@ -173,6 +173,21 @@ void run() { require(overlappingSections.size() == 1, "Section added on overlapping section"); } require(caughtOverlap, "Build succeeded with overlapping resource section"); + + std::vector virtualOverlap{ + {".data", 0x10000, Elfpatcher::Windows::SectionRead | 0x40u, std::vector(0x1000, 0), 0x41000} + }; + bool caughtVirtualOverlap = false; + try { + builder.Build(Png, iconPath, virtualOverlap, 0x50000); + } catch (const Domain::RelinkerException& error) { + caughtVirtualOverlap = true; + require(std::string(error.what()).find("Windows resource section overlaps preceding section") != std::string::npos, "Unexpected error for virtual overlap"); + require(virtualOverlap.size() == 1, "Section added on virtual overlap"); + } + require(caughtVirtualOverlap, "Build succeeded with a resource section overlapping virtual data"); + const auto following = builder.Build(Png, iconPath, virtualOverlap, 0x60000); + require(following.Rva == 0x60000 && virtualOverlap.size() == 2, "Build rejected a resource section after virtual data"); } } diff --git a/core/relinker/main.cpp b/core/relinker/main.cpp index 7d86b94db6..9eb2f4c633 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -71,6 +71,7 @@ ConvertedExecutable Convert(const Cli::Args& args, const std::string& inputPath, options.TrapSdkSyscallStubs = args.trapSdkSyscallStubs; options.UnusedFilterLevel = args.unusedFilterLevel; options.LazyBinding = args.lazyBinding; + options.OmitUnavailableDependencies = args.omitUnavailableDependencies; options.RunPath = args.runPath; options.WindowsGui = args.windowsGui; options.TraceImports = args.traceImports; diff --git a/core/relinker/relinker/include/relinker/analysis/CodeInstructionCollector.hpp b/core/relinker/relinker/include/relinker/analysis/CodeInstructionCollector.hpp index cdd140af64..1e4b325291 100644 --- a/core/relinker/relinker/include/relinker/analysis/CodeInstructionCollector.hpp +++ b/core/relinker/relinker/include/relinker/analysis/CodeInstructionCollector.hpp @@ -5,12 +5,13 @@ #include #include #include +#include namespace Relinker { class CodeInstructionCollector { public: - std::set Collect(const std::vector& bytes, const std::vector& headers, const std::function& progress = {}, std::set* entryRoots = nullptr, const std::function& message = {}) const; + std::vector Collect(const std::vector& bytes, const std::vector& headers, const std::function& progress = {}, std::set* entryRoots = nullptr, const std::function& message = {}) const; }; } diff --git a/core/relinker/relinker/include/relinker/guest/GuestImage.hpp b/core/relinker/relinker/include/relinker/guest/GuestImage.hpp index 2d6b51a189..35bc3d6959 100644 --- a/core/relinker/relinker/include/relinker/guest/GuestImage.hpp +++ b/core/relinker/relinker/include/relinker/guest/GuestImage.hpp @@ -85,7 +85,7 @@ std::vector> RemoveBacku class GuestModuleBuilder { public: - std::vector Build(const std::filesystem::path& inputPath, const std::filesystem::path& outputPath, Domain::SysVDynamicSection& dynamic, bool windows, bool macos, bool toIntel, bool toRosetta, ISyscallScanner& syscallScanner, bool lazyBinding, const std::string& runPath, const std::set& excludedModules, const std::filesystem::path& root, const std::vector& runtimeDirectories, std::vector modules, Domain::IProgress& progress) const; + std::vector Build(const std::filesystem::path& inputPath, const std::filesystem::path& outputPath, Domain::SysVDynamicSection& dynamic, bool windows, bool macos, bool toIntel, bool toRosetta, ISyscallScanner& syscallScanner, bool lazyBinding, bool omitUnavailableDependencies, const std::string& runPath, const std::set& excludedModules, const std::filesystem::path& root, const std::vector& runtimeDirectories, std::vector modules, Domain::IProgress& progress) const; }; } diff --git a/core/relinker/relinker/include/relinker/pipeline/Conversion.hpp b/core/relinker/relinker/include/relinker/pipeline/Conversion.hpp index 3a45a1ea10..8e4ddd63f9 100644 --- a/core/relinker/relinker/include/relinker/pipeline/Conversion.hpp +++ b/core/relinker/relinker/include/relinker/pipeline/Conversion.hpp @@ -19,6 +19,7 @@ struct ConversionOptions { bool SkipSyscallCheck = false; bool TrapSdkSyscallStubs = false; bool LazyBinding = false; + bool OmitUnavailableDependencies = false; bool WindowsGui = false; bool TraceImports = false; std::optional WindowsFixedBase; diff --git a/core/relinker/relinker/src/analysis/CallSiteResolver.cpp b/core/relinker/relinker/src/analysis/CallSiteResolver.cpp index a99edf14ee..ef491e1d10 100644 --- a/core/relinker/relinker/src/analysis/CallSiteResolver.cpp +++ b/core/relinker/relinker/src/analysis/CallSiteResolver.cpp @@ -19,10 +19,16 @@ std::int32_t readDisp32(const std::vector& text, std::size_t offse return static_cast(v); } -std::optional ripRelativeDispOffset(const std::vector& text, const Codegen::InstructionMatch& m) { +struct RelativeOperand { + std::size_t DisplacementOffset; + bool AddressSize32; +}; + +std::optional relativeOperand(const std::vector& text, const Codegen::InstructionMatch& m) { std::size_t cur = static_cast(m.Offset); std::size_t end = cur + m.Length; bool hasRexW = false; + bool addressSize32 = false; while (cur < end) { std::uint8_t b = text[cur]; if (b >= RexMin && b <= RexMax) { @@ -30,8 +36,9 @@ std::optional ripRelativeDispOffset(const std::vector ++cur; continue; } - if (b == PrefixRepne || b == PrefixSegDs) { + if (b == PrefixRepne || b == PrefixSegCs || b == PrefixSegDs || b == PrefixSegEs || b == PrefixSegSs || b == PrefixAddressSize) { hasRexW = false; + if (b == PrefixAddressSize) addressSize32 = true; ++cur; continue; } @@ -47,9 +54,9 @@ std::optional ripRelativeDispOffset(const std::vector if (mod != ModRmModIndirect || rm != ModRmRmRipRelative) return std::nullopt; const auto operation = (modrm >> ModRmRegShift) & ModRmRegMask; if (opcode == OneByteGrp5Rm && operation >= 2 && operation <= 6) - return cur + 2; + return RelativeOperand{cur + 2, addressSize32}; if (opcode == OneByteModRmRangeJMax && hasRexW) - return cur + 2; + return RelativeOperand{cur + 2, addressSize32}; return std::nullopt; } } @@ -79,11 +86,12 @@ std::vector CallSiteResolver::ResolveCallSites(const std::vector constexpr std::size_t MinRipRelativeInstructionBytes = 6; _cachedTargetToInstr.reserve(std::min(static_cast(candidateCount), textSection.size() / MinRipRelativeInstructionBytes)); scanner->VisitCodeSection(textSection, 0, textSection.size(), [&](const Codegen::InstructionMatch& m) { - const auto dispOff = ripRelativeDispOffset(textSection, m); - if (!dispOff || *dispOff + 4 > textSection.size()) return; - std::int32_t disp = readDisp32(textSection, *dispOff); + const auto operand = relativeOperand(textSection, m); + if (!operand || operand->DisplacementOffset + 4 > textSection.size()) return; + std::int32_t disp = readDisp32(textSection, operand->DisplacementOffset); VirtualAddress instrEnd = textSectionVAddr + m.Offset + static_cast(m.Length); VirtualAddress target = instrEnd + static_cast(static_cast(disp)); + if (operand->AddressSize32) target = static_cast(target); _cachedTargetToInstr.emplace_back(target, m.Offset); }); std::sort(_cachedTargetToInstr.begin(), _cachedTargetToInstr.end()); diff --git a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp index 1396968ea2..55e9d47efd 100644 --- a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp +++ b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp @@ -20,6 +20,7 @@ #include #include #include +#include namespace Relinker { @@ -42,7 +43,7 @@ public: } -std::set CodeInstructionCollector::Collect(const std::vector& bytes, const std::vector& headers, const std::function& progress, std::set* entryRoots, const std::function& message) const { +std::vector CodeInstructionCollector::Collect(const std::vector& bytes, const std::vector& headers, const std::function& progress, std::set* entryRoots, const std::function& message) const { std::uint64_t lastProgress = 0; const auto report = [&](std::uint64_t completed) { if (completed <= lastProgress) return; @@ -397,10 +398,10 @@ std::set CodeInstructionCollector::Collect(const std::ve std::erase_if(roots, inTable); } if (roots.empty()) throw Domain::RelinkerException("Code analysis: no code entry points"); - std::set instructions; + std::vector instructionList; const Codegen::X64InstructionDecoder decoder; std::vector> skipped; - std::set staticTargets; + std::vector staticTargets; std::uint64_t functionBytes = 0; for (const auto& [begin, end] : functions) functionBytes += end - begin; std::uint64_t decodedBytes = 0; @@ -427,11 +428,11 @@ std::set CodeInstructionCollector::Collect(const std::ve } if (info.Length == 0 || info.Length > end - address) throw Domain::RelinkerException("Code analysis: instruction crosses function boundary", address); - instructions.emplace_hint(instructions.end(), address); + instructionList.push_back(address); addAddressTakenRoot(address, bytes.data() + offset + address - begin, info); if (info.HasBranchTarget && !info.HasRipRelativeDisp) { const auto target = address + info.Length + static_cast(info.BranchDisp); - staticTargets.insert(target); + staticTargets.push_back(target); addRoot(target); } address += info.Length; @@ -507,6 +508,8 @@ std::set CodeInstructionCollector::Collect(const std::ve } return false; }; + std::sort(staticTargets.begin(), staticTargets.end()); + staticTargets.erase(std::unique(staticTargets.begin(), staticTargets.end()), staticTargets.end()); for (const auto target : staticTargets) { for (const auto& [skipBegin, skipEnd] : skipped) { if (skipBegin <= target && target < skipEnd && !decodesInSegment(target)) @@ -514,7 +517,7 @@ std::set CodeInstructionCollector::Collect(const std::ve } } std::size_t previousRoots = 0; - std::unordered_set decodedInstructions; + std::vector> decodedInstructions(codeHeaders.size()); std::vector> sectionReachable(codeHeaders.size()); do { previousRoots = roots.size(); @@ -526,12 +529,16 @@ std::set CodeInstructionCollector::Collect(const std::ve for (const auto root : roots) if (root >= header.MappedAddress && root - header.MappedAddress < header.FileSize) entries.push_back(root); if (entries.empty()) continue; const std::span text(bytes.data() + header.Offset, header.FileSize); + auto& decoded = decodedInstructions[i]; + if (decoded.empty()) decoded.resize(header.FileSize); auto& reached = sectionReachable[i]; if (reached.empty()) reached.resize(header.FileSize); for (const auto address : UnusedNidFilter::ExtendControlFlowGraph(text, header.MappedAddress, entries.front(), entries, pointers, sceFunctions.empty(), reached, [&](std::uint64_t count) { report(6000 + 2500 * count / std::max(scanBytes, 1)); }, inTable)) { - if (!decodedInstructions.insert(address).second) continue; - instructions.insert(address); const auto offset = address - header.MappedAddress; + if (offset >= decoded.size()) throw Domain::RelinkerException("Code analysis: reachable address outside its section", address); + if (decoded[offset]) continue; + decoded[offset] = true; + instructionList.push_back(address); const auto info = decoder.DecodeInstruction(text.data() + offset, text.size() - offset); addAddressTakenRoot(address, text.data() + offset, info); if (info.HasBranchTarget && !info.HasRipRelativeDisp) addRoot(address + info.Length + static_cast(info.BranchDisp)); @@ -539,6 +546,9 @@ std::set CodeInstructionCollector::Collect(const std::ve } } while (roots.size() != previousRoots); report(8500); + std::sort(instructionList.begin(), instructionList.end()); + instructionList.erase(std::unique(instructionList.begin(), instructionList.end()), instructionList.end()); + const auto& instructions = instructionList; std::uint64_t checked = 0; std::uint64_t previousEnd = 0; const Domain::ProgramHeader* cachedHeader = nullptr; @@ -578,7 +588,7 @@ std::set CodeInstructionCollector::Collect(const std::ve } } if (!spanHasFsPrefix) { - const auto previous = instructions.lower_bound(address); + const auto previous = std::lower_bound(instructions.begin(), instructions.end(), address); if (previous != instructions.begin()) { const auto previousStart = *std::prev(previous); if (const auto* header = findCodeHeader(previousStart)) { @@ -607,7 +617,7 @@ std::set CodeInstructionCollector::Collect(const std::ve *entryRoots = std::move(roots); entryRoots->insert(dataTargets.begin(), dataTargets.end()); } - return instructions; + return instructionList; } } diff --git a/core/relinker/relinker/src/analysis/RelativeJumpTables.cpp b/core/relinker/relinker/src/analysis/RelativeJumpTables.cpp index 799cc59c20..0633b2dd3e 100644 --- a/core/relinker/relinker/src/analysis/RelativeJumpTables.cpp +++ b/core/relinker/relinker/src/analysis/RelativeJumpTables.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -255,6 +256,7 @@ std::size_t FindTrailingRelativeJumpTable(std::span code, Vi } const Codegen::X64InstructionDecoder decoder; std::vector loads; + std::unordered_map> loadsAt; std::vector instructionStarts(code.size() + 1, false); bool flowEnded = false; bool sawIndirectJump = false; @@ -262,16 +264,18 @@ std::size_t FindTrailingRelativeJumpTable(std::span code, Vi for (std::size_t position = 0; position < code.size();) { instructionStarts[position] = true; if (flowEnded && sawIndirectJump) { - for (const auto& load : loads) { - if (load.Target != position || load.At >= lastIndirectJump) continue; - const auto end = TablesEnd(code, position, loads, instructionStarts); - if (end == 0) continue; - const auto aligned = std::min(code.size(), static_cast(((codeAddress + end + 15) & ~VirtualAddress{15}) - codeAddress)); - const auto checkedTo = std::max(aligned, std::min(code.size(), unwindEnd)); - const bool toRangeEnd = OnlyPadding(code, end, code.size(), decoder); - if (!toRangeEnd && !OnlyPadding(code, end, checkedTo, decoder)) continue; - if (tableEnd) *tableEnd = toRangeEnd ? code.size() : checkedTo; - return position; + if (const auto found = loadsAt.find(position); found != loadsAt.end()) { + for (const auto at : found->second) { + if (at >= lastIndirectJump) continue; + const auto end = TablesEnd(code, position, loads, instructionStarts); + if (end == 0) continue; + const auto aligned = std::min(code.size(), static_cast(((codeAddress + end + 15) & ~VirtualAddress{15}) - codeAddress)); + const auto checkedTo = std::max(aligned, std::min(code.size(), unwindEnd)); + const bool toRangeEnd = OnlyPadding(code, end, code.size(), decoder); + if (!toRangeEnd && !OnlyPadding(code, end, checkedTo, decoder)) continue; + if (tableEnd) *tableEnd = toRangeEnd ? code.size() : checkedTo; + return position; + } } } Codegen::DecodedInstructionInfo info; @@ -286,7 +290,10 @@ std::size_t FindTrailingRelativeJumpTable(std::span code, Vi std::int32_t displacement = 0; std::memcpy(&displacement, code.data() + next - 4, sizeof(displacement)); const auto target = static_cast(next) + displacement; - if (target > static_cast(next) && target < static_cast(code.size())) loads.push_back({position, static_cast(target)}); + if (target > static_cast(next) && target < static_cast(code.size())) { + loads.push_back({position, static_cast(target)}); + loadsAt[static_cast(target)].push_back(position); + } } if (info.FlowKind == Codegen::ControlFlowKind::IndirectJump) { sawIndirectJump = true; @@ -341,6 +348,7 @@ std::vector FindRelativeJumpTables(std::span pendingTargets(branchTargets.begin(), branchTargets.end()); std::set exploredGaps; + std::vector decoded(segment.size(), false); while (!pendingTargets.empty()) { const auto current = pendingTargets.back(); pendingTargets.pop_back(); @@ -350,7 +358,8 @@ std::vector FindRelativeJumpTables(std::spansecond; if (inKnownFunction || !exploredGaps.insert(current).second) continue; auto pos = static_cast(current - segmentVAddr); - while (pos < segment.size()) { + while (pos < segment.size() && !decoded[pos]) { + decoded[pos] = true; Codegen::DecodedInstructionInfo info; try { info = decoder.DecodeInstruction(segment.data() + pos, segment.size() - pos); diff --git a/core/relinker/relinker/src/analysis/UnusedNidFilter/ControlFlowGraph.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter/ControlFlowGraph.cpp index 2b3fad01a2..7a188fe323 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter/ControlFlowGraph.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter/ControlFlowGraph.cpp @@ -186,6 +186,7 @@ void walkGraph(std::span text, VirtualAddress textVaddr, Vir } void buildGraph(std::span text, VirtualAddress textVaddr, VirtualAddress entryVaddr, const std::vector& extraEntries, const IRelativeRelocationIndex& relativeRelocations, const std::vector* data, bool followCodeAddresses, std::unordered_set& reachable, const std::function& progress, const std::function& isData) { + if (reachable.empty()) reachable.reserve(text.size() / 4); if (text.size() > std::numeric_limits::max() - textVaddr) throw RelinkerException("Text section range exceeds the address space"); std::vector state(text.size(), 0); diff --git a/core/relinker/relinker/src/analysis/UnusedNidFilter/EhFrameReader.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter/EhFrameReader.cpp index 5045239694..2b6ca0a4a8 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter/EhFrameReader.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter/EhFrameReader.cpp @@ -35,10 +35,12 @@ public: if (position < segment.MappedAddress || position - segment.MappedAddress > segment.FileSize) throw RelinkerException("Strict filter: EH frame header exceeds segment", position); if (count > segment.FileSize / 2) throw RelinkerException("Strict filter: invalid EH frame count", position); + std::set> listed; for (std::uint64_t index = 0; index < count; ++index) { const auto begin = encoded(position, tableEncoding, segment.MappedAddress); const auto fde = encoded(position, tableEncoding, segment.MappedAddress); if (position < segment.MappedAddress || position - segment.MappedAddress > segment.FileSize) throw RelinkerException("Strict filter: EH frame table exceeds segment", position); + if (!listed.emplace(begin, fde).second) continue; auto region = readFde(begin, fde); if (region.End != region.Begin) result.push_back(std::move(region)); } diff --git a/core/relinker/relinker/src/analysis/UnusedNidFilter/EntryPointCollector.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter/EntryPointCollector.cpp index 3a095c196c..f0acff2b1c 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter/EntryPointCollector.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter/EntryPointCollector.cpp @@ -202,7 +202,7 @@ public: std::uint16_t shndx = read16(elfBytes, sPos + 6); std::uint64_t symVal = read64(elfBytes, sPos + 8); if ((stBind == STB_GLOBAL || stBind == STB_WEAK) && - stType == STT_FUNC && shndx != SHN_UNDEF) { + stType == STT_FUNC && shndx != SHN_UNDEF && shndx != SHN_ABS) { addIfInText(symVal); } } diff --git a/core/relinker/relinker/src/analysis/UnusedNidFilter/RelativeRelocationIndex.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter/RelativeRelocationIndex.cpp index ad534c1ddc..fb12165781 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter/RelativeRelocationIndex.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter/RelativeRelocationIndex.cpp @@ -154,6 +154,9 @@ std::unique_ptr BuildRelativeRelocationIndex( if (hasSceDynlibData) throw RelinkerException("Multiple PT_SCE_DYNLIBDATA segments", phPos); hasSceDynlibData = true; sceTableBase = read64(elfBytes, phPos + 8); + std::uint64_t sceSize = read64(elfBytes, phPos + 32); + if (sceTableBase > elfBytes.size() || sceSize > elfBytes.size() - sceTableBase) + throw RelinkerException("PT_SCE_DYNLIBDATA segment out of bounds", phPos); } } const auto headers = ElfReader(elfBytes).ReadProgramHeaders(); @@ -180,16 +183,22 @@ std::unique_ptr BuildRelativeRelocationIndex( std::optional relaVa; std::optional relaSz; + std::optional relaEnt; std::optional jmprelVa; std::optional pltrelsz; + std::optional pltrel; bool hasSysvRela = false; bool hasOsRela = false; bool hasSysvRelaSz = false; bool hasOsRelaSz = false; + bool hasSysvRelaEnt = false; + bool hasOsRelaEnt = false; bool hasSysvJmprel = false; bool hasOsJmprel = false; bool hasSysvPltrelsz = false; bool hasOsPltrelsz = false; + bool hasSysvPltrel = false; + bool hasOsPltrel = false; bool terminated = false; std::vector relrTags; @@ -202,6 +211,8 @@ std::unique_ptr BuildRelativeRelocationIndex( terminated = true; break; } + if (tag == DT_REL || tag == DT_RELSZ || tag == DT_RELENT) + throw RelinkerException("DT_REL relocations are not supported"); if (tag == DT_RELR || tag == DT_RELRSZ || tag == DT_RELRENT) relrTags.push_back({tag, val}); if (tag == DT_RELA) { if (hasOsRela) throw RelinkerException("Both DT_OS_ and DT_ variants present for DT_RELA"); @@ -227,6 +238,18 @@ std::unique_ptr BuildRelativeRelocationIndex( hasOsRelaSz = true; relaSz = val; } + if (tag == DT_RELAENT) { + if (hasOsRelaEnt) throw RelinkerException("Both DT_OS_ and DT_ variants present for DT_RELAENT"); + if (hasSysvRelaEnt && relaEnt.value() != val) throw RelinkerException("Conflicting duplicate DT_RELAENT tags"); + hasSysvRelaEnt = true; + relaEnt = val; + } + if (tag == DT_OS_RELAENT) { + if (hasSysvRelaEnt) throw RelinkerException("Both DT_OS_ and DT_ variants present for DT_RELAENT"); + if (hasOsRelaEnt && relaEnt.value() != val) throw RelinkerException("Conflicting duplicate DT_OS_RELAENT tags"); + hasOsRelaEnt = true; + relaEnt = val; + } if (tag == DT_JMPREL) { if (hasOsJmprel) throw RelinkerException("Both DT_OS_ and DT_ variants present for DT_JMPREL"); if (hasSysvJmprel && jmprelVa.value() != val) throw RelinkerException("Conflicting duplicate DT_JMPREL tags"); @@ -251,19 +274,39 @@ std::unique_ptr BuildRelativeRelocationIndex( hasOsPltrelsz = true; pltrelsz = val; } + if (tag == DT_PLTREL) { + if (hasOsPltrel) throw RelinkerException("Both DT_OS_ and DT_ variants present for DT_PLTREL"); + if (hasSysvPltrel && pltrel.value() != val) throw RelinkerException("Conflicting duplicate DT_PLTREL tags"); + hasSysvPltrel = true; + pltrel = val; + } + if (tag == DT_OS_PLTREL) { + if (hasSysvPltrel) throw RelinkerException("Both DT_OS_ and DT_ variants present for DT_PLTREL"); + if (hasOsPltrel && pltrel.value() != val) throw RelinkerException("Conflicting duplicate DT_OS_PLTREL tags"); + hasOsPltrel = true; + pltrel = val; + } } if (!terminated) throw RelinkerException("Unterminated dynamic segment", segOff); if ((hasSysvRela && hasOsRelaSz) || (hasOsRela && hasSysvRelaSz)) throw RelinkerException("Mismatched DT_RELA and DT_RELASZ tag variants"); + if ((hasSysvRela && hasOsRelaEnt) || (hasOsRela && hasSysvRelaEnt)) + throw RelinkerException("Mismatched DT_RELA and DT_RELAENT tag variants"); if (relaVa.has_value() != relaSz.has_value()) throw RelinkerException("Incomplete relocation table metadata"); + if (relaEnt.has_value() && *relaEnt != RelaEntSize) + throw RelinkerException("Unsupported DT_RELAENT value: " + std::to_string(*relaEnt) + "; expected 24 bytes"); if ((hasSysvJmprel && hasOsPltrelsz) || (hasOsJmprel && hasSysvPltrelsz)) throw RelinkerException("Mismatched DT_JMPREL and DT_PLTRELSZ tag variants"); + if ((hasSysvJmprel && hasOsPltrel) || (hasOsJmprel && hasSysvPltrel)) + throw RelinkerException("Mismatched DT_JMPREL and DT_PLTREL tag variants"); if (jmprelVa.has_value() != pltrelsz.has_value()) throw RelinkerException("Incomplete PLT relocation table metadata"); + if (pltrel.has_value() && *pltrel != static_cast(DT_RELA)) + throw RelinkerException("Unsupported DT_PLTREL type: " + std::to_string(*pltrel) + "; expected DT_RELA (7)"); if (relaVa.has_value()) collectRelativeEntries(elfBytes, loads, *relaVa, *relaSz, hasOsRela, sceTableBase, targets); diff --git a/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictNidFilter.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictNidFilter.cpp index 26b90ad1b7..c9131e9b4c 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictNidFilter.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictNidFilter.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -41,6 +42,7 @@ public: terminated = true; break; } + if (tag == 17 || tag == 18 || tag == 19) throw RelinkerException("Strict filter: DT_REL relocations are not supported"); if (tag == 1 || (tag >= 0x61000000 && tag != 0x6100002F && tag != 0x61000031 && tag != 0x61000033 && tag != 0x61000039 && tag != 0x6100003B && tag != 0x6100003F)) continue; if (!tags.emplace(tag, read(segment.Offset + offset + 8)).second) throw RelinkerException("Strict filter: duplicate dynamic tag", segment.Offset + offset); @@ -70,6 +72,8 @@ public: const auto rela = table(7, 0x6100002F); const auto relaSize = value(8, 0x61000031); if (rela.has_value() != relaSize.has_value()) throw RelinkerException("Strict filter: incomplete relocation table metadata"); + if (const auto entrySize = value(9, 0x61000033); entrySize && *entrySize != 24) + throw RelinkerException("Strict filter: invalid RELA entry size"); if (rela) { if (value(9, 0x61000033) != 24 || *relaSize % 24 != 0) throw RelinkerException("Strict filter: invalid RELA entry size"); @@ -133,7 +137,7 @@ public: const auto info = read(position + 4); const auto type = info & 15; if (type == 10) throw RelinkerException("Strict filter: IFUNC resolver is not modeled", position); - if (section == 0 || (info >> 4) == 0 || type != 2) continue; + if (section == 0 || section == Elfpatcher::SHN_ABS || (info >> 4) == 0 || type != 2) continue; input.Entries.push_back(read(position + 8)); } } diff --git a/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictReachability.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictReachability.cpp index 63b7b6cf43..9b17dfa924 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictReachability.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictReachability.cpp @@ -166,7 +166,7 @@ private: region.Edges.insert(target); } if (info.HasRipRelativeDisp) { - std::int32_t displacement; + std::int32_t displacement = 0; std::memcpy(&displacement, input.Text.data() + offset + info.RipRelativeDispOffset, sizeof(displacement)); auto target = next + static_cast(static_cast(displacement)); if (info.AddressSize32) target = static_cast(target); diff --git a/core/relinker/relinker/src/guest/GuestImageReader.cpp b/core/relinker/relinker/src/guest/GuestImageReader.cpp index 02a952e638..df2363ef25 100644 --- a/core/relinker/relinker/src/guest/GuestImageReader.cpp +++ b/core/relinker/relinker/src/guest/GuestImageReader.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -90,10 +91,10 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector } } if (!terminated) fail("Unterminated dynamic segment"); - for (const auto unsupported : {15u, 16u, 17u, 18u, 19u, 22u}) { + for (const auto unsupported : {15u, 16u, 22u}) { if (tags.contains(unsupported)) fail("Unsupported dynamic tag " + std::to_string(unsupported)); } - if (tags.contains(30) && (tags.at(30) & ~(2ull | 8ull)) != 0) fail("Unsupported guest dynamic flags"); + if (tags.contains(30) && (tags.at(30) & ~(2ull | 8ull | 16ull)) != 0) fail("Unsupported guest dynamic flags"); image.Symbolic = tags.contains(30) && (tags.at(30) & 2ull) != 0; const auto mapped = [&](std::uint64_t address, std::uint64_t size, std::uint32_t flags) { for (const auto& header : image.Headers) { @@ -101,6 +102,10 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector } fail("Unmapped or inaccessible guest address " + std::to_string(address)); }; + const auto relativeAddress = [&](std::uint64_t address) { + if (address < loads.begin()->first || address > loads.rbegin()->second) + fail("Relative relocation addend is outside the ELF image " + std::to_string(address)); + }; const auto translate = [&](std::uint64_t address, std::uint64_t size) { for (const auto& header : image.Headers) { if (header.Type == 1 && address >= header.MappedAddress && address - header.MappedAddress <= header.FileSize && size <= header.FileSize - (address - header.MappedAddress)) return header.Offset + address - header.MappedAddress; @@ -245,12 +250,13 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector if (size % 24 != 0) fail("Invalid relocation table size"); return {table(addressTag, sceAddressTag, size), size}; }; - if (tags.contains(7) || tags.contains(0x6100002f)) { + if (tags.contains(17) || tags.contains(18) || tags.contains(19)) fail("DT_REL relocations are not supported"); + if (tags.contains(7) || tags.contains(0x6100002f) || tags.contains(9) || tags.contains(0x61000033)) { if (get(9, 0x61000033) != 24) fail("Invalid RELA entry size"); } const auto [relaOffset, relaSize] = relocationRange(7, 0x6100002f, 8, 0x61000031); const auto [pltOffset, pltSize] = relocationRange(23, 0x61000029, 2, 0x6100002d); - if (pltSize != 0 && get(20, 0x6100002b) != 7) fail("Unsupported PLT relocation format"); + if ((pltSize != 0 || tags.contains(20) || tags.contains(0x6100002b)) && get(20, 0x6100002b) != 7) fail("Unsupported PLT relocation format"); if (relaSize != 0 && pltSize != 0 && relaOffset < pltOffset + pltSize && pltOffset < relaOffset + relaSize && relaOffset % 24 != pltOffset % 24) fail("Unaligned overlapping relocation tables"); @@ -260,7 +266,7 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector if (tag >= 35 && tag <= 37) relrTags.push_back({static_cast(tag), value}); for (const auto& [target, addend] : ReadRelrRelocations(bytes, image.Headers, relrTags)) { mapped(target, 8, 2); - mapped(addend, 0, 0); + relativeAddress(addend); relocationTargets.emplace(target, target + 8); Io::AppendU64(image.Dynamic.RelaData, target); Io::AppendU64(image.Dynamic.RelaData, 8); @@ -283,7 +289,7 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector mapped(target, 8, 2); const bool relative = !plt && relocation == 8 && (info >> 32) == 0; if (const auto earlier = relativeEntries.find(target); relative && earlier != relativeEntries.end()) { - mapped(addend, 0, 0); + relativeAddress(addend); Io::WriteU64(output, earlier->second + 16, addend); continue; } @@ -298,13 +304,13 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector if (symbol.Value != 0) fail("RELATIVE relocation has a symbol"); } else if (symbol.Section == 0 || addend > static_cast(std::numeric_limits::max()) || symbol.Value != addend) fail("RELATIVE relocation has a symbol"); - mapped(addend, 0, 0); + relativeAddress(addend); Io::AppendU64(output, target); Io::AppendU64(output, relocation); Io::AppendU64(output, addend); continue; } - mapped(addend, 0, 0); + relativeAddress(addend); } else if (relocation == 1 || relocation == 6 || relocation == 7) { const auto& symbol = image.Symbols[info >> 32]; if ((info >> 32) == 0 || (symbol.Info & 15) == 6 || (relocation != 1 && addend != 0) || (symbol.Section == 0 && symbol.Name.empty())) fail("Invalid guest symbol relocation"); @@ -342,11 +348,15 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector const auto address = tags.at(addressTag); mapped(address, size, 4); const auto fileOffset = translate(address, size); + std::unordered_map*, std::size_t>>> relocationsAt; + for (const auto* table : {&image.Dynamic.RelaData, &image.Dynamic.RelaPltData}) + for (std::size_t position = 0; position < table->size(); position += 24) + relocationsAt[Io::ReadU64(*table, position)].emplace_back(table, position); for (std::uint64_t offset = 0; offset < size; offset += 8) { bool relocated = false; - for (const auto* table : {&image.Dynamic.RelaData, &image.Dynamic.RelaPltData}) { - for (std::size_t position = 0; position < table->size(); position += 24) { - if (Io::ReadU64(*table, position) != address + offset) continue; + const auto found = relocationsAt.find(address + offset); + if (found != relocationsAt.end()) { + for (const auto& [table, position] : found->second) { relocated = true; const auto info = Io::ReadU64(*table, position + 8); const auto addend = Io::ReadU64(*table, position + 16); diff --git a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp index 4669660ff9..cd2273f081 100644 --- a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp +++ b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp @@ -90,7 +90,7 @@ std::string ModuleStem(std::string name, const bool windows) { } -std::vector GuestModuleBuilder::Build(const std::filesystem::path& inputPath, const std::filesystem::path& outputPath, Domain::SysVDynamicSection& dynamic, const bool windows, const bool macos, const bool toIntel, const bool toRosetta, ISyscallScanner& syscallScanner, const bool lazyBinding, const std::string& runPath, const std::set& excludedModules, const std::filesystem::path& root, const std::vector& runtimeDirectories, std::vector modules, Domain::IProgress& progress) const { +std::vector GuestModuleBuilder::Build(const std::filesystem::path& inputPath, const std::filesystem::path& outputPath, Domain::SysVDynamicSection& dynamic, const bool windows, const bool macos, const bool toIntel, const bool toRosetta, ISyscallScanner& syscallScanner, const bool lazyBinding, const bool omitUnavailableDependencies, const std::string& runPath, const std::set& excludedModules, const std::filesystem::path& root, const std::vector& runtimeDirectories, std::vector modules, Domain::IProgress& progress) const { std::vector paths; std::map discovered; progress.Report({Domain::ProgressStage::Modules, {}, 0, modules.size()}); @@ -326,7 +326,7 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path for (const auto provider : executableProviders) require(provider); for (std::size_t index = 0; index < images.size(); ++index) { if (!isRuntime(index)) require(index); - if (windows) for (const auto dependency : dependencies[index]) if (standardDirectory(dependency)) require(dependency); + if (windows) for (const auto dependency : dependencies[index]) require(dependency); } const auto deferred = [&](const std::size_t index) { return !required[index] && isRuntime(index); @@ -362,6 +362,39 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path std::vector> moduleHostLibraries(images.size()); std::set uniqueHosts; const auto hostLibrary = [&](const std::string& name) { return windows || macos ? name : Domain::NeededLibraryName(name); }; + std::vector runDirectories; + bool runDirectoriesExist = !windows && !macos; + for (std::size_t begin = 0; runDirectoriesExist && begin <= runPath.size();) { + const auto end = std::min(runPath.find(':', begin), runPath.size()); + const auto component = std::string_view(runPath).substr(begin, end - begin); + std::filesystem::path directory; + if (component == "$ORIGIN") directory = outputPath.parent_path(); + else if (component.starts_with("$ORIGIN/")) directory = outputPath.parent_path() / std::string(component.substr(8)); + else if (component.starts_with('/')) directory = std::string(component); + std::error_code error; + runDirectoriesExist = !directory.empty() && std::filesystem::is_directory(directory, error); + runDirectories.push_back(std::move(directory)); + begin = end + 1; + } + std::set> reportedDependencies; + const auto unavailableUnused = [&](std::size_t index, const std::string& dependency) { + if (!runDirectoriesExist || findGuest(dependency) != guestNames.end()) return false; + const auto stem = ModuleStem(dependency, windows); + if (stem.empty()) return false; + const auto& symbols = images[index].Symbols; + if (std::any_of(symbols.begin(), symbols.end(), [&](const auto& symbol) { return symbol.Section == 0 && !symbol.Library.empty() && (symbol.Library == dependency || symbol.Library == stem || ModuleStem(symbol.Library, windows) == stem); })) return false; + const auto file = hostLibrary(dependency); + for (const auto& directory : runDirectories) { + std::error_code error; + const bool exists = std::filesystem::exists(directory / file, error); + if (exists || error) return false; + } + if (reportedDependencies.emplace(index, dependency).second) { + const auto subject = Io::Utf8Path(images[index].SourcePath) + " needs " + dependency + ", which no module or run path library provides and from which it imports nothing"; + progress.Message(omitUnavailableDependencies ? "Warning: " + subject + "; it is left out of the needed libraries" : "Note: " + subject + "; --omit-unavailable-dependencies leaves it out"); + } + return omitUnavailableDependencies; + }; const auto addHost = [&](const std::string& name, const bool startup) { if (findGuest(name) != guestNames.end()) return; if (name.empty() || name.find_first_of("/\\:$") != std::string::npos) throw Domain::RelinkerException("Invalid host dependency: " + name); @@ -371,6 +404,7 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path for (std::size_t index = 0; index < images.size(); ++index) { auto& hosts = moduleHostLibraries[index]; for (const auto& dependency : images[index].Dependencies) { + if (unavailableUnused(index, dependency)) continue; addHost(dependency, windows || !deferred(index)); if (findGuest(dependency) == guestNames.end()) hosts.push_back(hostLibrary(dependency)); } @@ -493,7 +527,7 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path dynamic.GuestModules.push_back(std::move(runtime)); std::vector neededLibraries; for (const auto& name : image.Dependencies) - if (findGuest(name) == guestNames.end()) neededLibraries.push_back(hostLibrary(name)); + if (findGuest(name) == guestNames.end() && !unavailableUnused(index, name)) neededLibraries.push_back(hostLibrary(name)); artifacts.push_back({target, std::move(output), std::move(imports), image.SourcePath, std::move(neededLibraries)}); progress.Report({Domain::ProgressStage::Modules, Io::Utf8Path(image.SourcePath), artifacts.size(), order.size()}); } diff --git a/core/relinker/relinker/src/parsing/ElfReader.cpp b/core/relinker/relinker/src/parsing/ElfReader.cpp index f112da1bea..f4a874884a 100644 --- a/core/relinker/relinker/src/parsing/ElfReader.cpp +++ b/core/relinker/relinker/src/parsing/ElfReader.cpp @@ -125,6 +125,10 @@ std::vector ElfReader::ReadProgramHeaders() const { if (header.ProgramHeaderCount != 0 && header.ProgramHeaderEntrySize != 56) { throw RelinkerException("Invalid ELF program header entry size: expected 56 bytes", 0x36); } + const ByteCount tableSize = static_cast(header.ProgramHeaderCount) * header.ProgramHeaderEntrySize; + if (!_rangeFits(header.ProgramHeaderOffset, tableSize, _fileBuffer.size())) { + throw RelinkerException("FileByteOffset out of bounds", header.ProgramHeaderOffset); + } std::vector headers; FileByteOffset offset = header.ProgramHeaderOffset; @@ -220,6 +224,10 @@ std::vector ElfReader::ReadSectionHeaders() const { sh.Info = _readU32At(offset + 0x2c); sh.EntrySize = _readU64At(offset + kShdrEntSizeOffset); + if (sh.Type == SHT_SYMTAB && sh.EntrySize != kSymEntrySize) { + throw RelinkerException("Invalid symbol table entry size", offset + kShdrEntSizeOffset); + } + if (sh.Type != SHT_NULL && sh.Type != SHT_NOBITS && sh.SectionSize != 0) { if (!_rangeFits(sh.Offset, sh.SectionSize, _fileBuffer.size())) { throw RelinkerException("Section offset out of bounds", sh.Offset); diff --git a/core/relinker/relinker/src/pipeline/Conversion.cpp b/core/relinker/relinker/src/pipeline/Conversion.cpp index 628ec27254..7669e351cd 100644 --- a/core/relinker/relinker/src/pipeline/Conversion.cpp +++ b/core/relinker/relinker/src/pipeline/Conversion.cpp @@ -147,7 +147,7 @@ ConversionOutput Conversion::Convert(ConversionInput input, const ConversionOpti std::vector guestArtifacts; if (!input.Modules.empty()) { - guestArtifacts = Relinker::GuestModuleBuilder().Build(input.InputPath, input.OutputPath, result.DynamicSection, windows, macos, options.ToIntel, options.ToRosetta, *syscallScanner, options.LazyBinding, options.RunPath, options.ExcludedModules, input.ModuleRoot, input.RuntimeDirectories, std::move(input.Modules), inputProgress); + guestArtifacts = Relinker::GuestModuleBuilder().Build(input.InputPath, input.OutputPath, result.DynamicSection, windows, macos, options.ToIntel, options.ToRosetta, *syscallScanner, options.LazyBinding, options.OmitUnavailableDependencies, options.RunPath, options.ExcludedModules, input.ModuleRoot, input.RuntimeDirectories, std::move(input.Modules), inputProgress); } auto byteWriter = std::make_shared(); diff --git a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp index 58f6d1d533..aa81d36ca0 100644 --- a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp +++ b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp @@ -87,6 +87,8 @@ RelinkResult RelinkerPipeline::Relink() { ByteCount gotSize = 0; for (const auto& ph : programHeaders) { + if (ph.Type == PT_LOAD && (ph.Flags & ~7u) != 0) + throw RelinkerException("Unsupported PT_LOAD permission flags", ph.Offset); if (ph.Type == PT_LOAD && (ph.Flags & PF_X) != 0) { auto segment = elfReader->ReadSegment(ph); if (!segment.empty()) { @@ -356,9 +358,12 @@ RelinkResult RelinkerPipeline::Relink() { const FileByteOffset symOff = symbolEntryPos(symIdx); std::uint16_t section = 0; std::memcpy(§ion, raw.data() + symOff + 6, sizeof(section)); + const auto type = raw[symOff + 4] & 15; + if (section == SHN_UNDEF && type != STT_TLS) + throw RelinkerException("TLS relocation names an undefined symbol that is not STT_TLS", pos); if (section == SHN_UNDEF) return std::nullopt; - if ((raw[symOff + 4] & 15) != STT_TLS) + if (type != STT_TLS) throw RelinkerException("TLS relocation names a defined symbol that is not STT_TLS", pos); if (section >= SHN_LORESERVE) throw RelinkerException("Unsupported executable TLS symbol section", pos); @@ -535,7 +540,7 @@ RelinkResult RelinkerPipeline::Relink() { } exportedSymbols.emplace_back(off, std::move(name)); const auto binding = symbol[4] >> 4; - if ((binding == STB_GLOBAL || binding == STB_WEAK) && type == STT_FUNC) + if ((binding == STB_GLOBAL || binding == STB_WEAK) && type == STT_FUNC && section != SHN_ABS) exportedFunctions.push_back(value); } } diff --git a/core/relinker/relinker/tests/CallSiteResolverTests.cpp b/core/relinker/relinker/tests/CallSiteResolverTests.cpp index 16ca224079..5bbf57ba02 100644 --- a/core/relinker/relinker/tests/CallSiteResolverTests.cpp +++ b/core/relinker/relinker/tests/CallSiteResolverTests.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -12,14 +13,48 @@ void require(bool condition, const std::string& name) { if (!condition) throw std::runtime_error(name); } -void check(const std::string& name, std::initializer_list instruction, bool expected) { +void check(const std::string& name, const std::vector& text, bool expected) { auto resolver = Relinker::MakeCallSiteResolver(); - const std::vector text(instruction); const auto sites = resolver->ResolveCallSites(text, 0x1000, 0x1100, 8); require(sites == (expected ? std::vector{0x1000} : std::vector{}), name); require(resolver->ResolveCallSites(text, 0x1000, 0x1200, 8).empty(), name + " unexpected address"); } +void checkAddressSize() { + for (const auto prefix : {std::vector{0x67}, {0xf2, 0x67}, {0x3e, 0x67}, {0x67, 0x67}, {0x2e, 0x67}, {0x67, 0x26}, {0x36, 0x67}}) { + for (const auto opcode : {std::vector{0xff, 0x15}, {0xff, 0x25}, {0xff, 0x35}, {0x48, 0x8b, 0x05}}) { + for (const std::uint64_t address : {0x1000ULL, 0xfffff000ULL, 0x100001000ULL}) { + auto resolver = Relinker::MakeCallSiteResolver(); + auto text = prefix; + text.insert(text.end(), opcode.begin(), opcode.end()); + text.resize(text.size() + 4); + constexpr std::uint64_t slot = 0x2800; + const auto displacement = static_cast(slot - address - text.size()); + std::memcpy(text.data() + text.size() - 4, &displacement, 4); + const std::vector expected{address}; + require(resolver->ResolveCallSites(text, address, slot, 8) == expected, "EIP-relative import access missing"); + require(resolver->ResolveCallSites(text, address, slot + 0x100000000ULL, 8).empty(), "EIP-relative access used the unwrapped slot"); + require(resolver->ResolveCallSites(text, address, slot, 0).empty(), "Empty EIP-relative target range matched"); + require(resolver->ResolveCallSites(text, address, slot - 1, 1).empty(), "EIP-relative target matched range end"); + require(resolver->ResolveCallSites(text, address, slot - 1, 2) == expected, "EIP-relative target missing inside range"); + } + } + } + const std::vector rip{0x48, 0x8b, 0x05, 0xf9, 0x17, 0, 0}; + auto resolver = Relinker::MakeCallSiteResolver(); + require(resolver->ResolveCallSites(rip, 0x100001000ULL, 0x100002800ULL, 8) == std::vector{0x100001000ULL}, "RIP-relative access was truncated"); + require(resolver->ResolveCallSites(rip, 0x100001000ULL, 0x2800, 8).empty(), "RIP-relative access matched a wrapped slot"); +} + +std::vector ripInstruction(std::vector prefixes, std::uint8_t opcode, std::uint8_t modrm) { + prefixes.push_back(opcode); + prefixes.push_back(modrm); + const auto displacement = static_cast(0x100 - (prefixes.size() + 4)); + for (unsigned shift = 0; shift < 32; shift += 8) + prefixes.push_back(static_cast(displacement >> shift)); + return prefixes; +} + } int main() { @@ -50,7 +85,11 @@ int main() { check("NOTRACK preserves final narrow REX", {0x3e, 0x48, 0x40, 0x8b, 0x05, 0xf7, 0, 0, 0}, false); check("FS call is not a flat GOT reference", {0x64, 0xf2, 0xff, 0x15, 0xf8, 0, 0, 0}, false); check("GS jump is not a flat GOT reference", {0x3e, 0x65, 0xff, 0x25, 0xf8, 0, 0, 0}, false); - check("address-size override is not a RIP64 reference", {0x3e, 0x67, 0xff, 0x15, 0xf8, 0, 0, 0}, false); + check("address-size override is an EIP-relative reference", {0x3e, 0x67, 0xff, 0x15, 0xf8, 0, 0, 0}, true); + check("address-size prefix after REX.W leaves a narrow load", {0x48, 0x67, 0x8b, 0x05, 0xf8, 0, 0, 0}, false); + check("REX.W after address-size prefix widens a load", {0x67, 0x48, 0x8b, 0x05, 0xf8, 0, 0, 0}, true); + check("FS EIP-relative call is not a flat GOT reference", {0x64, 0x67, 0xff, 0x15, 0xf8, 0, 0, 0}, false); + check("GS EIP-relative load is not a flat GOT reference", {0x67, 0x65, 0x48, 0x8b, 0x05, 0xf7, 0, 0, 0}, false); check("BND register call has no RIP operand", {0xf2, 0xff, 0xd0}, false); check("NOTRACK register jump has no RIP operand", {0x3e, 0xff, 0xe0}, false); check("BND-prefixed increment is not an import access", {0xf2, 0xff, 0x05, 0xf9, 0, 0, 0}, false); @@ -60,6 +99,26 @@ int main() { check("increment memory", {0xff, 0x05, 0xfa, 0, 0, 0}, false); check("decrement memory", {0xff, 0x0d, 0xfa, 0, 0, 0}, false); check("push memory", {0xff, 0x35, 0xfa, 0, 0, 0}, true); + checkAddressSize(); + for (const std::uint8_t segment : {0x2e, 0x26, 0x36, 0x3e}) { + const auto name = "segment " + std::to_string(segment); + for (const std::uint8_t modrm : {0x15, 0x1d, 0x25, 0x2d, 0x35}) + check(name + " indirect access " + std::to_string(modrm), ripInstruction({segment}, 0xff, modrm), true); + check(name + " pointer load", ripInstruction({segment, 0x48}, 0x8b, 0x05), true); + check(name + " before BND call", ripInstruction({segment, 0xf2}, 0xff, 0x15), true); + check(name + " after BND jump", ripInstruction({0xf2, segment}, 0xff, 0x25), true); + check(name + " after REX.W leaves a narrow load", ripInstruction({0x48, segment}, 0x8b, 0x05), false); + check(name + " final REX.W restores pointer width", ripInstruction({0x48, segment, 0x48}, 0x8b, 0x05), true); + check(name + " final narrow REX", ripInstruction({segment, 0x48, 0x40}, 0x8b, 0x05), false); + check(name + " increment", ripInstruction({segment}, 0xff, 0x05), false); + check(name + " decrement", ripInstruction({segment}, 0xff, 0x0d), false); + check(name + " narrow load", ripInstruction({segment}, 0x8b, 0x05), false); + for (const std::uint8_t unsupported : {0x64, 0x65, 0x66, 0xf0, 0xf3}) { + const auto suffix = " unsupported prefix " + std::to_string(unsupported); + check(name + " before" + suffix, ripInstruction({segment, unsupported}, 0xff, 0x15), false); + check(name + " after" + suffix, ripInstruction({unsupported, segment}, 0xff, 0x15), false); + } + } std::cout << "Call-site resolver tests passed\n"; return 0; } catch (const std::exception& error) { diff --git a/core/relinker/relinker/tests/ControlFlowGraphTests.cpp b/core/relinker/relinker/tests/ControlFlowGraphTests.cpp index 2e2674124b..aa96305059 100644 --- a/core/relinker/relinker/tests/ControlFlowGraphTests.cpp +++ b/core/relinker/relinker/tests/ControlFlowGraphTests.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -220,6 +221,66 @@ void CheckAddressSizeOverride() { RequireOffsets(*BuildAddressTakenControlFlowGraph(lea, TextAddress, TextAddress, {}, missing, {}), {0, 8}); } +void CheckRipDisplacementBounds() { + constexpr auto low = VirtualAddress{0x10}; + constexpr auto high = std::numeric_limits::max() - 16; + const auto build = [](Bytes prefix, std::int32_t displacement, VirtualAddress base, VirtualAddress slot) { + Bytes text(16, 0xc3); + std::copy(prefix.begin(), prefix.end(), text.begin()); + std::memcpy(text.data() + prefix.size(), &displacement, sizeof(displacement)); + Pointers pointers; + pointers.Slots = {{slot, base + 12}}; + return BuildControlFlowGraph(text, base, base, {}, pointers); + }; + + const auto next = low + 6; + const auto underflowCall = build({0xff, 0x15}, -0x20, low, next - 0x20); + std::unordered_set expectedUnderflow{low, low + 6, low + 12}; + Require(underflowCall->ReachableVaddrs() == expectedUnderflow, "Unexpected reachable instruction set for underflowing call target"); + + const auto underflowJump = build({0xff, 0x25}, -0x20, low, next - 0x20); + std::unordered_set expectedUnderflowJump{low, low + 12}; + Require(underflowJump->ReachableVaddrs() == expectedUnderflowJump, "Unexpected reachable instruction set for underflowing jump target"); + + Bytes underflowLeaText(16, 0xc3); + const Bytes leaPrefix = {0x48, 0x8d, 0x05}; + std::copy(leaPrefix.begin(), leaPrefix.end(), underflowLeaText.begin()); + const std::int32_t underflowDisp = -0x20; + std::memcpy(underflowLeaText.data() + leaPrefix.size(), &underflowDisp, sizeof(underflowDisp)); + Pointers emptyPointers; + const auto underflowLea = BuildControlFlowGraph(underflowLeaText, low, low, {}, emptyPointers); + std::unordered_set expectedUnderflowLea{low, low + 7}; + Require(underflowLea->ReachableVaddrs() == expectedUnderflowLea, "Unexpected reachable instruction set for underflowing LEA target"); + + const auto overflowCall = build({0xff, 0x15}, 0x20, high, high + 6 + 0x20); + std::unordered_set expectedOverflow{high, high + 6, high + 12}; + Require(overflowCall->ReachableVaddrs() == expectedOverflow, "Unexpected reachable instruction set for overflowing call target"); + + const auto overflowJump = build({0xff, 0x25}, 0x20, high, high + 6 + 0x20); + std::unordered_set expectedOverflowJump{high, high + 12}; + Require(overflowJump->ReachableVaddrs() == expectedOverflowJump, "Unexpected reachable instruction set for overflowing jump target"); + + Bytes overflowLeaText(16, 0xc3); + std::copy(leaPrefix.begin(), leaPrefix.end(), overflowLeaText.begin()); + const std::int32_t overflowDisp = 0x20; + std::memcpy(overflowLeaText.data() + leaPrefix.size(), &overflowDisp, sizeof(overflowDisp)); + const auto overflowLea = BuildControlFlowGraph(overflowLeaText, high, high, {}, emptyPointers); + std::unordered_set expectedOverflowLea{high, high + 7}; + Require(overflowLea->ReachableVaddrs() == expectedOverflowLea, "Unexpected reachable instruction set for overflowing LEA target"); + + const auto validZero = build({0xff, 0x15}, -static_cast(next), low, 0); + std::unordered_set expectedZero{low, low + 6, low + 12}; + Require(validZero->ReachableVaddrs() == expectedZero, "Unexpected reachable instruction set for target 0"); + + const auto validMax = build({0xff, 0x15}, 10, high, high + 16); + std::unordered_set expectedMax{high, high + 6, high + 12}; + Require(validMax->ReachableVaddrs() == expectedMax, "Unexpected reachable instruction set for target max"); + + const auto validEip = build({0x67, 0xff, 0x15}, -0x20, low, static_cast(low + 7 - 0x20)); + std::unordered_set expectedEip{low, low + 7, low + 12}; + Require(validEip->ReachableVaddrs() == expectedEip, "Unexpected reachable instruction set for 0x67 wrap"); +} + void CheckCodeAddresses() { Bytes text(17, 0x90); text[0] = 0x48; @@ -488,6 +549,49 @@ void CheckRelativeJumpTables() { Require(Relinker::FindRelativeJumpTables(cutShort, TextAddress, {{TextAddress, TextAddress + cutShort.size()}}).empty(), "Table followed by more of its function was accepted"); } +Bytes GapStubTable(std::initializer_list stub) { + constexpr std::size_t function = 16; + constexpr std::size_t tableOffset = 40; + Bytes text(64, 0x90); + for (std::size_t i = 0; i < function; ++i) text[i] = 0xcc; + std::copy(stub.begin(), stub.end(), text.begin()); + text[function] = 0x48; + text[function + 1] = 0x8d; + text[function + 2] = 0x05; + SetDisplacement(text, function + 3, function + 7, TextAddress + tableOffset); + text[function + 7] = 0xe8; + SetDisplacement(text, function + 8, function + 12, TextAddress); + text[function + 12] = 0xc3; + for (std::size_t i = function + 13; i < tableOffset; ++i) text[i] = 0xcc; + text[tableOffset] = 0x0f; + text[tableOffset + 1] = 0x78; + text[tableOffset + 2] = 0x33; + text[tableOffset + 3] = 0x1e; + return text; +} + +std::vector GapStubTables(const Bytes& text) { + return Relinker::FindRelativeJumpTables(text, TextAddress, {{TextAddress + 16, TextAddress + 29}, {TextAddress + text.size(), TextAddress + text.size() + 8}}); +} + +void CheckGapWalkBranchTargets() { + const auto returning = GapStubTables(GapStubTable({0xc3})); + Require(returning.size() == 1 && returning[0].Begin == TextAddress + 40, "A gap stub that returns hid the data table"); + + Bytes direct = GapStubTable({0xe9, 0, 0, 0, 0}); + SetDisplacement(direct, 1, 5, TextAddress + 40); + Require(GapStubTables(direct).empty(), "A jump from a called gap stub into the table was missed"); + + Bytes afterOneByte = GapStubTable({0x55, 0xe9, 0, 0, 0, 0}); + SetDisplacement(afterOneByte, 2, 6, TextAddress + 40); + Require(GapStubTables(afterOneByte).empty(), "A jump after a one-byte instruction in a gap stub was missed"); + + Bytes chained = GapStubTable({0xe9, 0, 0, 0, 0, 0xc3, 0xcc, 0xcc, 0xe9, 0, 0, 0, 0}); + SetDisplacement(chained, 1, 5, TextAddress + 8); + SetDisplacement(chained, 9, 13, TextAddress + 40); + Require(GapStubTables(chained).empty(), "A jump from a gap stub reached only through another gap stub was missed"); +} + void CheckEmbeddedDataTables() { constexpr std::size_t tableOffset = 24; Bytes text(48, 0x90); @@ -751,6 +855,7 @@ int main() { CheckRelocatedTransfers(); CheckSegmentRelativeTransfers(); CheckAddressSizeOverride(); + CheckRipDisplacementBounds(); CheckCodeAddresses(); CheckSpeculativeRoots(); CheckTrailingZeroRoots(); @@ -765,6 +870,7 @@ int main() { CheckStubBeforeUndecodableData(); CheckTableWithPaddingAndReturnBytes(); CheckLargeZeroGaps(); + CheckGapWalkBranchTargets(); std::cout << "Control-flow graph tests passed\n"; return 0; } catch (const std::exception& error) { diff --git a/core/relinker/relinker/tests/ConversionTests.cpp b/core/relinker/relinker/tests/ConversionTests.cpp index 60a5bb4e85..0b73c77b26 100644 --- a/core/relinker/relinker/tests/ConversionTests.cpp +++ b/core/relinker/relinker/tests/ConversionTests.cpp @@ -283,7 +283,7 @@ void scanning() { Io::WriteU64(wrappingImage, 0x128, 0); reject([&] { Relinker::CodeInstructionCollector().Collect(wrappingImage, {execHeader, dynamicHeader, wrappingDataHeader}); }); const auto roots = Relinker::CodeInstructionCollector().Collect(wrappingImage, {execHeader, dynamicHeader, controlDataHeader}); - require(roots == std::set{0x2000, 0x2008}, "Valid initializer array roots were not collected"); + require(roots == std::vector{0x2000, 0x2008}, "Valid initializer array roots were not collected"); Io::WriteU64(wrappingImage, 0x3c0, 0); reject([&] { Relinker::CodeInstructionCollector().Collect(wrappingImage, {execHeader, dynamicHeader, wrappingDataHeader}); }); Io::WriteU64(wrappingImage, 0x3c0, 0x2008); @@ -310,8 +310,8 @@ void branchAcrossSignedBoundary() { header.MemorySize = header.FileSize; Io::WriteU64(image, 24, 0x8000000000000000); const auto instructions = Relinker::CodeInstructionCollector().Collect(image, {header}); - require(instructions.contains(0x8000000000000000), "Lost the entry above the signed boundary"); - require(instructions.contains(0x7ffffffffffffff0), "Lost the branch target below the signed boundary"); + require(std::binary_search(instructions.begin(), instructions.end(), 0x8000000000000000ull), "Lost the entry above the signed boundary"); + require(std::binary_search(instructions.begin(), instructions.end(), 0x7ffffffffffffff0ull), "Lost the branch target below the signed boundary"); } class FailingProgress final : public Domain::IProgress { @@ -415,21 +415,6 @@ void runtimeDirectories() { require(output.Modules.size() == 1, "Module was not converted"); require(exports(output.Modules.front().Bytes, Relinker::GuestInitializeExport) == (std::string_view(directory) == "Media/Modules"), "Wrong start of a runtime module the executable needs"); } - for (const auto* directory : {"sce_module", "sce_modules", "prx", "Media/Modules"}) { - auto source = input(); - const auto parent = source.ModuleRoot / directory; - const auto plugins = source.ModuleRoot / "Media/Plugins"; - source.Modules.push_back({plugins / "plugin.prx", fixture("needed.prx")}); - source.Modules.push_back({parent / "needed.prx", fixture()}); - source.RuntimeDirectories.push_back(plugins); - source.RuntimeDirectories.push_back(parent); - const auto output = converter.Convert(std::move(source), options); - require(output.Modules.size() == 2, "Modules were not converted"); - for (const auto& module : output.Modules) { - const bool plugin = module.SourcePath.filename() == "plugin.prx"; - require(exports(module.Bytes, Relinker::GuestInitializeExport) == (plugin || std::string_view(directory) == "Media/Modules"), "Wrong start of a runtime module another runtime module needs"); - } - } } void relocationValidation() { diff --git a/core/relinker/relinker/tests/DynamicTableBoundsTests.cpp b/core/relinker/relinker/tests/DynamicTableBoundsTests.cpp index c91f3640e0..4b7eccaad9 100644 --- a/core/relinker/relinker/tests/DynamicTableBoundsTests.cpp +++ b/core/relinker/relinker/tests/DynamicTableBoundsTests.cpp @@ -17,8 +17,12 @@ const Bytes kCode = {0xC3}; constexpr std::size_t kTagStride = 16; constexpr std::size_t kTagBase = kCodeOffset + kDynamicVaddr; +constexpr std::size_t kStrTabTag = 0; constexpr std::size_t kSymTabTag = 2; +constexpr std::size_t kRelaTag = 4; constexpr std::size_t kRelaSzTag = 5; +constexpr std::int64_t kDtStrTab = 5; +constexpr std::int64_t kDtRela = 7; constexpr std::int64_t kDtOsSymtab = 0x61000039; constexpr std::int64_t kDtRelaSz = 8; constexpr std::uint64_t kBogusOffset = 0xFFFFFFFFFFFFFFFEull; @@ -45,16 +49,41 @@ int main(const int argc, char** argv) { const auto input = directory.Path() / "input.elf"; const auto output = directory.Path() / "output.elf"; - Bytes image = MakeExecutable(kCode); - WriteRelaEntry(image, (1ull << 32) | 6ull); - WriteTag(image, kRelaSzTag, kDtRelaSz, 24); - WriteTag(image, kSymTabTag, kDtOsSymtab, kBogusOffset); - WriteFile(input, image); + { + Bytes image = MakeExecutable(kCode); + WriteRelaEntry(image, (1ull << 32) | 6ull); + WriteTag(image, kRelaSzTag, kDtRelaSz, 24); + WriteTag(image, kSymTabTag, kDtOsSymtab, kBogusOffset); + WriteFile(input, image); + + const auto run = RunRelinker(binary, {"--skip-sce-module", "--to-intel", input.string(), output.string()}, directory.Path() / "relinker.log"); + require(run.ExitCode != 0, "Relinker read a symbol table outside the file and exited 0:\n" + run.Output); + require(run.Output.find("Symbol table entry out of bounds") != std::string::npos, + "Relinker did not report the symbol table entry as out of bounds:\n" + run.Output); + } + + { + Bytes image = MakeExecutable(kCode); + WriteTag(image, kRelaTag, kDtRela, kBogusOffset); + WriteFile(input, image); + + const auto run = RunRelinker(binary, {"--skip-sce-module", "--to-intel", input.string(), output.string()}, directory.Path() / "relinker.log"); + require(run.ExitCode != 0, "Relinker read a relocation table outside the file and exited 0:\n" + run.Output); + require(run.Output.find("Virtual address not mapped by any PT_LOAD segment") != std::string::npos, + "Relinker did not report the relocation table entry as out of bounds:\n" + run.Output); + } + + { + Bytes image = MakeExecutable(kCode); + WriteTag(image, kStrTabTag, kDtStrTab, kBogusOffset); + WriteFile(input, image); + + const auto run = RunRelinker(binary, {"--skip-sce-module", "--to-intel", input.string(), output.string()}, directory.Path() / "relinker.log"); + require(run.ExitCode != 0, "Relinker read a string table outside the file and exited 0:\n" + run.Output); + require(run.Output.find("Virtual address not mapped by any PT_LOAD segment") != std::string::npos, + "Relinker did not report the string table entry as out of bounds:\n" + run.Output); + } - const auto run = RunRelinker(binary, {"--skip-sce-module", "--to-intel", input.string(), output.string()}, directory.Path() / "relinker.log"); - require(run.ExitCode != 0, "Relinker read a symbol table outside the file and exited 0:\n" + run.Output); - require(run.Output.find("Symbol table entry out of bounds") != std::string::npos, - "Relinker did not report the symbol table entry as out of bounds:\n" + run.Output); return 0; } catch (const std::exception& error) { std::cerr << "dynamic_table_bounds_tests: " << error.what() << '\n'; diff --git a/core/relinker/relinker/tests/ElfReaderBoundsTests.cpp b/core/relinker/relinker/tests/ElfReaderBoundsTests.cpp index b1122ee8a7..154a699dd9 100644 --- a/core/relinker/relinker/tests/ElfReaderBoundsTests.cpp +++ b/core/relinker/relinker/tests/ElfReaderBoundsTests.cpp @@ -93,10 +93,16 @@ void RejectAddress(Bytes bytes, const std::uint64_t address) { throw std::runtime_error("Invalid file mapping was accepted"); } -void RejectProgramHeaders(Bytes bytes) { +void RejectProgramHeaders(Bytes bytes, const std::string& expectedMessage = "", const std::uint64_t expectedOffset = std::numeric_limits::max()) { try { Relinker::ElfReader(std::move(bytes)).ReadProgramHeaders(); - } catch (const Relinker::RelinkerException&) { + } catch (const Relinker::RelinkerException& error) { + if (!expectedMessage.empty()) { + Require(std::string(error.what()).find(expectedMessage) != std::string::npos); + } + if (expectedOffset != std::numeric_limits::max()) { + Require(error.FailureOffset == expectedOffset); + } return; } throw std::runtime_error("Invalid program header was accepted"); @@ -106,6 +112,12 @@ void RejectProgramHeaders(Bytes bytes) { int main() { const std::vector>> cases = { + {"load file size exceeds memory size", [] { + auto bytes = Fixture(); + Write(bytes, 96, 64); + Write(bytes, 104, 32); + RejectProgramHeaders(std::move(bytes)); + }}, {"load file range past end of file", [] { auto bytes = Fixture(); Write(bytes, 96, 0xd0); @@ -190,10 +202,44 @@ int main() { Write(bytes, 56, 3); RejectProgramHeaders(std::move(bytes)); }}, + {"program header table out of bounds", [] { + for (const std::uint64_t offset : {std::uint64_t{0x300}, std::uint64_t{0x300} - 55, std::numeric_limits::max() - 31}) { + auto bytes = Fixture(); + Write(bytes, 32, offset); + RejectProgramHeaders(std::move(bytes), "FileByteOffset out of bounds", offset == std::uint64_t{0x300} - 55 ? offset : std::numeric_limits::max()); + } + auto truncated = Fixture(); + truncated.resize(64 + 56 - 1); + RejectProgramHeaders(std::move(truncated), "FileByteOffset out of bounds", 0x40); + auto precedence = Fixture(); + Write(precedence, 56, 20); + Write(precedence, 0x40 + 0x20, 0x200); + Write(precedence, 0x40 + 0x28, 0x100); + RejectProgramHeaders(std::move(precedence), "FileByteOffset out of bounds", 0x40); + auto zeroHeadersOobOffset = Fixture(); + Write(zeroHeadersOobOffset, 56, 0); + Write(zeroHeadersOobOffset, 32, std::numeric_limits::max()); + RejectProgramHeaders(std::move(zeroHeadersOobOffset), "FileByteOffset out of bounds", std::numeric_limits::max()); + }}, {"section entry size field", [] { const auto sections = Relinker::ElfReader(Fixture()).ReadSectionHeaders(); Require(sections.size() == 3 && sections[1].Name == ".text" && sections[1].EntrySize == 24); }}, + {"SHT_SYMTAB section entry size mismatch", [] { + for (const std::uint64_t entrySize : {0ULL, 1ULL, 16ULL, 20ULL, 23ULL, 25ULL, 32ULL, 64ULL, 0xffffffffffffffffULL}) { + auto bytes = Fixture(); + Write(bytes, 196, 2); + Write(bytes, 248, entrySize); + RejectSections(std::move(bytes)); + } + }}, + {"SHT_SYMTAB section entry size valid", [] { + auto bytes = Fixture(); + Write(bytes, 196, 2); + Write(bytes, 248, 24); + const auto sections = Relinker::ElfReader(std::move(bytes)).ReadSectionHeaders(); + Require(sections.size() == 3 && sections[1].Type == 2 && sections[1].EntrySize == 24); + }}, {"section header stride", [] { for (const std::uint16_t stride : {0, 56, 65}) { auto bytes = Fixture(); diff --git a/core/relinker/relinker/tests/ImportModuleTests.cpp b/core/relinker/relinker/tests/ImportModuleTests.cpp new file mode 100644 index 0000000000..a935c421b8 --- /dev/null +++ b/core/relinker/relinker/tests/ImportModuleTests.cpp @@ -0,0 +1,51 @@ +#include +#include +#include +#include +#include +#include +using Bytes = std::vector; +static void Ph(Bytes& b, int i, unsigned type, unsigned flags, unsigned off, unsigned va, unsigned size) { + const auto p=64+i*56; Io::WriteU32(b,p,type); Io::WriteU32(b,p+4,flags); + Io::WriteU64(b,p+8,off);Io::WriteU64(b,p+16,va);Io::WriteU64(b,p+24,va); + Io::WriteU64(b,p+32,size);Io::WriteU64(b,p+40,size);Io::WriteU64(b,p+48,8); +} +static Bytes Fixture(bool reverse=false) { + Bytes b(0x4000);std::memcpy(b.data(),"\x7f" "ELF\x02\x01\x01",7); + Io::WriteU16(b,16,3);Io::WriteU16(b,18,62);Io::WriteU32(b,20,1); + Io::WriteU64(b,32,64);Io::WriteU16(b,52,64);Io::WriteU16(b,54,56);Io::WriteU16(b,56,3); + Ph(b,0,1,5,0x1000,0,0x1000);Ph(b,1,1,6,0x2000,0x2000,0x1000); + b[0x1020]=0xc3;b[0x1028]=0xc3; + std::string strings(1,'\0');auto str=[&](const char* s){auto o=strings.size();strings+=s;strings+='\0';return o;}; + const auto a=str(reverse?"alias#B#A":"alias#A#A"),c=str(reverse?"alias#A#A":"alias#B#A"); + const auto imp=str("imported#A#B"),host=str("libmonosgen-2.0.prx"),module=str("fixtureModule"),logical=str("libmonosgen"); + std::copy(strings.begin(),strings.end(),b.begin()+0x2200); + const std::pair tags[]={{5,0x2200},{10,strings.size()},{6,0x2400},{11,24},{4,0x2600},{7,0x2700},{8,48},{9,24},{1,host},{0x61000043,module},{0x61000045,(1ull<<48)|logical},{0,0}}; + Ph(b,2,2,6,0x2000,0x2000,sizeof(tags)); + for(unsigned i=0;i modules{{1, "libmonosgen"}}; + const std::array matching{"libmonosgen-2.0.prx"}; + const std::array other{"libmonosgen-3.0.prx"}; + const std::array ambiguous{"libmonosgen-2.0.prx", "libmonosgen-2.0.sprx"}; + if (Domain::ImportModule("nid#A#B", modules, matching) != matching[0]) return 1; + if (!Domain::ImportModule("nid#A#B", modules, other).empty()) return 2; + try { Domain::ImportModule("nid#A#B", modules, ambiguous); return 3; } + catch (const Domain::RelinkerException&) {} + const std::map versioned{{1, "libmonosgen-2.0"}}; + const std::array legacy{"libmonosgen.prx"}; + const std::array both{"libmonosgen.prx", "libmonosgen-2.0.prx"}; + if (!Domain::ImportModule("nid#A#B", versioned, legacy).empty()) return 5; + if (Domain::ImportModule("nid#A#B", modules, both) != both[0]) return 6; + const std::array normalizedMono{"libmonosgen-module.prx"}; + if (Domain::ImportModule("nid#A#B", modules, normalizedMono) != normalizedMono[0]) return 43; + std::cout << "module mapping checks passed\n"; +} diff --git a/core/relinker/relinker/tests/RelrRelocationTests.cpp b/core/relinker/relinker/tests/RelrRelocationTests.cpp index 993b71e4a1..8afb2d6f8b 100644 --- a/core/relinker/relinker/tests/RelrRelocationTests.cpp +++ b/core/relinker/relinker/tests/RelrRelocationTests.cpp @@ -105,6 +105,8 @@ int main() try { Reject([](auto& input) { input.SetWords({0x200, 0x206}); }, "Overlapping RELR targets"); Reject([](auto& input) { input.SetWords({0x3000}); }, "target exceeds load"); Reject([](auto& input) { input.SetWords({0x2ffc}); }, "target exceeds load"); + Reject([](auto& input) { input.SetWords({0x4000}); }, "target exceeds load"); + Reject([](auto& input) { input.SetWords({0x2ff8, 3}); }, "target exceeds load"); Reject([](auto& input) { input.Tags[0].Value = 0x2000; }, "table exceeds file-backed"); Reject([](auto& input) { input.Tags[0].Value = 0x1ffc; }, "table exceeds file-backed"); Reject([](auto& input) { input.Tags[1].Value = 0xfffffffffffffff8ull; }, "table exceeds file-backed"); diff --git a/core/relinker/relinker/tests/StrictNidFilterTests.cpp b/core/relinker/relinker/tests/StrictNidFilterTests.cpp index bed3b24283..7de2f2db69 100644 --- a/core/relinker/relinker/tests/StrictNidFilterTests.cpp +++ b/core/relinker/relinker/tests/StrictNidFilterTests.cpp @@ -32,6 +32,18 @@ void requireFailure(const std::function& operation, const char* message) throw std::runtime_error(message); } +void requireFailureMessage(const std::function& operation, const std::string& expectedSubstring, const char* message) { + try { + operation(); + } catch (const Relinker::RelinkerException& ex) { + if (std::string(ex.what()).find(expectedSubstring) != std::string::npos) { + return; + } + throw std::runtime_error(std::string("Wrong exception message: ") + ex.what() + ", expected: " + expectedSubstring); + } + throw std::runtime_error(message); +} + template void write(std::vector& bytes, std::size_t offset, TValue value) { if (offset > bytes.size() || sizeof(value) > bytes.size() - offset) throw std::runtime_error("Test fixture write is out of bounds"); @@ -115,6 +127,124 @@ void addressSizeOverrideImport() { require(AnalyzeStrictReachability(input).ImportSlots == std::set{0x100002000ull}, "RIP-relative import lost its full 64-bit address"); } +std::vector elfFixture(const StrictReachabilityInput& input); + +void ripDisplacementOverflowUnderflowBounds() { + constexpr auto maxVaddr = std::numeric_limits::max(); + + auto input = fixture(); + input.Text.assign(32, 0); + input.TextVaddr = 0x1000; + input.Entries = {input.TextVaddr}; + emit(input, 0, {0xFF, 0x15}); + write(input.Text, 2, static_cast(-0x2000)); + emit(input, 6, {0xC3}); + const auto callUnderflow = static_cast(input.TextVaddr + 6 + static_cast(static_cast(-0x2000))); + input.ImportSlots = {callUnderflow}; + const std::vector callUnderflowRefs = {{"live", {}, 7, 0x300, callUnderflow, 0}}; + require(AnalyzeStrictReachability(input).ImportSlots == std::set{callUnderflow}, "Call RIP-relative displacement underflow did not retain its import"); + require(Relinker::MakeStrictUnusedNidFilter(testProgress)->Filter(callUnderflowRefs, elfFixture(input), input.Text, input.TextVaddr).size() == 1, + "Strict filter removed call RIP-relative displacement underflow import"); + + input.Text.assign(32, 0); + input.TextVaddr = maxVaddr - 32; + input.Entries = {input.TextVaddr}; + emit(input, 0, {0xFF, 0x15}); + write(input.Text, 2, static_cast(0x1000)); + emit(input, 6, {0xC3}); + const auto callOverflow = static_cast(input.TextVaddr + 6 + static_cast(static_cast(0x1000))); + input.ImportSlots = {callOverflow}; + const std::vector callOverflowRefs = {{"live", {}, 7, 0x300, callOverflow, 0}}; + require(AnalyzeStrictReachability(input).ImportSlots == std::set{callOverflow}, "Call RIP-relative displacement overflow did not retain its import"); + require(Relinker::MakeStrictUnusedNidFilter(testProgress)->Filter(callOverflowRefs, elfFixture(input), input.Text, input.TextVaddr).size() == 1, + "Strict filter removed call RIP-relative displacement overflow import"); + + input.Text.assign(32, 0); + input.TextVaddr = 0x1000; + input.Entries = {input.TextVaddr}; + emit(input, 0, {0xFF, 0x25}); + write(input.Text, 2, static_cast(-0x2000)); + emit(input, 6, {0xC3}); + const auto jmpUnderflow = static_cast(input.TextVaddr + 6 + static_cast(static_cast(-0x2000))); + input.ImportSlots = {jmpUnderflow}; + const std::vector jmpUnderflowRefs = {{"live", {}, 7, 0x300, jmpUnderflow, 0}}; + require(AnalyzeStrictReachability(input).ImportSlots == std::set{jmpUnderflow}, "Jump RIP-relative displacement underflow did not retain its import"); + require(Relinker::MakeStrictUnusedNidFilter(testProgress)->Filter(jmpUnderflowRefs, elfFixture(input), input.Text, input.TextVaddr).size() == 1, + "Strict filter removed jump RIP-relative displacement underflow import"); + + input.Text.assign(32, 0); + input.TextVaddr = maxVaddr - 32; + input.Entries = {input.TextVaddr}; + emit(input, 0, {0xFF, 0x25}); + write(input.Text, 2, static_cast(0x1000)); + emit(input, 6, {0xC3}); + const auto jmpOverflow = static_cast(input.TextVaddr + 6 + static_cast(static_cast(0x1000))); + input.ImportSlots = {jmpOverflow}; + const std::vector jmpOverflowRefs = {{"live", {}, 7, 0x300, jmpOverflow, 0}}; + require(AnalyzeStrictReachability(input).ImportSlots == std::set{jmpOverflow}, "Jump RIP-relative displacement overflow did not retain its import"); + require(Relinker::MakeStrictUnusedNidFilter(testProgress)->Filter(jmpOverflowRefs, elfFixture(input), input.Text, input.TextVaddr).size() == 1, + "Strict filter removed jump RIP-relative displacement overflow import"); + + input.Text.assign(32, 0); + input.TextVaddr = 0x1000; + input.Entries = {input.TextVaddr}; + emit(input, 0, {0x48, 0x8D, 0x05}); + write(input.Text, 3, static_cast(-0x2000)); + emit(input, 7, {0xC3}); + const auto leaUnderflow = static_cast(input.TextVaddr + 7 + static_cast(static_cast(-0x2000))); + input.ImportSlots = {leaUnderflow}; + const std::vector leaUnderflowRefs = {{"live", {}, 7, 0x300, leaUnderflow, 0}}; + const auto leaUnderflowResult = AnalyzeStrictReachability(input); + require(leaUnderflowResult.ImportSlots == std::set{leaUnderflow}, "LEA RIP-relative displacement underflow did not retain its import"); + require(leaUnderflowResult.AddressTakenRoots == 1, "LEA RIP-relative displacement underflow did not add an address-taken root"); + require(Relinker::MakeStrictUnusedNidFilter(testProgress)->Filter(leaUnderflowRefs, elfFixture(input), input.Text, input.TextVaddr).size() == 1, + "Strict filter removed LEA RIP-relative displacement underflow import"); + + input.Text.assign(32, 0); + input.TextVaddr = maxVaddr - 32; + input.Entries = {input.TextVaddr}; + emit(input, 0, {0x48, 0x8D, 0x05}); + write(input.Text, 3, static_cast(0x1000)); + emit(input, 7, {0xC3}); + const auto leaOverflow = static_cast(input.TextVaddr + 7 + static_cast(static_cast(0x1000))); + input.ImportSlots = {leaOverflow}; + const std::vector leaOverflowRefs = {{"live", {}, 7, 0x300, leaOverflow, 0}}; + const auto leaOverflowResult = AnalyzeStrictReachability(input); + require(leaOverflowResult.ImportSlots == std::set{leaOverflow}, "LEA RIP-relative displacement overflow did not retain its import"); + require(leaOverflowResult.AddressTakenRoots == 1, "LEA RIP-relative displacement overflow did not add an address-taken root"); + require(Relinker::MakeStrictUnusedNidFilter(testProgress)->Filter(leaOverflowRefs, elfFixture(input), input.Text, input.TextVaddr).size() == 1, + "Strict filter removed LEA RIP-relative displacement overflow import"); + + input.Text.assign(32, 0); + input.TextVaddr = 0x1000; + input.Entries = {input.TextVaddr}; + emit(input, 0, {0xFF, 0x15}); + write(input.Text, 2, static_cast(-0x1006)); + emit(input, 6, {0xC3}); + input.ImportSlots = {0}; + require(AnalyzeStrictReachability(input).ImportSlots == std::set{0}, "RIP-relative displacement to address 0 was not resolved"); + + constexpr auto maxTarget = std::numeric_limits::max(); + input.Text.assign(32, 0); + input.TextVaddr = maxTarget - 32; + input.Entries = {input.TextVaddr}; + emit(input, 0, {0xFF, 0x15}); + write(input.Text, 2, static_cast(26)); + emit(input, 6, {0xC3}); + input.ImportSlots = {maxTarget}; + require(AnalyzeStrictReachability(input).ImportSlots == std::set{maxTarget}, "RIP-relative displacement to maximum address was not resolved"); + + input.Text.assign(32, 0); + input.TextVaddr = 0x1000; + input.Entries = {input.TextVaddr}; + emit(input, 0, {0x67, 0xFF, 0x15}); + write(input.Text, 3, static_cast(-0x2000)); + emit(input, 7, {0xC3}); + const auto eipWrappedTarget = static_cast(input.TextVaddr + 7 + static_cast(static_cast(-0x2000))); + input.ImportSlots = {eipWrappedTarget}; + require(AnalyzeStrictReachability(input).ImportSlots == std::set{eipWrappedTarget}, "EIP-relative displacement underflow did not wrap modulo 2^32"); +} + void callbackAndRelocationRoots() { auto input = fixture(); ripOperand(input, 0, {0x48, 0x8D, 0x3D}, 0x1020); @@ -568,6 +698,154 @@ void relativeRelocationTableBounds() { write(badSize, 0x118, 25); requireFailure([&] { Relinker::UnusedNidFilter::BuildRelativeRelocationIndex(badSize); }, "Unaligned relocation table size was accepted"); + auto validRelaEnt = bytes; + write(validRelaEnt, 0x120, 9); + write(validRelaEnt, 0x128, 24); + write(validRelaEnt, 0x130, 0); + write(validRelaEnt, 0x138, 0); + auto indexRelaEnt = Relinker::UnusedNidFilter::BuildRelativeRelocationIndex(validRelaEnt); + require(indexRelaEnt->TargetOfSlot(0x5000) == 0x6000, "Valid DT_RELAENT = 24 was not accepted"); + + auto badRelaEnt = bytes; + write(badRelaEnt, 0x120, 9); + write(badRelaEnt, 0x128, 16); + write(badRelaEnt, 0x130, 0); + write(badRelaEnt, 0x138, 0); + requireFailureMessage([&] { Relinker::UnusedNidFilter::BuildRelativeRelocationIndex(badRelaEnt); }, "Unsupported DT_RELAENT value: 16", "Invalid DT_RELAENT was accepted"); + + auto conflictingRelaEnt = bytes; + write(conflictingRelaEnt, 0x120, 9); + write(conflictingRelaEnt, 0x128, 24); + write(conflictingRelaEnt, 0x130, 9); + write(conflictingRelaEnt, 0x138, 32); + write(conflictingRelaEnt, 0x140, 0); + write(conflictingRelaEnt, 0x148, 0); + requireFailureMessage([&] { Relinker::UnusedNidFilter::BuildRelativeRelocationIndex(conflictingRelaEnt); }, "Conflicting duplicate DT_RELAENT tags", "Conflicting DT_RELAENT was accepted"); + + auto relaEntConflictLastValid = bytes; + write(relaEntConflictLastValid, 0x100, 0x9); + write(relaEntConflictLastValid, 0x108, 32); + write(relaEntConflictLastValid, 0x110, 0x9); + write(relaEntConflictLastValid, 0x118, 24); + write(relaEntConflictLastValid, 0x120, 0x0); + write(relaEntConflictLastValid, 0x128, 0); + requireFailureMessage([&] { Relinker::UnusedNidFilter::BuildRelativeRelocationIndex(relaEntConflictLastValid); }, "Conflicting duplicate DT_RELAENT tags", "Conflicting DT_RELAENT ending in 24 was accepted"); + + auto osRelaEntConflictLastValid = bytes; + write(osRelaEntConflictLastValid, 0x100, 0x61000033); + write(osRelaEntConflictLastValid, 0x108, 32); + write(osRelaEntConflictLastValid, 0x110, 0x61000033); + write(osRelaEntConflictLastValid, 0x118, 24); + write(osRelaEntConflictLastValid, 0x120, 0x0); + write(osRelaEntConflictLastValid, 0x128, 0); + requireFailureMessage([&] { Relinker::UnusedNidFilter::BuildRelativeRelocationIndex(osRelaEntConflictLastValid); }, "Conflicting duplicate DT_OS_RELAENT tags", "Conflicting DT_OS_RELAENT ending in 24 was accepted"); + + auto bothRelaEntVariants = bytes; + write(bothRelaEntVariants, 0x120, 9); + write(bothRelaEntVariants, 0x128, 24); + write(bothRelaEntVariants, 0x130, 0x61000033); + write(bothRelaEntVariants, 0x138, 24); + write(bothRelaEntVariants, 0x140, 0); + write(bothRelaEntVariants, 0x148, 0); + requireFailureMessage([&] { Relinker::UnusedNidFilter::BuildRelativeRelocationIndex(bothRelaEntVariants); }, "Both DT_OS_ and DT_ variants present for DT_RELAENT", "Both DT_OS_ and DT_ variants for DT_RELAENT was accepted"); + + auto relaEntBothVariantsWithoutTable = bytes; + write(relaEntBothVariantsWithoutTable, 0x100, 0x9); + write(relaEntBothVariantsWithoutTable, 0x108, 24); + write(relaEntBothVariantsWithoutTable, 0x110, 0x61000033); + write(relaEntBothVariantsWithoutTable, 0x118, 24); + write(relaEntBothVariantsWithoutTable, 0x120, 0x0); + write(relaEntBothVariantsWithoutTable, 0x128, 0); + requireFailureMessage([&] { Relinker::UnusedNidFilter::BuildRelativeRelocationIndex(relaEntBothVariantsWithoutTable); }, "Both DT_OS_ and DT_ variants present for DT_RELAENT", "Both DT_RELAENT variants without a RELA table were accepted"); + + auto mismatchedRelaEnt = bytes; + write(mismatchedRelaEnt, 0x120, 0x61000033); + write(mismatchedRelaEnt, 0x128, 24); + write(mismatchedRelaEnt, 0x130, 0); + write(mismatchedRelaEnt, 0x138, 0); + requireFailureMessage([&] { Relinker::UnusedNidFilter::BuildRelativeRelocationIndex(mismatchedRelaEnt); }, "Mismatched DT_RELA and DT_RELAENT tag variants", "Mismatched DT_RELA and DT_RELAENT was accepted"); + + auto validPltRel = bytes; + write(validPltRel, 0x100, 23); + write(validPltRel, 0x108, 0x10E8); + write(validPltRel, 0x110, 2); + write(validPltRel, 0x118, 24); + write(validPltRel, 0x120, 20); + write(validPltRel, 0x128, 7); + write(validPltRel, 0x130, 0); + write(validPltRel, 0x138, 0); + auto indexPltRel = Relinker::UnusedNidFilter::BuildRelativeRelocationIndex(validPltRel); + require(indexPltRel->TargetOfSlot(0x5000) == 0x6000, "Valid DT_PLTREL = 7 was not accepted"); + + auto badPltRel = bytes; + write(badPltRel, 0x100, 23); + write(badPltRel, 0x108, 0x10E8); + write(badPltRel, 0x110, 2); + write(badPltRel, 0x118, 24); + write(badPltRel, 0x120, 20); + write(badPltRel, 0x128, 17); + write(badPltRel, 0x130, 0); + write(badPltRel, 0x138, 0); + requireFailureMessage([&] { Relinker::UnusedNidFilter::BuildRelativeRelocationIndex(badPltRel); }, "Unsupported DT_PLTREL type: 17; expected DT_RELA (7)", "Unsupported DT_PLTREL type was accepted"); + + auto pltRelConflictLastValid = bytes; + write(pltRelConflictLastValid, 0x100, 0x14); + write(pltRelConflictLastValid, 0x108, 17); + write(pltRelConflictLastValid, 0x110, 0x14); + write(pltRelConflictLastValid, 0x118, 7); + write(pltRelConflictLastValid, 0x120, 0x0); + write(pltRelConflictLastValid, 0x128, 0); + requireFailureMessage([&] { Relinker::UnusedNidFilter::BuildRelativeRelocationIndex(pltRelConflictLastValid); }, "Conflicting duplicate DT_PLTREL tags", "Conflicting DT_PLTREL ending in DT_RELA was accepted"); + + auto osPltRelConflictLastValid = bytes; + write(osPltRelConflictLastValid, 0x100, 0x6100002B); + write(osPltRelConflictLastValid, 0x108, 17); + write(osPltRelConflictLastValid, 0x110, 0x6100002B); + write(osPltRelConflictLastValid, 0x118, 7); + write(osPltRelConflictLastValid, 0x120, 0x0); + write(osPltRelConflictLastValid, 0x128, 0); + requireFailureMessage([&] { Relinker::UnusedNidFilter::BuildRelativeRelocationIndex(osPltRelConflictLastValid); }, "Conflicting duplicate DT_OS_PLTREL tags", "Conflicting DT_OS_PLTREL ending in DT_RELA was accepted"); + + auto pltRelBothVariantsWithoutTable = bytes; + write(pltRelBothVariantsWithoutTable, 0x100, 0x14); + write(pltRelBothVariantsWithoutTable, 0x108, 7); + write(pltRelBothVariantsWithoutTable, 0x110, 0x6100002B); + write(pltRelBothVariantsWithoutTable, 0x118, 7); + write(pltRelBothVariantsWithoutTable, 0x120, 0x0); + write(pltRelBothVariantsWithoutTable, 0x128, 0); + requireFailureMessage([&] { Relinker::UnusedNidFilter::BuildRelativeRelocationIndex(pltRelBothVariantsWithoutTable); }, "Both DT_OS_ and DT_ variants present for DT_PLTREL", "Both DT_PLTREL variants without a PLT table were accepted"); + + auto jmprelWithOsPltRel = bytes; + write(jmprelWithOsPltRel, 0x100, 0x17); + write(jmprelWithOsPltRel, 0x108, 0x10E8); + write(jmprelWithOsPltRel, 0x110, 0x2); + write(jmprelWithOsPltRel, 0x118, 24); + write(jmprelWithOsPltRel, 0x120, 0x6100002B); + write(jmprelWithOsPltRel, 0x128, 7); + write(jmprelWithOsPltRel, 0x130, 0x0); + write(jmprelWithOsPltRel, 0x138, 0); + requireFailureMessage([&] { Relinker::UnusedNidFilter::BuildRelativeRelocationIndex(jmprelWithOsPltRel); }, "Mismatched DT_JMPREL and DT_PLTREL tag variants", "Mismatched DT_JMPREL and DT_PLTREL variants were accepted"); + + auto consistentEntryTagsWithoutTables = bytes; + write(consistentEntryTagsWithoutTables, 0x100, 0x9); + write(consistentEntryTagsWithoutTables, 0x108, 24); + write(consistentEntryTagsWithoutTables, 0x110, 0x9); + write(consistentEntryTagsWithoutTables, 0x118, 24); + write(consistentEntryTagsWithoutTables, 0x120, 0x14); + write(consistentEntryTagsWithoutTables, 0x128, 7); + write(consistentEntryTagsWithoutTables, 0x130, 0x14); + write(consistentEntryTagsWithoutTables, 0x138, 7); + write(consistentEntryTagsWithoutTables, 0x140, 0x0); + write(consistentEntryTagsWithoutTables, 0x148, 0); + (void)Relinker::UnusedNidFilter::BuildRelativeRelocationIndex(consistentEntryTagsWithoutTables); + + auto unsupportedRel = bytes; + write(unsupportedRel, 0x120, 17); + write(unsupportedRel, 0x128, 0x10E8); + write(unsupportedRel, 0x130, 0); + write(unsupportedRel, 0x138, 0); + requireFailureMessage([&] { Relinker::UnusedNidFilter::BuildRelativeRelocationIndex(unsupportedRel); }, "DT_REL relocations are not supported", "DT_REL relocations were accepted"); + auto pltOverextended = bytes; write(pltOverextended, 0x100, 23); write(pltOverextended, 0x108, 0x10E8); @@ -688,6 +966,17 @@ void sceDynlibDataOffsets() { requireFailure([&] { (void)Relinker::UnusedNidFilter::BuildRelativeRelocationIndex(emptyJmprel); }, "Empty SCE JMPREL table with overflowing offset was accepted"); + auto validOsPltRel = jmprel; + write(validOsPltRel, 0x338, 0x430); + write(validOsPltRel, 208, 0x80); + write(validOsPltRel, 216, 0x80); + write(validOsPltRel, 0x350, 0x6100002B); + write(validOsPltRel, 0x358, 7); + write(validOsPltRel, 0x360, 0); + write(validOsPltRel, 0x368, 0); + require(Relinker::UnusedNidFilter::BuildRelativeRelocationIndex(validOsPltRel)->TargetOfSlot(0x2050) == 0x1010, + "Valid DT_OS_PLTREL = 7 was not accepted"); + auto emptyRela = bytes; write(emptyRela, 0x318, 0); require(!Relinker::UnusedNidFilter::BuildRelativeRelocationIndex(emptyRela)->TargetOfSlot(0x2040), @@ -714,6 +1003,40 @@ void sceDynlibDataOffsets() { requireFailure([&] { (void)Relinker::MakeStrictUnusedNidFilter(testProgress)->Filter(references, duplicate, input.Text, input.TextVaddr); }, "Strict filter accepted duplicate PT_SCE_DYNLIBDATA segments"); + auto overflowSce = bytes; + write(overflowSce, 240, ~std::uint64_t{0} - 16); + requireFailure([&] { (void)Relinker::UnusedNidFilter::BuildRelativeRelocationIndex(overflowSce); }, + "PT_SCE_DYNLIBDATA offset near UINT64_MAX was accepted"); + + auto sceOverextended = bytes; + write(sceOverextended, 0x318, 0); + write(sceOverextended, 264, sceOverextended.size() - 0x500 + 1); + requireFailureMessage([&] { (void)Relinker::UnusedNidFilter::BuildRelativeRelocationIndex(sceOverextended); }, + "PT_SCE_DYNLIBDATA segment out of bounds", + "PT_SCE_DYNLIBDATA extending beyond file was accepted"); + + auto strictRel = bytes; + write(strictRel, 0x320, 17); + write(strictRel, 0x328, 0); + requireFailureMessage([&] { (void)Relinker::MakeStrictUnusedNidFilter(testProgress)->Filter(references, strictRel, input.Text, input.TextVaddr); }, + "Strict filter: DT_REL relocations are not supported", + "Strict filter accepted DT_REL dynamic tag"); + + auto strictBadRelaEnt = bytes; + write(strictBadRelaEnt, 0x300, 0x61000021); + write(strictBadRelaEnt, 0x310, 0x61000021); + write(strictBadRelaEnt, 0x328, 16); + requireFailureMessage([&] { (void)Relinker::MakeStrictUnusedNidFilter(testProgress)->Filter(references, strictBadRelaEnt, input.Text, input.TextVaddr); }, + "Strict filter: invalid RELA entry size", + "Strict filter accepted invalid DT_OS_RELAENT value"); + + auto strictValidRelaEntNoTable = bytes; + write(strictValidRelaEntNoTable, 0x300, 0x61000021); + write(strictValidRelaEntNoTable, 0x310, 0x61000021); + write(strictValidRelaEntNoTable, 0x328, 24); + require(Relinker::MakeStrictUnusedNidFilter(testProgress)->Filter(references, strictValidRelaEntNoTable, input.Text, input.TextVaddr).empty(), + "Strict filter failed to filter without relocations"); + auto sectionRelative = bytes; write(sectionRelative, 208, 0x70); write(sectionRelative, 216, 0x70); @@ -961,6 +1284,57 @@ void negativeRelativeRelocationTarget() { "Strict filter rejected a valid relative relocation target address"); } +void absoluteFunctionSymbols() { + auto input = fixture(); + emit(input, 0, {0xC3}); + importThunk(input, 16, 0x2000); + auto bytes = elfFixture(input); + bytes.resize(0xA00); + write(bytes, 56, 4); + write(bytes, 176, 2); + write(bytes, 180, 6); + write(bytes, 184, 0x300); + write(bytes, 192, 0x2000); + write(bytes, 208, 0x40); + write(bytes, 216, 0x40); + write(bytes, 224, 8); + write(bytes, 232, 0x61000000); + write(bytes, 236, 4); + write(bytes, 240, 0x500); + write(bytes, 264, 0x500); + write(bytes, 272, 0x500); + write(bytes, 280, 0x10); + write(bytes, 0x300, 0x61000039); + write(bytes, 0x308, 0x100); + write(bytes, 0x310, 0x6100003B); + write(bytes, 0x318, 24); + write(bytes, 0x320, 0x6100003F); + write(bytes, 0x328, 48); + const std::vector references = {{"dead", {}, 6, 0x900, 0x2000, 0}}; + + write(bytes, 0x61C, 0x12); + write(bytes, 0x61E, 1); + write(bytes, 0x620, 0x1010); + require(Relinker::MakeStrictUnusedNidFilter(testProgress)->Filter(references, bytes, input.Text, input.TextVaddr).size() == 1, + "Strict filter dropped an import used by a regular exported function"); + + for (const std::uint8_t binding : {std::uint8_t{1}, std::uint8_t{2}}) { + for (const std::uint64_t constant : {std::uint64_t{0x123456789}, std::uint64_t{0x1010}, std::uint64_t{0}}) { + write(bytes, 0x61C, static_cast((binding << 4) | 2)); + write(bytes, 0x61E, 0xFFF1); + write(bytes, 0x620, constant); + require(Relinker::MakeStrictUnusedNidFilter(testProgress)->Filter(references, bytes, input.Text, input.TextVaddr).empty(), + "Strict filter treated an SHN_ABS function symbol as a code entry point"); + } + } + + write(bytes, 0x61C, 0x1A); + write(bytes, 0x61E, 0xFFF1); + write(bytes, 0x620, 0x123456789); + requireFailure([&] { (void)Relinker::MakeStrictUnusedNidFilter(testProgress)->Filter(references, bytes, input.Text, input.TextVaddr); }, + "Strict filter accepted an SHN_ABS IFUNC symbol"); +} + } int main() { @@ -970,6 +1344,7 @@ int main() { conditionalTailCall(); fallthroughAfterZeroPadding(); addressSizeOverrideImport(); + ripDisplacementOverflowUnderflowBounds(); callbackAndRelocationRoots(); registerImportCall(); mergedJumpTable(); @@ -990,6 +1365,7 @@ int main() { duplicatePointerRelocations(); constructorArrayBounds(); negativeRelativeRelocationTarget(); + absoluteFunctionSymbols(); std::cout << "Strict NID filter tests passed\n"; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; diff --git a/core/relinker/relinker/tests/UnwindRecordBoundsTests.cpp b/core/relinker/relinker/tests/UnwindRecordBoundsTests.cpp index fe1fdf9976..dd3ab410de 100644 --- a/core/relinker/relinker/tests/UnwindRecordBoundsTests.cpp +++ b/core/relinker/relinker/tests/UnwindRecordBoundsTests.cpp @@ -352,6 +352,41 @@ int main() { lsdaS2Rejected = std::string(error.what()).find("unwind pointer address overflowed") != std::string::npos; } Require(lsdaS2Rejected); - std::cout << "30 unwind record cases passed\n"; + Domain::ProgramHeader augLoad{}; augLoad.Type = 1; augLoad.MappedAddress = 0x1000; augLoad.FileSize = 128; + Domain::ProgramHeader augEh = augLoad; augEh.Type = 0x6474E550; augEh.FileSize = 32; + auto unterminatedAugBytes = Fixture(); + unterminatedAugBytes[73] = 'z'; + unterminatedAugBytes[74] = 'R'; + unterminatedAugBytes[75] = 'L'; + unterminatedAugBytes[76] = 'P'; + bool unterminatedAugRejected = false; + try { Relinker::UnusedNidFilter::ReadExceptionFunctions(unterminatedAugBytes, {augLoad, augEh}, {}, {}); } + catch (const Domain::RelinkerException& error) { + unterminatedAugRejected = std::string(error.what()).find("unterminated CIE augmentation") != std::string::npos; + } + Require(unterminatedAugRejected); + auto outOfBoundsAugBytes = Fixture(); + Io::WriteU32(outOfBoundsAugBytes, 64, 12); + outOfBoundsAugBytes[73] = 'z'; + outOfBoundsAugBytes[74] = 0; + outOfBoundsAugBytes[75] = 1; + outOfBoundsAugBytes[76] = 0x78; + outOfBoundsAugBytes[77] = 16; + outOfBoundsAugBytes[78] = 10; + bool outOfBoundsAugRejected = false; + try { Relinker::UnusedNidFilter::ReadExceptionFunctions(outOfBoundsAugBytes, {augLoad, augEh}, {}, {}); } + catch (const Domain::RelinkerException& error) { + outOfBoundsAugRejected = std::string(error.what()).find("CIE augmentation exceeds record") != std::string::npos; + } + Require(outOfBoundsAugRejected); + auto unterminatedRecordBytes = Fixture(); + Io::WriteU32(unterminatedRecordBytes, 64, 8); + bool unterminatedRecordRejected = false; + try { Relinker::UnusedNidFilter::ReadExceptionFunctions(unterminatedRecordBytes, {augLoad, augEh}, {}, {}); } + catch (const Domain::RelinkerException& error) { + unterminatedRecordRejected = std::string(error.what()).find("CIE fields exceed record") != std::string::npos; + } + Require(unterminatedRecordRejected); + std::cout << "33 unwind record cases passed\n"; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; return 1; } } diff --git a/core/relinker/relinker/tests/WindowsExportAliasTests.cpp b/core/relinker/relinker/tests/WindowsExportAliasTests.cpp index 8c7c3cdd72..e11b78f6bf 100644 --- a/core/relinker/relinker/tests/WindowsExportAliasTests.cpp +++ b/core/relinker/relinker/tests/WindowsExportAliasTests.cpp @@ -68,7 +68,7 @@ int main() { const std::array,7> changes={{{4,0x22},{4,0x11},{5,0},{6,2},{8,0x28},{16,2},{6,Relinker::AbsoluteSection}}}; for(const auto& [field,value]:changes){auto b=Fixture();auto off=0x2400+48+field;if(field<6)b[off]=value;else if(field==6)Io::WriteU16(b,off,value);else Io::WriteU64(b,off,value);Reject([&]{Relinker::GuestImageReader().Read("generic.prx",b,true);},"Duplicate or empty export");} auto good=Relinker::GuestImageReader().Read("generic.prx",Fixture(),true); - auto symbolic=good;symbolic.Symbolic=true;Domain::GuestRuntime symbolicRuntime;Reject([&]{Elfpatcher::GuestModuleWriter().WriteWindows(symbolic,symbolicRuntime);},"DF_SYMBOLIC"); + auto symbolic=good;symbolic.Symbolic=true;Domain::GuestRuntime symbolicRuntime,ordinaryRuntime;Check(Elfpatcher::GuestModuleWriter().WriteWindows(symbolic,symbolicRuntime)==Elfpatcher::GuestModuleWriter().WriteWindows(good,ordinaryRuntime)&&symbolicRuntime.Imports.size()==ordinaryRuntime.Imports.size(),"DF_SYMBOLIC changed Windows binding"); for(unsigned kind=0;kind<10;++kind){auto g=good;auto& s=g.Symbols[2];switch(kind){case 0:s.Value+=8;break;case 1:s.Size+=1;break;case 2:s.Info=0x22;break;case 3:s.Info=0x11;break;case 4:s.Visibility=0;break;case 5:s.Section=2;break;case 6:g.Symbols[1].Info=s.Info=0x11;break;case 7:g.Symbols[1].Section=s.Section=Relinker::AbsoluteSection;break;case 8:g.Symbols[1].Info=s.Info=0x16;g.Dynamic.RelaData.clear();break;case 9:g.Symbols[1].Info=s.Info=0x10;break;}Check(!Relinker::IdenticalOrdinaryFunctionExport(g.Symbols[1],s),"nonordinary/conflicting tuple accepted");Domain::GuestRuntime r;Reject([&]{Elfpatcher::GuestModuleWriter().WriteWindows(g,r);},kind==8?"TLS":"Duplicate guest export");} Check(Domain::DeclaredImportModule("same#A#B",{{1,"one"},{2,"two"}})=="one"&&Domain::DeclaredImportModule("same#A#C",{{1,"one"},{2,"two"}})=="two","module-qualified IDs merged"); auto zeroBucket=Fixture();Io::WriteU32(zeroBucket,0x2600,0); @@ -87,6 +87,27 @@ int main() { Io::WriteU32(multiBucket,0x2600,2);Io::WriteU32(multiBucket,0x2604,4);Io::WriteU32(multiBucket,0x2608,1);Io::WriteU32(multiBucket,0x260c,2); auto goodMulti=Relinker::GuestImageReader().Read("generic.prx",multiBucket,true); Check(goodMulti.Symbols.size()==4,"valid multi-bucket SysV hash accepted"); + auto rel=Fixture(); + Io::WriteU64(rel,0x2080,17); + Io::WriteU64(rel,0x2088,0x2700); + Reject([&]{Relinker::GuestImageReader().Read("generic.prx",rel,true);},"DT_REL relocations are not supported"); + auto badRelaEnt=Fixture(); + Io::WriteU64(badRelaEnt,0x2050,0x61000021); + Io::WriteU64(badRelaEnt,0x2060,0x61000021); + Io::WriteU64(badRelaEnt,0x2078,16); + Reject([&]{Relinker::GuestImageReader().Read("generic.prx",badRelaEnt,true);},"Invalid RELA entry size"); + auto badPltRel=Fixture(); + Io::WriteU64(badPltRel,0x2080,20); + Io::WriteU64(badPltRel,0x2088,17); + Reject([&]{Relinker::GuestImageReader().Read("generic.prx",badPltRel,true);},"Unsupported PLT relocation format"); + auto validNoPlt=Fixture(); + Io::WriteU64(validNoPlt,0x2050,0x61000021); + Io::WriteU64(validNoPlt,0x2060,0x61000021); + Io::WriteU64(validNoPlt,0x2078,24); + Io::WriteU64(validNoPlt,0x2080,20); + Io::WriteU64(validNoPlt,0x2088,7); + auto goodNoPlt=Relinker::GuestImageReader().Read("generic.prx",validNoPlt,true); + Check(goodNoPlt.Symbols.size()==4,"valid RELAENT and PLTREL without tables accepted"); std::cout<<"WINDOWS_EXPORT_ALIAS_CASES_PASSED\npreserved-indices reversed-order conflicting-fields strict-nonwindows qualified-import writer-guard\n"; } catch(const std::exception& e){std::cerr<> 32 else entry for entry in rela if entry[1] & 0xffffffff] @@ -108,17 +120,23 @@ def check_output(data, system, rela, plt, initial): pe_header, = struct.unpack_from("> 32 and symbol_name(image, tags, info >> 32) == HELPER: + symbol = file_offset(image, tags[6]) + (info >> 32) * 24 + slots.append((target, info & 0xffffffff, addend, image[symbol + 4], struct.unpack_from(" +#include +#include +static void permissions(void *address) { + FILE *maps = fopen("/proc/self/maps", "r"); + if (!maps) _exit(92); + char line[512], access[5]; + unsigned long start, end; + while (fgets(line, sizeof(line), maps)) { + if (sscanf(line, "%lx-%lx %4s", &start, &end, access) == 3 + && (uintptr_t)address >= start && (uintptr_t)address < end) { + dprintf(1, "%s\n", access); + fclose(maps); + return; + } + } + _exit(93); +} +int shared(void *slot, int last) { + permissions(slot); + permissions((char *)slot + 7); + if (last) _exit(11); + return 11; +} +""" +PERMISSIONS_HOOK = r""" +#include +#include +static int (*original)(void *, int); +static int replacement(void *slot, int last) { + int result = original(slot, 0) + 66; + if (last) _exit(result); + return result; +} +void *TraceImport_nid_no_patch(void *target, const char *name, const char *library) { + if (strcmp(name, "shared") != 0 || strcmp(library, "a.prx") != 0) _exit(94); + original = target; + write(2, "hook\n", 5); + return replacement; +} +""" + + +def permissions_fixture(slots, segments): + image = executable("a.prx", extra_dependencies=("libc.prx",), + extra_symbols=("shared#A#B",) * (len(slots) - 1)) + struct.pack_into(" code) { std::uint64_t hash = 0x9e3779b97f4a7c15ull ^ (static_cast(code.size()) * 0x100000001b3ull); @@ -90,6 +97,7 @@ private: append(key, request.context.vertex); append(key, request.context.floatMode); append(key, request.context.convertedSamples); + append(key, request.context.layerRowSamples); appendFallbacks(key, request); appendMesh(key, request); appendTessellation(key, request); @@ -301,7 +309,11 @@ private: append(key, value.maxEvaluationOutputComponents); } - static void appendFeatureDigest(std::vector& key, std::span capabilities, std::span extensions) { + static std::array featureSpans(const SpirvTarget& target) { + return {reinterpret_cast(target.supportedCapabilities.data()), target.supportedCapabilities.size(), reinterpret_cast(target.supportedExtensions.data()), target.supportedExtensions.size()}; + } + + static std::array digestFeatures(std::span capabilities, std::span extensions) { std::uint64_t low = 0xcbf29ce484222325ull; std::uint64_t high = 0x6a09e667f3bcc909ull; const auto mix = [&](std::uint64_t value) { @@ -320,8 +332,13 @@ private: mix(chunk); } } - key.push_back(low); - key.push_back(high); + return {low, high}; + } + + static void appendFeatureDigest(std::vector& key, const SpirvTarget& target) { + const auto digest = target.featureDigestSealed && target.featureDigestSpans == featureSpans(target) ? target.featureDigest : digestFeatures(target.supportedCapabilities, target.supportedExtensions); + key.push_back(digest[0]); + key.push_back(digest[1]); } static void append(std::vector& key, const SpirvTarget& value) { @@ -329,7 +346,7 @@ private: append(key, value.spirvVersion); append(key, value.subgroupSize); append(key, value.bdaAbiVersion); - appendFeatureDigest(key, value.supportedCapabilities, value.supportedExtensions); + appendFeatureDigest(key, value); append(key, value.fragmentShaderBarycentricEnabled); append(key, value.maxWorkgroupSize); append(key, value.maxWorkgroupInvocations); @@ -338,6 +355,7 @@ private: append(key, value.tessellation); append(key, value.nonConstantImageOffsets); append(key, value.srgbDecodeFormats); + append(key, value.maxImageArrayLayers); append(key, value.narrowSubgroupClock); append(key, value.fragmentBarycentricBuiltins); append(key, value.float64DenormPreserve); @@ -359,6 +377,8 @@ private: append(key, value.deviceScopeLdsMemoryBarriers); append(key, value.ldsLockFences); append(key, value.meshWindowBuffer); + append(key, value.hostInterpolationFirst); + append(key, value.shaderImageMinLod); } }; diff --git a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp index e6b6c9de1a..f14fda01ec 100644 --- a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp +++ b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp @@ -244,7 +244,7 @@ ConservativeZExport readConservativeZExport(Reader& reader) { ColorExportPacking readColorExportPacking(Reader& reader) { const auto value = reader.ReadU8(); - if (value > static_cast(ColorExportPacking::Uint)) { + if (value > static_cast(ColorExportPacking::UintClamp16)) { throw std::runtime_error("RequestSerializer: invalid ColorExportPacking value"); } return static_cast(value); @@ -898,7 +898,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const std::string buffer; Writer writer(buffer); writer.WriteU32(0x41505335u); - writer.WriteU32(51u); + writer.WriteU32(53u); writeShaderBinary(writer, request.shader); writeGuestContext(writer, request.context); writeSpirvTarget(writer, request.target); @@ -941,6 +941,8 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const writer.WriteU32(request.target.pixelFunctionStorageDwords); writer.WriteBool(request.target.fragmentBarycentricBuiltins); writer.WriteBool(request.target.tessellationEvaluationSubgroups); + writer.WriteBool(request.target.hostInterpolationFirst); + writer.WriteBool(request.target.shaderImageMinLod); return base64Encode(buffer); } @@ -949,7 +951,7 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const Reader reader(decoded); if (reader.ReadU32() != 0x41505335u) throw std::runtime_error("invalid recompile request signature"); const auto version = reader.ReadU32(); - if (version < 1u || version > 51u || version == 50u) throw std::runtime_error("unsupported recompile request serialization version"); + if (version < 1u || version > 53u || version == 50u) throw std::runtime_error("unsupported recompile request serialization version"); DeserializedRequest result{}; result.request.shader = readShaderBinary(reader, result.shaderCode, result.shaderHeader); result.request.context = readGuestContext(reader, result, version); @@ -1010,6 +1012,8 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const } if (version >= 48u) result.request.target.fragmentBarycentricBuiltins = reader.ReadBool(); if (version >= 51u) result.request.target.tessellationEvaluationSubgroups = reader.ReadBool(); + if (version >= 52u) result.request.target.hostInterpolationFirst = reader.ReadBool(); + if (version >= 53u) result.request.target.shaderImageMinLod = reader.ReadBool(); if (version >= 21u && reader.Remaining() != 0u) throw std::runtime_error("RequestSerializer: unexpected trailing data"); return result; } diff --git a/core/shader/recompiler/FragmentOutputSpecialization.hpp b/core/shader/recompiler/FragmentOutputSpecialization.hpp index 515c9d635f..2122914908 100644 --- a/core/shader/recompiler/FragmentOutputSpecialization.hpp +++ b/core/shader/recompiler/FragmentOutputSpecialization.hpp @@ -32,7 +32,7 @@ inline std::vector SpecializeFragmentOutputs(std::vector= PipelineSpecialization::ExportPackingBase + 8u) continue; - if (constant.value > static_cast(ColorExportPacking::Uint)) throw std::runtime_error("invalid prepared fragment export packing"); + if (constant.value > static_cast(ColorExportPacking::UintClamp16)) throw std::runtime_error("invalid prepared fragment export packing"); if (constant.value != static_cast(ColorExportPacking::None)) packed.insert(constant.id - PipelineSpecialization::ExportPackingBase); } if (packed.empty() && !dualSource && reversed == 0u) return words; diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ResourcePlan.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ResourcePlan.hpp index 08b2529bc0..be48288d1e 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ResourcePlan.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ResourcePlan.hpp @@ -32,6 +32,28 @@ struct DescriptorValue { bool operator==(const DescriptorValue& other) const = default; }; +struct RegisteredImageView { + std::uint32_t resource = 0; + std::uint32_t record = 0; + DescriptorValue provenance; + DescriptorValue effective; + std::uint64_t generation = 0; + std::uint64_t address = 0; + std::uint64_t rangeAddress = 0; + std::uint64_t rangeBytes = 0; + std::uint64_t allocationAddress = 0; + std::uint64_t allocationBytes = 0; + std::uint64_t backingSerial = 0; + bool hasRange = false; + bool readable = false; + bool writable = false; + bool gpu = false; + + bool operator==(const RegisteredImageView& other) const { + return resource == other.resource && record == other.record && provenance == other.provenance && effective == other.effective && address == other.address && rangeAddress == other.rangeAddress && rangeBytes == other.rangeBytes && allocationAddress == other.allocationAddress && allocationBytes == other.allocationBytes && backingSerial == other.backingSerial && hasRange == other.hasRange && readable == other.readable && writable == other.writable && gpu == other.gpu; + } +}; + template class SharedValues { public: @@ -112,6 +134,14 @@ struct ImageTableColumnSnapshot { SharedValues codes; SharedValues words; SharedValues> readRanges; + struct ResourceView { + std::uint32_t resource = 0; + SharedValues words; + std::vector identities; + + bool operator==(const ResourceView& other) const = default; + }; + std::vector resourceViews; bool operator==(const ImageTableColumnSnapshot& other) const = default; }; @@ -134,6 +164,8 @@ struct ResourceSnapshot { std::vector srtPoison; std::uint32_t nullRootReads = 0; ImageTableSnapshot tables; + std::vector registeredImages; + std::uint64_t imageAllocationGeneration = 0; }; struct UniformFillPlan { @@ -227,6 +259,7 @@ struct IrResourcePlan { std::uint32_t userDataBase = 0; std::uint32_t userDataCount = 64; std::uint32_t srgbDecodeFormats = 0; + std::uint32_t maxImageArrayLayers = 0; IrValueArena valueStorage; std::vector> blockStorage; std::vector memoryInfo; diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp index e908262aa2..af39e2bd1a 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp @@ -102,6 +102,9 @@ struct ImageResource { bool foldTexelOffsetsOnly = false; bool convertedSamples = false; bool remappedDepth1D = false; + bool layerRowsCompatible = true; + bool layerRows = false; + std::uint32_t maxArrayLayers = 0u; bool optionalDescriptor = false; std::uint32_t byElements = 0; std::uint32_t byComponents = 0; diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ShaderInfo.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ShaderInfo.hpp index 76c0af830f..40f197a264 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ShaderInfo.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ShaderInfo.hpp @@ -13,8 +13,8 @@ struct ShaderInfo { std::uint32_t scratchDwords = 0; std::uint32_t sharedMemoryBytes = 0; static constexpr std::uint32_t MaxImages = 64; - static constexpr std::uint32_t MaxSamplers = 32; - static constexpr std::uint32_t MaxSampledPairs = 64; + static constexpr std::uint32_t MaxSamplers = 64; + static constexpr std::uint32_t MaxSampledPairs = 256; std::vector buffers; std::vector images; diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrProgram.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrProgram.hpp index bb66d00d95..69abc3bb2b 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrProgram.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrProgram.hpp @@ -3,13 +3,100 @@ #include "IntermediateRepresentation/IrBlock.hpp" #include "IntermediateRepresentation/IrMetadata.hpp" +#include #include #include #include +#include +#include #include namespace ShaderRecompiler { +class IrValuePool { +public: + IrValuePool() = default; + IrValuePool(const IrValuePool&) = delete; + IrValuePool& operator=(const IrValuePool&) = delete; + IrValuePool(IrValuePool&& other) noexcept : live(std::exchange(other.live, {})), freeSlots(std::exchange(other.freeSlots, {})), chunks(std::exchange(other.chunks, {})), chunkUsed(std::exchange(other.chunkUsed, 0)), chunkCapacity(std::exchange(other.chunkCapacity, 0)) {} + IrValuePool& operator=(IrValuePool&& other) noexcept { + if (this != &other) { + release(); + live = std::exchange(other.live, {}); + freeSlots = std::exchange(other.freeSlots, {}); + chunks = std::exchange(other.chunks, {}); + chunkUsed = std::exchange(other.chunkUsed, 0); + chunkCapacity = std::exchange(other.chunkCapacity, 0); + } + return *this; + } + ~IrValuePool() { release(); } + + IrValue& Create(IrOpcode opcode, IrType type, std::uint32_t id, std::pmr::memory_resource* useArena) { + live.push_back(nullptr); + const bool reuse = !freeSlots.empty(); + if (!reuse && chunkUsed == chunkCapacity) { + try { + grow(); + } catch (...) { + live.pop_back(); + throw; + } + } + IrValue* slot = reuse ? freeSlots.back() : chunks.back() + chunkUsed; + IrValue* value = nullptr; + try { + value = new (slot) IrValue(opcode, type, id, useArena); + } catch (...) { + live.pop_back(); + throw; + } + if (reuse) freeSlots.pop_back(); + else ++chunkUsed; + live.back() = value; + return *value; + } + template + std::size_t EraseIf(TPredicate predicate) { + const auto dead = std::count_if(live.begin(), live.end(), [&](IrValue* value) { return predicate(*value); }); + if (dead == 0) return 0; + freeSlots.reserve(freeSlots.size() + static_cast(dead)); + const auto removed = std::erase_if(live, [&](IrValue* value) { + if (!predicate(*value)) return false; + value->~IrValue(); + freeSlots.push_back(value); + return true; + }); + live.shrink_to_fit(); + return removed; + } + [[nodiscard]] std::vector::const_iterator begin() const { return live.begin(); } + [[nodiscard]] std::vector::const_iterator end() const { return live.end(); } + +private: + void grow() { + const std::size_t size = chunkCapacity == 0 ? 64 : std::min(chunkCapacity * 2, 4096); + chunks.reserve(chunks.size() + 1); + chunks.push_back(static_cast(::operator new(size * sizeof(IrValue), std::align_val_t{alignof(IrValue)}))); + chunkUsed = 0; + chunkCapacity = size; + } + void release() { + for (IrValue* value : live) value->~IrValue(); + for (IrValue* chunk : chunks) ::operator delete(chunk, std::align_val_t{alignof(IrValue)}); + live.clear(); + freeSlots.clear(); + chunks.clear(); + chunkUsed = 0; + chunkCapacity = 0; + } + std::vector live; + std::vector freeSlots; + std::vector chunks; + std::size_t chunkUsed = 0; + std::size_t chunkCapacity = 0; +}; + class IrProgram { public: [[nodiscard]] std::vector>& Blocks(); @@ -41,7 +128,7 @@ private: std::vector> blocks; IrUseArena useArena; std::vector blockOrder; - std::vector> values; + IrValuePool values; IrResourcePlan resourcePlan; IrProgramMetadata metadata; IrBlock* entryBlock = nullptr; diff --git a/core/shader/recompiler/IntermediateRepresentation/src/IrProgram.cpp b/core/shader/recompiler/IntermediateRepresentation/src/IrProgram.cpp index 34a251932b..ac91506ab8 100644 --- a/core/shader/recompiler/IntermediateRepresentation/src/IrProgram.cpp +++ b/core/shader/recompiler/IntermediateRepresentation/src/IrProgram.cpp @@ -224,7 +224,7 @@ std::string valueToString(const std::map& ids, cons std::size_t IrProgram::ReclaimUnusedValues() { std::vector owned(nextValueId, nullptr); - for (const auto& value : values) owned[value->Id()] = value.get(); + for (IrValue* value : values) owned[value->Id()] = value; std::vector live(nextValueId, false); std::unordered_set liveElsewhere; std::vector pending; @@ -262,16 +262,14 @@ std::size_t IrProgram::ReclaimUnusedValues() { pending.pop_back(); for (std::size_t index = 0; index < value->ArgumentCount(); ++index) keep(value->Argument(index)); } - for (const auto& value : values) { + for (IrValue* value : values) { if (isLive(*value)) continue; for (std::size_t index = 0; index < value->ArgumentCount(); ++index) value->ReplaceArgument(index, nullptr); } - for (const auto& value : values) { + for (const IrValue* value : values) { if (!isLive(*value) && value->HasUses()) throw std::runtime_error("an unused IR value has an external user"); } - const auto removed = std::erase_if(values, [&](const auto& value) { return !isLive(*value); }); - values.shrink_to_fit(); - return removed; + return values.EraseIf([&](const IrValue& value) { return !isLive(value); }); } void IrProgram::ReleaseUseArena() { @@ -320,10 +318,8 @@ IrBlock& IrProgram::CreateBlock() { } IrValue& IrProgram::CreateValue(IrOpcode opcode, IrType type) { - auto value = std::make_unique(opcode, type, nextValueId, useArena.Resource()); + IrValue& created = values.Create(opcode, type, nextValueId, useArena.Resource()); nextValueId++; - IrValue& created = *value; - values.push_back(std::move(value)); return created; } diff --git a/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp b/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp index cc0a71caf7..0a57fb7a27 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp @@ -16,7 +16,7 @@ struct PreparedDescriptorBinding { std::vector identityImageSwizzle; std::vector samplerFilterElements; std::vector inactive; - std::vector tableSamplerMasks; + std::vector tableSamplerMasks; std::size_t writtenBuffers = 0; std::size_t readOnlyBuffers = 0; }; @@ -49,7 +49,7 @@ struct DescriptorBindingPlan { class DescriptorBindingBuilder { public: - DescriptorBindingPlan Prepare(const IrBindingLayout& layout, const ShaderInfo& info, IrShaderStage stage, const ResourceSnapshot& snapshot, std::span exportMappings = {}, bool registrationModules = false, bool fragmentDescriptorSet = false, std::span substitutes = {}) const; + DescriptorBindingPlan Prepare(const IrBindingLayout& layout, const ShaderInfo& info, IrShaderStage stage, const ResourceSnapshot& snapshot, std::span exportMappings = {}, bool registrationModules = false, bool fragmentDescriptorSet = false, std::span substitutes = {}, bool shaderImageMinLod = false) const; DescriptorBindingPlan Select(const DescriptorBindingPlan& plan, std::span bindings) const; void Populate(BindingAllocationResult& allocation, const CompiledBindingLayout& compiled, const DescriptorBindingPlan& plan, std::uint32_t userDataBase, const ResourceSnapshot& snapshot, const std::array& partialThreads, const ResolvedImageTables* tables = nullptr) const; void Populate(BindingAllocationResult& allocation, const IrProgram& program, const ResourceSnapshot& snapshot, const std::array& partialThreads, std::span exportMappings = {}) const; diff --git a/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp b/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp index ad1a0f01b5..3ff81bb440 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp @@ -19,7 +19,7 @@ struct ImageTableElement { RdnaImageDimension dimension = RdnaImageDimension::Unknown; bool depthCompare = false; DescriptorBindingKind kind = DescriptorBindingKind::Buffers; - std::uint32_t samplers = 0; + std::uint64_t samplers = 0; bool atomic = false; bool atomic64 = false; }; @@ -66,6 +66,11 @@ public: using std::runtime_error::runtime_error; }; +class LayerRowSamplesRequired : public std::runtime_error { +public: + using std::runtime_error::runtime_error; +}; + class ResourceMaterializer { public: static std::uint32_t EmulatedCompareState(const ShaderInfo& info, const ResourceSnapshot& snapshot, std::uint32_t index); diff --git a/core/shader/recompiler/Optimization/include/Optimization/SrtWalker.hpp b/core/shader/recompiler/Optimization/include/Optimization/SrtWalker.hpp index c44b2faf44..a229b26ba6 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/SrtWalker.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/SrtWalker.hpp @@ -5,6 +5,7 @@ #include "UserDataUse.hpp" #include +#include #include #include #include @@ -66,6 +67,25 @@ inline constexpr std::size_t SrtDescriptorForceSync = std::numeric_limits::max() - 1u; using SrtDescriptorScope = std::uint64_t (*)(void* userData, std::size_t bytes); +struct SrtImageViewIdentity { + std::uint64_t generation = 0; + std::uint64_t address = 0; + std::uint64_t rangeAddress = 0; + std::uint64_t rangeBytes = 0; + std::uint64_t allocationAddress = 0; + std::uint64_t allocationBytes = 0; + std::uint64_t backingSerial = 0; + bool hasRange = false; + bool readable = false; + bool writable = false; + bool gpu = false; + + bool operator==(const SrtImageViewIdentity& other) const = default; +}; + +using SrtImageViewNormalizer = bool (*)(void* userData, std::uint32_t maxArrayLayers, std::array& descriptor, SrtImageViewIdentity& identity); +using SrtImageViewGeneration = std::uint64_t (*)(void* userData); + // The guest addresses a walk dereferenced (Detail::Evaluator::EvaluateRawRead): the leaf read of // each pure flat slot (IrResourcePlan::pureFlatSlots) as (flat offset, address), set by the // flattened loop through `leaf`/`leafSlot` around that slot's evaluation, and every other raw read @@ -92,6 +112,8 @@ struct SrtRuntime { SrtMemorySupplier supplyWords = nullptr; SrtUserDataUse* userDataUse = nullptr; SrtDescriptorScope descriptorScope = nullptr; + SrtImageViewNormalizer normalizeImageView = nullptr; + SrtImageViewGeneration imageViewGeneration = nullptr; }; struct SrtForceSyncScope { @@ -112,6 +134,16 @@ enum class RuntimeValueType { Integer }; +bool& SrtEvaluatingBranch(); + +struct SrtBranchScope { + bool saved; + SrtBranchScope() : saved(SrtEvaluatingBranch()) { SrtEvaluatingBranch() = true; } + ~SrtBranchScope() { SrtEvaluatingBranch() = saved; } + SrtBranchScope(const SrtBranchScope&) = delete; + SrtBranchScope& operator=(const SrtBranchScope&) = delete; +}; + class SrtWalker { public: void BuildPlan(IrProgram& program) const; diff --git a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp index 883feb8800..db5911ef11 100644 --- a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp +++ b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -207,6 +208,47 @@ const char* UnnormalizedUseReason(std::uint32_t uses) { } } +std::array RelativeMinLod(std::span words) { + const auto minLod = (words[1] >> 8u) & 0xfffu; + const auto baseLevel = (words[3] >> 12u) & 0xfu; + const auto maxMip = (words[5] >> 4u) & 0xfu; + const auto lastLevel = std::max(baseLevel <= maxMip ? std::min((words[3] >> 16u) & 0xfu, maxMip) : (words[3] >> 16u) & 0xfu, baseLevel); + return {minLod > baseLevel * 256u ? std::min(minLod - baseLevel * 256u, (lastLevel - baseLevel) * 256u) : 0u, lastLevel - baseLevel}; +} + +void RequireShaderMinLod(const ShaderInfo& info, const ResourceSnapshot& snapshot, std::uint32_t image, const ImageResource& mode, std::uint32_t clamp) { + const auto prefix = "image " + std::to_string(image) + " applies its T#'s minimum LOD of " + std::to_string(clamp) + "/256 in the shader"; + if ((mode.emulatedCompare & EmulatedCompare::Enabled) != 0u || mode.conversionFormat == IrBufferFormat::Format11_11_10UNorm) fail(prefix + " to an emulated depth-compare or 11:11:10 sample, which is not implemented"); + for (const auto& pair : info.sampledPairs) { + if (pair.image != image) continue; + const auto& sampler = info.samplers.at(pair.sampler); + const auto name = "S# " + std::to_string(pair.sampler); + if (sampler.table != NoTable) fail(prefix + " and is sampled through " + name + " from a table, whose LOD range is unknown, which is not implemented"); + if ((sampler.uses & SamplerUseQueryLod) != 0u) fail(prefix + " and " + name + " is used by image_get_lod, which is not implemented"); + if ((sampler.uses & SamplerUseGather) != 0u && clamp >= 256u) fail(prefix + " and " + name + " is used by a gather, which reads below that level, which is not implemented"); + const auto& words = snapshot.samplers.at(pair.sampler).dwords; + const bool unnormalized = (words[0] & ForceUnnormalizedBit) != 0u; + const bool mipmapped = !unnormalized && ((words[2] >> 26u) & 3u) != 0u; + const auto maxLod = mipmapped ? (words[1] >> 12u) & 0xfffu : 0u; + if (!unnormalized && maxLod == 0u) { + if (((words[2] >> 20u) & 3u) != ((words[2] >> 22u) & 3u)) fail(prefix + " and " + name + ", which holds the LOD at 0, has different magnification and minification filters, between which the clamp moves the choice, which is not implemented"); + for (const auto& other : info.sampledPairs) { + if (other.sampler != pair.sampler || other.image == image) continue; + const auto otherName = "image " + std::to_string(other.image); + if (info.images.at(other.image).table != NoTable) fail(prefix + " and " + name + ", which holds the LOD at 0, also samples " + otherName + " from a table, which is not implemented"); + const auto [otherClamp, otherLevels] = RelativeMinLod(snapshot.images.at(other.image).dwords); + if (otherClamp != clamp && otherLevels != 0u) fail(prefix + " and " + name + ", which holds the LOD at 0, also samples " + otherName + " with another minimum LOD, which is not implemented"); + } + continue; + } + if ((sampler.uses & (SamplerUseExplicitLod | SamplerUseImplicitLod | SamplerUseGradient)) == 0u) continue; + if (maxLod < clamp) fail(prefix + " above the maximum LOD of " + std::to_string(maxLod) + "/256 of " + name + (mipmapped ? "" : unnormalized ? " (unnormalized)" : " (no mip filter)") + ", which is not implemented"); + if ((((words[2] >> 20u) ^ (words[2] >> 22u)) & 1u) != 0u) fail(prefix + " and " + name + " has different magnification and minification filters, between which the clamp moves the choice, which is not implemented"); + const auto bias = static_cast(((words[2] & 0x3fffu) ^ 0x2000u) - 0x2000u); + if (bias != 0 && (sampler.uses & SamplerUseExplicitLod) != 0u) fail(prefix + " to an explicit LOD that " + name + " biases by " + std::to_string(bias) + "/256, which is not implemented"); + } +} + struct UnnormalizedProof { std::vector samplers; std::vector images; @@ -347,7 +389,7 @@ BufferSubstitute BufferSpecializationFields(const DescriptorValue& descriptor, s return {descriptor.dwords[1] & 0xffff0000u, descriptor.dwords[3], present ? descriptor.dwords[0] & 3u : 0u, present}; } -DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& layout, const ShaderInfo& info, IrShaderStage stage, const ResourceSnapshot& snapshot, std::span exportMappings, bool registrationModules, bool fragmentDescriptorSet, std::span substitutes) const { +DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& layout, const ShaderInfo& info, IrShaderStage stage, const ResourceSnapshot& snapshot, std::span exportMappings, bool registrationModules, bool fragmentDescriptorSet, std::span substitutes, bool shaderImageMinLod) const { if (stage == IrShaderStage::Pixel && exportMappings.size() != 8u) fail("fragment runtime export mappings are missing"); if (info.images.size() > ShaderInfo::MaxImages) fail("runtime image count exceeds the static capacity"); DescriptorBindingPlan plan; @@ -407,6 +449,12 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l const auto first = PipelineSpecialization::ImageBase + index * PipelineSpecialization::ImageWords; plan.specialization.push_back({first, imageModes[index]}); for (std::uint32_t component = 0; component < 4u; ++component) plan.specialization.push_back({first + 1u + component, (snapshot.images.at(index).dwords[3] >> (component * 3u)) & 7u}); + if (shaderImageMinLod) { + const auto& words = snapshot.images.at(index).dwords; + const auto clamp = RelativeMinLod(words)[0]; + if (clamp != 0u) RequireShaderMinLod(info, snapshot, index, modes.at(imageModes[index]), clamp); + plan.specialization.push_back({PipelineSpecialization::MinLodBase + index, std::bit_cast(static_cast(clamp) / 256.0f)}); + } const auto& image = info.images[index]; if (image.resourceClass != ImageResourceClass::Storage || !image.written || image.atomic) continue; const auto code = (snapshot.images.at(index).dwords[1] >> 20u) & 0x1ffu; @@ -566,12 +614,12 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l plan.runtimeState.assign(layout.runtimeStateDwords, 0u); for (std::uint32_t target = 0; target < exportMappings.size(); ++target) plan.runtimeState.at(RuntimeAbi::StateExportMappings + target / 4u) |= static_cast(exportMappings[target]) << (target % 4u * 8u); } - std::vector samplerMasks(info.samplers.size()); + std::vector samplerMasks(info.samplers.size()); for (const auto& binding : plan.bindings) { if (binding.descriptor.role != DescriptorRole::GuestSamplers) continue; - if (binding.resources.size() > ShaderInfo::MaxSamplers) fail("prepared sampler mask capacity exceeded"); + if (binding.resources.size() > std::numeric_limits::digits) fail("prepared sampler mask capacity exceeded"); for (std::uint32_t element = 0; element < binding.resources.size(); ++element) { - if (binding.inactive.empty() || !binding.inactive[element]) samplerMasks.at(binding.resources[element]) |= 1u << element; + if (binding.inactive.empty() || !binding.inactive[element]) samplerMasks.at(binding.resources[element]) |= std::uint64_t{1} << element; } } for (auto& binding : plan.bindings) { @@ -666,9 +714,9 @@ void DescriptorBindingBuilder::Populate(BindingAllocationResult& allocation, con physical.imageWritten.push_back(ImageBindingResourceClass(element.kind) == ImageResourceClass::Storage); physical.imageAtomic.push_back(element.atomic); physical.imageAtomic64.push_back(element.atomic64); - std::uint32_t samplers = 0; + std::uint64_t samplers = 0; for (std::uint32_t sampler = 0; sampler < entry.tableSamplerMasks.size(); ++sampler) { - if ((element.samplers & (1u << sampler)) != 0u) samplers |= entry.tableSamplerMasks[sampler]; + if (((element.samplers >> sampler) & 1u) != 0u) samplers |= entry.tableSamplerMasks[sampler]; } physical.imageSamplers.push_back(samplers); } diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index 7d3a8815c0..533f170f65 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -34,6 +34,8 @@ namespace ShaderRecompiler { +static_assert(ShaderInfo::MaxSamplers <= std::numeric_limits::digits); + namespace { using ImageTableAbi::PoisonReason; @@ -59,6 +61,7 @@ struct DecodedImage { bool depthUnorm16 = false; IrBufferFormat packedFormat = IrBufferFormat::Invalid; bool srgbDecode = false; + bool layerRows = false; }; ShaderBufferResource decodeBufferDescriptor(const DescriptorValue& value) { @@ -76,13 +79,20 @@ bool nullImageDescriptor(const DescriptorValue& descriptor) { return descriptor.dwords[0] == 0u && (descriptor.dwords[1] & 0xffu) == 0u; } +bool imageFormatUnset(const DescriptorValue& descriptor) { + const auto format = (descriptor.dwords[1] >> 20u) & 0x1ffu; + return format == 0u || format == 0x1ffu; +} + ImageType rawImageType(const DescriptorValue& descriptor) { return static_cast((descriptor.dwords[3] >> 28u) & 0xfu); } bool nullImageDescriptor(const ImageResource& image, const DescriptorValue& descriptor) { if (nullImageDescriptor(descriptor)) return true; - return static_cast(rawImageType(descriptor)) <= 1u && image.table == NoTable && (image.read || image.written) && !image.atomic; + const auto type = rawImageType(descriptor); + if (image.table == NoTable && image.resourceClass == ImageResourceClass::Sampled && image.read && !image.written && !image.atomic && type >= ImageType::Color1D && type <= ImageType::Color2DMsaa && imageFormatUnset(descriptor)) return true; + return static_cast(type) <= 1u && image.table == NoTable && (image.read || image.written) && !image.atomic; } IrBufferFormat rawImageFormat(const DescriptorValue& descriptor) { @@ -199,6 +209,31 @@ PoisonReason invalidImageReason(const DescriptorValue& descriptor) { return PoisonReason::Dimension; } +bool layerRowsCandidate(const ImageResource& image) { + return image.dimension == RdnaImageDimension::Dim1DArray && image.layerRowsCompatible && image.resourceClass != ImageResourceClass::Storage && image.table == NoTable && !image.depthCompare; +} + +bool registeredSampledArrayCandidate(const ImageResource& image) { + return image.dimension == RdnaImageDimension::Dim1DArray && image.layerRowsCompatible && image.resourceClass == ImageResourceClass::Sampled && !image.depthCompare && !image.atomic && !image.written; +} + +void normalizeRegisteredImageView(const ImageResource& image, const SrtRuntime& runtime, std::uint32_t maxArrayLayers, std::uint32_t resource, std::uint32_t record, DescriptorValue& descriptor, std::vector& views) { + if (runtime.normalizeImageView == nullptr || !registeredSampledArrayCandidate(image) || descriptor.dwordCount != 8u || nullImageDescriptor(image, descriptor)) return; + const auto provenance = descriptor; + auto identity = SrtImageViewIdentity{}; + auto effective = descriptor.dwords; + const bool normalized = runtime.normalizeImageView(runtime.userContext, maxArrayLayers, effective, identity); + descriptor.dwords = effective; + static_cast(normalized); + views.push_back({resource, record, provenance, descriptor, identity.generation, identity.address, identity.rangeAddress, identity.rangeBytes, identity.allocationAddress, identity.allocationBytes, identity.backingSerial, identity.hasRange, identity.readable, identity.writable, identity.gpu}); +} + +bool viewsPastLayerLimit(const DescriptorValue& descriptor, std::uint32_t limit) { + const auto lastArray = descriptor.dwords[4] & 0x1fffu; + const auto baseArray = (descriptor.dwords[4] >> 16u) & 0x1fffu; + return limit != 0u && baseArray <= lastArray && lastArray - baseArray >= limit; +} + ImageDecode inspectImageDescriptor(const DescriptorValue& descriptor, const ImageResource& base, std::uint32_t srgbDecodeFormats) { ImageDecode result; auto& decoded = result.decoded; @@ -230,6 +265,10 @@ ImageDecode inspectImageDescriptor(const DescriptorValue& descriptor, const Imag 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"); } + if (decoded.dimension == RdnaImageDimension::Dim1DArray && layerRowsCandidate(base) && viewsPastLayerLimit(descriptor, base.maxArrayLayers)) { + decoded.dimension = RdnaImageDimension::Dim2D; + decoded.layerRows = true; + } decoded.cube = descriptorIsCube(descriptor); const auto format = rawImageFormat(descriptor); if (decoded.dimension == RdnaImageDimension::Dim2DMsaa && rawImageType(descriptor) == ImageType::Color2DMsaa && multisamplePlaneRead(base) && !IsFmaskTextureFormat(format) && !IsDepthBitsTexture(descriptor.dwords[1], descriptor.dwords[3])) decoded.dimension = RdnaImageDimension::Dim2D; @@ -1371,6 +1410,15 @@ private: out.codes = records.contents.codes; out.words = records.contents.words; out.readRanges = records.contents.readRanges; + for (std::uint32_t resource = 0; resource < plan.info.images.size(); ++resource) { + const auto& image = plan.info.images[resource]; + if (image.table != table || !registeredSampledArrayCandidate(image) || runtime.normalizeImageView == nullptr) continue; + auto effectiveWords = records.contents.words.Copy(); + std::vector identities; + identities.reserve(effectiveWords.size()); + for (std::uint32_t record = 0; record < effectiveWords.size(); ++record) normalizeRegisteredImageView(image, runtime, plan.maxImageArrayLayers, resource, record, effectiveWords[record], identities); + out.resourceViews.push_back({resource, SharedValues(std::move(effectiveWords)), std::move(identities)}); + } auto& counters = tableCounters(); if (records.fault != 0u) counters.poison[records.fault & ~ImageTableAbi::PoisonFlag].fetch_add(1, std::memory_order_relaxed); if (records.contents.unmapped != 0u) counters.poison[static_cast(PoisonReason::Unmapped)].fetch_add(records.contents.unmapped, std::memory_order_relaxed); @@ -1396,6 +1444,7 @@ private: void materializeSnapshot(const IrResourcePlan& plan, const SrtRuntime& runtime, SrtWalker& walker, ResourceSnapshot& snapshot, std::vector& activeSources) { snapshot = ResourceSnapshot{}; + snapshot.imageAllocationGeneration = runtime.imageViewGeneration != nullptr ? runtime.imageViewGeneration(runtime.userContext) : 0u; if (plan.uniformFill.fill.kind != UniformFillKind::None) { const auto words = plan.uniformFill.fill.words; if (words == 0u || words > plan.uniformFill.values.size()) { @@ -1472,6 +1521,8 @@ void materializeSnapshot(const IrResourcePlan& plan, const SrtRuntime& runtime, descriptor = DescriptorValue{}; descriptor.dwordCount = 8u; } + const auto maxArrayLayers = image.maxArrayLayers != 0u ? image.maxArrayLayers : plan.maxImageArrayLayers; + normalizeRegisteredImageView(image, runtime, maxArrayLayers, i, 0u, descriptor, snapshot.registeredImages); snapshot.images[i] = descriptor; } @@ -1664,6 +1715,11 @@ void materializeTables(const IrResourcePlan& plan, ResourceSnapshot& snapshot) { if (!image.convertedSamples && !image.srgbDecodeCompatible && convertedSampleCompatible(image) && descriptor.dwordCount == 8u && !nullImageDescriptor(image, descriptor)) { if (rawImageFormat(descriptor) == IrBufferFormat::Format11_11_10UNorm) throw ConvertedSampleRequired("samples a converted image through a shader prepared without converted-sample modes"); } + if (image.maxArrayLayers == 0u && plan.maxImageArrayLayers != 0u && layerRowsCandidate(image) && descriptor.dwordCount == 8u && !nullImageDescriptor(image, descriptor)) { + auto rows = image; + rows.maxArrayLayers = plan.maxImageArrayLayers; + if (inspectImageDescriptor(descriptor, rows, image.srgbDecodeFormats).decoded.layerRows) throw LayerRowSamplesRequired("samples a 1D array past maxImageArrayLayers through a shader prepared without layer-row modes"); + } static_cast(ResourceMaterializer::RuntimeImageMode(image, descriptor, plan.info.runtimeImageModes.at(i))); } } @@ -1776,14 +1832,17 @@ bool sameColumns(const std::vector& left, const std::v for (std::size_t index = 0; index < left.size(); index++) { const auto& a = left[index]; const auto& b = right[index]; - if (a.base != b.base || a.size != b.size || a.records != b.records || a.keys != b.keys || a.fault != b.fault || a.outside != b.outside || !a.codes.Shares(b.codes) || !a.words.Shares(b.words)) return false; + if (a.base != b.base || a.size != b.size || a.records != b.records || a.keys != b.keys || a.fault != b.fault || a.outside != b.outside || !a.codes.Shares(b.codes) || !a.words.Shares(b.words) || a.resourceViews != b.resourceViews) return false; } return true; } std::size_t resolutionFootprint(const ResolvedImageTables& resolved, const std::vector& tables) { std::size_t footprint = resolved.map.size() + (resolved.elements.size() * sizeof(ImageTableElement) + resolved.samplers.size() * sizeof(ImageTableSamplerElement) + resolved.poison.size() * sizeof(ImageTablePoisonRecord)) / sizeof(std::uint32_t); - for (const auto& table : tables) footprint += 16u + table.codes.size() + table.words.size() * 9u; + for (const auto& table : tables) { + footprint += 16u + table.codes.size() + table.words.size() * 9u; + for (const auto& view : table.resourceViews) footprint += view.words.size() * 9u + view.identities.size() * (sizeof(RegisteredImageView) / sizeof(std::uint32_t)); + } return footprint; } @@ -1831,9 +1890,9 @@ ResolvedImageTables ResourceMaterializer::ResolveImageTables(const ShaderInfo& i }; HashScratchIndex elementIndex{threadScratch()}; HashScratchIndex samplerIndex{threadScratch()}; - std::vector directSamplers(info.images.size(), 0u); + std::vector directSamplers(info.images.size(), 0u); for (const auto& pair : info.sampledPairs) { - if (info.samplers.at(pair.sampler).table == NoTable) directSamplers.at(pair.image) |= 1u << pair.sampler; + if (info.samplers.at(pair.sampler).table == NoTable) directSamplers.at(pair.image) |= std::uint64_t{1} << pair.sampler; } const auto reducingSampler = reducingSamplers(info, snapshot); const auto imageRecord = [&](std::uint32_t resource, const DescriptorValue& words) { @@ -1950,7 +2009,9 @@ ResolvedImageTables ResourceMaterializer::ResolveImageTables(const ShaderInfo& i if (const auto known = records.Find(code)) { mapped = *known; } else { - const auto& words = column.words.at(code); + const auto& rawWords = column.words.at(code); + const auto view = sampler ? column.resourceViews.end() : std::find_if(column.resourceViews.begin(), column.resourceViews.end(), [resource](const auto& candidate) { return candidate.resource == resource; }); + const auto& words = !sampler && view != column.resourceViews.end() ? view->words.at(code) : rawWords; if (sampler || (words.availableDwordMask & (1u << 3u)) == 0u || !nullImageDescriptor(info.images[resource], words)) mapped = sampler ? samplerRecord(resource, words) : imageRecord(resource, words); records.Set(code, mapped); } @@ -2107,6 +2168,12 @@ std::vector ResourceMaterializer::RuntimeImageModes(const ImageRe cube.cube = true; modes.push_back(cube); } + if (layerRowsCandidate(image) && image.maxArrayLayers != 0u && !depth && conversion == IrBufferFormat::Invalid && packed == IrBufferFormat::Invalid) { + auto rows = mode; + rows.dimension = RdnaImageDimension::Dim2D; + rows.layerRows = true; + modes.push_back(rows); + } if (image.dimension == RdnaImageDimension::Dim1DArray || image.dimension == RdnaImageDimension::Dim2DArray || image.dimension == RdnaImageDimension::Dim2DMsaaArray) { auto plain = mode; plain.dimension = image.dimension == RdnaImageDimension::Dim1DArray ? RdnaImageDimension::Dim1D : image.dimension == RdnaImageDimension::Dim2DArray ? RdnaImageDimension::Dim2D : RdnaImageDimension::Dim2DMsaa; @@ -2228,6 +2295,7 @@ bool ResourceMaterializer::NullImageDescriptor(const ImageResource& image, const const char* ResourceMaterializer::UnnormalizedSampleMismatch(const ImageResource& image) { if (image.table != NoTable) return "samples an image selected at run time"; if (image.constantSwizzle) return nullptr; + if (image.layerRows) return "samples a 1D array laid out as rows of a 2D image"; if ((image.dimension != RdnaImageDimension::Dim1D && image.dimension != RdnaImageDimension::Dim2D) || image.cube) return "samples a 1D-array, 2D-array, 3D, cube or multisampled image"; if (image.depthCompare) return "is used with depth comparison"; if (image.conversionFormat != IrBufferFormat::Invalid || image.packed) return "samples an image that needs a format conversion or packed access"; @@ -2280,7 +2348,7 @@ RuntimeImageModeMatch ResourceMaterializer::TryRuntimeImageMode(const ImageResou const auto& mode = modes[index]; if (mode.constantSwizzle) continue; if (((mode.emulatedCompare & EmulatedCompare::Enabled) != 0u) != emulated) continue; - if (mode.numericClass == decoded.numericClass && mode.dimension == decoded.dimension && mode.conversionFormat == decoded.conversionFormat && mode.packedFormat == decoded.packedFormat && mode.cube == decoded.cube && mode.depthBits == decoded.depthBits && mode.depthUnorm16 == decoded.depthUnorm16 && mode.srgbDecode == decoded.srgbDecode) { + if (mode.numericClass == decoded.numericClass && mode.dimension == decoded.dimension && mode.conversionFormat == decoded.conversionFormat && mode.packedFormat == decoded.packedFormat && mode.cube == decoded.cube && mode.depthBits == decoded.depthBits && mode.depthUnorm16 == decoded.depthUnorm16 && mode.srgbDecode == decoded.srgbDecode && mode.layerRows == decoded.layerRows) { result.mode = index; return result; } @@ -2572,6 +2640,7 @@ IrResourcePlan ResourceMaterializer::ExtractPlan(const IrProgram& program) const plan.userDataBase = source.userDataBase; plan.userDataCount = source.userDataCount; plan.srgbDecodeFormats = source.srgbDecodeFormats; + plan.maxImageArrayLayers = source.maxImageArrayLayers; plan.memoryInfo = source.memoryInfo; plan.descriptorSources = source.descriptorSources; plan.controlFlow = source.controlFlow; @@ -2642,15 +2711,21 @@ void ResourceMaterializer::Materialize(const IrResourcePlan& program, const SrtR for (std::uint32_t attempt = 0; attempt < 3u; ++attempt) { const SrtForceSyncScope forceSync(runtime, attempt == 2u); const auto beforeSnapshot = runtime.descriptorScope != nullptr ? runtime.descriptorScope(runtime.userContext, 0u) : 0u; + const auto beforeImageGeneration = runtime.imageViewGeneration != nullptr ? runtime.imageViewGeneration(runtime.userContext) : 0u; activeSources.clear(); try { materializeSnapshot(plan, runtime, walker, nextSnapshot, activeSources); const auto afterSnapshot = runtime.descriptorScope != nullptr ? runtime.descriptorScope(runtime.userContext, 0u) : 0u; TableSnapshotter(plan, runtime, walker, activeSources, nextSnapshot.tables).Run(); const auto afterTables = runtime.descriptorScope != nullptr ? runtime.descriptorScope(runtime.userContext, 0u) : 0u; - if (runtime.descriptorScope != nullptr && (afterTables != afterSnapshot || (plan.uniformFill.fill.kind != UniformFillKind::None && afterSnapshot != beforeSnapshot))) { + const auto afterImageGeneration = runtime.imageViewGeneration != nullptr ? runtime.imageViewGeneration(runtime.userContext) : 0u; + if ((runtime.descriptorScope != nullptr && (afterTables != afterSnapshot || (plan.uniformFill.fill.kind != UniformFillKind::None && afterSnapshot != beforeSnapshot))) || afterImageGeneration != beforeImageGeneration) { continue; } + for (const auto& table : nextSnapshot.tables.tables) { + for (const auto& view : table.resourceViews) nextSnapshot.registeredImages.insert(nextSnapshot.registeredImages.end(), view.identities.begin(), view.identities.end()); + } + nextSnapshot.imageAllocationGeneration = nextSnapshot.registeredImages.empty() ? 0u : afterImageGeneration; const auto started = MaterializeProfiled() ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; materializeForcedDegamma(plan, nextSnapshot); materializeTables(plan, nextSnapshot); @@ -2661,8 +2736,13 @@ void ResourceMaterializer::Materialize(const IrResourcePlan& program, const SrtR } catch (const ConvertedSampleRequired&) { throw; } catch (const std::runtime_error&) { - if (runtime.descriptorScope == nullptr || attempt == 2u) throw; - if (runtime.descriptorScope(runtime.userContext, 0u) == beforeSnapshot) attempt = 1u; + if (attempt == 2u) throw; + const auto afterImageGeneration = runtime.imageViewGeneration != nullptr ? runtime.imageViewGeneration(runtime.userContext) : 0u; + if (runtime.descriptorScope == nullptr) { + if (runtime.imageViewGeneration == nullptr || afterImageGeneration == beforeImageGeneration) throw; + } else if (runtime.descriptorScope(runtime.userContext, 0u) == beforeSnapshot && (runtime.imageViewGeneration == nullptr || afterImageGeneration == beforeImageGeneration)) { + attempt = 1u; + } } } if (!completed) throw std::runtime_error("runtime resource snapshot did not stabilize"); diff --git a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp index 7138601d41..a7e26c97d0 100644 --- a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp @@ -1,4 +1,5 @@ #include "Optimization/ResourceTracker.hpp" +#include "Optimization/DeadCodeEliminator.hpp" #include "Optimization/SrtWalker.hpp" #include "Optimization/ResourceMaterializer.hpp" #include "SpirvBackend/SpirvBufferFormat.hpp" @@ -121,6 +122,9 @@ public: fail("SRT plan is not ready"); } SplitDescriptorPhis(); + if (m_foldedWeb) { + DeadCodeEliminator{}.Eliminate(m_program); + } PlanTableColumns(); std::unordered_map blockIndices; for (const IrBlock* block : m_program.BlockOrder()) { @@ -1449,7 +1453,7 @@ private: inst.Parent()->RemoveInstruction(&inst); } - static bool HoldsControlValue(IrValue* value, std::uint32_t depth, std::vector& seen) { + static bool HoldsFoldableValue(IrValue* value, std::uint32_t depth, std::vector& seen) { value = value->Resolve(); if (value->HasImmediate()) { return false; @@ -1459,7 +1463,12 @@ private: case IrOpcode::GetExec: case IrOpcode::GetExecLo: case IrOpcode::GetExecHi: - case IrOpcode::Ballot: return true; + case IrOpcode::Ballot: + case IrOpcode::LaneId: + case IrOpcode::ImageRead: + case IrOpcode::ImageSampleExplicitLod: + case IrOpcode::ImageSampleImplicitLod: + case IrOpcode::ImageSampleRaw: return true; default: break; } if (depth == 0u) { @@ -1474,16 +1483,16 @@ private: return false; } for (std::size_t index = 0; index < value->ArgumentCount(); index++) { - if (HoldsControlValue(value->Argument(index), depth - 1u, seen)) { + if (HoldsFoldableValue(value->Argument(index), depth - 1u, seen)) { return true; } } return false; } - static bool HoldsControlValue(IrValue* value) { + static bool HoldsFoldableValue(IrValue* value) { std::vector seen; - return HoldsControlValue(value, phiSearchDepth, seen); + return HoldsFoldableValue(value, phiSearchDepth, seen); } struct DescriptorWeb { @@ -1491,7 +1500,7 @@ private: static constexpr std::size_t maxArms = 64u; static constexpr std::uint32_t foldedArm = 0xffffffffu; std::string failure; - bool foldedControlValue = false; + bool foldedValue = false; std::vector> arms; std::map, IrValue*> selectors; std::vector> phis; @@ -1562,17 +1571,35 @@ private: if (const auto known = web.selectors.find(tuple); known != web.selectors.end()) { return known->second; } + if (web.order.empty()) { + const auto& order = m_program.BlockOrder(); + for (std::size_t index = 0; index < order.size(); index++) { + web.order.emplace(order[index], index); + } + } const auto phi = std::ranges::find_if(tuple, [](const IrValue* value) { return value->IsPhi(); }); - const auto lane = std::ranges::find_if(tuple, [](const IrValue* value) { return value->Opcode() == IrOpcode::SelectU32 || value->Opcode() == IrOpcode::ReadFirstLane; }); - if (phi == tuple.end() && lane != tuple.end()) { - return LaneSelector(tuple, **lane, web, position); + IrValue* lane = nullptr; + std::size_t laneOrder = 0; + for (IrValue* value : tuple) { + if (value->Opcode() != IrOpcode::SelectU32 && value->Opcode() != IrOpcode::ReadFirstLane) { + continue; + } + const auto found = web.order.find(value->Parent()); + const std::size_t index = found == web.order.end() ? 0u : found->second; + if (lane == nullptr || index > laneOrder) { + lane = value; + laneOrder = index; + } + } + if (phi == tuple.end() && lane != nullptr) { + return LaneSelector(tuple, *lane, web, position); } if (phi == tuple.end()) { - if (std::ranges::any_of(tuple, [](IrValue* value) { return HoldsControlValue(value); })) { + if (std::ranges::any_of(tuple, [](IrValue* value) { return HoldsFoldableValue(value); })) { if (StrictDescriptorWebs()) { - return GiveUp(web, "a descriptor dword depends on the program counter, EXEC or a lane mask"); + return GiveUp(web, "a descriptor dword depends on the program counter, EXEC, a lane mask, the lane index or texels"); } - web.foldedControlValue = true; + web.foldedValue = true; return &m_builder.Constant(DescriptorWeb::foldedArm); } for (IrValue* value : tuple) { @@ -1592,12 +1619,6 @@ private: web.arms.push_back(std::move(tuple)); return &m_builder.Constant(static_cast(web.arms.size() - 1u)); } - if (web.order.empty()) { - const auto& order = m_program.BlockOrder(); - for (std::size_t index = 0; index < order.size(); index++) { - web.order.emplace(order[index], index); - } - } IrBlock* block = nullptr; std::size_t latest = 0; for (const IrValue* value : tuple) { @@ -1721,7 +1742,7 @@ private: } DescriptorWeb web; IrValue* selector = WebSelector(tuple, web, inst); - const bool select = selector != nullptr && !web.arms.empty() && (web.arms.size() > 1u || web.foldedControlValue); + const bool select = selector != nullptr && !web.arms.empty() && (web.arms.size() > 1u || web.foldedValue); if (select) { for (const auto& [block, phi] : web.phis) { block->InsertInstructionBefore(nullptr, phi); @@ -1752,7 +1773,7 @@ private: } } if (selector == nullptr || web.arms.empty()) { - const std::string reason = !web.failure.empty() ? web.failure : web.foldedControlValue ? std::string("every path's descriptor depends on the program counter, EXEC or a lane mask") : std::string("no descriptor was found on any path"); + const std::string reason = !web.failure.empty() ? web.failure : web.foldedValue ? std::string("every path's descriptor depends on the program counter, EXEC, a lane mask, the lane index or texels") : std::string("no descriptor was found on any path"); for (std::uint32_t slot = 0; slot < handleCount; slot++) { if (split[slot]) { m_splitFailures.try_emplace(handles[slot], reason); @@ -1761,7 +1782,8 @@ private: return; } - if (web.foldedControlValue) { + if (web.foldedValue) { + m_foldedWeb = true; const std::array compare {selector, &m_builder.Constant(DescriptorWeb::foldedArm)}; IrValue* folded = EmitBefore(inst, IrOpcode::IEqual32, IrType::Bool, compare); EmitDescriptorFault(inst, *folded, m_builder.Constant(0u), m_builder.Constant(0u)); @@ -2300,6 +2322,8 @@ private: const bool flatLineAccess = image.dimension == RdnaImageDimension::Dim2DArray ? flatLineLevelZero || flatLineExplicitLod : flatLineLoad || flatLineSample || flatLineExplicitLod; image.flatLineCompatible = image.flatLineCompatible && flatLineAccess && !memory.imagePacked && memory.imageByElements == 0u; image.flatVolumeLineCompatible = image.flatVolumeLineCompatible && (flatLineLoad || flatLineSample) && !memory.imagePacked && memory.imageByElements == 0u; + const bool layerRowsSample = op == IrOpcode::ImageSampleRaw && (memory.imageSampleFlags == RdnaImageSampleFlagLevelZero || memory.imageSampleFlags == RdnaImageSampleFlagLod); + image.layerRowsCompatible = image.layerRowsCompatible && layerRowsSample && !memory.imageR128 && !memory.imagePacked && memory.imageByElements == 0u; image.dimensionQueryOnly = image.dimensionQueryOnly && op == IrOpcode::ImageQueryDimensions; image.sampledOnly = image.sampledOnly && ImageOpcodeInfoOf(op).needsSampler; if ((memory.imageSampleFlags & RdnaImageSampleFlagCompare) != 0u) { @@ -2522,6 +2546,7 @@ private: }; std::vector m_edgeSelectors; std::unordered_map m_splitFailures; + bool m_foldedWeb = false; }; } diff --git a/core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp b/core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp index c31352d9ea..c7ab511132 100644 --- a/core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp +++ b/core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp @@ -104,7 +104,12 @@ bool evaluateRuntimeSources(const IrResourcePlan& program, std::span #include #include +#include #include #include +#include #include #include +#include #include #include #include @@ -399,11 +402,13 @@ bool BasicEvaluator::MayEvaluate(const IrResourcePlan& program, IrValue* template bool BasicEvaluator::MayEvaluate(const IrResourcePlan& program, IrValue* value, const IrValue* activeMask, std::unordered_set& firstLanes) { - std::unordered_map known; + std::array scratch; + std::pmr::monotonic_buffer_resource arena(scratch.data(), scratch.size()); + std::pmr::unordered_map known(&arena); const auto takesActiveArm = [activeMask](const IrValue* current) { return activeMask != nullptr && IsRuntimeSelect(current->Opcode()) && current->ArgumentCount() == 3 && ActiveMaskImplies(activeMask, current->Argument(0)); }; - std::vector required; + std::pmr::vector required(&arena); const auto requirements = [&program, &takesActiveArm, &required](IrValue* current) { required.clear(); if (takesActiveArm(current)) { @@ -443,8 +448,9 @@ bool BasicEvaluator::MayEvaluate(const IrResourcePlan& program, IrValue* required.push_back(argument != nullptr ? argument->Resolve() : nullptr); } }; - std::unordered_set expanding; - std::vector> pending{{value->Resolve(), false}}; + std::pmr::unordered_set expanding(&arena); + std::pmr::vector> pending(&arena); + pending.emplace_back(value->Resolve(), false); while (!pending.empty()) { const auto [current, expanded] = pending.back(); pending.pop_back(); diff --git a/core/shader/recompiler/Optimization/src/SrtWalker/SrtTape.cpp b/core/shader/recompiler/Optimization/src/SrtWalker/SrtTape.cpp index 80df0bf6be..def355c32b 100644 --- a/core/shader/recompiler/Optimization/src/SrtWalker/SrtTape.cpp +++ b/core/shader/recompiler/Optimization/src/SrtWalker/SrtTape.cpp @@ -640,7 +640,11 @@ bool SrtTape::Run(const IrResourcePlan& plan, const SrtRuntime& runtime, std::ve for (const auto image : block.images) active[imageBase + image] = 1u; for (const auto slot : block.flatSlots) activeFlat[slot] = 1u; std::uint64_t condition = 0; - const bool cleanEvaluable = block.condition != nullptr && runtime.readSpecializationMemory != nullptr && conditionRoots[index] != NoNode && evaluate(frame, conditionRoots[index], condition); + bool cleanEvaluable = false; + { + const SrtBranchScope branch; + cleanEvaluable = block.condition != nullptr && runtime.readSpecializationMemory != nullptr && conditionRoots[index] != NoNode && evaluate(frame, conditionRoots[index], condition); + } if (frame.nullRoot) return refuse(); if (cleanEvaluable) { if (block.successors.size() < 2) return refuse(); diff --git a/core/shader/recompiler/Optimization/src/SrtWalker/SrtWalker.cpp b/core/shader/recompiler/Optimization/src/SrtWalker/SrtWalker.cpp index 8cba8936c8..ba5d00c582 100644 --- a/core/shader/recompiler/Optimization/src/SrtWalker/SrtWalker.cpp +++ b/core/shader/recompiler/Optimization/src/SrtWalker/SrtWalker.cpp @@ -8,6 +8,11 @@ namespace ShaderRecompiler { +bool& SrtEvaluatingBranch() { + thread_local bool evaluating = false; + return evaluating; +} + void SrtWalker::BuildPlan(IrProgram& program) const { if (program.Resources().resourceTrackingComplete) { throw std::runtime_error("shader SRT planning failed: cannot rebuild SRT after resource tracking"); diff --git a/core/shader/recompiler/PipelineSpecialization.hpp b/core/shader/recompiler/PipelineSpecialization.hpp index cb6c36d98f..efbb4dfc45 100644 --- a/core/shader/recompiler/PipelineSpecialization.hpp +++ b/core/shader/recompiler/PipelineSpecialization.hpp @@ -49,6 +49,7 @@ inline constexpr std::uint32_t FoldOffsetBase = 57344u; inline constexpr std::uint32_t UnnormalizedBaseLevelBase = 57408u; inline constexpr std::uint32_t FmaskFormatBase = 57472u; inline constexpr std::uint32_t MirrorUnnormalizedBase = 57536u; +inline constexpr std::uint32_t MinLodBase = 57600u; inline constexpr std::uint32_t DescriptorIndex(std::uint32_t binding, std::uint32_t element) { return DescriptorIndexBase + binding * DescriptorIndexStride + element; diff --git a/core/shader/recompiler/RdnaDecoder/src/GcnInstructionDecoder.cpp b/core/shader/recompiler/RdnaDecoder/src/GcnInstructionDecoder.cpp index e5ccedb328..b80856cbc8 100644 --- a/core/shader/recompiler/RdnaDecoder/src/GcnInstructionDecoder.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/GcnInstructionDecoder.cpp @@ -46,6 +46,7 @@ struct Encoded { std::array words{}; std::uint32_t rdnaWords = 0; std::uint32_t gcnWords = 0; + std::optional compareDestination; }; std::uint32_t literalAt(std::span code, std::uint32_t index, std::uint32_t programCounter) { @@ -176,11 +177,12 @@ Encoded encodeVop3(std::span code, std::uint32_t index, std 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)); + auto out = encodeVop3Words(word0, code[index + 1], *mapped, gcnVop3bCarryOut(opcode)); + if (compareWritesExec(opcode)) out.compareDestination = word0 & 0xffu; + return out; } 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) { @@ -241,7 +243,6 @@ Encoded encodeVop1(std::span code, std::uint32_t index, std 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; @@ -251,6 +252,7 @@ Encoded encodeVopc(std::span code, std::uint32_t index, std out.words[1] = literalAt(code, index + 1, programCounter); out.rdnaWords = out.gcnWords = 2; } + if (compareWritesExec(opcode)) out.compareDestination = VccSdst; return out; } @@ -343,6 +345,7 @@ RdnaInstruction DecodeGcnInstruction(std::uint32_t programCounter, std::span(encoded.words.data(), encoded.rdnaWords), 0); instruction.wordCount = encoded.gcnWords; + if (encoded.compareDestination) instruction.destination2 = DecodeRdnaScalarDestination(*encoded.compareDestination, programCounter); return instruction; } diff --git a/core/shader/recompiler/RdnaDecoder/src/GcnOpcodeTables.hpp b/core/shader/recompiler/RdnaDecoder/src/GcnOpcodeTables.hpp index cbe30ec329..03fdcc596a 100644 --- a/core/shader/recompiler/RdnaDecoder/src/GcnOpcodeTables.hpp +++ b/core/shader/recompiler/RdnaDecoder/src/GcnOpcodeTables.hpp @@ -94,15 +94,20 @@ inline constexpr std::array, 216> Vopc{{ {0xf0, 0xf0}, {0xf1, 0xf1}, {0xf2, 0xf2}, {0xf3, 0xf3}, {0xf4, 0xf4}, {0xf5, 0xf5}, {0xf6, 0xf6}, {0xf7, 0xf7}, }}; -inline constexpr std::array, 324> Vop3{{ +inline constexpr std::array, 360> 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}, + {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}, {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}, + {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}, {0x80, 0x80}, {0x81, 0x81}, {0x82, 0x82}, {0x83, 0x83}, + {0x3b, 0x3b}, {0x3c, 0x3c}, {0x3d, 0x3d}, {0x3e, 0x3e}, + {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}, + {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}, + {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}, @@ -117,7 +122,7 @@ inline constexpr std::array, 324> Vop3{{ {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}, + {0x121, 0x121}, {0x122, 0x364}, {0x123, 0x365}, {0x124, 0x366}, {0x125, 0x30f}, {0x126, 0x310}, {0x127, 0x319}, {0x128, 0x128}, {0x129, 0x129}, {0x12a, 0x12a}, {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}, diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp index 996f1b0de3..9a9f01921d 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp @@ -25,7 +25,6 @@ enum class Vop2SdwaProfile { None, Float, Integer, - Float16, }; struct Vop2OpcodeInfo { @@ -78,19 +77,19 @@ constexpr Vop2OpcodeInfo vop2Opcodes[] = { {0x2cu, RdnaOpcode::VMadmkF32}, {0x2du, RdnaOpcode::VMadakF32}, {0x2fu, RdnaOpcode::VCvtPkrtzF16F32, Vop2SdwaProfile::Float}, - {0x32u, RdnaOpcode::VAddF16, Vop2SdwaProfile::Float16}, - {0x33u, RdnaOpcode::VSubF16, Vop2SdwaProfile::Float16}, - {0x34u, RdnaOpcode::VSubrevF16, Vop2SdwaProfile::Float16}, - {0x35u, RdnaOpcode::VMulF16, Vop2SdwaProfile::Float16}, + {0x32u, RdnaOpcode::VAddF16, Vop2SdwaProfile::Float}, + {0x33u, RdnaOpcode::VSubF16, Vop2SdwaProfile::Float}, + {0x34u, RdnaOpcode::VSubrevF16, Vop2SdwaProfile::Float}, + {0x35u, RdnaOpcode::VMulF16, Vop2SdwaProfile::Float}, {0x36u, RdnaOpcode::VFmacF16}, {0x37u, RdnaOpcode::VFmamkF16}, {0x38u, RdnaOpcode::VFmaakF16}, - {0x39u, RdnaOpcode::VMaxF16, Vop2SdwaProfile::Float16}, - {0x3au, RdnaOpcode::VMinF16, Vop2SdwaProfile::Float16}, + {0x39u, RdnaOpcode::VMaxF16, Vop2SdwaProfile::Float}, + {0x3au, RdnaOpcode::VMinF16, Vop2SdwaProfile::Float}, {0x3cu, RdnaOpcode::VPkFmacF16}, {0x06u, RdnaOpcode::VMacLegacyF32}, {0x07u, RdnaOpcode::VMulLegacyF32, Vop2SdwaProfile::Float}, - {0x3bu, RdnaOpcode::VLdexpF16, Vop2SdwaProfile::Float16}, + {0x3bu, RdnaOpcode::VLdexpF16, Vop2SdwaProfile::Float}, }; constexpr VectorOpcodeInfo vop1Opcodes[] = { @@ -1333,14 +1332,12 @@ struct Vop2SdwaRule { std::uint32_t src1Selectors = sdwaSelFull(); bool partialDst = false; bool sourceModifiers = false; - bool byteSelectorClamp = true; }; constexpr Vop2SdwaRule vop2SdwaRules[] = { {}, {sdwaSelAll(), sdwaSelAll(), sdwaSelAll(), true, true}, {sdwaSelAll(), sdwaSelAll(), sdwaSelAll(), true, false}, - {sdwaSelAll(), sdwaSelAll(), sdwaSelAll(), true, true, false}, }; const Vop2SdwaRule* findVop2SdwaRule(std::uint32_t encoding) { @@ -1401,10 +1398,6 @@ void validateVop2Sdwa(const RdnaInstruction& instruction, std::uint32_t opcode, if (!rule->sourceModifiers && hasSourceModifiers && !cndmaskFloatModifiers) { throw std::invalid_argument("VOP2 SDWA source modifiers are not supported"); } - const bool byteSelector = fields.src0Sel < 4u || fields.src1Sel < 4u || fields.dstSel < 4u; - if (!rule->byteSelectorClamp && fields.clamp != 0u && byteSelector) { - throw std::invalid_argument("VOP2 SDWA clamp with byte selectors is not implemented"); - } } void finalizeVop2Instruction(std::span code, std::uint32_t wordIndex, RdnaInstruction& instruction) { diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 4a231e26c9..685b0467e7 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -256,7 +256,8 @@ IrProgram PrepareResourceProgram(const RecompileRequest& request, ShaderPreparat translateOptions.userDataCount = static_cast(request.context.userData.size()); translateOptions.scratchDwords = request.context.compute.has_value() ? request.context.compute->scratchDwords : 0u; translateOptions.sharedMemoryBytes = DeviceMemoryLdsBytes(request); - translateOptions.fragmentShaderBarycentricEnabled = request.target.fragmentShaderBarycentricEnabled; + const bool hostInterpolationFirst = stageKind == ShaderStageKind::Pixel && request.target.fragmentShaderBarycentricEnabled && request.target.hostInterpolationFirst; + translateOptions.fragmentShaderBarycentricEnabled = request.target.fragmentShaderBarycentricEnabled && !hostInterpolationFirst; const auto supports = [&](spv::Capability capability) { return std::ranges::find(request.target.supportedCapabilities, static_cast(capability)) != request.target.supportedCapabilities.end(); }; translateOptions.nativeFloat16 = supports(spv::CapabilityFloat16) && supports(spv::CapabilityRoundingModeRTE) && supports(spv::CapabilityDenormPreserve); translateOptions.interpolationFunctions = supports(spv::CapabilityInterpolationFunction); @@ -333,11 +334,11 @@ IrProgram PrepareResourceProgram(const RecompileRequest& request, ShaderPreparat } }; simplify(); - if (stageKind == ShaderStageKind::Pixel && !translateOptions.fragmentShaderBarycentricEnabled) program.Metadata().barycentricBuiltinsUnnormalized = request.target.fragmentBarycentricBuiltins; - if (stageKind == ShaderStageKind::Pixel && !translateOptions.fragmentShaderBarycentricEnabled && !HostInterpolationChecker{}.Lower(program, *inputInfo.pixel, request.target.fragmentBarycentricBuiltins)) { + if (stageKind == ShaderStageKind::Pixel && !translateOptions.fragmentShaderBarycentricEnabled) program.Metadata().barycentricBuiltinsUnnormalized = request.target.fragmentBarycentricBuiltins && !hostInterpolationFirst; + if (stageKind == ShaderStageKind::Pixel && !translateOptions.fragmentShaderBarycentricEnabled && !HostInterpolationChecker{}.Lower(program, *inputInfo.pixel, request.target.fragmentBarycentricBuiltins && !hostInterpolationFirst)) { translateOptions.fragmentShaderBarycentricEnabled = true; program = translator.Translate(decoded, cfg, translateOptions); - program.Metadata().barycentricEmulation = true; + program.Metadata().barycentricEmulation = !hostInterpolationFirst; simplify(); } @@ -363,9 +364,13 @@ IrProgram PrepareResourceProgram(const RecompileRequest& request, ShaderPreparat deadCodeEliminator.Eliminate(program); dumpIr("resources"); program.Resources().srgbDecodeFormats = request.target.srgbDecodeFormats; + program.Resources().maxImageArrayLayers = request.target.maxImageArrayLayers; if (request.context.convertedSamples) { for (auto& image : program.Resources().info.images) image.convertedSamples = true; } + if (request.context.layerRowSamples) { + for (auto& image : program.Resources().info.images) image.maxArrayLayers = request.target.maxImageArrayLayers; + } return program; } @@ -632,6 +637,7 @@ CompiledVariant emitVariant(const RecompileRequest& request, IrProgram& program) targetOptions.supportedCapabilities = request.target.supportedCapabilities; targetOptions.supportedExtensions = request.target.supportedExtensions; targetOptions.nonConstantImageOffsets = request.target.nonConstantImageOffsets; + targetOptions.shaderImageMinLod = request.target.shaderImageMinLod; targetOptions.narrowSubgroupClock = request.target.narrowSubgroupClock; targetOptions.float64DenormPreserve = request.target.float64DenormPreserve; targetOptions.depthRangeRemap = request.target.depthRangeRemap; @@ -724,6 +730,7 @@ CompiledVariant emitVariant(const RecompileRequest& request, IrProgram& program) result.fullSubgroups = emitted.fullSubgroups; result.functionLdsBuffer = emitted.functionLdsBuffer; result.meshWindowBuffer = emitted.meshWindowBuffer; + result.scratchPoolSlotDwords = emitted.scratchPoolSlotDwords; result.vertexOffsetSgpr = program.Info().vertexOffsetSgpr; result.instanceOffsetSgpr = program.Info().instanceOffsetSgpr; result.vertexOffsetShared = program.Info().vertexOffsetShared; @@ -903,17 +910,19 @@ SpecializedSpirv buildSpecializedModule(const CompiledShaderArtifact& artifact, std::map supplied; for (const auto& constant : constants) supplied.emplace(constant.id, constant.value); std::map bindingNumbers; - std::unordered_set functionLdsBuffer; + std::unordered_set ldsBuffers; + bool compute = false; for (std::size_t cursor = 5; cursor < materialized.size();) { const auto count = materialized[cursor] >> 16u; const auto op = static_cast(materialized[cursor] & 0xffffu); + if (op == spv::OpEntryPoint && count >= 2u) compute = materialized[cursor + 1u] == spv::ExecutionModelGLCompute; if (op == spv::OpDecorate && count == 4u && materialized[cursor + 2u] == spv::DecorationBinding) bindingNumbers.emplace(materialized[cursor + 1u], materialized[cursor + 3u]); - if (op == spv::OpDecorate && count == 4u && materialized[cursor + 2u] == spv::DecorationDescriptorSet && (materialized[cursor + 3u] == PixelFunctionLdsDescriptorSet || materialized[cursor + 3u] == MeshWindowDescriptorSet)) functionLdsBuffer.insert(materialized[cursor + 1u]); + if (op == spv::OpDecorate && count == 4u && materialized[cursor + 2u] == spv::DecorationDescriptorSet && (materialized[cursor + 3u] == PixelFunctionLdsDescriptorSet || materialized[cursor + 3u] == MeshWindowDescriptorSet || (compute && materialized[cursor + 3u] == WorkgroupMemoryDescriptorSet))) ldsBuffers.insert(materialized[cursor + 1u]); if (op == spv::OpDecorate && count == 4u && materialized[cursor + 2u] == spv::DecorationSpecId) { const auto id = materialized[cursor + 3u]; if (!PipelineSpecialization::IsWorkgroupOffset(id)) module->specialization.push_back({id, supplied.at(id)}); } - if (op == spv::OpVariable && count >= 4u && !functionLdsBuffer.contains(materialized[cursor + 2u])) { + if (op == spv::OpVariable && count >= 4u && !ldsBuffers.contains(materialized[cursor + 2u])) { if (const auto found = bindingNumbers.find(materialized[cursor + 2u]); found != bindingNumbers.end()) module->bindings.push_back(found->second); module->pushData |= materialized[cursor + 3u] == spv::StorageClassPushConstant; } @@ -1003,7 +1012,7 @@ bool bufferSubstitutesEnabled() { return enabled; } -std::shared_ptr preparedBindingPlan(const CompiledVariant& variant, const ResourceSnapshot& snapshot, std::span exports, bool fragmentDescriptorSet, std::span substitutes) { +std::shared_ptr preparedBindingPlan(const CompiledVariant& variant, const ResourceSnapshot& snapshot, std::span exports, bool fragmentDescriptorSet, std::span substitutes, bool shaderImageMinLod) { struct BindingPlanKeyStorage {}; auto& key = HostThreadLocal, BindingPlanKeyStorage>(); key.clear(); @@ -1044,7 +1053,7 @@ std::shared_ptr preparedBindingPlan(const CompiledVariant& } std::call_once(plan->ready, [&] { try { - plan->bindings = DescriptorBindingBuilder{}.Prepare(variant.bindings.layout, variant.info.info, variant.info.stage, snapshot, exports, variant.artifact.registrationModule, fragmentDescriptorSet, substitutes); + plan->bindings = DescriptorBindingBuilder{}.Prepare(variant.bindings.layout, variant.info.info, variant.info.stage, snapshot, exports, variant.artifact.registrationModule, fragmentDescriptorSet, substitutes, shaderImageMinLod); } catch (...) { plan->failure = std::current_exception(); } @@ -1159,6 +1168,7 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil .fullSubgroups = artifact.fullSubgroups, .functionLdsBuffer = artifact.functionLdsBuffer, .meshWindowBuffer = artifact.meshWindowBuffer, + .scratchPoolSlotDwords = artifact.scratchPoolSlotDwords, .parameterExports = artifact.parameterExports, .fragmentParameters = artifact.fragmentParameters, .barycentricEmulation = artifact.barycentricEmulation, @@ -1174,7 +1184,7 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil const auto index = reg - request.context.userDataBaseRegister; result.userDataRegisterReads |= reg >= request.context.userDataBaseRegister && index < 64u ? std::uint64_t{1} << index : ~std::uint64_t{0}; } - const auto plan = preparedBindingPlan(variant, snapshot, request.context.pixel ? std::span(request.context.pixel->targetExportMapping) : std::span{}, request.target.registrationModules, substitutes); + const auto plan = preparedBindingPlan(variant, snapshot, request.context.pixel ? std::span(request.context.pixel->targetExportMapping) : std::span{}, request.target.registrationModules, substitutes, request.target.shaderImageMinLod); const DescriptorBindingPlan* bindingPlan = &plan->bindings; std::shared_ptr entry; struct LocalModuleKeyStorage {}; @@ -1493,19 +1503,29 @@ RecompileResult recompileOnce(const RecompileRequest& request) { return materializeResult(variant, request, snapshot); } const auto source = getSource(request); - materializer.Materialize(*acquirePlan(request, source, true), runtime, snapshot); + try { + materializer.Materialize(*acquirePlan(request, source, true), runtime, snapshot); + } catch (...) { + std::lock_guard lock(source->mutex); + source->program.reset(); + throw; + } return materializeVariant(*source, request, snapshot); } RecompileResult RecompileImpl(const RecompileRequest& request) { - try { - return recompileOnce(request); - } catch (const ConvertedSampleRequired&) { - if (request.context.convertedSamples) throw; + auto current = request; + while (true) { + try { + return recompileOnce(current); + } catch (const ConvertedSampleRequired&) { + if (current.context.convertedSamples) throw; + current.context.convertedSamples = true; + } catch (const LayerRowSamplesRequired&) { + if (current.context.layerRowSamples) throw; + current.context.layerRowSamples = true; + } } - auto converted = request; - converted.context.convertedSamples = true; - return recompileOnce(converted); } // APS5_NO_RESULT_MEMO=1: every Recompile(request, capture) materializes its own result as before. @@ -1771,6 +1791,23 @@ std::uint64_t snapshotHash(const RecompileRequest& request, const ResourceSnapsh } mixDescriptors(snapshot.images); mixDescriptors(snapshot.samplers); + mix(snapshot.registeredImages.size()); + for (const auto& view : snapshot.registeredImages) { + mix(view.resource); + mix(view.record); + mix(view.address); + mix(view.rangeAddress); + mix(view.rangeBytes); + mix(view.allocationAddress); + mix(view.allocationBytes); + mix(view.backingSerial); + mix(view.hasRange); + mix(view.readable); + mix(view.writable); + mix(view.gpu); + for (const auto word : view.provenance.dwords) mix(word); + for (const auto word : view.effective.dwords) mix(word); + } if (masked) { mix(snapshot.flattenedSrt.size()); mix(snapshot.userData.size()); diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index de7eeae604..aca3fdd71b 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -129,7 +129,9 @@ enum class ColorExportPacking : std::uint8_t { Unorm10_11_11, Float11_11_10, Unorm4_4_4_4, - Uint + Uint, + UintClamp8, + UintClamp16 }; enum class RasterPrimitive : std::uint8_t { @@ -292,6 +294,7 @@ struct GuestContext { std::optional floatMode; std::span linkedCallFallbacks; bool convertedSamples = false; + bool layerRowSamples = false; }; struct MeshTargetLimits { @@ -329,8 +332,12 @@ struct SpirvTarget { std::uint32_t maxWorkgroupSharedMemoryBytes; std::optional mesh; std::optional tessellation; + std::array featureDigest{}; + std::array featureDigestSpans{}; + bool featureDigestSealed = false; bool nonConstantImageOffsets = false; std::uint32_t srgbDecodeFormats = 0; + std::uint32_t maxImageArrayLayers = 0; bool narrowSubgroupClock = false; bool fixedPushSlots = false; bool float64DenormPreserve = false; @@ -353,6 +360,8 @@ struct SpirvTarget { bool ldsLockFences = false; bool fragmentBarycentricBuiltins = false; bool meshWindowBuffer = false; + bool hostInterpolationFirst = false; + bool shaderImageMinLod = false; }; struct BindingLayout { @@ -428,6 +437,7 @@ inline constexpr std::uint32_t MeshCapturedItemAddressDword = 0; inline constexpr std::uint32_t TessellationCaptureArgumentAddressDword = 2; inline constexpr std::uint32_t MeshIndexBufferUserWord = 4; inline constexpr std::uint32_t WorkgroupMemoryDescriptorSet = 1; +inline constexpr std::uint32_t ScratchPoolDescriptorSet = 2; struct GraphicsDrawParameters { std::uint64_t indexAddress; @@ -552,7 +562,7 @@ struct DescriptorBinding { std::vector samplerCoordinatesIgnored; std::vector imageUnnormalized; std::vector imageUnnormalizedBaseLevel; - std::vector imageSamplers; + std::vector imageSamplers; std::vector bufferRead; std::vector bufferExtent; }; @@ -760,6 +770,7 @@ struct CompiledShaderArtifact { bool fullSubgroups = false; bool functionLdsBuffer = false; bool meshWindowBuffer = false; + std::uint32_t scratchPoolSlotDwords = 0; std::vector parameterExports; std::vector fragmentParameters; BarycentricEmulation barycentricEmulation; diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index aa06feb746..22b9999751 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -87,7 +87,9 @@ namespace ShaderRecompiler::ShaderDiskCache { #if defined(__linux__) && defined(__x86_64__) && defined(__GLIBCXX__) static_assert(sizeof(CompiledShaderArtifact) == 288, "CompiledShaderArtifact changed: update the artifact encoder"); static_assert(sizeof(ShaderInvocation) == 216, "ShaderInvocation changed: update the invocation encoder"); +#if defined(__cpp_lib_atomic_shared_ptr) static_assert(sizeof(RecompileResult) == 552, "RecompileResult changed: update EncodeResult and DecodeResult"); +#endif static_assert(sizeof(DescriptorBinding) == 656, "DescriptorBinding changed: update the binding encoder"); static_assert(sizeof(VertexAttribute) == 32, "VertexAttribute changed: update the attribute encoder"); static_assert(sizeof(VertexInput) == 16, "VertexInput changed: update the vertex input encoder"); @@ -95,7 +97,7 @@ static_assert(sizeof(FragmentParameter) == 16, "FragmentParameter changed: updat static_assert(sizeof(CompiledShaderInfo) == 352, "CompiledShaderInfo changed: update the info encoder"); static_assert(sizeof(ShaderInfo) == 232, "ShaderInfo changed: update the info encoder"); static_assert(sizeof(BufferResource) == 32, "BufferResource changed: update the info encoder"); -static_assert(sizeof(ImageResource) == 96, "ImageResource changed: update the info encoder"); +static_assert(sizeof(ImageResource) == 108, "ImageResource changed: update the info encoder"); static_assert(sizeof(SamplerResource) == 24, "SamplerResource changed: update the info encoder"); static_assert(sizeof(SampledResourcePair) == 12, "SampledResourcePair changed: update the info encoder"); static_assert(sizeof(StageInput) == 56, "StageInput changed: update the info encoder"); @@ -141,12 +143,12 @@ constexpr std::size_t fieldCount() { static_assert(fieldCount() == 24, "ShaderInfo changed: update the info encoder"); static_assert(fieldCount() == 12, "BufferResource changed: update the info encoder"); -static_assert(fieldCount() == 46, "ImageResource changed: update the info encoder"); +static_assert(fieldCount() == 49, "ImageResource changed: update the info encoder"); static_assert(fieldCount() == 8, "SamplerResource changed: update the info encoder"); static_assert(fieldCount() == 3, "SampledResourcePair changed: update the info encoder"); static_assert(fieldCount() == 5, "StageInput changed: update the info encoder"); static_assert(fieldCount() == 4, "StageOutput changed: update the info encoder"); -static_assert(fieldCount() == 24, "IrResourcePlan changed: update EncodePlan and DecodePlan"); +static_assert(fieldCount() == 25, "IrResourcePlan changed: update EncodePlan and DecodePlan"); static_assert(fieldCount() == 2, "GuestRegister changed: update the plan value encoder"); static_assert(fieldCount() == 32, "MemoryInfo changed: update the plan encoder"); static_assert(fieldCount() == 4, "DescriptorSource changed: update the plan encoder"); @@ -475,7 +477,7 @@ void encodeBinding(Writer& writer, const DescriptorBinding& binding) { writer.Flags(binding.imageUnnormalizedBaseLevel); writer.Flags(binding.samplerIntegerBorder); writer.Flags(binding.samplerCoordinatesIgnored); - writer.Values(std::span(binding.imageSamplers)); + writer.Values(std::span(binding.imageSamplers)); writer.Flags(binding.bufferRead); writer.Values(std::span(binding.bufferExtent)); } @@ -542,6 +544,7 @@ void encodeArtifact(Writer& writer, const CompiledShaderArtifact& result) { writer.Value(result.fullSubgroups); writer.Value(result.functionLdsBuffer); writer.Value(result.meshWindowBuffer); + writer.Value(result.scratchPoolSlotDwords); writer.Values(std::span(result.parameterExports)); writer.List(result.fragmentParameters, [](Writer& out, const FragmentParameter& parameter) { out.Value(parameter.location); @@ -608,6 +611,7 @@ void decodeArtifact(Reader& reader, CompiledShaderArtifact& result) { reader.Value(result.fullSubgroups); reader.Value(result.functionLdsBuffer); reader.Value(result.meshWindowBuffer); + reader.Value(result.scratchPoolSlotDwords); reader.Values(result.parameterExports); reader.List(result.fragmentParameters, 10, [](Reader& in, FragmentParameter& parameter) { in.Value(parameter.location); @@ -811,6 +815,9 @@ void encodeShaderInfo(Writer& writer, const ShaderInfo& info) { out.Value(image.foldTexelOffsetsOnly); out.Value(image.convertedSamples); out.Value(image.remappedDepth1D); + out.Value(image.layerRowsCompatible); + out.Value(image.layerRows); + out.Value(image.maxArrayLayers); out.Value(image.optionalDescriptor); out.Value(image.byElements); out.Value(image.byComponents); @@ -922,6 +929,9 @@ void decodeShaderInfo(Reader& reader, ShaderInfo& info) { in.Value(image.foldTexelOffsetsOnly); in.Value(image.convertedSamples); in.Value(image.remappedDepth1D); + in.Value(image.layerRowsCompatible); + in.Value(image.layerRows); + in.Value(image.maxArrayLayers); in.Value(image.optionalDescriptor); in.Value(image.byElements); in.Value(image.byComponents); @@ -1112,6 +1122,7 @@ bool encodePlan(Writer& writer, const IrResourcePlan& plan) { writer.Value(plan.userDataBase); writer.Value(plan.userDataCount); writer.Value(plan.srgbDecodeFormats); + writer.Value(plan.maxImageArrayLayers); writer.List(plan.blockStorage, [](Writer& out, const std::unique_ptr& block) { out.Value(block->Id()); }); writer.Value(plan.valueStorage.Size()); for (const auto& stored : plan.valueStorage) { @@ -1221,6 +1232,7 @@ bool decodePlan(Reader& reader, IrResourcePlan& plan) { reader.Value(plan.userDataBase); reader.Value(plan.userDataCount); reader.Value(plan.srgbDecodeFormats); + reader.Value(plan.maxImageArrayLayers); std::vector blockIds; reader.Values(blockIds); std::vector records; diff --git a/core/shader/recompiler/ShaderDiskCache.hpp b/core/shader/recompiler/ShaderDiskCache.hpp index dad5115a4d..b937c57213 100644 --- a/core/shader/recompiler/ShaderDiskCache.hpp +++ b/core/shader/recompiler/ShaderDiskCache.hpp @@ -13,7 +13,7 @@ namespace ShaderRecompiler::ShaderDiskCache { -inline constexpr std::uint32_t FormatVersion = 51; +inline constexpr std::uint32_t FormatVersion = 52; enum class LoadStatus { Loaded, diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp index 876c3eddbe..5938cf31fc 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp @@ -20,6 +20,7 @@ struct SpirvTargetOptions { std::span supportedCapabilities; std::span supportedExtensions; bool nonConstantImageOffsets = false; + bool shaderImageMinLod = false; bool narrowSubgroupClock = false; bool float64DenormPreserve = false; bool depthRangeRemap = false; @@ -51,6 +52,7 @@ struct SpirvEmitInfo { bool fullSubgroups = false; bool functionLdsBuffer = false; bool meshWindowBuffer = false; + std::uint32_t scratchPoolSlotDwords = 0; }; struct SpannedWaveUnsupported : std::runtime_error { diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp index f0f5a1c4d1..777eda1ea3 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp @@ -117,6 +117,13 @@ bool LdsInDeviceMemory(const SpirvEmitterState& state); std::uint32_t LdsMemorySemantics(const SpirvEmitterState& state); std::uint32_t LdsMemoryBarrierScope(const SpirvEmitterState& state); void EmitLdsBufferBase(SpirvEmitterState& state); +bool ScratchPoolActive(const SpirvEmitterState& state); +std::uint32_t ScratchPoolSlotDwords(const SpirvEmitterState& state); +void DefineScratchPool(SpirvEmitterState& state); +void EmitScratchPoolAcquire(SpirvEmitterState& state); +std::uint32_t EmitScratchPoolElementPointer(SpirvEmitterState& state, std::uint32_t index); +void EmitReturnReleasingScratch(SpirvEmitterState& state); +void VerifyScratchPoolReturns(const SpirvEmitterState& state, const std::vector& words); std::uint32_t EmitLdsLockPointer(SpirvEmitterState& state); std::uint32_t EmitLdsBufferU64Pointer(SpirvEmitterState& state, std::uint32_t index); MemoryResourceAccess PrepareMemoryResourceAccess(SpirvEmitterState& state, const MemoryInfo& mem); @@ -249,6 +256,12 @@ std::uint32_t EmitValueIfElse(SpirvEmitterState& state, std::uint32_t condition, template std::uint32_t AtomicUpdateTyped(SpirvEmitterState& state, std::uint32_t pointer, ResourceKind kind, std::uint32_t type, TFunction&& function) { + if (kind == ResourceKind::Scratch && ScratchPoolActive(state)) { + const auto previous = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, type, previous, pointer); + state.module.AddFunction(spv::OpStore, pointer, function(previous)); + return previous; + } const auto scope = kind == ResourceKind::Lds ? spv::ScopeWorkgroup : spv::ScopeDevice; const auto memory = [&]() -> std::uint32_t { switch (kind) { diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index 21cefd6303..09aec42aa3 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -138,6 +138,7 @@ struct SpirvEmitterState { std::span supportedCapabilities; std::span supportedExtensions; bool nonConstantImageOffsets = false; + bool shaderImageMinLod = false; bool tableIndexNonUniform = true; std::uint32_t storageBufferVariable = 0; std::uint32_t storageBufferU64Variable = 0; @@ -220,6 +221,14 @@ struct SpirvEmitterState { bool pixelFunctionLdsBuffer = false; std::uint32_t numWorkgroupsVariable = 0; std::array scratchVariable {}; + std::uint32_t scratchPoolVariable = 0; + std::uint32_t scratchPoolSlotVariable = 0; + std::uint32_t scratchPoolCandidateVariable = 0; + std::uint32_t scratchPoolWonVariable = 0; + std::uint32_t scratchPoolSlotCount = 0; + std::uint32_t scratchPoolSlot = 0; + std::array scratchPoolLaneBase {}; + std::uint32_t scratchPoolReturns = 0; std::array vectorWindowVariable {}; std::uint32_t vectorWindowIndexed = 0; std::array, 2> vectorWindowSlots {}; @@ -254,6 +263,8 @@ struct SpirvEmitterState { std::uint32_t sampleMaskInputVariable = 0; std::uint32_t quadSampleCoverage = 0; std::uint32_t sampleOrderWord = 0; + std::uint32_t alphaCoverageVariable = 0; + std::uint32_t exportedCoverageVariable = 0; std::uint32_t subgroupLocalInvocationIdVariable = 0; std::uint32_t subgroupIdVariable = 0; std::uint32_t perVertexVariable = 0; diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvBufferAccess.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvBufferAccess.hpp index 18e703456f..3fbdbef112 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvBufferAccess.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvBufferAccess.hpp @@ -15,6 +15,13 @@ bool LdsInDeviceMemory(const SpirvEmitterState& state); std::uint32_t LdsMemorySemantics(const SpirvEmitterState& state); std::uint32_t LdsMemoryBarrierScope(const SpirvEmitterState& state); void EmitLdsBufferBase(SpirvEmitterState& state); +bool ScratchPoolActive(const SpirvEmitterState& state); +std::uint32_t ScratchPoolSlotDwords(const SpirvEmitterState& state); +void DefineScratchPool(SpirvEmitterState& state); +void EmitScratchPoolAcquire(SpirvEmitterState& state); +std::uint32_t EmitScratchPoolElementPointer(SpirvEmitterState& state, std::uint32_t index); +void EmitReturnReleasingScratch(SpirvEmitterState& state); +void VerifyScratchPoolReturns(const SpirvEmitterState& state, const std::vector& words); std::uint32_t EmitLdsLockPointer(SpirvEmitterState& state); std::uint32_t EmitLdsBufferU64Pointer(SpirvEmitterState& state, std::uint32_t index); MemoryResourceAccess PrepareMemoryResourceAccess(SpirvEmitterState& state, const MemoryInfo& mem); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp index d27deeba81..9a1b313b0f 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp @@ -1283,8 +1283,8 @@ std::uint32_t EmitFPLog2(SpirvEmitterState& state, std::uint32_t arg0) { const auto series = Log2SeriesNearOne(state, difference); const auto valueBits = Unary(state, spv::OpBitcast, TypeU32(state), value); const auto seriesBits = Unary(state, spv::OpBitcast, TypeU32(state), series); - const auto distance = EmitExt(state, TypeU32(state), GLSLstd450UMin, {Binary(state, spv::OpISub, TypeU32(state), valueBits, seriesBits), Binary(state, spv::OpISub, TypeU32(state), seriesBits, valueBits)}); - const auto far = Binary(state, spv::OpUGreaterThan, TypeBool(state), distance, ConstantU32(state, 4u)); + const auto biasedDifference = Binary(state, spv::OpIAdd, TypeU32(state), Binary(state, spv::OpISub, TypeU32(state), valueBits, seriesBits), ConstantU32(state, 4u)); + const auto far = Binary(state, spv::OpUGreaterThan, TypeBool(state), biasedDifference, ConstantU32(state, 8u)); return Select(state, TypeF32(state), far, series, value); }); } @@ -1310,8 +1310,8 @@ std::uint32_t CosTurnSeries(SpirvEmitterState& state, std::uint32_t turns) { std::uint32_t SeriesWhenFar(SpirvEmitterState& state, std::uint32_t value, std::uint32_t series) { const auto valueBits = Unary(state, spv::OpBitcast, TypeU32(state), value); const auto seriesBits = Unary(state, spv::OpBitcast, TypeU32(state), series); - const auto distance = EmitExt(state, TypeU32(state), GLSLstd450UMin, {Binary(state, spv::OpISub, TypeU32(state), valueBits, seriesBits), Binary(state, spv::OpISub, TypeU32(state), seriesBits, valueBits)}); - return Select(state, TypeF32(state), Binary(state, spv::OpUGreaterThan, TypeBool(state), distance, ConstantU32(state, 4u)), series, value); + const auto biasedDifference = Binary(state, spv::OpIAdd, TypeU32(state), Binary(state, spv::OpISub, TypeU32(state), valueBits, seriesBits), ConstantU32(state, 4u)); + return Select(state, TypeF32(state), Binary(state, spv::OpUGreaterThan, TypeBool(state), biasedDifference, ConstantU32(state, 8u)), series, value); } std::uint32_t TrigLinearNearZero(SpirvEmitterState& state, std::uint32_t value, std::uint32_t distance, std::uint32_t linearConstant, std::uint32_t limit) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvBdaFault.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvBdaFault.cpp index 53f65c7c1c..cb6d2a7cda 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvBdaFault.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvBdaFault.cpp @@ -121,7 +121,7 @@ void StopBdaInvocationIf(SpirvEmitterState& state, std::uint32_t condition) { state.module.AddFunction(spv::OpEndInvocationInterlockEXT); } if (state.program.Resources().stage != IrShaderStage::Pixel) { - state.module.AddFunction(spv::OpReturn); + EmitReturnReleasingScratch(state); } else if (state.pixelStopFlag != 0u) { state.module.AddFunction(spv::OpStore, state.pixelStopFlag, ConstantBool(state, true)); state.module.AddFunction(spv::OpReturn); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp index 7ab079e28d..591fac5fd9 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp @@ -263,6 +263,7 @@ std::vector SpirvEmitter::Emit(const IrProgram& program, const Sh state.supportedCapabilities = target.supportedCapabilities; state.supportedExtensions = target.supportedExtensions; state.nonConstantImageOffsets = target.nonConstantImageOffsets; + state.shaderImageMinLod = target.shaderImageMinLod; state.narrowSubgroupClock = target.narrowSubgroupClock; state.float64DenormPreserve = target.float64DenormPreserve; state.depthRangeRemap = target.depthRangeRemap; @@ -341,7 +342,9 @@ std::vector SpirvEmitter::Emit(const IrProgram& program, const Sh info->fullSubgroups = state.spannedWave; info->functionLdsBuffer = state.pixelFunctionLdsBuffer; info->meshWindowBuffer = state.meshWindowBuffer; + info->scratchPoolSlotDwords = ScratchPoolActive(state) ? ScratchPoolSlotDwords(state) : 0u; } + VerifyScratchPoolReturns(state, words); return words; } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp index 231e68ac02..d22f571409 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp @@ -108,7 +108,7 @@ void EmitReturnTerminator(SpirvValueEmitContext& ctx) { state.module.AddFunction(spv::OpLoad, TypeU32(state), expected, pointer); EmitKillIfBoolFalse(state, Binary(state, spv::OpIEqual, TypeBool(state), masked, expected)); } - state.module.AddFunction(spv::OpReturn); + EmitReturnReleasingScratch(state); } // The loop guard (see SpirvEmitterState::loopGuardLimit) at a branch that may leave a loop: counts the diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index 5b9cec0cbe..58d08e6ca9 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -204,6 +204,31 @@ std::uint32_t CoordF32(SpirvValueEmitContext& ctx, const ImageEmitAccess& access return result; } +std::uint32_t LayerRowsCoord(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, std::uint32_t coord) { + auto& state = ctx.state; + const auto f32 = TypeF32(state); + const auto u32 = TypeU32(state); + const auto extract = [&](std::uint32_t type, std::uint32_t composite, std::uint32_t index) { + const auto value = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, type, value, composite, index); + return value; + }; + state.module.EmitCapability(spv::CapabilityImageQuery); + const auto size = state.module.AllocateId(); + state.module.AddFunction(spv::OpImageQuerySizeLod, ImageViewSizeType(state, RdnaImageDimension::Dim2D), size, LoadSampledImageDescriptor(state, access.mem.resource), ConstantU32(state, 0u)); + const auto rows = extract(u32, size, 1u); + const auto lastLayer = Binary(state, spv::OpISub, u32, Binary(state, spv::OpShiftRightLogical, u32, rows, ConstantU32(state, 1u)), ConstantU32(state, 1u)); + const auto rounded = state.module.AllocateId(); + state.module.AddFunction(spv::OpExtInst, f32, rounded, GlslStd450(state), GLSLstd450RoundEven, extract(f32, coord, 1u)); + const auto clamped = state.module.AllocateId(); + state.module.AddFunction(spv::OpExtInst, f32, clamped, GlslStd450(state), GLSLstd450NClamp, rounded, ConstantF32(state, 0u), Unary(state, spv::OpConvertUToF, f32, lastLayer)); + const auto center = Binary(state, spv::OpFAdd, f32, Binary(state, spv::OpFMul, f32, clamped, ConstantF32(state, std::bit_cast(2.0f))), ConstantF32(state, std::bit_cast(1.0f))); + const auto v = Binary(state, spv::OpFDiv, f32, center, Unary(state, spv::OpConvertUToF, f32, rows)); + const auto result = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeConstruct, TypeF32Vector(state, 2), result, extract(f32, coord, 0u), v); + return result; +} + std::uint32_t CoordU32(SpirvValueEmitContext& ctx, const ImageEmitAccess& access) { const auto components = RdnaImageDimensionInfoFor(access.image.dimension).coordinateComponents; const auto encoded = std::min(components, AddressDimension(access).coordinateComponents); @@ -1272,7 +1297,11 @@ SampleSetup MakeSampleSetup(SpirvValueEmitContext& ctx, const ImageEmitAccess& a if (ImageConversionFormat(access.image).type == SpirvFormatComponentType::Float) { ctx.Fail(access.inst, "samples or gathers a converted float image, which needs filtering in the shader and is not implemented"); } - const auto coord = CoordF32(ctx, access, layout.coord, dimensionInfo.coordinateComponents, AddressDimension(access).coordinateComponents); + auto coord = CoordF32(ctx, access, layout.coord, dimensionInfo.coordinateComponents, AddressDimension(access).coordinateComponents); + if (access.image.layerRows) { + if (dref || access.inst.Opcode() != IrOpcode::ImageSampleRaw || (access.mem.imageSampleFlags != RdnaImageSampleFlagLevelZero && access.mem.imageSampleFlags != RdnaImageSampleFlagLod)) ctx.Fail(access.inst, "reads a 1D array laid out as rows of a 2D image other than by image_sample_lz or image_sample_l"); + coord = LayerRowsCoord(ctx, access, coord); + } return {dimensionInfo, layout, access.image.numericClass, dref, coord}; } @@ -1634,18 +1663,25 @@ void EmitSampleOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, con operandMask |= spv::ImageOperandsBiasMask; operands.push_back(AddressF32(ctx, access, setup.layout.bias)); } - if (setup.layout.clamp != NoImageComponent) { + const bool descriptorClamp = state.shaderImageMinLod && state.program.Info().images.at(mem.resource).table == NoTable; + if (descriptorClamp && state.registrationModules) ctx.Fail(access.inst, "applies its T#'s minimum LOD clamp in the shader, which registration modules do not implement"); + if (setup.layout.clamp != NoImageComponent || descriptorClamp) { const auto& capabilities = state.supportedCapabilities; if (std::find(capabilities.begin(), capabilities.end(), static_cast(spv::CapabilityMinLod)) == capabilities.end()) ctx.Fail(access.inst, "clamps its LOD, which needs the device's shaderResourceMinLod"); - const auto clamp = AddressF32(ctx, access, setup.layout.clamp); + const auto fmax = [&](std::uint32_t left, std::uint32_t right) { + const auto value = state.module.AllocateId(); + state.module.AddFunction(spv::OpExtInst, TypeF32(state), value, GlslStd450(state), GLSLstd450FMax, left, right); + return value; + }; + const auto clamp = setup.layout.clamp != NoImageComponent ? AddressF32(ctx, access, setup.layout.clamp) : 0u; + const auto viewClamp = descriptorClamp ? Unary(state, spv::OpBitcast, TypeF32(state), state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::MinLodBase + mem.resource, 0u)) : 0u; if ((operandMask & spv::ImageOperandsLodMask) != 0u) { - const auto clamped = state.module.AllocateId(); - state.module.AddFunction(spv::OpExtInst, TypeF32(state), clamped, GlslStd450(state), GLSLstd450FMax, operands.back(), clamp); - operands.back() = clamped; + if (clamp != 0u) operands.back() = fmax(operands.back(), clamp); + if (viewClamp != 0u) operands.back() = Select(state, TypeF32(state), Binary(state, spv::OpFOrdGreaterThan, TypeBool(state), viewClamp, ZeroF32(state)), fmax(operands.back(), viewClamp), operands.back()); } else { state.module.EmitCapability(spv::CapabilityMinLod); operandMask |= spv::ImageOperandsMinLodMask; - operands.push_back(clamp); + operands.push_back(clamp == 0u ? viewClamp : viewClamp == 0u ? clamp : fmax(clamp, viewClamp)); } } const auto constantOffset = [&]() -> const IrValue* { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp index e2f95214ee..9a2b7a33c8 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp @@ -199,6 +199,11 @@ MemoryResourceAccess PrepareMemoryResourceAccess(SpirvEmitterState& state, const access.length = state.gdsLength; return access; case ResourceKind::Scratch: + if (ScratchPoolActive(state)) { + access.objectPointer = state.scratchPoolVariable; + access.length = ConstantU32(state, state.program.Info().scratchDwords); + return access; + } if (state.scratchVariable.at(state.laneHalf) == 0) { FailEmit("scratch storage was not prepared before function emission"); } @@ -260,6 +265,9 @@ std::uint32_t EmitMemoryElementPointer(SpirvEmitterState& state, const MemoryRes if (access.kind == ResourceKind::Lds && LdsInDeviceMemory(state)) { return EmitStorageBufferElementPointer(state, access, EmitAddU32(state, state.ldsBufferBase, index), TypeStorageBufferElementPointer(state)); } + if (access.kind == ResourceKind::Scratch && ScratchPoolActive(state)) { + return EmitScratchPoolElementPointer(state, index); + } if (access.kind == ResourceKind::Lds || access.kind == ResourceKind::Scratch) { const auto pointer = state.module.AllocateId(); const std::uint32_t storageClass = InFunctionStorage(state, access.kind) ? spv::StorageClassFunction : spv::StorageClassWorkgroup; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp index 27ee033600..e7cc195fa0 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp @@ -201,12 +201,12 @@ void DefineInputs(SpirvEmitterState& state) { addBuiltin(StageInputKind::WorkgroupId, 3u, "gl_WorkGroupID"); } } - if (state.requirements.ldsLock && std::none_of(state.inputs.begin(), state.inputs.end(), [](const SpirvInputBinding& input) { + if ((state.requirements.ldsLock || ScratchPoolActive(state)) && std::none_of(state.inputs.begin(), state.inputs.end(), [](const SpirvInputBinding& input) { return input.kind == StageInputKind::LocalInvocationIndex; })) { state.inputs.push_back(SpirvInputBinding {{StageInputKind::LocalInvocationIndex, 0u, 1u, "gl_LocalInvocationIndex", false}}); } - if (LdsInDeviceMemory(state)) { + if (LdsInDeviceMemory(state) || ScratchPoolActive(state)) { if (std::none_of(state.inputs.begin(), state.inputs.end(), [](const SpirvInputBinding& input) { return input.kind == StageInputKind::WorkgroupId; })) { @@ -457,7 +457,12 @@ void DefineOutputs(SpirvEmitterState& state) { BuiltIn(state.clipDistanceVariable, F32ArrayType(state, clipDistanceCount), "gl_ClipDistance", spv::BuiltInClipDistance); } if (state.program.Resources().stage == IrShaderStage::Pixel && state.inputInfo.pixel != nullptr && state.inputInfo.pixel->psAlphaToCoverage) { - if (state.inputInfo.pixel->psSampleMaskExportEnable) throw std::runtime_error("alpha-to-coverage with a sample-mask export is unsupported"); + if (state.inputInfo.pixel->psSampleMaskExportEnable) { + for (auto* variable : {&state.alphaCoverageVariable, &state.exportedCoverageVariable}) { + *variable = state.module.DefineGlobalVariable(TypePointer(state, spv::StorageClassPrivate, TypeI32(state)), spv::StorageClassPrivate); + if (state.spirvVersion >= 0x00010400u) state.interfaceVariables.push_back(*variable); + } + } BuiltIn(state.sampleMaskVariable, SampleMaskArrayType(state), "gl_SampleMask", spv::BuiltInSampleMask); } if (state.program.Resources().stage == IrShaderStage::Pixel && state.inputInfo.pixel != nullptr && state.inputInfo.pixel->QuadSampleMaskPartial()) { @@ -605,6 +610,22 @@ void DefineDescriptors(SpirvEmitterState& state) { state.module.AddAnnotation(spv::OpDecorate, state.ldsBufferVariable, spv::DecorationCoherent); if (state.spirvVersion >= 0x00010400u) state.interfaceVariables.push_back(state.ldsBufferVariable); } + DefineScratchPool(state); +} + +void DefineScratchPool(SpirvEmitterState& state) { + if (!ScratchPoolActive(state)) return; + state.scratchPoolVariable = state.module.DefineGlobalVariable(TypePointer(state, spv::StorageClassStorageBuffer, StorageBufferBlockType(state)), spv::StorageClassStorageBuffer); + state.module.AddName(state.scratchPoolVariable, "scratch_pool"); + state.module.AddAnnotation(spv::OpDecorate, state.scratchPoolVariable, spv::DecorationDescriptorSet, ScratchPoolDescriptorSet); + state.module.AddAnnotation(spv::OpDecorate, state.scratchPoolVariable, spv::DecorationBinding, 0u); + state.module.AddAnnotation(spv::OpDecorate, state.scratchPoolVariable, spv::DecorationCoherent); + state.scratchPoolSlotVariable = state.module.DefineGlobalVariable(TypePointer(state, spv::StorageClassWorkgroup, TypeU32(state)), spv::StorageClassWorkgroup); + state.module.AddName(state.scratchPoolSlotVariable, "scratch_pool_slot"); + if (state.spirvVersion >= 0x00010400u) { + state.interfaceVariables.push_back(state.scratchPoolVariable); + state.interfaceVariables.push_back(state.scratchPoolSlotVariable); + } } } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp index fc51264bfa..6164c79c5b 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp @@ -338,7 +338,10 @@ void DefineModule(SpirvEmitterState& state) { if (state.requirements.functionLds && !state.pixelFunctionLdsBuffer) { state.ldsVariable = state.module.AllocateId(); } - if (state.requirements.functionScratch) { + if (ScratchPoolActive(state)) { + state.scratchPoolCandidateVariable = state.module.AllocateId(); + state.scratchPoolWonVariable = state.module.AllocateId(); + } else if (state.requirements.functionScratch) { for (std::uint32_t half = 0; half < state.laneCount; half++) { state.scratchVariable.at(half) = state.module.AllocateId(); } @@ -538,7 +541,10 @@ void DefineModule(SpirvEmitterState& state) { if (state.requirements.functionLds && !state.pixelFunctionLdsBuffer) { state.module.AddName(state.ldsVariable, "lds_dwords"); } - if (state.requirements.functionScratch) { + if (ScratchPoolActive(state)) { + state.module.AddName(state.scratchPoolCandidateVariable, "scratch_pool_candidate"); + state.module.AddName(state.scratchPoolWonVariable, "scratch_pool_won"); + } else if (state.requirements.functionScratch) { for (std::uint32_t half = 0; half < state.laneCount; half++) { state.module.AddName(state.scratchVariable.at(half), "scratch_dwords"); } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvScratchPool.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvScratchPool.cpp new file mode 100644 index 0000000000..cf40d62fc2 --- /dev/null +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvScratchPool.cpp @@ -0,0 +1,164 @@ +#include "SpirvBackend/SpirvMemory/SpirvBufferAccess.hpp" +#include "SpirvBackend/SpirvAnalysis.hpp" +#include "SpirvBackend/SpirvEmitterHelpers.hpp" +#include "SpirvBackend/SpirvMemory/SpirvTypes.hpp" +#include "SpirvBackend/SpirvEmitterInstructions.hpp" +#include "SpirvBackend/SpirvMemory/SpirvConstants.hpp" +#include +#include + +namespace ShaderRecompiler { +namespace { + +constexpr std::uint32_t HeaderWordsPerSlot = 2u; + +std::uint32_t PoolWord(SpirvEmitterState& state, std::uint32_t index) { + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpAccessChain, TypeStorageBufferElementPointer(state), pointer, state.scratchPoolVariable, ConstantU32(state, 0u), index); + return pointer; +} + +std::uint32_t LoadU32(SpirvEmitterState& state, std::uint32_t pointer) { + const auto value = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, TypeU32(state), value, pointer); + return value; +} + +std::uint32_t InvocationsPerGroup(const SpirvEmitterState& state) { + const auto size = WorkgroupLocalSize(state, *ShaderWorkgroupInput(state)); + return size[0] * size[1] * size[2]; +} + +std::uint32_t LinearWorkgroupId(SpirvEmitterState& state) { + const auto groups = [&](std::uint32_t component) { + const auto pointer = state.module.AllocateId(); + const auto value = state.module.AllocateId(); + state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassInput, TypeU32(state)), pointer, state.numWorkgroupsVariable, ConstantU32(state, component)); + state.module.AddFunction(spv::OpLoad, TypeU32(state), value, pointer); + return value; + }; + const auto group = [&](std::uint32_t component) { return EmitInputComponentU32(state, StageInputKind::WorkgroupId, component); }; + const auto z = group(2u); + const auto countY = groups(1u); + const auto y = group(1u); + const auto plane = EmitAddU32(state, EmitBinaryU32(state, spv::OpIMul, z, countY), y); + const auto countX = groups(0u); + return EmitAddU32(state, EmitBinaryU32(state, spv::OpIMul, plane, countX), group(0u)); +} + +} + +bool ScratchPoolActive(const SpirvEmitterState& state) { + return state.requirements.functionScratch && state.program.Resources().stage == IrShaderStage::Compute; +} + +std::uint32_t ScratchPoolSlotDwords(const SpirvEmitterState& state) { + const auto* workgroup = ShaderWorkgroupInput(state); + if (workgroup == nullptr) FailEmit("scratch pool needs a compute workgroup size"); + const auto size = WorkgroupLocalSize(state, *workgroup); + const auto invocations = size[0] * size[1] * size[2]; + const auto lanes = state.laneCount == 2u ? (invocations - 1u) + ((invocations - 1u) & ~31u) + 33u : invocations; + return lanes * state.program.Info().scratchDwords; +} + +void EmitScratchPoolAcquire(SpirvEmitterState& state) { + if (!ScratchPoolActive(state)) return; + if (state.scratchPoolVariable == 0u || state.numWorkgroupsVariable == 0u || state.scratchPoolCandidateVariable == 0u || state.scratchPoolWonVariable == 0u) { + FailEmit("scratch pool was not declared before function emission"); + } + const auto device = ConstantU32(state, spv::ScopeDevice); + const auto acquire = ConstantU32(state, spv::MemorySemanticsAcquireMask | spv::MemorySemanticsUniformMemoryMask); + const auto length = state.module.AllocateId(); + state.module.AddFunction(spv::OpArrayLength, TypeU32(state), length, state.scratchPoolVariable, 0u); + const auto slotDwords = ScratchPoolSlotDwords(state); + const auto slots = EmitBinaryU32(state, spv::OpUDiv, length, ConstantU32(state, slotDwords + HeaderWordsPerSlot)); + state.scratchPoolSlotCount = slots; + state.laneHalf = 0u; + const auto first = state.module.AllocateId(); + state.module.AddFunction(spv::OpIEqual, TypeBool(state), first, EmitLocalInvocationIndex(state), ConstantU32(state, 0u)); + const auto linear = LinearWorkgroupId(state); + EmitIfCondition(state, first, [&] { + state.module.AddFunction(spv::OpStore, state.scratchPoolCandidateVariable, EmitBinaryU32(state, spv::OpUMod, linear, slots)); + state.module.AddFunction(spv::OpStore, state.scratchPoolWonVariable, ConstantU32(state, 0u)); + const auto header = state.module.AllocateId(); + const auto body = state.module.AllocateId(); + const auto continuation = state.module.AllocateId(); + const auto merge = state.module.AllocateId(); + state.module.AddFunction(spv::OpBranch, header); + EmitLabel(state, header); + const auto won = LoadU32(state, state.scratchPoolWonVariable); + const auto pending = state.module.AllocateId(); + state.module.AddFunction(spv::OpIEqual, TypeBool(state), pending, won, ConstantU32(state, 0u)); + state.module.AddFunction(spv::OpLoopMerge, merge, continuation, spv::LoopControlMaskNone); + state.module.AddFunction(spv::OpBranchConditional, pending, body, merge); + EmitLabel(state, body); + const auto candidate = LoadU32(state, state.scratchPoolCandidateVariable); + const auto observed = state.module.AllocateId(); + state.module.AddFunction(spv::OpAtomicLoad, TypeU32(state), observed, PoolWord(state, candidate), device, ConstantU32(state, spv::MemorySemanticsMaskNone)); + EmitIfCondition(state, EmitCompareU32Constant(state, spv::OpIEqual, observed, 0u), [&] { + const auto previous = state.module.AllocateId(); + state.module.AddFunction(spv::OpAtomicCompareExchange, TypeU32(state), previous, PoolWord(state, candidate), device, acquire, acquire, ConstantU32(state, 1u), ConstantU32(state, 0u)); + state.module.AddFunction(spv::OpStore, state.scratchPoolWonVariable, Select(state, TypeU32(state), EmitCompareU32Constant(state, spv::OpIEqual, previous, 0u), ConstantU32(state, 1u), ConstantU32(state, 0u))); + }); + const auto acquired = EmitCompareU32Constant(state, spv::OpIEqual, LoadU32(state, state.scratchPoolWonVariable), 1u); + const auto next = EmitAddU32(state, candidate, ConstantU32(state, 1u)); + const auto atEnd = state.module.AllocateId(); + state.module.AddFunction(spv::OpIEqual, TypeBool(state), atEnd, next, slots); + const auto wrapped = Select(state, TypeU32(state), atEnd, ConstantU32(state, 0u), next); + state.module.AddFunction(spv::OpStore, state.scratchPoolCandidateVariable, Select(state, TypeU32(state), acquired, candidate, wrapped)); + state.module.AddFunction(spv::OpBranch, continuation); + EmitLabel(state, continuation); + state.module.AddFunction(spv::OpBranch, header); + EmitLabel(state, merge); + const auto slot = LoadU32(state, state.scratchPoolCandidateVariable); + state.module.AddFunction(spv::OpAtomicStore, PoolWord(state, EmitAddU32(state, slots, slot)), device, ConstantU32(state, 0u), ConstantU32(state, InvocationsPerGroup(state))); + state.module.AddFunction(spv::OpStore, state.scratchPoolSlotVariable, slot); + }); + state.module.AddFunction(spv::OpControlBarrier, ConstantU32(state, spv::ScopeWorkgroup), ConstantU32(state, spv::ScopeWorkgroup), ConstantU32(state, spv::MemorySemanticsAcquireReleaseMask | spv::MemorySemanticsUniformMemoryMask | spv::MemorySemanticsWorkgroupMemoryMask)); + state.scratchPoolSlot = LoadU32(state, state.scratchPoolSlotVariable); + const auto header = EmitBinaryU32(state, spv::OpIMul, slots, ConstantU32(state, HeaderWordsPerSlot)); + const auto slotBase = EmitAddU32(state, header, EmitBinaryU32(state, spv::OpIMul, state.scratchPoolSlot, ConstantU32(state, slotDwords))); + for (std::uint32_t half = 0; half < state.laneCount; half++) { + state.laneHalf = half; + state.scratchPoolLaneBase.at(half) = EmitAddU32(state, slotBase, EmitBinaryU32(state, spv::OpIMul, EmitLocalInvocationIndex(state), ConstantU32(state, state.program.Info().scratchDwords))); + } + state.laneHalf = 0u; +} + +std::uint32_t EmitScratchPoolElementPointer(SpirvEmitterState& state, std::uint32_t index) { + if (state.scratchPoolLaneBase.at(state.laneHalf) == 0u) FailEmit("scratch pool slot was not acquired before function emission"); + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpAccessChain, TypeStorageBufferElementPointer(state), pointer, state.scratchPoolVariable, ConstantU32(state, 0u), EmitAddU32(state, state.scratchPoolLaneBase.at(state.laneHalf), index)); + return pointer; +} + +void EmitReturnReleasingScratch(SpirvEmitterState& state) { + if (ScratchPoolActive(state)) { + const auto device = ConstantU32(state, spv::ScopeDevice); + const auto previous = state.module.AllocateId(); + state.module.AddFunction(spv::OpAtomicIDecrement, TypeU32(state), previous, PoolWord(state, EmitAddU32(state, state.scratchPoolSlotCount, state.scratchPoolSlot)), device, ConstantU32(state, spv::MemorySemanticsAcquireReleaseMask | spv::MemorySemanticsUniformMemoryMask)); + EmitIfCondition(state, EmitCompareU32Constant(state, spv::OpIEqual, previous, 1u), [&] { + state.module.AddFunction(spv::OpAtomicStore, PoolWord(state, state.scratchPoolSlot), device, ConstantU32(state, spv::MemorySemanticsReleaseMask | spv::MemorySemanticsUniformMemoryMask), ConstantU32(state, 0u)); + }); + state.scratchPoolReturns++; + } + state.module.AddFunction(spv::OpReturn); +} + +void VerifyScratchPoolReturns(const SpirvEmitterState& state, const std::vector& words) { + if (!ScratchPoolActive(state)) return; + std::uint32_t returns = 0u; + bool inMain = false; + for (std::size_t cursor = 5u; cursor < words.size();) { + const auto count = words[cursor] >> 16u; + const auto op = words[cursor] & 0xffffu; + if (count == 0u) FailEmit("malformed SPIR-V while checking scratch pool releases"); + if (op == spv::OpFunction) inMain = words[cursor + 2u] == state.mainFunc; + else if (op == spv::OpFunctionEnd) inMain = false; + else if (inMain && op == spv::OpReturn) returns++; + cursor += count; + } + if (returns != state.scratchPoolReturns) FailEmit("a compute exit does not release its scratch pool slot"); +} + +} diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index e9368332fd..e0628730b3 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -457,7 +457,7 @@ std::uint32_t PackExport(SpirvEmitterState& state, IrBufferFormat format, std::u return packed; } -std::uint32_t SelectExportPacking(SpirvEmitterState& state, std::uint32_t target, std::uint32_t color) { +std::uint32_t SelectExportPacking(SpirvEmitterState& state, std::uint32_t target, std::uint32_t color, bool half) { const auto packing = state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::ExportPackingBase + target, static_cast(ColorExportPacking::None)); const auto unorm = EmitCompareU32Constant(state, spv::OpIEqual, packing, static_cast(ColorExportPacking::Unorm10_11_11)); const auto floating = EmitCompareU32Constant(state, spv::OpIEqual, packing, static_cast(ColorExportPacking::Float11_11_10)); @@ -465,10 +465,16 @@ std::uint32_t SelectExportPacking(SpirvEmitterState& state, std::uint32_t target const auto wide = Select(state, TypeU32(state), floating, PackExport(state, IrBufferFormat::Format10_11_11Float, color), PackExport(state, IrBufferFormat::Format11_11_10UNorm, color)); const auto word = Select(state, TypeU32(state), small, PackExport(state, IrBufferFormat::Format4_4_4_4UNorm, color), wide); const auto packed = Binary(state, spv::OpLogicalOr, TypeBool(state), Binary(state, spv::OpLogicalOr, TypeBool(state), unorm, floating), small); + const auto limit = half ? Select(state, TypeU32(state), EmitCompareU32Constant(state, spv::OpIEqual, packing, static_cast(ColorExportPacking::UintClamp8)), ConstantU32(state, 0xffu), Select(state, TypeU32(state), EmitCompareU32Constant(state, spv::OpIEqual, packing, static_cast(ColorExportPacking::UintClamp16)), ConstantU32(state, 0xffffu), ConstantU32(state, 0xffffffffu))) : 0u; std::array components{}; for (std::uint32_t component = 0; component < components.size(); ++component) { - const auto unpacked = state.module.AllocateId(); + auto unpacked = state.module.AllocateId(); state.module.AddFunction(spv::OpCompositeExtract, TypeF32(state), unpacked, color, component); + if (half) { + const auto clamped = state.module.AllocateId(); + state.module.AddFunction(spv::OpExtInst, TypeU32(state), clamped, GlslStd450(state), GLSLstd450UMin, Unary(state, spv::OpBitcast, TypeU32(state), unpacked), limit); + unpacked = Unary(state, spv::OpBitcast, TypeF32(state), clamped); + } components[component] = Select(state, TypeF32(state), packed, component == 0u ? Unary(state, spv::OpBitcast, TypeF32(state), word) : ConstantF32(state, 0u), unpacked); } const auto result = state.module.AllocateId(); @@ -603,6 +609,17 @@ void StoreCoverage(SpirvEmitterState& state, std::uint32_t mask) { state.module.AddFunction(spv::OpStore, pointer, mask); } +void StoreCombinedCoverage(SpirvEmitterState& state, std::uint32_t coverage, std::uint32_t own, std::uint32_t other) { + if (own == 0u) { + StoreCoverage(state, coverage); + return; + } + state.module.AddFunction(spv::OpStore, own, coverage); + const auto loaded = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, TypeI32(state), loaded, other); + StoreCoverage(state, Binary(state, spv::OpBitwiseAnd, TypeI32(state), coverage, loaded)); +} + void StoreDitheredAlphaCoverage(SpirvEmitterState& state, std::uint32_t color) { constexpr std::array twoSampleMasks{{ 0x00000000u, 0x02000002u, 0x01000201u, 0x01020201u, 0x01020101u, 0x03020103u, 0x03010303u, 0x03030303u, @@ -644,7 +661,7 @@ void StoreDitheredAlphaCoverage(SpirvEmitterState& state, std::uint32_t color) { const auto pixel = Binary(state, spv::OpBitwiseOr, TypeU32(state), Binary(state, spv::OpShiftLeftLogical, TypeU32(state), parity[0], ConstantU32(state, 1u)), parity[1]); const auto shift = Binary(state, spv::OpShiftLeftLogical, TypeU32(state), pixel, ConstantU32(state, 3u)); const auto mask = Binary(state, spv::OpBitwiseAnd, TypeU32(state), Binary(state, spv::OpShiftRightLogical, TypeU32(state), packed, shift), ConstantU32(state, 255u)); - StoreCoverage(state, Unary(state, spv::OpBitcast, TypeI32(state), mask)); + StoreCombinedCoverage(state, Unary(state, spv::OpBitcast, TypeI32(state), mask), state.alphaCoverageVariable, state.exportedCoverageVariable); } void StoreAlphaCoverage(SpirvEmitterState& state, std::uint32_t color) { @@ -658,7 +675,7 @@ void StoreAlphaCoverage(SpirvEmitterState& state, std::uint32_t color) { state.module.AddFunction(spv::OpFUnordLessThan, TypeBool(state), below, alpha, ConstantF32(state, 0x3f000000u)); const auto mask = state.module.AllocateId(); state.module.AddFunction(spv::OpSelect, TypeI32(state), mask, below, ConstantI32(state, 0), ConstantI32(state, -1)); - StoreCoverage(state, mask); + StoreCombinedCoverage(state, mask, state.alphaCoverageVariable, state.exportedCoverageVariable); } std::uint32_t ExportRawComponent(SpirvValueEmitContext& ctx, std::uint32_t vector, std::uint32_t component) { @@ -667,11 +684,30 @@ std::uint32_t ExportRawComponent(SpirvValueEmitContext& ctx, std::uint32_t vecto return value; } +std::uint32_t WidenedHalfWord(SpirvEmitterState& state, std::uint32_t half) { + const auto u32 = TypeU32(state); + const auto op = [&](spv::Op opcode, std::uint32_t lhs, std::uint32_t rhs) { return Binary(state, opcode, u32, lhs, rhs); }; + const auto is = [&](std::uint32_t value, std::uint32_t constant) { return Binary(state, spv::OpIEqual, TypeBool(state), value, ConstantU32(state, constant)); }; + const auto sign = op(spv::OpShiftLeftLogical, op(spv::OpBitwiseAnd, half, ConstantU32(state, 0x8000u)), ConstantU32(state, 16u)); + const auto exponent = op(spv::OpBitwiseAnd, op(spv::OpShiftRightLogical, half, ConstantU32(state, 10u)), ConstantU32(state, 0x1fu)); + const auto mantissa = op(spv::OpBitwiseAnd, half, ConstantU32(state, 0x3ffu)); + const auto fraction = op(spv::OpShiftLeftLogical, mantissa, ConstantU32(state, 13u)); + const auto normal = op(spv::OpBitwiseOr, op(spv::OpShiftLeftLogical, op(spv::OpIAdd, exponent, ConstantU32(state, 127u - 15u)), ConstantU32(state, 23u)), fraction); + const auto quiet = Select(state, u32, is(mantissa, 0u), ConstantU32(state, 0u), ConstantU32(state, 0x400000u)); + const auto special = op(spv::OpBitwiseOr, op(spv::OpBitwiseOr, ConstantU32(state, 0x7f800000u), fraction), quiet); + const auto msb = state.module.AllocateId(); + state.module.AddFunction(spv::OpExtInst, u32, msb, GlslStd450(state), GLSLstd450FindUMsb, op(spv::OpBitwiseOr, mantissa, ConstantU32(state, 1u))); + const auto subnormal = op(spv::OpBitwiseOr, op(spv::OpShiftLeftLogical, op(spv::OpIAdd, msb, ConstantU32(state, 127u - 24u)), ConstantU32(state, 23u)), op(spv::OpBitwiseAnd, op(spv::OpShiftLeftLogical, mantissa, op(spv::OpISub, ConstantU32(state, 23u), msb)), ConstantU32(state, 0x7fffffu))); + const auto small = Select(state, u32, is(mantissa, 0u), ConstantU32(state, 0u), subnormal); + return op(spv::OpBitwiseOr, sign, Select(state, u32, is(exponent, 0u), small, Select(state, u32, is(exponent, 0x1fu), special, normal))); +} + std::uint32_t ExportVector(SpirvValueEmitContext& ctx, std::uint32_t data, const ExportInfo& exp, bool uintOutput) { auto& state = ctx.state; if (exp.compr && !uintOutput) { const auto mode = MrtOutputMode(state, exp); const auto unpack = mode == 5u ? GLSLstd450UnpackUnorm2x16 : mode == 6u ? GLSLstd450UnpackSnorm2x16 : GLSLstd450UnpackHalf2x16; + const auto words = mode == 4u ? EmitCompareU32Constant(state, spv::OpUGreaterThanEqual, state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::ExportPackingBase + exp.index, static_cast(ColorExportPacking::None)), static_cast(ColorExportPacking::Uint)) : 0u; std::array f32 {ConstantF32(state, 0u), ConstantF32(state, 0u), ConstantF32(state, 0u), ConstantF32(state, 0x3f800000u)}; for (std::uint32_t pair = 0; pair < 2u; pair++) { if ((exp.en & (3u << (pair * 2u))) == 0u) { @@ -686,6 +722,11 @@ std::uint32_t ExportVector(SpirvValueEmitContext& ctx, std::uint32_t data, const if (((exp.en >> component) & 1u) != 0u) { f32.at(component) = state.module.AllocateId(); state.module.AddFunction(spv::OpCompositeExtract, TypeF32(state), f32.at(component), unpacked, lane); + if (words != 0u) { + const auto half = state.module.AllocateId(); + state.module.AddFunction(spv::OpBitFieldUExtract, TypeU32(state), half, packed, ConstantU32(state, lane * 16u), ConstantU32(state, 16u)); + f32.at(component) = Select(state, TypeF32(state), words, Unary(state, spv::OpBitcast, TypeF32(state), WidenedHalfWord(state, half)), f32.at(component)); + } } } } @@ -1810,7 +1851,10 @@ void EmitProgram(SpirvEmitterState& state) { if (state.requirements.functionLds && !state.pixelFunctionLdsBuffer) { state.module.AddFunction(spv::OpVariable, TypeU32ArrayPointer(state, spv::StorageClassFunction, LdsStorageDwords(state)), state.ldsVariable, spv::StorageClassFunction); } - if (state.requirements.functionScratch) { + if (ScratchPoolActive(state)) { + state.module.AddFunction(spv::OpVariable, TypePointer(state, spv::StorageClassFunction, TypeU32(state)), state.scratchPoolCandidateVariable, spv::StorageClassFunction); + state.module.AddFunction(spv::OpVariable, TypePointer(state, spv::StorageClassFunction, TypeU32(state)), state.scratchPoolWonVariable, spv::StorageClassFunction); + } else if (state.requirements.functionScratch) { for (std::uint32_t half = 0; half < state.laneCount; half++) { state.module.AddFunction(spv::OpVariable, TypeU32ArrayPointer(state, spv::StorageClassFunction, state.program.Info().scratchDwords), state.scratchVariable.at(half), spv::StorageClassFunction); } @@ -1841,6 +1885,7 @@ void EmitProgram(SpirvEmitterState& state) { } } EmitLdsBufferBase(state); + EmitScratchPoolAcquire(state); if (state.requirements.ldsLock) { const auto firstInvocation = state.module.AllocateId(); state.module.AddFunction(spv::OpIEqual, TypeBool(state), firstInvocation, EmitLocalInvocationIndex(state), ConstantU32(state, 0u)); @@ -1868,7 +1913,12 @@ void EmitProgram(SpirvEmitterState& state) { for (std::uint32_t pixel = 3u; pixel-- > 0u;) selected = Select(state, TypeU32(state), EmitCompareU32Constant(state, spv::OpIEqual, quad, pixel), word(pixel), selected); state.sampleOrderWord = selected; } - if (AlphaToCoverage(state)) StoreCoverage(state, ConstantI32(state, -1)); + if (AlphaToCoverage(state)) { + StoreCoverage(state, ConstantI32(state, -1)); + for (const auto variable : {state.alphaCoverageVariable, state.exportedCoverageVariable}) { + if (variable != 0u) state.module.AddFunction(spv::OpStore, variable, ConstantI32(state, -1)); + } + } EmitBdaAnchors(state); if (state.pointSizeVariable != 0u && state.inputInfo.vertex != nullptr && state.inputInfo.vertex->pointList) { const auto& vertex = *state.inputInfo.vertex; @@ -1949,7 +1999,7 @@ void EmitProgram(SpirvEmitterState& state) { state.module.AddFunction(spv::OpKill); EmitLabel(state, mergeLabel); } - state.module.AddFunction(spv::OpReturn); + EmitReturnReleasingScratch(state); } else { StructuredFunctionState functionState; EmitControlFlow(ctx, functionState, program); @@ -2222,7 +2272,7 @@ void EmitSetAttribute(SpirvValueEmitContext& ctx, const IrValue& inst) { const auto raw = ExportRawComponent(ctx, data, 2); const auto value = state.module.AllocateId(); state.module.AddFunction(spv::OpBitcast, TypeI32(state), value, raw); - StoreCoverage(state, value); + StoreCombinedCoverage(state, value, state.exportedCoverageVariable, state.alphaCoverageVariable); } return; } @@ -2257,7 +2307,7 @@ void EmitSetAttribute(SpirvValueEmitContext& ctx, const IrValue& inst) { if (uintOutput || mode == 5u || mode == 6u) FailEmit(std::string(merge->blend.enabled ? "blending into an unclamped normalized color target" : "a partial write of a packed color target") + " cannot take an integer or 16-bit normalized export"); MergeColorTarget(state, static_cast(merge - merges.begin()), value); } - if (!uintOutput && mode != 5u && mode != 6u) value = SelectExportPacking(state, exp.index, value); + if (!uintOutput && mode != 5u && mode != 6u) value = SelectExportPacking(state, exp.index, value, mode == 4u); } if (exp.kind == ExportTargetKind::Position) { if (state.inputInfo.vertex == nullptr) { diff --git a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp index 52528646d8..8b210a30ec 100644 --- a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp +++ b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp @@ -88,7 +88,7 @@ private: IrF32 flushTinyProduct(IrValue* lhs, IrValue* rhs, IrValue* product, IrValue* addend = nullptr, bool afterRounding = true); IrU32 quietNan32(IrU32 bits); IrU32 quietNan16(IrU32 bits); - IrValue* nanResultF32(std::initializer_list sources, IrValue* result, IrValue* invalidProduct = nullptr); + IrValue* nanResultF32(std::initializer_list sources, IrValue* result, IrValue* invalidProduct = nullptr, bool multiplication = false); IrValue& invalidProductF32(IrValue* lhs, IrValue* rhs); IrValue& invalidOperationF32(std::initializer_list sources, IrValue& invalidProduct); std::array quietNan64(const std::array& bits); diff --git a/core/shader/recompiler/Translation/src/CompareInstructions.cpp b/core/shader/recompiler/Translation/src/CompareInstructions.cpp index a562ccc2d0..6d55ce0862 100644 --- a/core/shader/recompiler/Translation/src/CompareInstructions.cpp +++ b/core/shader/recompiler/Translation/src/CompareInstructions.cpp @@ -86,7 +86,8 @@ void TranslationContext::emitCompareResult(const RdnaInstruction& inst, IrU1 val } IrValue& masked = ir.LogicalAnd(ir.GetExec(), value.Value()); if (cmpx) { - const std::array mask = ballotMask(IrU1(masked)); + const bool writesDestination = inst.destination2.kind != RdnaOperandKind::Unknown && inst.destination2.kind != RdnaOperandKind::Null; + const std::array mask = writesDestination ? writeMask(inst.destination2, IrU1(masked)) : ballotMask(IrU1(masked)); ir.SetExec(masked); ir.SetExecLo(mask[0].Value()); ir.SetExecHi(mask[1].Value()); diff --git a/core/shader/recompiler/Translation/src/FloatInstructions.cpp b/core/shader/recompiler/Translation/src/FloatInstructions.cpp index 4364ddcc51..10c65e6179 100644 --- a/core/shader/recompiler/Translation/src/FloatInstructions.cpp +++ b/core/shader/recompiler/Translation/src/FloatInstructions.cpp @@ -655,7 +655,7 @@ bool TranslationContext::floatBinary(const RdnaInstruction& inst, IrOpcode opcod } IrValue* result = &ir.Emit(opcode, IrOpcodeType(opcode), {args[0], args[1]}); if (opcode == IrOpcode::FPMul32) result = &flushTinyProduct(args[0], args[1], result).Value(); - if (opcode == IrOpcode::FPAdd32 || opcode == IrOpcode::FPMul32) result = nanResultF32({args[0], args[1]}, result); + if (opcode == IrOpcode::FPAdd32 || opcode == IrOpcode::FPMul32) result = nanResultF32({args[0], args[1]}, result, nullptr, opcode == IrOpcode::FPMul32); if (opcode == IrOpcode::FPSub32) { IrValue* negated = &ir.BitCastF32(ir.BitwiseXor(ir.BitCastU32(*args[1]), ir.Constant(0x80000000u))); result = reverse ? nanResultF32({negated, args[0]}, result) : nanResultF32({args[0], negated}, result); diff --git a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp index 9d507754c6..3bac17290d 100644 --- a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp +++ b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp @@ -296,9 +296,12 @@ void emitEntryPrologue(IrProgram& program, IrBlock& entryBlock, const TranslateO const auto unlessRestarted = [&](IrValue& offset) -> IrValue& { return strip ? entryIr.Select(entryIr.ULessThan(stripPosition, stripStart), entryIr.IMul(vertex, item), offset) : offset; }; - IrValue& first = fan ? entryIr.IMul(entryIr.IAdd(vertex, u32(1u)), item) : unlessRestarted(entryIr.IMul(entryIr.IAdd(vertex, parity), item)); - IrValue& second = fan ? entryIr.IMul(entryIr.IAdd(vertex, u32(2u)), item) : size >= 2u ? unlessRestarted(entryIr.IMul(entryIr.ISub(entryIr.IAdd(vertex, u32(1u)), parity), item)) : u32(0u); - IrValue& third = fan ? u32(0u) : size == 3u ? unlessRestarted(entryIr.IMul(entryIr.IAdd(vertex, u32(2u)), item)) : u32(0u); + const bool last = mesh.provokingVertex != 0u; + IrValue& fanSecond = entryIr.IMul(entryIr.IAdd(vertex, u32(1u)), item); + IrValue& fanThird = entryIr.IMul(entryIr.IAdd(vertex, u32(2u)), item); + IrValue& first = fan ? (last ? u32(0u) : fanSecond) : unlessRestarted(entryIr.IMul(last ? entryIr.IAdd(vertex, parity) : vertex, item)); + IrValue& second = fan ? (last ? fanSecond : fanThird) : size >= 2u ? unlessRestarted(entryIr.IMul(last ? entryIr.ISub(entryIr.IAdd(vertex, u32(1u)), parity) : entryIr.IAdd(entryIr.IAdd(vertex, u32(1u)), parity), item)) : u32(0u); + IrValue& third = fan ? (last ? fanThird : u32(0u)) : size == 3u ? unlessRestarted(entryIr.IMul(last ? entryIr.IAdd(vertex, u32(2u)) : entryIr.ISub(entryIr.IAdd(vertex, u32(2u)), parity), item)) : u32(0u); setVector(static_cast(0), entryIr.BitwiseOr(entryIr.BitwiseAnd(first, u32(0xffffu)), entryIr.ShiftLeftLogical(second, u32(16u)))); setVector(static_cast(1), entryIr.BitwiseAnd(third, u32(0xffffu))); setVector(static_cast(2), entryIr.IAdd(firstPrimitive, local)); diff --git a/core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp b/core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp index 4677a173cf..42468ccf48 100644 --- a/core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp +++ b/core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp @@ -342,11 +342,17 @@ IrU32 TranslationContext::quietNan32(IrU32 bits) { return ieeeMode ? IrU32(ir.BitwiseOr(bits.Value(), ir.Constant(0x00400000u))) : bits; } -IrValue* TranslationContext::nanResultF32(std::initializer_list sources, IrValue* result, IrValue* invalidProduct) { +IrValue* TranslationContext::nanResultF32(std::initializer_list sources, IrValue* result, IrValue* invalidProduct, bool multiplication) { if (!ExactF32Arithmetic()) return result; const auto isNan = [&](IrValue& bits) -> IrValue& { return ir.UGreaterThan(ir.BitwiseAnd(bits, ir.Constant(0x7fffffffu)), ir.Constant(0x7f800000u)); }; IrValue* bits = &ir.BitCastU32(*result); - bits = &ir.Select(isNan(*bits), ir.Constant(0xffc00000u), *bits); + const bool normalFactor = multiplication && std::any_of(sources.begin(), sources.end(), [](const IrValue* source) { + const IrValue* constant = constantF32Bits(*source); + if (constant == nullptr) return false; + const std::uint32_t exponent = (constant->ImmediateU32() >> 23u) & 0xffu; + return exponent != 0u && exponent != 0xffu; + }); + if (!normalFactor) bits = &ir.Select(isNan(*bits), ir.Constant(0xffc00000u), *bits); for (auto source = sources.end(); source != sources.begin();) { --source; IrValue& sourceBits = ir.BitCastU32(**source); diff --git a/core/shader/recompiler/tests/RequestSerializerTests.cpp b/core/shader/recompiler/tests/RequestSerializerTests.cpp index 58f91c2f36..135e1969a1 100644 --- a/core/shader/recompiler/tests/RequestSerializerTests.cpp +++ b/core/shader/recompiler/tests/RequestSerializerTests.cpp @@ -316,7 +316,7 @@ int main() { Require(request.graphics->draw.indexAddress == 0xabcdef3400u && request.graphics->draw.indexCount == 57u && request.graphics->draw.indexElementBytes == 4u && request.graphics->draw.instanceCount == 9u); Require(request.target.float16DenormPreserve == preserve); auto upgraded = bytes; - upgraded[4] = 51u; + upgraded[4] = 53u; InsertMeshBarycentricFlag(upgraded); if (fixture.pixel) upgraded.insert(upgraded.begin() + 258u, {0u, 0u, 0u}); if (fixture.pixel) InsertPixelDefaultsAfterFrontFace(upgraded); @@ -327,6 +327,8 @@ int main() { upgraded.insert(upgraded.end(), {0u, 0x10u, 0u, 0u}); upgraded.push_back(0u); upgraded.push_back(1u); + upgraded.push_back(0u); + upgraded.push_back(0u); Require(serializer.Serialize(request) == EncodeBase64(upgraded)); auto truncated = bytes; truncated.pop_back(); @@ -361,7 +363,7 @@ int main() { layerRejected = std::string_view(error.what()) == "RequestSerializer: truncated data"; } Require(layerRejected); - layered[4] = 51u; + layered[4] = 53u; InsertMeshBarycentricFlag(layered); layered.insert(layered.begin() + 259u, {0u, 0u}); InsertPixelDefaultsAfterFrontFace(layered); @@ -372,6 +374,8 @@ int main() { layered.insert(layered.end(), {0u, 0x10u, 0u, 0u}); layered.push_back(0u); layered.push_back(1u); + layered.push_back(0u); + layered.push_back(0u); Require(serializer.Serialize(layerReplay.request) == EncodeBase64(layered)); } } @@ -392,7 +396,7 @@ int main() { Require(replay.request.context.pixel->rasterPrimitive == ShaderRecompiler::RasterPrimitive::Rectangle); Require(replay.request.context.vertex->paClVsOutCntl == 0x00050a05u); Require(replay.request.target.float16DenormPreserve == preserve); - bytes[4] = 51u; + bytes[4] = 53u; InsertMeshBarycentricFlag(bytes); bytes.insert(bytes.begin() + 259u, {0u, 0u}); InsertPixelDefaultsAfterFrontFace(bytes); @@ -402,6 +406,8 @@ int main() { bytes.insert(bytes.end(), {0u, 0x10u, 0u, 0u}); bytes.push_back(0u); bytes.push_back(1u); + bytes.push_back(0u); + bytes.push_back(0u); Require(serializer.Serialize(replay.request) == EncodeBase64(bytes)); auto truncated23 = DecodeBase64(encoded23); @@ -527,7 +533,7 @@ int main() { if (fixture.pixel) main38.insert(main38.begin() + 259u, {0u, 0u}); if (fixture.pixel) InsertPixelDefaultsAfterFrontFace(main38, false, false); auto upgraded = main38; - upgraded[4] = 51u; + upgraded[4] = 53u; if (fixture.pixel) { const auto tail = PixelTailDefaults(true, true); upgraded.insert(upgraded.begin() + 317u, tail.begin(), tail.end()); @@ -540,6 +546,8 @@ int main() { upgraded.insert(upgraded.end(), {0u, 0x10u, 0u, 0u}); upgraded.push_back(0u); upgraded.push_back(1u); + upgraded.push_back(0u); + upgraded.push_back(0u); Require(serializer.Serialize(request) == EncodeBase64(upgraded)); const auto replay38 = serializer.Deserialize(EncodeBase64(main38)); Require(replay38.request.graphics.has_value() && replay38.request.graphics->tessellation.has_value() == fixture.tessellation); @@ -555,8 +563,14 @@ int main() { pointed.context.vertex->pointSize = 3.0f; pointed.context.vertex->pointSizeMin = 1.0f; pointed.context.vertex->pointSizeMax = 8.0f; - auto version51 = DecodeBase64(serializer.Serialize(pointed)); - Require(version51.size() > 17u && version51[4] == 51u); + auto version53 = DecodeBase64(serializer.Serialize(pointed)); + Require(version53.size() > 19u && version53[4] == 53u && version53.back() == 0u && version53[version53.size() - 2u] == 0u); + auto version52 = version53; + version52.pop_back(); + version52[4] = 52u; + auto version51 = version52; + version51.pop_back(); + version51[4] = 51u; auto version48 = version51; version48.pop_back(); version48[4] = 48u; @@ -576,7 +590,7 @@ int main() { version43.pop_back(); version43[4] = 43u; const auto replay43 = serializer.Deserialize(EncodeBase64(version43)); - Require(serializer.Serialize(replay43.request) == EncodeBase64(version51)); + Require(serializer.Serialize(replay43.request) == EncodeBase64(version53)); Require(replay43.request.graphics->tessellation->mergedHull == merged && replay43.request.context.vertex->pointList && replay43.request.context.vertex->pointSize == 3.0f && replay43.request.context.vertex->pointSizeMin == 1.0f && replay43.request.context.vertex->pointSizeMax == 8.0f); auto version42 = version43; version42.resize(version42.size() - 13u); @@ -603,7 +617,7 @@ int main() { const auto replay25 = serializer.Deserialize(EncodeBase64(main25)); Require(!fixture.pixel || (replay25.request.context.pixel->layerExported == layer && replay25.request.context.pixel->ditheredAlphaToCoverage == dither && replay25.request.context.pixel->reversedBlend == 0u)); Require(replay25.request.target.float16DenormPreserve == preserve && (!fixture.tessellation || replay25.request.graphics->tessellation->primitiveGroupSize == 37u)); - main25[4] = 51u; + main25[4] = 53u; InsertMeshBarycentricFlag(main25); if (fixture.pixel) { main25.insert(main25.begin() + 260u, 0u); @@ -616,6 +630,8 @@ int main() { main25.insert(main25.end(), {0u, 0x10u, 0u, 0u}); main25.push_back(0u); main25.push_back(1u); + main25.push_back(0u); + main25.push_back(0u); Require(serializer.Serialize(replay25.request) == EncodeBase64(main25)); } } @@ -633,8 +649,10 @@ int main() { const auto replay = serializer.Deserialize(encoded); Require(replay.request.target.float16DenormPreserve == preserve && replay.request.target.float32FlushToZero == !preserve && replay.request.target.float32DenormPreserve == preserve && replay.request.target.largePixelFunctionLdsBuffer == !preserve && replay.request.target.pixelFunctionStorageDwords == request.target.pixelFunctionStorageDwords); auto bytes = DecodeBase64(encoded); - Require(bytes.size() >= 8u && bytes[4] == 51u && bytes[5] == 0u && bytes[6] == 0u && bytes[7] == 0u); + Require(bytes.size() >= 8u && bytes[4] == 53u && bytes[5] == 0u && bytes[6] == 0u && bytes[7] == 0u); auto version40 = bytes; + version40.pop_back(); + version40.pop_back(); version40.resize(version40.size() - sizeof(std::uint32_t)); version40.pop_back(); version40.pop_back(); @@ -645,7 +663,7 @@ int main() { const auto replay40 = serializer.Deserialize(EncodeBase64(version40)); Require(!replay40.request.target.float32DenormPreserve && replay40.request.target.float32FlushToZero == !preserve); auto future = bytes; - future[4] = 52u; + future[4] = 54u; bool futureRejected = false; try { static_cast(serializer.Deserialize(EncodeBase64(future))); @@ -691,6 +709,8 @@ int main() { const auto orderOffset = std::search(version42.begin(), version42.end(), pixel.sampleLocations.begin(), pixel.sampleLocations.end()) - version42.begin() + static_cast(pixel.sampleLocations.size() + 1u); Require(version42.end() - version42.begin() > orderOffset + static_cast(sizeof(pixel.sampleOrder)) && version42[static_cast(orderOffset)] == 0x10u); EraseDefaultTargetMerge(version42, orderOffset + static_cast(sizeof(pixel.sampleOrder))); + version42.pop_back(); + version42.pop_back(); version42.resize(version42.size() - 4u); version42.pop_back(); version42.pop_back(); @@ -714,6 +734,8 @@ int main() { Require(serializer.Deserialize(serializer.Serialize(request)).request.target.spannedWaves == spanned); request.target.float32FlushToZero = true; auto version35 = DecodeBase64(serializer.Serialize(request)); + version35.pop_back(); + version35.pop_back(); version35.resize(version35.size() - sizeof(std::uint32_t)); version35.pop_back(); version35.pop_back(); @@ -765,7 +787,7 @@ int main() { for (const std::uint8_t version : {32u, 33u, 34u, 35u, 36u, 37u, 38u}) { pointRequest.target.spannedWaves = spanned; auto legacy = DecodeBase64(serializer.Serialize(pointRequest)); - legacy.resize(legacy.size() - (version >= 36u ? 22u : version >= 35u ? 23u : 28u)); + legacy.resize(legacy.size() - (version >= 36u ? 24u : version >= 35u ? 25u : 30u)); legacy[4] = version; const auto replayLegacy = serializer.Deserialize(EncodeBase64(legacy)); Require(replayLegacy.request.target.spannedWaves == spanned); @@ -777,7 +799,7 @@ int main() { Require(!replayLegacy.request.context.vertex->pointList && replayLegacy.request.context.vertex->pointSize == 1.0f); Require(replayLegacy.request.context.vertex->pointSizeMin == 0.0f && replayLegacy.request.context.vertex->pointSizeMax == 0.0f); const auto upgradedLegacy = DecodeBase64(serializer.Serialize(replayLegacy.request)); - Require(upgradedLegacy[4] == 51u && upgradedLegacy.size() == legacy.size() + (version >= 36u ? 22u : version >= 35u ? 23u : 28u)); + Require(upgradedLegacy[4] == 53u && upgradedLegacy.size() == legacy.size() + (version >= 36u ? 24u : version >= 35u ? 25u : 30u)); } } @@ -788,6 +810,8 @@ int main() { auto legacy43 = DecodeBase64(serializer.Serialize(pointRequest)); const auto mergeOffset = std::search(legacy43.begin(), legacy43.end(), pixel.sampleLocations.begin(), pixel.sampleLocations.end()) - legacy43.begin() + static_cast(pixel.sampleLocations.size() + 1u + sizeof(pixel.sampleOrder)); EraseDefaultTargetMerge(legacy43, mergeOffset); + legacy43.pop_back(); + legacy43.pop_back(); legacy43.resize(legacy43.size() - 4u); legacy43.pop_back(); legacy43.pop_back(); @@ -805,7 +829,7 @@ int main() { pixel.sampleOrder = {}; pointRequest.context.pixel = pixel; auto legacy34 = DecodeBase64(serializer.Serialize(pointRequest)); - legacy34.resize(legacy34.size() - 28u); + legacy34.resize(legacy34.size() - 30u); const auto stencil34 = std::search(legacy34.begin(), legacy34.end(), pixel.sampleLocations.begin(), pixel.sampleLocations.begin() + 4) + pixel.sampleLocations.size(); const auto legacyPixelTail = static_cast(1u + sizeof(pixel.sampleOrder)); Require(legacy34.end() - stencil34 > legacyPixelTail && std::all_of(stencil34, stencil34 + legacyPixelTail, [](std::uint8_t value) { return value == 0u; })); @@ -820,7 +844,9 @@ int main() { request.target.fusedExactArithmetic = fused; request.target.largePixelFunctionLdsBuffer = false; auto bytes = DecodeBase64(serializer.Serialize(request)); - Require(serializer.Deserialize(EncodeBase64(bytes)).request.target.fusedExactArithmetic == fused && bytes[bytes.size() - 8u] == (fused ? 1u : 0u) && bytes[bytes.size() - 7u] == 0u); + Require(serializer.Deserialize(EncodeBase64(bytes)).request.target.fusedExactArithmetic == fused && bytes[bytes.size() - 10u] == (fused ? 1u : 0u) && bytes[bytes.size() - 9u] == 0u); + bytes.pop_back(); + bytes.pop_back(); bytes.pop_back(); bytes.pop_back(); const auto fusedMerge = std::search(bytes.begin(), bytes.end(), pixel.sampleLocations.begin(), pixel.sampleLocations.end()) - bytes.begin() + static_cast(pixel.sampleLocations.size() + 1u + sizeof(pixel.sampleOrder)); @@ -864,6 +890,8 @@ int main() { auto version46Dither = twoSample; version46Dither.pop_back(); version46Dither.pop_back(); + version46Dither.pop_back(); + version46Dither.pop_back(); version46Dither[4] = 46u; Require(rejectsDither(version46Dither)); version46Dither[ditherByte] = 1u; @@ -878,9 +906,15 @@ int main() { domain.target.tessellationEvaluationSubgroups = subgroups; const auto encoded = serializer.Serialize(domain); auto bytes = DecodeBase64(encoded); - Require(bytes.size() > 8u && bytes[4] == 51u && bytes.back() == (subgroups ? 1u : 0u)); + Require(bytes.size() > 9u && bytes[4] == 53u && bytes.back() == 0u && bytes[bytes.size() - 2u] == 0u && bytes[bytes.size() - 3u] == (subgroups ? 1u : 0u)); Require(serializer.Deserialize(encoded).request.target.tessellationEvaluationSubgroups == subgroups); bytes.pop_back(); + bytes[4] = 52u; + Require(serializer.Deserialize(EncodeBase64(bytes)).request.target.tessellationEvaluationSubgroups == subgroups); + bytes.pop_back(); + bytes[4] = 51u; + Require(serializer.Deserialize(EncodeBase64(bytes)).request.target.tessellationEvaluationSubgroups == subgroups); + bytes.pop_back(); bool truncatedRejected = false; try { static_cast(serializer.Deserialize(EncodeBase64(bytes))); @@ -892,8 +926,10 @@ int main() { const auto replay49 = serializer.Deserialize(EncodeBase64(bytes)); Require(replay49.request.shader.stage == ShaderRecompiler::ShaderStage::TessellationEvaluation && replay49.request.target.tessellationEvaluationSubgroups); auto upgraded = bytes; - upgraded[4] = 51u; + upgraded[4] = 53u; upgraded.push_back(1u); + upgraded.push_back(0u); + upgraded.push_back(0u); Require(serializer.Serialize(replay49.request) == EncodeBase64(upgraded)); for (auto skipped : {bytes, upgraded}) { skipped[4] = 50u; @@ -906,6 +942,32 @@ int main() { Require(skippedRejected); } } + std::string hostFirstEncoded[2]; + for (const bool hostFirst : {false, true}) { + request.target.hostInterpolationFirst = hostFirst; + hostFirstEncoded[hostFirst] = serializer.Serialize(request); + const auto replay = serializer.Deserialize(hostFirstEncoded[hostFirst]); + Require(replay.request.target.hostInterpolationFirst == hostFirst && serializer.Serialize(replay.request) == hostFirstEncoded[hostFirst]); + auto bytes = DecodeBase64(hostFirstEncoded[hostFirst]); + Require(bytes[4] == 53u && bytes.back() == 0u && bytes[bytes.size() - 2u] == (hostFirst ? 1u : 0u)); + bytes.pop_back(); + bytes[4] = 52u; + Require(serializer.Deserialize(EncodeBase64(bytes)).request.target.hostInterpolationFirst == hostFirst); + bytes.pop_back(); + bytes[4] = 51u; + Require(!serializer.Deserialize(EncodeBase64(bytes)).request.target.hostInterpolationFirst); + } + Require(hostFirstEncoded[0] != hostFirstEncoded[1]); + request.target.hostInterpolationFirst = false; + for (const bool minLod : {false, true}) { + request.target.shaderImageMinLod = minLod; + auto bytes = DecodeBase64(serializer.Serialize(request)); + Require(serializer.Deserialize(EncodeBase64(bytes)).request.target.shaderImageMinLod == minLod && bytes[4] == 53u && bytes.back() == (minLod ? 1u : 0u)); + bytes.pop_back(); + bytes[4] = 52u; + Require(!serializer.Deserialize(EncodeBase64(bytes)).request.target.shaderImageMinLod); + } + request.target.shaderImageMinLod = false; return 0; } diff --git a/demo/web/tests/archive-root.test.mjs b/demo/web/tests/archive-root.test.mjs index b7467885f0..e598bd8a13 100644 --- a/demo/web/tests/archive-root.test.mjs +++ b/demo/web/tests/archive-root.test.mjs @@ -219,7 +219,7 @@ for (const directories of [[], parents, [...parents].reverse()]) { const { downloadLibraries } = await import(pathToFileURL(path.join(process.argv[2], "worker.mjs"))); const bytes = Uint8Array.from({ length: 32 }, (_, index) => index); const sha256 = [...new Uint8Array(await crypto.subtle.digest("SHA-256", bytes))].map(byte => byte.toString(16).padStart(2, "0")).join(""); - const asset = { path: "libraries/linux-1234.zip", size: bytes.length, expandedBytes: 100, sha256 }; + const asset = { path: `libraries/linux-${sha256}.zip`, size: bytes.length, expandedBytes: 100, sha256 }; let cancelled = false; let pulled = 0; const configureDownload = (chunks, size = bytes.length) => { diff --git a/demo/web/tests/archive.test.mjs b/demo/web/tests/archive.test.mjs index 9b798f7798..4448c6f651 100644 --- a/demo/web/tests/archive.test.mjs +++ b/demo/web/tests/archive.test.mjs @@ -210,10 +210,10 @@ globalThis.self = { postMessage: message => workerMessages.push(message) }; const worker = await import(site("worker.mjs")); const libraryArchive = await zip([["libs/system.prx", new Uint8Array([1, 2, 3])]]); const libraryBytes = new Uint8Array(await libraryArchive.arrayBuffer()); -const libraryPath = "libraries/windows-acde1234.zip"; const librarySha256 = [...new Uint8Array(await crypto.subtle.digest("SHA-256", libraryBytes))] .map(byte => byte.toString(16).padStart(2, "0")) .join(""); +const libraryPath = `libraries/windows-${librarySha256}.zip`; const release = { tag: "v1", platforms: { diff --git a/demo/web/tests/worker-manifest-fixture.mjs b/demo/web/tests/worker-manifest-fixture.mjs new file mode 100644 index 0000000000..d516cf32ab --- /dev/null +++ b/demo/web/tests/worker-manifest-fixture.mjs @@ -0,0 +1,61 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import vm from "node:vm"; +import { webcrypto } from "node:crypto"; + +const bytes = new Uint8Array([1, 2, 3]); +const digest = Buffer.from(await webcrypto.subtle.digest("SHA-256", bytes)).toString("hex"); +const valid = target => ({ tag: "v1.2.3", platforms: { [target]: { size: 3, expandedBytes: 100, sha256: digest, path: `libraries/${target}-${digest}.zip` } } }); + +async function load(release, options = {}) { + const requests = []; + let cancelled = false; + const context = vm.createContext({ console, Uint8Array, Blob, ReadableStream, WritableStream, performance, crypto: webcrypto, + self: { postMessage() {} }, fetch: async url => { + requests.push(url); + if (url === "./release.json") return { ok: true, json: async () => release }; + if (url.startsWith("https:")) return options.rateLimit ? { ok: false, status: 403, headers: new Map([["x-ratelimit-remaining", "0"]]) } : { ok: true, json: async () => ({ tag_name: release.tag }) }; + const contents = options.contents ?? bytes; + return { ok: true, body: new ReadableStream({ start(controller) { controller.enqueue(contents); if (!options.keepOpen) controller.close(); }, cancel() { cancelled = true; } }) }; + } }); + const module = new vm.SourceTextModule(readFileSync(new URL("../worker.mjs", import.meta.url), "utf8"), { context }); + await module.link(async name => { + const exports = name === "./batch.mjs" ? { batchMemoryEstimate() {}, batchOutputDirectory() {}, MEMORY_LIMIT: 0 } : { Archive: class {}, CONVERSION_LIMIT: 0, createPackageWriter() {}, packageArchive() {} }; + return new vm.SyntheticModule(Object.keys(exports), function() { for (const [key, value] of Object.entries(exports)) this.setExport(key, value); }, { context }); + }); + await module.evaluate(); + return { api: module.namespace, requests, cancelled: () => cancelled }; +} + +const invalid = [null, [], {}, { tag: "v1", platforms: null }, { ...valid("linux"), tag: "" }, { ...valid("linux"), tag: "v1." }]; +for (const [key, value] of [["path", `libraries/windows-${digest}.zip`], ["path", "libraries/linux-a.zip"], ["sha256", null], ["sha256", "a".repeat(63)], ["sha256", 123], ["size", 0], ["size", -1], ["size", 1.5], ["size", "3"], ["size", 512 * 1024 * 1024 + 1], ["expandedBytes", -1], ["expandedBytes", 1.5], ["expandedBytes", null], ["expandedBytes", 512 * 1024 * 1024 + 1]]) { + const release = valid("linux"); + release.platforms.linux[key] = value; + invalid.push(release); +} +for (const release of invalid) { + const { api, requests } = await load(release); + await assert.rejects(api.downloadLibraries("linux"), /Invalid release manifest/); + assert.deepEqual(requests, ["./release.json"]); +} +const unsupported = await load(valid("linux")); +await assert.rejects(unsupported.api.downloadLibraries("macos"), /Invalid library target/); +assert.deepEqual(unsupported.requests, []); +let validCount = 0; +for (const target of ["linux", "windows"]) { + for (const rateLimit of [false, true]) { + const release = valid(target); + const { api, requests } = await load(release, { rateLimit }); + const downloaded = await api.downloadLibraries(target); + assert.equal(downloaded.expandedBytes, 100); + assert.deepEqual(new Uint8Array(await downloaded.file.arrayBuffer()), bytes); + assert.equal(requests.at(-1), release.platforms[target].path); + validCount++; + } +} +const corrupt = await load(valid("linux"), { contents: new Uint8Array([3, 2, 1]) }); +await assert.rejects(corrupt.api.downloadLibraries("linux"), /checksum mismatch/); +const oversized = await load(valid("linux"), { contents: new Uint8Array([1, 2, 3, 4]), keepOpen: true }); +await assert.rejects(oversized.api.downloadLibraries("linux"), /download is incomplete/); +assert.equal(oversized.cancelled(), true); +console.log(JSON.stringify({ invalid: invalid.length + 1, valid: validCount })); diff --git a/demo/web/tests/worker-manifest.test.mjs b/demo/web/tests/worker-manifest.test.mjs new file mode 100644 index 0000000000..3a445c247d --- /dev/null +++ b/demo/web/tests/worker-manifest.test.mjs @@ -0,0 +1,12 @@ +import assert from "node:assert/strict"; +import { execFile } from "node:child_process"; +import { promisify } from "node:util"; +import { fileURLToPath } from "node:url"; +import { test } from "node:test"; + +test("library manifest validation runs before downloading and preserves valid downloads", async () => { + const { stdout } = await promisify(execFile)(process.execPath, ["--experimental-vm-modules", fileURLToPath(new URL("./worker-manifest-fixture.mjs", import.meta.url))], { timeout: 15000 }); + const result = JSON.parse(stdout); + assert.ok(result.invalid >= 15); + assert.equal(result.valid, 4); +}); diff --git a/demo/web/worker.mjs b/demo/web/worker.mjs index be35844090..4adc71b8ee 100644 --- a/demo/web/worker.mjs +++ b/demo/web/worker.mjs @@ -31,12 +31,17 @@ function reportFor(item, index) { } export async function downloadLibraries(target) { + if (target !== "linux" && target !== "windows") throw new Error("Invalid library target"); report({ phase: "Downloading system libraries" }); const response = await fetch("./release.json", { cache: "no-cache" }); if (!response.ok) throw new Error(`Cannot load release manifest (${response.status})`); const release = await response.json(); - const asset = release.platforms[target]; - if (!asset || !Number.isSafeInteger(asset.size) || asset.size < 0 || asset.size > 512 * 1024 * 1024 || !/^libraries\/(windows|linux)-[a-f0-9]+\.zip$/u.test(asset.path) || !/^[a-f0-9]{64}$/u.test(asset.sha256)) throw new Error("Invalid release manifest"); + const asset = release?.platforms?.[target]; + if (!release || typeof release.tag !== "string" || !/^v[0-9A-Za-z][0-9A-Za-z._-]*$/u.test(release.tag) || release.tag.endsWith(".") + || !asset || typeof asset.sha256 !== "string" || !/^[a-f0-9]{64}$/u.test(asset.sha256) + || asset.path !== `libraries/${target}-${asset.sha256}.zip` + || !Number.isSafeInteger(asset.size) || asset.size <= 0 || asset.size > 512 * 1024 * 1024 + || !Number.isSafeInteger(asset.expandedBytes) || asset.expandedBytes < 0 || asset.expandedBytes > 512 * 1024 * 1024) throw new Error("Invalid release manifest"); const latestResponse = await fetch("https://api.github.com/repos/boykopovar/AnyPS5/releases/latest", { headers: { Accept: "application/vnd.github+json" } }); if (!latestResponse.ok) { const rateLimited = latestResponse.status === 429 || (latestResponse.status === 403 && latestResponse.headers.get("x-ratelimit-remaining") === "0"); diff --git a/docs/dev/BUILD.md b/docs/dev/BUILD.md index 5983527abe..b2f12324e4 100644 --- a/docs/dev/BUILD.md +++ b/docs/dev/BUILD.md @@ -2,7 +2,7 @@ ## Relinker only -The relinker can be built without initializing submodules or configuring SDL, Vulkan, FFmpeg, FreeType or the shader recompiler. It requires CMake, a C++20 compiler and a build tool. Python 3.9 or newer enables the Python regression tests. +The relinker can be built without initializing submodules or configuring SDL, Vulkan, FFmpeg, FreeType or the shader recompiler. It requires CMake, a C++20 compiler and a build tool. Python 3.9 or newer enables the Python regression tests; Python 3.11 or newer also registers `validate_patch_catalog`, which needs `tomllib`. ```sh cmake -S . -B build-relinker -G Ninja -DCMAKE_BUILD_TYPE=Release -DANYPS5_RELINKER_ONLY=ON -DBUILD_TESTING=ON @@ -151,6 +151,8 @@ Build configuration parameters: | `-DCMAKE_C_COMPILER_LAUNCHER=ccache` | Optional C compiler cache; requires `ccache`. | | `-DCMAKE_CXX_COMPILER_LAUNCHER=ccache` | Optional C++ compiler cache; requires `ccache`. | | `-DFFMPEG_PREBUILT_DIR=` | Unpacked FFmpeg package for the target platform; empty by default. | +| `-DANYPS5_NODE=` | Node.js 26 or newer that runs the memory64 Emscripten tests `guest_long_double` and `emscripten_module_unwind`; `node` by default, Emscripten builds only. | +| `-DANYPS5_VIDEO_OUT_TEST_DRIVER=` | SDL video driver for the VideoOut flip tests; `windows` on Windows and `offscreen` elsewhere. | | `-DANYPS5_AGC_EXECUTION_TESTS=` | Semicolon-separated file names, without `.cpp`, of the `core/libs/prx/libSceAgcDriver/tests/execution` tests to build. Every other execution test is built from a stub that prints `skipped, this build leaves the test out with ANYPS5_AGC_EXECUTION_TESTS` and exits 77. A name with no source there fails the configure. Empty by default, which builds them all. | SDL and FreeType settings forced by the root `CMakeLists.txt` cannot be overridden with `-D`. @@ -297,7 +299,7 @@ The summary's scope durations are wall time aggregated across threads. Nested sc The shader preparation threads are traced as well, `shader prepare`, with a buffer for 32,768 events each. A wait for the GPU mutex on a traced thread is a `GpuMutex.wait.` slice (`dispatch`, `draw`, `present`, `flush` and so on, as on the `[lock]` line). -The `gpu` track holds one `batch` slice per recorder batch, from the batch's timestamp pair. With `APS5_PROFILE_GPU=1` it also holds a slice per command class range, under the class names of the `[gputime]` line (`draw`, `copy`, `fill`, `present-blit`, ...); program ranges are left out. The track keeps up to 262,144 ranges; more set `trace_dropped`. The GPU clock is mapped onto the CPU clock with calibrated timestamps (`VK_KHR_calibrated_timestamps` or the EXT version, enabled only when a trace switch is set) taken when the window opens and when it closes, so the two clocks are matched at both ends of the window. A batch is added when the driver reads its timestamps, after its fence signals, so the batches of the last frames of the window, which complete after it closes, are missing. Without calibrated timestamps the trace has no `gpu` track, and the `[trace]` line says so. +The `gpu` track holds one `batch` slice per recorder batch, from the batch's timestamp pair. With `APS5_PROFILE_GPU=1` it also holds a slice per command class range, under the class names of the `[gputime]` line (`draw`, `copy`, `fill`, `present-blit`, ...); program ranges are left out. The track keeps up to 262,144 ranges; more set `trace_dropped`. The GPU clock is mapped onto the CPU clock with calibrated timestamps (`VK_KHR_calibrated_timestamps` or the EXT version, enabled only when a trace switch is set) against the performance counter on Windows and `CLOCK_MONOTONIC` elsewhere, or `CLOCK_MONOTONIC_RAW` on a device that calibrates against no `CLOCK_MONOTONIC` (MoltenVK), taken when the window opens and when it closes, so the two clocks are matched at both ends of the window. A batch is added when the driver reads its timestamps, after its fence signals, so the batches of the last frames of the window, which complete after it closes, are missing. Without calibrated timestamps the trace has no `gpu` track, and the `[trace]` line says so. With any of the three switches set, the recorder writes a timestamp pair per batch for the whole run, as with `APS5_PROFILE_DRAW`. Without them, a `PerformanceTimer`, a GPU mutex acquisition and a read of batch timestamps each cost one more predictable branch. diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index 7b7d72ced4..b26ac9c915 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -22,6 +22,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | Name | Kind | Source | Default | Description | |------|------|--------|---------|-------------| +| `ANYPS5_AUTORUN_NO_PAUSE` | string | `core/relinker/cli/src/Autorun.cpp` | | exactly `1` skips the exit prompt and stdin read after autorun; unset, empty and other values keep the prompt | | `ANYPS5_GUEST_MODULE_ARGS` | string | `core/relinker/elfpatcher/tests/GuestModuleArgsHost.cpp` | | path of the file the module-argument fixture appends its events to | ### core/shader @@ -62,13 +63,14 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_SAVEEXEC_WRITE_FIRST` | presence | `core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp` | | writes the saved exec to the destination before reading the source in s_*_saveexec | | `APS5_SINGLE_LANE` | string | `core/shader/recompiler/Recompiler.cpp` | | hex code addresses (comma separated, or all) of wave64 programs kept at one guest lane per invocation | | `APS5_SRT_DEBUG` | presence | `core/shader/recompiler/Optimization/src/RequestMemoryView.cpp` | | prints extra SRT descriptor-walk debug output, such as guest reads outside the request memory | -| `APS5_STRICT_DESCRIPTOR_WEBS` | string | `core/shader/recompiler/Optimization/src/ResourceTracker.cpp` | | makes a T# or S# read in a descriptor web throw when a path's descriptor dwords depend on the program counter, EXEC or a lane mask, instead of selecting the first descriptor and recording an `InvalidDescriptor` fault when a lane takes that path; 0 leaves it off | +| `APS5_STRICT_DESCRIPTOR_WEBS` | string | `core/shader/recompiler/Optimization/src/ResourceTracker.cpp` | | makes a T# or S# read in a descriptor web throw when a path's descriptor dwords depend on the program counter, EXEC, a lane mask, the lane index or an image read or sample result (not a gather or an image atomic), instead of selecting the first descriptor and recording an `InvalidDescriptor` fault when a lane takes that path; 0 leaves it off | | `APS5_TRACE_BDA` | presence | `core/shader/recompiler/Optimization/src/ResourceTracker.cpp` | | prints the first few accesses that make a program take the address-based path | | `APS5_TRACE_BUFFER_WRITTEN` | presence | `core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp` | | prints how many guest buffer elements a program may store to and how many it only loads | | `APS5_TRACE_IMAGE_TABLE` | presence | `core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp` | | traces image-table resolution | | `APS5_VERIFY_MASKED_RESULT_MEMO` | presence | `core/shader/recompiler/Recompiler.cpp` | | also materializes every patched result memo hit, prints the first twenty whose patched result differs and counts them in the result memo report of `APS5_PROFILE_DRAW` | | `APS5_VERIFY_SRT_COMPACT` | presence | `core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp` | | walks every SRT descriptor a second time through the IR evaluator and throws when the two walks differ | | `APS5_VERIFY_SRT_TAPE` | presence | `core/shader/recompiler/Optimization/src/SrtWalker/SrtTape.cpp` | | walks a plan's runtime sources a second time through the evaluator, adopts its answers where the two differ and prints the first twenty differences | +| `APS5_WAIT_PENDING_BRANCHES` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/ShaderMemory.cpp` | | a resource capture that evaluates a scalar branch condition over a word with a pending GPU write waits for the write to prune the branch, instead of leaving the branch unpruned (both successors' resources captured) | | `APS5_WAVE_LDS_SUBGROUP` | presence | `core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp` | | restores subgroup-scope LDS barriers for single-wave workgroups instead of workgroup scope | ### libSceAgc @@ -116,6 +118,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_CAPTURE_INPUTS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | records each draw's inputs in the capture trace (requires APS5_CAPTURE_TRACE) | | `APS5_CAPTURE_TRACE` | presence | `core/libs/prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp` | | writes capture events to capture-trace.log | | `APS5_CAPTURE_TRACE_PATH` | string | `core/libs/prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp` | capture-trace.log | file path for the capture events log | +| `APS5_CHANGED_COPY_ALWAYS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | makes the changed-byte copy-back store every byte of the range, not only those the shader changed and the import still holds at their copy-in value | | `APS5_CHECK_INDIRECT_ARGS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | reads GPU-side indirect draw records back on the CPU and prints the first ones | | `APS5_COMPLETION_STORE_SYNC` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | makes a store made by a completion action wait for later batches over its range | | `APS5_COPY_DESTINATION_STRICT` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | refuses a CPU copy whose destination has any pending writer, not only an unsettled one | @@ -157,6 +160,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_DUMP_DRAW_SLOT1` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp` | 0 | hex slot-1 address whose draw's shaders and register state are dumped | | `APS5_DUMP_FRAMES` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | 0 | saves the first n presented frames as frame_.bmp | | `APS5_DUMP_FRAMES_EVERY` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | 1 | dumps every n-th presented frame | +| `APS5_DUMP_PREPARE_INPUTS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderDiagnostics.cpp` | off | saves inputs before preparation as `shader_
.input---.prepare.req`, respecting `APS5_DUMP_SHADERS` selection; logs begin after saving and end after successful preparation | | `APS5_DUMP_QUEUE` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp` | | dumps the first submissions of the queue with this hex id | | `APS5_DUMP_REJECTED` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/CommandValidation.cpp` | | writes a rejected submission to rejected_submission_.bin | | `APS5_DUMP_SCALE` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | 4 | keeps every n-th pixel in each direction of a dumped frame (1 to 16384; any other value fails) | @@ -233,6 +237,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_NO_BLOCK_TRACKING` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | tracks array layers, or the whole surface, instead of 64 KiB blocks | | `APS5_NO_BUFFER_CLASSES` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp` | | matches exact sizes in the buffer pool instead of rounding up to size classes | | `APS5_NO_CACHED_PRECOLLECT` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | leaves the write-watch walks of a cached object's Revalidate under the GPU mutex | +| `APS5_NO_CHANGED_COPY_BACK` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | copies a GPU-copied buffer's whole range back into its import instead of only the bytes the shader changed | | `APS5_NO_CLEAR_HAND_OVER` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | writes other images' pending results back at a clear-key storage refresh instead of handing them over to the refreshed image | | `APS5_NO_CLEARED_VIEW` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | keeps fast-cleared surfaces as CPU snapshots instead of viewing the cleared storage image | | `APS5_NO_COLLECT_MEMO` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp` | | disables the per-thread memo of write-watch collects | @@ -247,6 +252,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_NO_DCC_WORD_SCAN` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp` | | restores the byte-by-byte DCC key scan instead of the word-at-a-time compare | | `APS5_NO_DEFERRED_EVICTION` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | destroys displaced resource-cache entries under the mutex instead of deferring them to the open batch | | `APS5_NO_DEFERRED_SETTLES` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | settles watched private host imports as they open instead of through the deferred block-settling path (Windows only) | +| `APS5_NO_DEPTH_TRANSFER_PASS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp` | | copies a D32 depth plane into a 32-bit color storage image (one `vkCmdCopyImage` where `APS5_NO_DIRECT_DEPTH_COPY` allows it, else through the staging buffer) instead of reading it into the image in a compute pass (used when `VK_KHR_push_descriptor` is enabled); copies back into the depth plane never use the pass | | `APS5_NO_DIRECT_DEPTH_COPY` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp` | | keeps every copy between a depth plane and a color storage image on the staging buffer instead of one `vkCmdCopyImage` (used when `VK_KHR_maintenance8` is enabled and the storage format is a single component of the aspect's size) | | `APS5_NO_DISPATCH_CACHE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp` | | disables the dispatch cache so every dispatch takes the uncached path | | `APS5_NO_DISPATCH_KEY_HYGIENE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp` | | mixes every shader register into the dispatch key instead of only the registers that affect the dispatch | @@ -285,10 +291,12 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_NO_HOOK_COMPLETION_GUARD` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | lets the flush hook record the accessing worker's queued labels even from inside a completion action | | `APS5_NO_HOOK_SNAPSHOT` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | takes the GpuMutex on every access instead of using the pending-write snapshot | | `APS5_NO_HOST_IMPORT` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | disables importing guest memory, so address-based shaders copy instead of using it in place | +| `APS5_NO_HOST_INTERPOLATION_FIRST` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | on RADV, reads pixel shader inputs through VK_KHR_fragment_shader_barycentric again instead of trying host interpolation first | | `APS5_NO_HW_F32_FLUSH` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | skips the startup f32 denormal flush probe and keeps the emulated flush in every shader | | `APS5_NO_HW_F32_PRESERVE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | stops requesting DenormPreserve for 32-bit floats, so the device keeps its default f32 denormal handling | | `APS5_NO_IDLE_STORE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/QueueWorker.cpp` | | stops queue 0's worker from storing pending storage-image results after the queues stay idle for 50 ms, so they stay pending until a reader, label or flush stores them; queue 0 still waits for work while they are pending | | `APS5_NO_IMAGE_PAGE_CACHE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp` | | leaves the main guest image's page-protection table uncached | +| `APS5_NO_IMAGE_VIEW_MIN_LOD` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | leaves VK_EXT_image_view_min_lod disabled, so a T#'s minimum LOD is applied in the shader where shaderResourceMinLod allows it and graphics pipeline libraries are off | | `APS5_NO_INDIRECT_DRAW` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp` | | rejects indirect draw packets at submission validation | | `APS5_NO_JOIN_WAW_CHECK` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | lets a contiguous store join the pending one without the write-after-write check | | `APS5_NO_KEEP_NEW_STORAGE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | does not keep a newly created storage image alive with the open batch; such an image evicted before the batch completes can hand its memory to another image through the buffer pool while the batch still writes it | @@ -347,6 +355,8 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_NO_SAMPLER_CACHE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | creates a sampler per binding instead of caching them | | `APS5_NO_SEPARATE_QUEUED_LABELS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | lets a queued label enter the recorded table and replace a recorded entry of its dword | | `APS5_NO_SHAPE_MEMO` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | looks up every draw's resident targets and pipeline anew instead of repeating the ones an earlier draw of the same register shape used | +| `APS5_NO_SHARED_FAULT_SLOTS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp` | | gives every BDA resource object its own full-size fault buffer for its whole life instead of a slot from the device's shared fault chunks that an idle cached object gives back (a 32-byte slot when its programs can write only the fault report) | +| `APS5_NO_SHARED_GDS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp` | | keeps the GDS in a private guest mapping, which the GPU cannot import, instead of anonymous shared memory on an integrated GPU with host imports; DMA_DATA into the GDS behind GPU writes to it then drains, and DMA_DATA out of it is read on the CPU | | `APS5_NO_SINGLE_PASS_COLLECT` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp` | | walks the written pages once without clearing them and, when any were written, a second time clearing them, instead of one cleared pass | | `APS5_NO_SITE_SAMPLING` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WriteEvidence.cpp` | | disables per-site sampling of write evidence | | `APS5_NO_SNAPSHOT_CHECK` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | skips the live compare that proves a captured snapshot still equals the guest bytes | @@ -413,6 +423,8 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_SAMPLE_THREADS` | string | `core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp` | | samples every thread of the process every 2 ms and writes each busy thread's hottest frames to a file (Windows only) | | `APS5_SAMPLE_WORKER` | string | `core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp` | | samples the calling queue worker every millisecond and writes its stacks to a file (Windows only) | | `APS5_SAMPLE_WORKER_QUEUE` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp` | 0 | selects the queue worker sampled by `APS5_SAMPLE_WORKER`, accepting decimal or hexadecimal IDs such as `0x50`; process and committer sampling remain independent | +| `APS5_SCRATCH_POOL_MIB` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | 64 | cap in MiB on the device buffer that backs the scratch of one compute program (header plus workgroup slots, never below one slot), also held under `APS5_SCRATCH_POOL_TOTAL_MIB` and 2 GiB; a nonzero value only | +| `APS5_SCRATCH_POOL_TOTAL_MIB` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | 256 | budget in MiB for the scratch pool buffers of all compute programs together: a new or enlarged pool first drops the least recently used pools until it fits (one pool is also capped at it); a nonzero value only | | `APS5_SHADOW_VERIFY` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/UnitShadow.cpp` | | verifies that the shadow-published bytes equal the guest bytes | | `APS5_SHARED_COLLECT_MEMO` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp` | | uses the tracker's shared collect-memo ring under its mutex instead of the per-thread ring | | `APS5_SHARED_MEMORY_COUNTERS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp` | | accumulates the memory counters into one shared set with atomic RMWs | @@ -453,6 +465,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_TRACE_BIGBUF` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | lists every copied guest region of at least 1 MiB | | `APS5_TRACE_CAPSYNC` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WriteEvidence.cpp` | | prints every unlocked hook wait | | `APS5_TRACE_CONTEXT_STATE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp` | | logs context-state operations and the context registers written | +| `APS5_TRACE_COPY_BACK` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | logs, per copied-back range, the dwords in which the shader and the CPU changed the same bytes, and whether the CPU's bytes were kept or, with APS5_CHANGED_COPY_ALWAYS, the shader's stored | | `APS5_TRACE_COPY_HLE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferCopy.cpp` | | traces the CPU copy HLE's refusals | | `APS5_TRACE_DCC_KEYS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp` | | traces stores of uncompressed DCC keys | | `APS5_TRACE_DIR` | string | `core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTrace.cpp` | . | directory for the trace and summary files | @@ -571,7 +584,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | Name | Kind | Source | Default | Description | |------|------|--------|---------|-------------| -| `APS5_PLAYGO_UNLISTED_CHUNKS` | string | `core/libs/prx/libScePlayGo/Export.cpp` | | answer for a chunk ID the playgo manifest does not list: installed or absent; unset throws instead of answering | +| `APS5_PLAYGO_UNLISTED_CHUNKS` | string | `core/libs/prx/libScePlayGo/Export.cpp` | | answer for a chunk ID the playgo manifest does not list: installed, absent, or a comma-separated list of installed chunk ids; unset throws instead of answering | ### libSceSaveData.native @@ -584,7 +597,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | Name | Kind | Source | Default | Description | |------|------|--------|---------|-------------| -| `ANYPS5_LANGUAGE` | integer | `core/libs/prx/libSceSystemService/SystemService.hpp` | 1 | console language number from 0 to 30 returned by sceSystemServiceParamGetInt, defaulting to English US; any other value throws | +| `ANYPS5_LANGUAGE` | integer | `core/libs/prx/libSceSystemService/SystemService.hpp` | 1 | console language number from 0 to 30 returned by sceSystemServiceParamGetInt, defaulting to English US; `auto` (any letter case) maps the host's locale to a console language once per process and falls back to English US with one stderr line; any other value throws | ### libSceUsbStorage @@ -628,7 +641,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_NO_WRITE_WATCH` | presence | `core/libs/prx/libc/src/GuestArena.cpp` | | disables write watching so guest buffers are fully re-read on every use | | `APS5_PIN_WAIT_MS` | integer | `core/libs/prx/libc/src/GuestAllocations.cpp` | 60000 | milliseconds to wait for a pinned allocation lease before failing | | `APS5_TRACE_AFFINITY` | presence | `core/libs/prx/libc/include/CpuTopology.hpp` | | prints a line naming the role, tid and mask of every thread pinning | -| `APS5_TRACE_CALLS` | string | `core/libs/prx/libc/src/specifics/x86_64/CallTrace.cpp` | | comma-separated libraries (or all) whose guest calls are traced | +| `APS5_TRACE_CALLS` | string | `core/libs/prx/libc/src/specifics/x86_64/CallTrace.cpp` | | comma-separated libraries (or all) whose guest calls are traced; an empty import-library label uses the defining host module filename when it can be found | | `APS5_TRACE_CALLS_FILE` | string | `core/libs/prx/libc/src/specifics/x86_64/CallTrace.cpp` | | file rewritten every 10 seconds with each traced import called so far and its number of calls | | `APS5_TRACE_EXIT` | presence | `core/libs/prx/libc/src/Process.cpp` | | prints a backtrace of the caller of exit, _Exit and the process-exit path | | `APS5_TRACE_SSE4A` | presence | `core/libs/prx/libc/src/specifics/windows/CrashReport.cpp` | | prints each emulated SSE4a instruction once per rip | diff --git a/docs/dev/GAME_TESTING.md b/docs/dev/GAME_TESTING.md index e8025d0af8..ce3a6874f7 100644 --- a/docs/dev/GAME_TESTING.md +++ b/docs/dev/GAME_TESTING.md @@ -95,7 +95,7 @@ cmd /c '.\app.exe > run.log 2>&1' ## Title notes -- **PlayGo:** dumps without PlayGo metadata stop at start unless `APS5_PLAYGO_UNLISTED_CHUNKS` is set; report the value used. +- **PlayGo:** dumps without PlayGo metadata stop at start unless `APS5_PLAYGO_UNLISTED_CHUNKS` is set (installed, absent, or a comma-separated list of installed chunk ids); report the value used. - **UE4 titles:** keep the startup movies in place. Renaming them to skip the intro has made a level render without its 3D view, which looked like an emulator bug and wasn't. ## Input diff --git a/docs/dev/HW_ORACLE.md b/docs/dev/HW_ORACLE.md index 01583cdc19..f4e4c14c1a 100644 --- a/docs/dev/HW_ORACLE.md +++ b/docs/dev/HW_ORACLE.md @@ -20,7 +20,7 @@ the tool, but differs from gfx1013 in more places. Say which GPU you measured on `PATH`, they are taken from `$ROCM_PATH/llvm/bin`. The first run builds `oracle.c` into `$XDG_CACHE_HOME/anyps5-hw-oracle/`, or `~/.cache/anyps5-hw-oracle/` when -`XDG_CACHE_HOME` is unset or empty (`HW_ORACLE_CACHE` overrides both). The target is read from the GPU (`HW_ORACLE_TARGET` +`XDG_CACHE_HOME` is unset, empty or relative (`HW_ORACLE_CACHE` overrides both). The target is read from the GPU (`HW_ORACLE_TARGET` overrides it). ### Windows @@ -31,7 +31,7 @@ overrides it). On Windows `oracle_hip.py` replaces `oracle.c`: comgr assembles and links the kernel, and HIP runs it. The CLI, the Python interface and the results are the same as on Linux. `HW_ORACLE_RUNTIME` picks the runtime (`hip` on Windows, `hsa` -elsewhere). `HW_ORACLE_HIP` and `HW_ORACLE_COMGR` name other DLLs. `ROCR_VISIBLE_DEVICES` selects the GPU, through +elsewhere; `vulkan` is never the default, see [Recompiler comparison](#recompiler-comparison)). `HW_ORACLE_HIP` and `HW_ORACLE_COMGR` name other DLLs. `ROCR_VISIBLE_DEVICES` selects the GPU, through `HIP_VISIBLE_DEVICES`, unless that variable is already set. ## Kernel @@ -89,6 +89,41 @@ results = run(body, rows, wave64=True, ieee=0, dx10_clamp=1, Usual workflow: run the instruction on rows that separate the candidate models (edge values, NaN payloads, rounding ties), write a model that matches every row, implement it, and put measured rows in the execution test. +## Recompiler comparison + +The `vulkan` runtime runs the same assembled kernel through the shader recompiler on the Vulkan device instead of on AMD +hardware, so one body and one set of rows can be measured on both and the results compared. Build the runner: + +```sh +cmake --build build --target agc_oracle_runner +``` + +| Variable | Meaning | +| --- | --- | +| `HW_ORACLE_RUNTIME=vulkan` | selects the runtime | +| `HW_ORACLE_VULKAN_RUNNER` | path of the built `agc_oracle_runner` executable | +| `ANYPS5_GPU` | case-insensitive substring of the Vulkan device name, as for every AnyPS5 tool | + +On Windows the runner needs `build/core/libs/libs/unpatched` in `PATH` ([BUILD](BUILD.md)). Run the hardware with the default +runtime and the recompiler with `vulkan`, with the same body, rows and settings, each with `--json`, then compare: + +```sh +SETTINGS="--ieee 0 --dx10-clamp 1 --denorm32 0 --denorm16 3 --round32 0 --round16 0 --fp16-overflow 0" +python3 tools/hw-oracle/hw_oracle.py body.s rows.txt --json $SETTINGS > hardware.json +HW_ORACLE_RUNTIME=vulkan HW_ORACLE_VULKAN_RUNNER= \ + python3 tools/hw-oracle/hw_oracle.py body.s rows.txt --json $SETTINGS > recompiler.json +python3 tools/hw-oracle/compare_results.py hardware.json recompiler.json +``` + +The report's `runtime` field says which side produced it. + +- Compute only. +- Whole waves: Python pads the rows with zeros to the wave size, so cross-lane instructions see the padded lanes. +- At most 1024 lanes per dispatch. +- No `coarse`. +- The assembly target comes from the AMD driver on Windows or from `HW_ORACLE_TARGET`; the recompiler has no target of its + own, and the encoding assembled is the RDNA2 one. + ## Limits Only compute kernels. Pixel-shader ops that read their inputs from LDS also work: `v_interp_*` reads the attribute diff --git a/docs/dev/debt/libAacs.md b/docs/dev/debt/libAacs.md new file mode 100644 index 0000000000..4fcada1564 --- /dev/null +++ b/docs/dev/debt/libAacs.md @@ -0,0 +1,287 @@ +# libAacs technical debt + +### Callable exports + +- [Callable exports](../../../core/libs/prx/libAacs/Export.cpp) - 264 entries are import-resolution declarations only. Parameter lists, return types and behavior are unverified; parameterless `int` declarations are placeholders, not verified ABI contracts. Every entry throws through `NotImplemented_nid_no_patch`. Unidentified names use literal NID aliases. No runtime functionality is implemented. Data exports require separate layout and ownership analysis. Candidate names match their NIDs in the [public symbol database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv) or [supplemental catalog](https://github.com/claimore22/ps5rs/blob/2304be5131221bae0dac24b9767cabffda33f747/data/nids.csv). + +| Name | NID | +| --- | --- | +| `Unknown` | `+0gHTNd4xs4` | +| `Unknown` | `+3dyyKZMTyw` | +| `Unknown` | `+7ZoZWbIhlE` | +| `Unknown` | `+CiBKXuK8c8` | +| `Unknown` | `+RinEsZfOmw` | +| `Unknown` | `+XVwfwtkAi4` | +| `Unknown` | `-2QNA1pCQqU` | +| `Unknown` | `-kAJeMNy-0E` | +| `Unknown` | `-lknLNU2NAU` | +| `Unknown` | `-xuQAxlUvmk` | +| `Unknown` | `0bhkzdWU6Y4` | +| `Unknown` | `0oisD9Dr3FI` | +| `Unknown` | `12CP5PHvlgs` | +| `Unknown` | `15QPrQC1usM` | +| `Unknown` | `1CNq2v6hoig` | +| `Unknown` | `1aMnfYcnn1I` | +| `Unknown` | `1bzdduTshZ4` | +| `Unknown` | `1eNkqOSGM+A` | +| `Unknown` | `1hyWLxCZ-X8` | +| `Unknown` | `1pvpAK9Rg1k` | +| `Unknown` | `23BaEjHuFUA` | +| `Unknown` | `2HaRuPcSWtE` | +| `Unknown` | `2KQWcFTN8ZE` | +| `Unknown` | `2YklE8dG4v8` | +| `Unknown` | `38ZtQUT4lMs` | +| `Unknown` | `3LFB-Ey4DUk` | +| `Unknown` | `3OYBQUy9yVI` | +| `Unknown` | `3ZqZuNL3qrM` | +| `Unknown` | `3odIlZa6t2I` | +| `Unknown` | `4IQhgxl103g` | +| `Unknown` | `4r3seYGdIK0` | +| `AacsPermissionActivate` | `4zU8rDPa4r4` | +| `Unknown` | `54hkTYBIxqs` | +| `AacsBusDecrypt` | `6KcHbwBESLA` | +| `AacsModuleStop` | `6T773MR5QPI` | +| `Unknown` | `6ToH2wFRWf4` | +| `Unknown` | `6XcWJGADJeU` | +| `Unknown` | `753FlqNkJlM` | +| `Unknown` | `7ETkLi9auE0` | +| `Unknown` | `7KCIejRxaB0` | +| `AacsDecrypt` | `7TaiaYkvLFo` | +| `Unknown` | `7eM7N1Anox4` | +| `Unknown` | `7iLrHfVzuvs` | +| `Unknown` | `7qIZfzr0Dgk` | +| `Unknown` | `85kBXEP1moE` | +| `Unknown` | `8Gj9M3o-rrY` | +| `Unknown` | `8TJlScPxUa8` | +| `Unknown` | `8YaNJTuYU+g` | +| `Unknown` | `9S1fVMePqpI` | +| `Unknown` | `9UJqpmNn494` | +| `Unknown` | `9eTs6-zAOJs` | +| `Unknown` | `9rQgwWQB9NE` | +| `Unknown` | `AVRC3P8reF0` | +| `Unknown` | `Apl9mjDSCE0` | +| `Unknown` | `B10mYlgLwMM` | +| `Unknown` | `BFdZY3ocsJo` | +| `Unknown` | `BO+NBS2KrZc` | +| `Unknown` | `BaMuhhCt-dQ` | +| `AacsPermissionCheck` | `CJOLmTNwg6g` | +| `Unknown` | `DWJK8GjbNHo` | +| `AacsModuleInit` | `DdD8hWNsUSc` | +| `Unknown` | `EBGsSrhNRnM` | +| `Unknown` | `EFpFjkHGewY` | +| `Unknown` | `EIEQ4mWz5sU` | +| `Unknown` | `ELsVqt8cS3Q` | +| `Unknown` | `EQWDDRWfuOU` | +| `Unknown` | `ESDneLy+ExU` | +| `Unknown` | `ETDNd-CDbkc` | +| `Unknown` | `EX5Y3UepfU4` | +| `Unknown` | `EgaEREMnudw` | +| `Unknown` | `FA1OyesdYiA` | +| `Unknown` | `FEgD32GcLdM` | +| `Unknown` | `FEi5mHUWB0M` | +| `Unknown` | `FHWdAzU-rRI` | +| `Unknown` | `Fl0RrZ7B27g` | +| `Unknown` | `G4zfSFYC1f8` | +| `Unknown` | `GD6inRSAHfo` | +| `Unknown` | `GWA91aXeU1Y` | +| `Unknown` | `GXLnqJJ85dM` | +| `Unknown` | `GpsjAdWq5hk` | +| `Unknown` | `HXafooMuNTY` | +| `Unknown` | `HhIFaBlod8A` | +| `Unknown` | `HmdBrUh2Xz4` | +| `Unknown` | `HuTXuHqMrVE` | +| `Unknown` | `ILwfo94p-ng` | +| `Unknown` | `Iofb1lKwTNA` | +| `Unknown` | `IxvgFI5JtxE` | +| `Unknown` | `JXvlGrRcNlg` | +| `Unknown` | `Jmviii0cDro` | +| `Unknown` | `JolZPea7pio` | +| `Unknown` | `JqeAwcly1Lc` | +| `Unknown` | `JqrjeblDpqo` | +| `Unknown` | `KG5C7s3p+mM` | +| `Unknown` | `KLxEGrIPbbo` | +| `Unknown` | `L6tIkKT5Vdg` | +| `Unknown` | `LFpG7wYNZ+I` | +| `Unknown` | `LT-aek5dBRQ` | +| `Unknown` | `LTIva3bGCXI` | +| `Unknown` | `MCPkB4DYvAM` | +| `Unknown` | `MILG1Bi10BQ` | +| `Unknown` | `MLHm3u-UfPk` | +| `Unknown` | `MRMYIqUmmcQ` | +| `Unknown` | `MVoCh7vg9Bk` | +| `Unknown` | `NK6Ke-y45cI` | +| `Unknown` | `NL6gDScOFl8` | +| `Unknown` | `NOWYNS5AnRg` | +| `Unknown` | `NRv4EFuYq9I` | +| `Unknown` | `NSph9LkyLmI` | +| `Unknown` | `OI1om0rmEYk` | +| `Unknown` | `OOrYw04s+VU` | +| `Unknown` | `ObJaLtAn3TM` | +| `AacsModuleStart` | `OiQkT8AUD5k` | +| `Unknown` | `Ozlqilsf3Tw` | +| `Unknown` | `PHgHus4a3M0` | +| `Unknown` | `PUDoXp1l-Po` | +| `Unknown` | `Q2eGSNdsRq0` | +| `Unknown` | `Q3vOGHtSk9Y` | +| `Unknown` | `Q74CET98b04` | +| `Unknown` | `QCq8qc+kt7g` | +| `Unknown` | `QDc8xyN5AQw` | +| `Unknown` | `QabaLwU3b9o` | +| `Unknown` | `RI7fQrbtZko` | +| `Unknown` | `RZFK7f3Oi24` | +| `Unknown` | `RkkCme3dsyo` | +| `Unknown` | `SJ9GmthhZDk` | +| `Unknown` | `SjEjF5V5XHc` | +| `Unknown` | `SjewOoJlr9c` | +| `Unknown` | `TK5prfVU4b0` | +| `Unknown` | `TQCLQjHkkW0` | +| `Unknown` | `TuTNDTTOXHo` | +| `Unknown` | `U2zlEU4AhRk` | +| `Unknown` | `U6RAXYIdJJE` | +| `Unknown` | `UArM6xy6B3g` | +| `Unknown` | `UMZJ2F8Lr4o` | +| `Unknown` | `UqqcPoUuAqE` | +| `AacsPermissionGetNonce` | `V1THGaJc23o` | +| `Unknown` | `VnixdJqGnjU` | +| `Unknown` | `WOxxWZjZMfQ` | +| `Unknown` | `WaOsDN+9lZg` | +| `Unknown` | `WknurdpKTqo` | +| `Unknown` | `WrEjdzfOo2I` | +| `Unknown` | `XQuTyX2WprY` | +| `Unknown` | `XUL7r8v2Q3Y` | +| `Unknown` | `XbP3p94ujkg` | +| `Unknown` | `XhMh4CEdKGU` | +| `Unknown` | `Xokc3inTVbM` | +| `Unknown` | `XrkLs6PFhac` | +| `Unknown` | `Xy748BBvB2w` | +| `Unknown` | `YDl9jLvW+SM` | +| `Unknown` | `YOlHXj4rrzQ` | +| `Unknown` | `Yqt0NzhE-eU` | +| `Unknown` | `Z9yhz8+DS3k` | +| `Unknown` | `ZCRVIAxld6o` | +| `Unknown` | `ZEdbi75pkX0` | +| `Unknown` | `ZUEIg4QZJ+k` | +| `Unknown` | `ZnJgSGpLzGs` | +| `Unknown` | `a3D2-Eh-DW8` | +| `Unknown` | `aOX5VPrNfqo` | +| `Unknown` | `aXeMNVrwOTw` | +| `Unknown` | `aZmsxlYbNes` | +| `Unknown` | `aacj8q7PTFI` | +| `Unknown` | `b0ZC6N9oQlU` | +| `Unknown` | `bHqBriHrgXA` | +| `Unknown` | `bQl9nHS7Jgg` | +| `Unknown` | `bQmoqxNo0rE` | +| `Unknown` | `c04ksdAcncM` | +| `AacsSelectKeyFrom` | `c5xyR+kt8mo` | +| `Unknown` | `c9tUe3nNpxo` | +| `Unknown` | `cI0YmNGV22E` | +| `Unknown` | `cnGJD38STwQ` | +| `AacsModuleTerm` | `dSZ7mcs3MNk` | +| `Unknown` | `e-SciZxh1HY` | +| `Unknown` | `e-yS+nlBtlE` | +| `Unknown` | `e0UQuzSUkI8` | +| `Unknown` | `e5qwpKPf1aI` | +| `Unknown` | `eRXHQokealY` | +| `Unknown` | `egjNL-sA-s0` | +| `Unknown` | `eoQNtjg5QyQ` | +| `Unknown` | `f5FWQj--MwM` | +| `Unknown` | `fWmfE-A0k90` | +| `Unknown` | `fkra1GmOMmQ` | +| `Unknown` | `flzhCbf4xnw` | +| `Unknown` | `gAV79zVGfzI` | +| `Unknown` | `gPhz2sY+lHM` | +| `Unknown` | `gTXUpBIgmcg` | +| `Unknown` | `gmOvd0UR6rk` | +| `Unknown` | `h6JW5ZErMHE` | +| `Unknown` | `hDCeckXsgIw` | +| `Unknown` | `hFXdc25NmT4` | +| `Unknown` | `hWhpklSoZ7g` | +| `Unknown` | `hct40EnKHyM` | +| `Unknown` | `i0ZMaUuKzkE` | +| `Unknown` | `iIgtd05IlbA` | +| `Unknown` | `iJ3zPE02P1w` | +| `Unknown` | `iOLQbPwqsJU` | +| `Unknown` | `igIuBDqdlZo` | +| `Unknown` | `j0quzCtmBr4` | +| `Unknown` | `jXZJmZu3ihs` | +| `Unknown` | `jdd4KuD7bOg` | +| `Unknown` | `jpKit8Qv76g` | +| `AacsPermissionSet` | `kDxzgCcMty0` | +| `Unknown` | `kX1hCR9FDo0` | +| `Unknown` | `kYQis7lBy+U` | +| `Unknown` | `kaxfKxeOvbU` | +| `Unknown` | `keNMJVznBe0` | +| `Unknown` | `kk8ELO4Y2Fo` | +| `Unknown` | `l18OOzQPPKo` | +| `Unknown` | `l8uscgUMuWE` | +| `Unknown` | `ldkS5zx7P6s` | +| `Unknown` | `llme91x0I-k` | +| `Unknown` | `lq8QnjYyTGQ` | +| `Unknown` | `mExJcLz6QWE` | +| `Unknown` | `mR3leJVBYbw` | +| `Unknown` | `mbKaZoh30DM` | +| `Unknown` | `mlZY17mBv9M` | +| `Unknown` | `mnf5OTi-urc` | +| `Unknown` | `moU18PvmqEU` | +| `AacsPermissionInit` | `n8SlmGnoGtI` | +| `Unknown` | `o33b+5keZic` | +| `Unknown` | `oJobSSDssQw` | +| `Unknown` | `oU59phxP5U4` | +| `Unknown` | `oij+E5v8qjM` | +| `Unknown` | `p8Lv5g5o9pY` | +| `Unknown` | `p9sZ84FK-qw` | +| `Unknown` | `ptrqNcGG0eM` | +| `Unknown` | `qFchDSE1J3o` | +| `Unknown` | `qGMyKAQhniM` | +| `Unknown` | `qKmr8FI7Too` | +| `Unknown` | `qPi2nzCUYiQ` | +| `Unknown` | `qpebtLhTJBc` | +| `Unknown` | `r960kCkFF3I` | +| `Unknown` | `rMR6nG8LdyQ` | +| `Unknown` | `rOM7gjFX3nM` | +| `Unknown` | `raaoezfuSqY` | +| `Unknown` | `ruidkq8amJE` | +| `Unknown` | `s+BvUCe+t+I` | +| `Unknown` | `s6CgM5B+8zc` | +| `Unknown` | `sjNkMwi80Os` | +| `Unknown` | `sv8IiSJQ0Mo` | +| `Unknown` | `tPYLJSOXQ8k` | +| `Unknown` | `tWHiNKLyUn4` | +| `Unknown` | `uG7mSoxfjL0` | +| `Unknown` | `ujgxfJSpLrc` | +| `Unknown` | `ulInPmGONKo` | +| `Unknown` | `uvJDUwXFtw0` | +| `Unknown` | `uvLxjEqwYKk` | +| `Unknown` | `v5IGfww3qSA` | +| `Unknown` | `vM2spx6P6RU` | +| `Unknown` | `vcFZSn+m4-o` | +| `Unknown` | `w4Go9bJ-JGQ` | +| `Unknown` | `x34TWztyXy8` | +| `Unknown` | `x82CfygyUPA` | +| `Unknown` | `xQoCQNT3skQ` | +| `Unknown` | `y56l-E5S0Qs` | +| `Unknown` | `y5OsRUqNoX4` | +| `Unknown` | `yFNWXXeNxF0` | +| `AacsUpdateKeyArea` | `yUEiOjyQK2w` | +| `Unknown` | `yZjYlfcfEFE` | +| `Unknown` | `yaNpz6jnOzE` | +| `Unknown` | `yrqEHk366AM` | +| `Unknown` | `z0b7NKDfWpo` | +| `Unknown` | `zgZWaxsJGyM` | +| `Unknown` | `zreU+KU3nLw` | +| `Unknown` | `zt4Xv-4fjx0` | + +### Unresolved data exports + +These older data exports remain undeclared. Their NIDs and sizes also appear in the later inventory, but their types, initialization and lifetime have not been established. Relocation counts refer to entries within each object in the older image; relocation-free does not establish safe semantics or ownership. + +| NID | Size (bytes) | Relocations | +| --- | ---: | ---: | +| `Ig+KGFjQHCg` | 384 | 46 | +| `0hCijDZ52LQ` | 384 | 46 | +| `NTbhN7TXzI0` | 736 | 90 | +| `50ti-+HJsoY` | 8192 | 0 | +| `UVmPYK3n8Ac` | 736 | 90 | +| `kvmcOEpZYhU` | 4 | 0 | +| `adiny53QVzk` | 384 | 46 | +| `H8V9nrBQMJc` | 736 | 90 | diff --git a/docs/dev/debt/libCss.md b/docs/dev/debt/libCss.md new file mode 100644 index 0000000000..559fc1192e --- /dev/null +++ b/docs/dev/debt/libCss.md @@ -0,0 +1,89 @@ +# libCss technical debt + +### Callable exports + +- [Callable exports](../../../core/libs/prx/libCss/Export.cpp) - 65 entries are import-resolution declarations only. Parameter lists, return types and behavior are unverified; parameterless `int` declarations are placeholders, not verified ABI contracts. Every entry throws through `NotImplemented_nid_no_patch`. Unidentified names use literal NID aliases. No runtime functionality is implemented. Data exports require separate layout and ownership analysis. Candidate names match their NIDs in the [public symbol database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv) or [supplemental catalog](https://github.com/claimore22/ps5rs/blob/2304be5131221bae0dac24b9767cabffda33f747/data/nids.csv). + +| Name | NID | +| --- | --- | +| `Unknown` | `+zpMhCilHTQ` | +| `Unknown` | `-WR4-J79nn0` | +| `Unknown` | `45oyUIl85dg` | +| `Unknown` | `4qq5OHYaFU8` | +| `Unknown` | `6BDNwI1e59c` | +| `Unknown` | `6QhXfVttAyU` | +| `Unknown` | `74insBAQhg8` | +| `Unknown` | `7YdC9p2cTSQ` | +| `Unknown` | `81obgoH2X2g` | +| `Unknown` | `9T-RYQ7HwqI` | +| `Unknown` | `9wgr5dQqxyg` | +| `Unknown` | `AfC4Omb8svg` | +| `Unknown` | `B314wB+fU6g` | +| `Unknown` | `BpqeQbNU0qo` | +| `Unknown` | `CnQiuTNrJcM` | +| `Unknown` | `E96QCyLgJ3o` | +| `dvdAuthInitialize` | `Ej2QCEJXBec` | +| `DvdMac` | `EvvQwl+bIv4` | +| `Unknown` | `FTFSIzVj7gE` | +| `Unknown` | `FsJO2R6pcFY` | +| `Unknown` | `HaAxhqb2xoM` | +| `Unknown` | `Hm2XEUlTjSk` | +| `Unknown` | `IcknOdMyfxQ` | +| `Unknown` | `J6BYqrHA5UM` | +| `Unknown` | `KUvq4G5VBPo` | +| `Unknown` | `LAcpQCmLWwI` | +| `Unknown` | `MRbX1Lg7JPo` | +| `Unknown` | `OM3ptXltjzo` | +| `Unknown` | `Q4g0Ji0W5GY` | +| `Unknown` | `SRPTG7u8o98` | +| `Unknown` | `TJdtvWbNKhs` | +| `Unknown` | `UCT-EFL9GlY` | +| `Unknown` | `UlBK7McbIBI` | +| `Unknown` | `VLaRWTMO9U8` | +| `Unknown` | `VMBVlZevTLw` | +| `Unknown` | `Wdf6DHvrt4g` | +| `Unknown` | `X9dl5pCTGiE` | +| `Unknown` | `XWqQyI3K47s` | +| `dvdAuthFinalize` | `ZzaGM24XNFI` | +| `Unknown` | `bQpZTilHSL0` | +| `Unknown` | `cN12O1uQ6Fo` | +| `Unknown` | `cTnMCeNpGqc` | +| `Unknown` | `czKT3U30FFE` | +| `Unknown` | `daklpMlbo00` | +| `Unknown` | `f7iq9LsamYo` | +| `Unknown` | `gbja3RUMWJ4` | +| `Unknown` | `giIpEkyo9Hg` | +| `Unknown` | `hD3HhdNG+xw` | +| `Unknown` | `hbTTMmwE3lo` | +| `Unknown` | `hbvafh9uQqY` | +| `Unknown` | `jYGAlLsijgw` | +| `Unknown` | `k1+Z1AtnLYI` | +| `Unknown` | `k5AW5Xs4aqg` | +| `Unknown` | `kc5B1ELiUv4` | +| `Unknown` | `nhK3hP3XL3Y` | +| `Unknown` | `ohRg1gu5G1w` | +| `Unknown` | `pTycImStaM4` | +| `Unknown` | `rylZqUytLj8` | +| `Unknown` | `szOnkd+7b3U` | +| `Unknown` | `utwkEaQy6D4` | +| `Unknown` | `wDe5E-Nfd7E` | +| `Unknown` | `xQX0HWKQKDA` | +| `Unknown` | `z4KQSeB3+6U` | +| `CpsInitKey` | `z7D3a8pXEeE` | +| `Unknown` | `zxv6uaL0lAs` | + +### Unresolved data exports + +These nine older data exports remain undeclared. Their NIDs and sizes also appear in the later inventory. Three contain relocation entries in the older image, and the other six occupy non-file-backed storage. Their types, initialization and ownership remain unverified; no object contents or behavior are inferred from matching sizes. + +| NID | Size (bytes) | Relocations | +| --- | ---: | ---: | +| `Ea79aJutCLc` | 40 | 3 | +| `+ItBPjmv86M` | 40 | 3 | +| `r6olfOgegxU` | 32 | 2 | +| `0wlN5i59xrM` | 1 | 0 | +| `dqpZ10KTssY` | 10 | 0 | +| `CjPeOAnJSIs` | 10 | 0 | +| `KZbHx6nNRQw` | 5 | 0 | +| `ZDKe0O0ynIs` | 5 | 0 | +| `mkFxjg4B3Dg` | 1 | 0 | diff --git a/docs/dev/debt/libMozaSDK.md b/docs/dev/debt/libMozaSDK.md new file mode 100644 index 0000000000..00dd80f10b --- /dev/null +++ b/docs/dev/debt/libMozaSDK.md @@ -0,0 +1,5 @@ +# libMozaSDK technical debt + +### Functional + +- [Exports](../../../core/libs/prx/libMozaSDK/Export.cpp): the library's exports, signatures and behavior are unknown, so it exports only a throwing import-resolution placeholder. The library is absent from the PS5 3.20 export inventory that the other placeholder libraries come from, and its firmware source and version are not verified. PPSA01317 (01.710.000) lists `libMozaSDK.prx` as a needed module and library without shipping it and binds no import to it, so on Linux the loader stops before the title runs without a file of that name; Windows skips the missing dependency. diff --git a/docs/dev/debt/libReactNative.Modules.Vsh.WebAppLauncher.md b/docs/dev/debt/libReactNative.Modules.Vsh.WebAppLauncher.md new file mode 100644 index 0000000000..3b65836960 --- /dev/null +++ b/docs/dev/debt/libReactNative.Modules.Vsh.WebAppLauncher.md @@ -0,0 +1,72 @@ +# libReactNative.Modules.Vsh.WebAppLauncher technical debt + +### Callable exports + +- [Callable exports](../../../core/libs/prx/libReactNative.Modules.Vsh.WebAppLauncher/Export.cpp) - 45 callable NIDs match the [public 3.20 catalogue](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libReactNative.Modules.Vsh.WebAppLauncher.c) and the [selected export metadata](https://gist.github.com/kyon-eth/b36a766ab0a33d469711188e09f19e2a/cc181f10ffa02358b56e235c69f5af19cfaf6d89) from two version-labelled images (SHA-256 `14fb80674fe2054933885dc3fa8a8abaad406191cdae915eb1a91b5580cae0a8`). The labels are not authenticated. Prototypes and behavior are unverified; the declarations throw through `NotImplemented_nid_no_patch` and implement no runtime functionality. Three other functions shared with the React Native Vsh library need separate provider-resolution analysis. + +| Name | NID | +| --- | --- | +| `CheckSystemEvents` | `VC8PCwSx3g0` | +| `CommerceDialogBrowseCategory` | `yNFPPeNhIwk` | +| `CommerceDialogBrowseProduct` | `YpeJJ7DL1Zo` | +| `CommerceDialogInit` | `o3tYKx8oBBA` | +| `CommerceDialogRedeemPromotionCode` | `ZLINx2XbQQ8` | +| `CommerceHidePsStoreIcon` | `k7y31RwRcGg` | +| `CommerceSetPsStoreIconLayout` | `3rXpEBCRi1o` | +| `CommerceShowPsStoreIcon` | `9-iVwdotSr4` | +| `DisableSystemMedia` | `jlIfgOEzZ2A` | +| `ErrorDialogClose` | `PUnaOnE5qv4` | +| `ErrorDialogInit` | `WeZCXuG60OQ` | +| `ErrorDialogOpen` | `ds9BJbeJ0ZQ` | +| `GetAccountAgeA` | `mypq-tQJOlo` | +| `GetAccountCountryA` | `+UmoBB-Kur0` | +| `GetAccountIdA` | `W17CX8bEsI4` | +| `GetAccountLanguage2` | `AitA5hf0b4o` | +| `GetAuthorizationCode` | `zn1GN6GVKwY` | +| `GetClosedCaptionSettings` | `xVlRy5NAUsw` | +| `GetOnlineId` | `2uw51wxVsiI` | +| `GetProcessTime` | `FQz66HusRxA` | +| `GetVideoOutputStatus` | `19LEprfkGS0` | +| `GetVideoOutputStatusInt` | `ndc59UeFwpE` | +| `Hide` | `6dhVu-SHjHI` | +| `IsDebuggingSupported` | `6eoxFFSEuoY` | +| `NpWebAbortRequest` | `eNQ0FStvU1c` | +| `NpWebDeleteRequest` | `yjjDwtgu4jE` | +| `NpWebInitialize` | `0zuAEore8pY` | +| `NpWebReadData` | `9gfFGr6B1Fc` | +| `NpWebTerminate` | `sXn8uN-ijvw` | +| `OpenClosedCaptionAdvancedSettings` | `fqERqkvThgQ` | +| `OpenClosedCaptionSettings` | `kroxOU831V4` | +| `SoundInitialize` | `82UnAk1CC98` | +| `SoundLoad` | `7x711wfwKEM` | +| `SoundPlay` | `k77hI5zSW1c` | +| `SoundTerminate` | `pO9cpXuuPwU` | +| `SpeechCancel` | `rwnTeqYe0bY` | +| `SpeechClose` | `7SmqB-eXB+4` | +| `SpeechInitialize` | `9ozXeR1-w4c` | +| `SpeechSpeak` | `DceITXrEqMI` | +| `SpeechStatus` | `cJ1aLzpbyow` | +| `SpeechTerminate` | `W5pdIiQSrmY` | +| `SystemDialogInit` | `zmUqZyLpEhs` | +| `SystemDialogOpen` | `AVyuvygnd0w` | +| `SystemServiceParamGetInt` | `Ewd+kXqU2Bc` | +| `SystemServiceParamGetString` | `vqMvoe76W6k` | + +### Additional unidentified exports + +- Twelve [callable exports](../../../core/libs/prx/libReactNative.Modules.Vsh.WebAppLauncher/Export.cpp) are throwing placeholders. Names, signatures and behavior are unverified. Matching module and library identities occur in two version-labelled images; those labels are not authenticated. + +| Name | NID | +| --- | --- | +| `Unknown` | `+ucrAt9fI7c` | +| `Unknown` | `06N-dAyE+HY` | +| `Unknown` | `41blU2JCqLQ` | +| `Unknown` | `BGgSEXB1Now` | +| `Unknown` | `CAK-S2JVdsk` | +| `Unknown` | `QxCdpkEsOgo` | +| `Unknown` | `Tjj++S2qvOU` | +| `Unknown` | `f5NHqUziG+4` | +| `Unknown` | `mI-ltMuVN+Q` | +| `Unknown` | `vDup+0mWZf8` | +| `Unknown` | `wR1ucKGDM9Q` | +| `Unknown` | `xPA-tXzPGpc` | diff --git a/docs/dev/debt/libReactNative.Modules.Vsh.md b/docs/dev/debt/libReactNative.Modules.Vsh.md new file mode 100644 index 0000000000..dfc85d4513 --- /dev/null +++ b/docs/dev/debt/libReactNative.Modules.Vsh.md @@ -0,0 +1,98 @@ +# libReactNative.Modules.Vsh technical debt + +### Callable exports + +- [Callable exports](../../../core/libs/prx/libReactNative.Modules.Vsh/Export.cpp) - 65 callable NIDs match the [public 3.20 catalogue](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libReactNative.Modules.Vsh.c) and the [selected export metadata](https://gist.github.com/kyon-eth/c09d6fdb71ceddd6d272f23ccc02ea4d/c875018fc77e54dcf76c639ceb7a58b0a85cf16b) from two version-labelled images (SHA-256 `ee343a3ad0c3badb1ed2ebff777bcc24cc00fbfedf7797ae37ed51b4d46dcd9e`). The labels are not authenticated. Prototypes and behavior are unverified; the declarations throw through `NotImplemented_nid_no_patch` and implement no runtime functionality. Data exports are not included. Three other functions shared with the WebAppLauncher library need separate provider-resolution analysis. + +| Name | NID | +| --- | --- | +| `CreateWebApiLibCtx` | `MjYzx34cnmE` | +| `CryptographyDecryptAES128CBC` | `x4fETeuAcVU` | +| `CryptographyDecryptAES128CBC_HS256` | `1HSkObnLOC0` | +| `CryptographyDecryptAES128GCM` | `MaAqY5ufzmk` | +| `CryptographyEncryptAES128CBC` | `WM4KfSxNA3I` | +| `CryptographyEncryptAES128GCM` | `SYc9wxeZia8` | +| `CryptographyGenerateHmacSha256` | `0L0rPrqMqqE` | +| `DestroyWebApiLibCtx` | `RJ9NntjPcEs` | +| `GetLibHttpCtxId` | `cCMDYO9tVWc` | +| `GetPs4AppCategoryForTitleId` | `bmzRityxR44` | +| `InetStatGetResultNative` | `nNDGebQ9xr0` | +| `InetStatGetStateNative` | `katLYKl5Xhw` | +| `InetStatStartTestNative` | `iTT38xYbOqI` | +| `InitializeNetLib` | `YzB8-Aa5UV4` | +| `LoginMgrCreateUser` | `jpb8rbo4MwU` | +| `LoginMgrDestroyUser` | `-N9SbnOwNto` | +| `LoginMgrInitialize` | `lvsjPQ8LMH0` | +| `LoginMgrSetLoginFlag` | `xXkwDDcyfBw` | +| `LoginMgrSetUserStatus` | `cxRN3ES1Hmk` | +| `LoginMgrTerminate` | `CBgGpKwrHSA` | +| `NpStateManagerRegisterCallback` | `D3CRTfQdhbA` | +| `NpStateManagerRtcSetTick` | `P6fVk0LPubc` | +| `NpStateManagerStart` | `eXL2pNf3JSA` | +| `NpStateManagerStop` | `5gsmp2Ox0Vs` | +| `NpStateManagerUnregisterCallback` | `f5Zum3vudFM` | +| `PSNowGetStatus` | `BFyDpKjORBg` | +| `PSNowGetStreamingStatus` | `ln8IwqIWE4I` | +| `PSNowInitialize` | `9Gsw6lvdddE` | +| `PSNowReset` | `gm2mcS8itac` | +| `PSNowSendCommand` | `mjcI9VABYqw` | +| `PSNowSetAuthCodes` | `PMAYiRpjoMY` | +| `PSNowSetSettings` | `H0yW7HkbWzI` | +| `PSNowTerminate` | `FbdEMaWO06g` | +| `RemotePlayClearConnectHistory` | `1nU-9usW+G0` | +| `RemotePlayConfirmDeviceRegist` | `Sh2l-tqTK68` | +| `RemotePlayDisconnect` | `Tjr5ksLUG1o` | +| `RemotePlayGeneratePinCode` | `Ktw+4DDd-60` | +| `RemotePlayGetConnectHistory` | `Pp9o-vFsZxc` | +| `RemotePlayGetConnectUserId` | `sB3dPDza3bk` | +| `RemotePlayGetConnectionStatus` | `PIt3ROlWOic` | +| `RemotePlayGetRemoteplayStatus` | `Mio-My1B0Ek` | +| `RemotePlayGetRpMode` | `4U7XQYIZAI0` | +| `RemotePlayIsRemotePlaying` | `RXRWD6iVzvI` | +| `RemotePlayNotifyCanWakeup` | `p1zwikCkFho` | +| `RemotePlayNotifyPinCodeError` | `xiJubl4cUak` | +| `RemotePlayNotifyUserDelete` | `GyKvp1lgCFw` | +| `RemotePlayProhibitStreaming` | `JSbNmt85AcQ` | +| `RemotePlaySetProhibition` | `BOtuzpAvdiU` | +| `RemotePlaySetProhibitionForVsh` | `4T9kRhpb-V0` | +| `RemotePlaySetRpMode` | `2YfXXadue0s` | +| `RtcGetCurrentAdNetworkTickNative` | `BMqsRTJXTRk` | +| `RtcGetCurrentDebugNetworkTickNative` | `8WzUhuCvVx4` | +| `RtcGetCurrentNetworkTickNative` | `m7od1ikoZpQ` | +| `TerminateNetLib` | `n38wJyalO6c` | +| `WebBrowserGetEnvelopeData` | `Z5HNtKvubuo` | +| `sceRemotePlayClientAddEventListener` | `gezFjY7iHbk` | +| `sceRemotePlayClientAttachVirtualMicrophone` | `nQ9t43vahAE` | +| `sceRemotePlayClientDetachVirtualMicrophone` | `nePk3oFOMXA` | +| `sceRemotePlayClientGetClientStatus` | `ux9VESJUgh4` | +| `sceRemotePlayClientInitialize` | `R0TfhbZMHm4` | +| `sceRemotePlayClientRemoveController` | `tDjU6UN7meM` | +| `sceRemotePlayClientShowHomeScreen` | `T589M5rT9iw` | +| `sceRemotePlayClientStartMicCapture` | `r9MlfKcu6N4` | +| `sceRemotePlayClientStopMicCapture` | `jLpO843jV50` | +| `sceRemotePlayClientTerminateStreaming` | `N1dFmppU9j0` | + +### Additional unidentified exports + +- Eighteen [callable exports](../../../core/libs/prx/libReactNative.Modules.Vsh/Export.cpp) are throwing placeholders. Names, signatures and behavior are unverified. Matching module and library identities occur in two version-labelled images; those labels are not authenticated. + +| Name | NID | +| --- | --- | +| `Unknown` | `+I5sPs9FLXw` | +| `Unknown` | `-9hYGDAsb4E` | +| `Unknown` | `BzJsgzD97LU` | +| `Unknown` | `IQNf2nnftNI` | +| `Unknown` | `N3mcm-qDFR0` | +| `Unknown` | `OME2Be+FlwA` | +| `Unknown` | `PLEe-toB-KE` | +| `Unknown` | `QGGsQrtLW9Y` | +| `Unknown` | `Rm+rgnBoPIE` | +| `Unknown` | `SJU+qK+8yrM` | +| `Unknown` | `T7dL-hEySvU` | +| `Unknown` | `b+MJDz3HB6s` | +| `Unknown` | `bbsP8-e8GZA` | +| `Unknown` | `fxEQAPDRhLw` | +| `Unknown` | `j917Pp4k-CI` | +| `Unknown` | `lZ-JDEqB34k` | +| `Unknown` | `tKy-Qdg+FRc` | +| `Unknown` | `uQfQgwm36L4` | diff --git a/docs/dev/debt/libSceAbstractStorage.md b/docs/dev/debt/libSceAbstractStorage.md index 133265d1eb..7890899db0 100644 --- a/docs/dev/debt/libSceAbstractStorage.md +++ b/docs/dev/debt/libSceAbstractStorage.md @@ -2,7 +2,7 @@ ### Known export names; ABI prototypes unverified -- 110 pending exports in [Export.cpp](../../../core/libs/prx/libSceAbstractStorage/Export.cpp) are present in the [PS5-3.20_Libs](https://github.com/DNNDHH/PS5-3.20_Libs) inventory. Their prototypes and behavior are unverified. They use the no-parameter import-resolution convention and throw through `NotImplemented_nid_no_patch`; these declarations make no ABI or behavior claim. +- 110 pending exports in [Export.cpp](../../../core/libs/prx/libSceAbstractStorage/Export.cpp) are present in the [PS5-3.20_Libs](https://github.com/DNNDHH/PS5-3.20_Libs) inventory. Each is named after the mangled symbol the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv) gives for its NID, and the NID of that name matches. Their prototypes and behavior are unverified. They use the no-parameter import-resolution convention and throw through `NotImplemented_nid_no_patch`; these declarations make no ABI or behavior claim. ### Additional callable exports diff --git a/docs/dev/debt/libSceAbstractYoutube.md b/docs/dev/debt/libSceAbstractYoutube.md index 9b35c970a8..7e05322fa8 100644 --- a/docs/dev/debt/libSceAbstractYoutube.md +++ b/docs/dev/debt/libSceAbstractYoutube.md @@ -3,3 +3,51 @@ ### Known export names; ABI prototypes unverified - 17 pending exports in [Export.cpp](../../../core/libs/prx/libSceAbstractYoutube/Export.cpp) are present in the [PS5-3.20_Libs](https://github.com/DNNDHH/PS5-3.20_Libs) inventory. Their prototypes and behavior are unverified. They use the no-parameter import-resolution convention and throw through `NotImplemented_nid_no_patch`; these declarations make no ABI or behavior claim. + +### Additional callable exports + +- [Additional callable exports](../../../core/libs/prx/libSceAbstractYoutube/Export.cpp) - 39 callable NIDs match the [public 3.20 catalogue](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAbstractYoutube.c) and the [selected export metadata](https://gist.github.com/kyon-eth/d35bcd082a172a28ada2052647b0ff20/72a4f16fcb238e61dc8c6424dc26eee106e6c8ce) from two version-labelled images (SHA-256 `3d83607a2ed81110a37d677e5ba1936a01cb78c5847a2e9b582a4a981d313050`). The labels are not authenticated. Prototypes and behavior are unverified; the declarations throw through `NotImplemented_nid_no_patch` and implement no runtime functionality. + +| Name | NID | +| --- | --- | +| `_ZN15AbstractStorage13YoutubeFolder6RemoveEb` | `sFDmBXjzJKs` | +| `_ZN15AbstractStorage13YoutubeFolderD0Ev` | `oFx2mmWanvg` | +| `_ZN15AbstractStorage13YoutubeFolderD1Ev` | `lkGvBL1ly-Y` | +| `_ZN15AbstractStorage13YoutubeFolderD2Ev` | `KdoemZ8qogY` | +| `_ZN15AbstractStorage14YoutubeContent11SetMetadataEPN23sceMetadataReaderWriter8MetadataE` | `835p-ywJWEY` | +| `_ZN15AbstractStorage14YoutubeContent13ParseMetadataEPN23sceMetadataReaderWriter8MetadataES3_` | `iYDibzjdBf8` | +| `_ZN15AbstractStorage14YoutubeContent4OpenEit` | `gID4V0+Uk+k` | +| `_ZN15AbstractStorage14YoutubeContent4ReadEPvlPl` | `MJP2vvOKllQ` | +| `_ZN15AbstractStorage14YoutubeContent5CloseEv` | `BbkdawQaHyg` | +| `_ZN15AbstractStorage14YoutubeContent5FlushEv` | `Tz3OEt87piQ` | +| `_ZN15AbstractStorage14YoutubeContent5WriteEPKvlPl` | `95+sJrPf30A` | +| `_ZN15AbstractStorage14YoutubeContent6RemoveEv` | `XSYZ7Ac3HBQ` | +| `_ZN15AbstractStorage14YoutubeContent8SeekByteEliPl` | `d5LwAYdjVqk` | +| `_ZN15AbstractStorage14YoutubeContent8SeekTimeElPl` | `uHAsYIPFXPA` | +| `_ZN15AbstractStorage14YoutubeContentD0Ev` | `3XwULRPa25E` | +| `_ZN15AbstractStorage14YoutubeContentD1Ev` | `TFyVtXqnZrA` | +| `_ZN15AbstractStorage14YoutubeContentD2Ev` | `F09B23u5GSU` | +| `_ZN15AbstractStorage14YoutubeService13SearchStorageEv` | `6OnmSnvjzPI` | +| `_ZN15AbstractStorage14YoutubeService4StopEv` | `7JwKchX2P8o` | +| `_ZN15AbstractStorage14YoutubeService5StartEv` | `DGbb+VBzV6c` | +| `_ZN15AbstractStorage14YoutubeServiceC1Ev` | `UyVVO5GxZl4` | +| `_ZN15AbstractStorage14YoutubeServiceC2Ev` | `sYyou0hGbP8` | +| `_ZN15AbstractStorage14YoutubeServiceD0Ev` | `J526sLKBCc8` | +| `_ZN15AbstractStorage14YoutubeServiceD1Ev` | `jGcnzPEHoZ8` | +| `_ZN15AbstractStorage14YoutubeServiceD2Ev` | `dhk0U1yIuJo` | +| `_ZN15AbstractStorage14YoutubeStorage14GetStorageSizeEPlS1_S1_` | `u6FSGzt9Ay0` | +| `_ZN15AbstractStorage14YoutubeStorageD0Ev` | `VkzubmxnY7s` | +| `_ZN15AbstractStorage14YoutubeStorageD1Ev` | `D5iaQceaLcI` | +| `_ZN15AbstractStorage14YoutubeStorageD2Ev` | `hfifwAJDCvU` | +| `_ZNK15AbstractStorage13YoutubeFolder10GetServiceEv` | `cyheZerH-jM` | +| `_ZNK15AbstractStorage13YoutubeFolder13GetCapabilityEv` | `2jgqoWLtP4o` | +| `_ZNK15AbstractStorage13YoutubeFolder7GetStatEPNS_4StatE` | `Frm1N1BC9XE` | +| `_ZNK15AbstractStorage14YoutubeContent10GetServiceEv` | `zIMH5hIuWk4` | +| `_ZNK15AbstractStorage14YoutubeContent13GetCapabilityEv` | `EDw-LQKe3fE` | +| `_ZNK15AbstractStorage14YoutubeContent7GetStatEPNS_4StatE` | `+SMBrTixsiA` | +| `_ZNK15AbstractStorage14YoutubeService14GetServiceTypeEv` | `UTsKij05zBY` | +| `_ZNK15AbstractStorage14YoutubeStorage10GetServiceEv` | `uVLETIxb0yA` | +| `_ZNK15AbstractStorage14YoutubeStorage13GetCapabilityEv` | `jgO+nc-VCgU` | +| `_ZNK15AbstractStorage14YoutubeStorage7GetStatEPNS_4StatE` | `GGthfnlokVw` | + +- [Older callable NIDs](../../../core/libs/prx/libSceAbstractYoutube/Export.cpp): 20 unidentified exports have unverified names, signatures and behavior. These are throwing import-resolution placeholders; no functionality is implemented. No matching NIDs were found in the later module inventory. diff --git a/docs/dev/debt/libSceAgc.md b/docs/dev/debt/libSceAgc.md index 1a745ef7ec..81f8fce239 100644 --- a/docs/dev/debt/libSceAgc.md +++ b/docs/dev/debt/libSceAgc.md @@ -34,7 +34,7 @@ - [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) - [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. In PPSA12544 the Acb function is called from 2 sites (0x11d9538, 0x11d95a4) and the Dcb function from 4 (0x11d76d4, 0x11d7740, 0x16e5eac, 0x16f32a6), each with three arguments and no read of the result; which of the last two arguments is the handle and which the index is not shown. - [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. +- [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. PPSA25286 01.000.568, PPSA34502 01.010.000 and PPSA36591 01.002.000 each link one wrapper per function (`SetIndexIndirectArgs`/`GetSize` at 0x405AC0/0x406030, 0xAA1C10/0xAA2180 and 0x898300/0x898870): the first passes index 0 with the buffer and address it was given, the second passes its arguments unchanged. Run-time use is untraced; PPSA01494, PPSA02225, PPSA03984, PPSA18194, PPSA30839 and PPSA31162 import neither. - [sceAgcSuspendPointAndCheckStatus](../../../core/libs/prx/libSceAgc/Export.cpp) - this named callable export is listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAgc.c) and was cross-checked against the same module in PS5 firmware 13.60. Its signature and behaviour are unverified; the declaration uses the no-parameter unknown-signature convention and throws through `NotImplemented_nid_no_patch`. diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index ee53f48003..57a7b26ed0 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -33,13 +33,17 @@ ### Unknown function info +- [libSceAgcDriver pending exports](../../../core/libs/prx/libSceAgcDriver/Export.cpp) - 24 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAgcDriver.c). Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. + - [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. Compute queues are still accepted only for 0x20 to 0x57; queue 0x58 and above throw. Console acceptance of 0x58 on these entrypoints was not measured +- [sceAgcDriverAddEqEvent, sceAgcDriverDeleteEqEvent](../../../core/libs/prx/libSceAgcDriver/Eq/src/Event.cpp) (libSceAgcDriver) - validates the event queue descriptor and non-negative event IDs for graphics and compute end-of-pipe event registration. Deletion unregisters from the kernel event queue before removing the registration from driver state, ensuring failed deletions preserve registered events. Event delivery and filter semantics for alternate event filters remain unverified on console hardware. + - [sceAgcDriverSubmitAcb](../../../core/libs/prx/libSceAgcDriver/Submit/src/Acb.cpp) (libSceAgcDriver) accepts compute queues 0x20 to 0x58 and throws for any other. On a PS5 (firmware 13.40, measured 2026-10-19 from inside a retail title) it returned 0 and ran a label-only buffer on queues 0x20, 0x30, 0x40, 0x50, 0x57 and 0x58; queue 0x1F faulted inside the console's driver (SIGSEGV in libkernel on the calling thread), which throws here too. Queues above 0x58 were not measured. Four titles (PPSA15246 at 0x71dc9b, PPSA13356 at 0xd0c7b, PPSA21480 at 0xa920b, PPSA35169 at 0x6d08bb) disassembled for this entry call it once each from the same small wrapper, with queue 0x20 + 8 * the low byte of its argument | the next byte, and return the result unchanged; which values occur is not shown. Closes when: the console's highest accepted compute queue is measured - [sceAgcDriverSubmitMultiAcbs](../../../core/libs/prx/libSceAgcDriver/Submit/src/Acb.cpp) (libSceAgcDriver) still accepts only compute queues 0x20 to 0x57; queue 0x58 and above throw. Console acceptance of 0x58 on this entrypoint was not measured - [sceAgcDriverAcquireHDRScopesACQ, sceAgcDriverHp3dLockEnd, sceAgcDriverHp3dLockStart, sceAgcDriverIsEsvmProcess, 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`. -- [sceAgcDriverSetFlip, sceAgcDriverSetWorkloadsActive, sceAgcDriverSetWorkloadComplete, sceAgcDriverUserDataWritePacket, sceAgcDriverUserDataWritePushMarker, sceAgcDriverUserDataWritePopMarker, sceAgcDriverUserDataWriteSetMarker](../../../core/libs/prx/libSceAgcDriver/Export.cpp) (libSceAgcDriver) - declared without parameters; they throw through `NotImplemented_nid_no_patch`. The tree answers their packet sizes (`sceAgcDriverGetSetFlipPacketSizeInDwords`, `sceAgcDriverGetSetWorkloadsActivePacketSize`, `sceAgcDriverGetSetWorkloadCompletePacketSize`, `sceAgcDriverUserDataGetPacketSize` in `Submit/src/Dcb.cpp`), and libSceAgc writes its own flip, workload and marker packets through a `CommandBuffer*` (`libSceAgc/DcbState/src/Display.cpp`, `Workload.cpp`, `Marker.cpp`), but it has no driver-level prototype, so whether these calls take the same arguments and write the same words is not known +- [sceAgcDriverSetFlip, sceAgcDriverSetWorkloadsActive, sceAgcDriverSetWorkloadComplete](../../../core/libs/prx/libSceAgcDriver/Export.cpp) (libSceAgcDriver) - declared without parameters; they throw through `NotImplemented_nid_no_patch`. The tree answers their packet sizes (`sceAgcDriverGetSetFlipPacketSizeInDwords`, `sceAgcDriverGetSetWorkloadsActivePacketSize`, `sceAgcDriverGetSetWorkloadCompletePacketSize` in `Submit/src/Dcb.cpp`), and libSceAgc writes its own flip and workload packets through a `CommandBuffer*` (`libSceAgc/DcbState/src/Display.cpp`, `Workload.cpp`), but it has no driver-level prototype, so whether these calls take the same arguments and write the same words is not known - [sceAgcDriverCreateQueue, sceAgcDriverDestroyQueue, sceAgcDriverSetupAsyncGraphics, sceAgcDriverSuspendPointSubmit, sceAgcDriverIDHSSubmit, sceAgcDriverTmpInitIdhs, sceAgcDriverSubmitToHDRScopesACQ, sceAgcDriverCwsrSuspendAcq, sceAgcDriverCwsrResumeAcq](../../../core/libs/prx/libSceAgcDriver/Export.cpp) (libSceAgcDriver) - declared without parameters; they throw through `NotImplemented_nid_no_patch`. The tree submits only through the calls in `Submit/src` and knows a queue context only as `sceAgcDriverSubmitCommandBuffer` reads it; it has no arguments or results for queue creation, suspend-point or IDHS submission, or the HDR-scopes and CWSR queue calls - [sceAgcDriverGetRegShadowInfo, sceAgcDriverGetRegShadowInfoAgr, sceAgcDriverSetupRegisterShadow, sceAgcDriverPatchClearState, sceAgcDriverNotifyDefaultStates, sceAgcDriverPassInfoDownward, sceAgcDriverGetReservedDmemForAgc, sceAgcDriverGetWorkloadStreamInfo](../../../core/libs/prx/libSceAgcDriver/Export.cpp) (libSceAgcDriver) - declared without parameters; they throw through `NotImplemented_nid_no_patch`. The executor keeps no register-shadow memory and runs `CLEAR_STATE` itself (`Execution/src/Pm4.cpp`) and workload streams are kept only as registered ids (`Resource/src/Registration.cpp`); the arguments and results of these calls are not in the tree - [sceAgcDriverDebugHardwareStatus, sceAgcDriverSysEnableSubmitDone45Exception, sceAgcDriverSysGetClientNumber, sceAgcDriverSysIsGameClosed](../../../core/libs/prx/libSceAgcDriver/Export.cpp) (libSceAgcDriver) - declared without parameters; they throw through `NotImplemented_nid_no_patch`. They are not among the retail constant results in `Unimplemented.cpp`, and the tree has no prototype or console answer for them @@ -62,6 +66,7 @@ - [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, and D32 with an 8-bit stencil plane whose state is in HTILE or, with a depth-only HTILE, in the plane, two/four/eight samples, one Z_X slice and mip, and pipe-aligned HTILE, or a depth-only HTILE that is not pipe-aligned whose words are all one clear word, without 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, with the stencil in HTILE, SMem 0 (the stencil holds `DB_STENCIL_CLEAR`), or with a depth-only HTILE every stencil plane byte must equal it; and the complete sample-aware depth, stencil 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 fill of the whole HTILE, or a one-byte fill of the whole stencil plane (a stencil clear), is proven at the next sample like an HTILE mask clear: complete tracked stores since the fill, or a fill still queued that is the one write recorded over the range since it was noted (a fill noted again once that write is recorded counts once), and every word proving the depth and, with the stencil in HTILE, the stencil clear, or every plane byte the fill byte; partial fills, an HTILE mask clear of a target with stencil, and an HTILE fill noted while an HTILE mask clear dispatch is pending (from its notification until a sample takes it) are refused. A queued fill of more than 64 KiB is stored through a host import, so where host imports are compared rather than write-watched (always on Windows, and on Linux where the import probe finds that imports resolve write protection) its HTILE is no longer write-watched after the store and the fill is refused. A stencil plane whose exact footprint a resident multisampled R8 colour target of the same sample count covers takes its value from that target's newest uniform content (a fill it absorbed, a CMASK fast clear, or a recorded clear with no draw since), and a plane fill is proven the same way, without reading guest memory; any colour target over the depth or HTILE, or a colour draw over a plane taken this way, refuses sampling, and other colour content over the plane leaves it unproven as listed below. A descriptor without metadata whose XOR and ITERATE_256 match is sampled from the decompressed host image only after an observed in-place expand, a draw over the target with `DB_RENDER_CONTROL.DEPTH_COMPRESS_DISABLE` and, with a stencil plane, `STENCIL_COMPRESS_DISABLE`, and until an HTILE clear or fill, a plane fill or a clear draw; a later depth write that recompresses tiles is not observed. Other metadata is refused. 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, raw-bit sampling, arrays and storage access remain unimplemented; stencil plane sampling is listed below. 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 D32S8 depth without HTILE](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) is imported only when its depth plane repeats one float in [0, 1], its separate stencil plane repeats one byte and no multisampled colour target covers either plane; the host image starts at those values and the sampled descriptor must name no metadata, whose pipe-aligned and ITERATE_256 bits are then not compared. The depth plane is sampled as a 32-bit float and the stencil plane through the R8_UINT descriptor of the stencil plane sampling entry below. That every sample then holds them is inferred from the uncompressed layout, not measured on the console. Planes holding more than one value, and D16 or D32 without HTILE, still throw. - [Native multisample stencil plane drawn as colour](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) - a native D32S8 target whose separate stencil plane a multisampled colour target covers with anything but a known R8 value of the same sample count is imported with the plane unproven: its host stencil is not taken from it, and a stencil-tested draw or a sample of the plane refuses (`native multisample stencil plane aliases a multisampled colour target whose samples are only on the host`) until a whole-plane fill gives its byte, which a colour target still over the plane must have absorbed as the same R8 byte at the same sample count. Taking a drawn plane without a fill needs a GPU copy of the colour target's R8 samples, in the same sample order, into the depth image's stencil aspect, which is not implemented. Whether the console's stencil reads of a plane written through the colour path match that copy is not measured. - [Native multisample stencil plane sampling](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) - the separate stencil plane of a native multisampled D32S8 target with a depth-only HTILE is sampled through an R8_UINT 2D MSAA descriptor at the plane with the target's extent, samples, tile mode and XOR and no metadata, as a stencil-aspect view of the host depth image or of its sample shadow, which then copies the stencil with the depth. A stencil kept in HTILE, another format, metadata or layout is refused, and a plane left unproven by a colour target refuses as listed above. Whether the console's reads of the plane through such a descriptor match the host stencil is not measured. - [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 @@ -72,7 +77,7 @@ - [GS_FAST_LAUNCH geometry](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - `VGT_SHADER_STAGES_EN.GS_FAST_LAUNCH = 1` with `GE_CNTL` groups of one vertex and one primitive decodes as one point per subgroup whatever `VGT_PRIMITIVE_TYPE` holds, with the primitive limit taken from `GE_NGG_SUBGRP_CNTL.PRIM_AMP_FACTOR` and the GS/ES VGPR counts not required. Follows how RADV drives mesh shaders on gfx10.3 (`ac_nir_lower_ngg.c`, a point list whose vertex id gives the workgroup index), not measured on the console; for a line or triangle type (PPSA34547 uses a triangle strip) one subgroup per vertex is assumed. Fast launch 2, other group sizes and indexed fast-launch draws still throw. PPSA03671's fast-launch draws decode, but the title stops earlier in a wave64 vertex program, so they have not reached the GPU yet. - [Point geometry output](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - a geometry program with `VGT_GS_OUT_PRIM_TYPE = 0` emits mesh points with `OutputPoints` and scalar `PrimitivePointIndicesEXT` as required by [SPV_EXT_mesh_shader](https://github.khronos.org/SPIRV-Registry/extensions/EXT/SPV_EXT_mesh_shader.html). Each packed primitive export contributes its first 9-bit vertex index and its null bit, using the same NGG primitive packing as line and triangle outputs. The subgroup reserves at least `GE_MAX_OUTPUT_PER_SUBGROUP` point slots, as the line path does; the console budget is not measured. Points use the register size or the misc-X point-size export and clamp interpreted by the vertex-point path; those register, clamp and rasterization inferences remain unmeasured on the console. Point sprites remain unsupported. Point output from a vertex workgroup still throws. Cocoon (PPSA08766) reaches this path with triangle-strip input. - [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) - `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. Two-sample `DB_ALPHA_TO_MASK = 0xe701` (Bugsnax, PPSA01503) uses an eight-level table measured on gfx1032 with the locations (4, 4), (-4, -4) and (-4, 4), (4, -4), not on gfx1013; other sample counts, locations and exports throw, and `MASK_EXPORT_NUM_SAMPLES` 0 is accepted though measured at 1 +- [Alpha-to-coverage](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - `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`, 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. Two-sample `DB_ALPHA_TO_MASK = 0xe701` (Bugsnax, PPSA01503) uses an eight-level table measured on gfx1032 with the locations (4, 4), (-4, -4) and (-4, 4), (4, -4), not on gfx1013; other sample counts, locations and exports throw, and `MASK_EXPORT_NUM_SAMPLES` 0 is accepted though measured at 1 - [Single-sample dithered alpha-to-coverage](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - single-sample targets with dithered `DB_ALPHA_TO_MASK` (0xe701 or 0x18701) throw `requires two matched raster/DB_EQAA samples and one pixel iteration` (for 0xe701) or `requires eight matched raster/DB_EQAA samples and one pixel iteration` (for 0x18701) in `DecodeState` and `DrawRejection`. An initial Vulkan `alphaToCoverageEnable` readback on RADV (RX 6650 XT) for a 4x4 target exhibited quad-granular coverage rather than a per-pixel Bayer pattern inside each 2x2 quad, but the spatial repetition period and PS5 console silicon behaviour remain unmeasured across larger targets. Bugsnax (PPSA01503) reaches 0xe701 with one sample at the Flavor Falls bridge cutscene transition. Stays unsupported until measured on hardware. Closes when: spatial repetition period is measured on AMD RADV or PS5 hardware - [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 (with a one-sample depth target, `ANCILLARY` only under `PS_ITER_SAMPLE`), 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. 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. With a one-sample depth target and without `PS_ITER_SAMPLE`, the [packed ancillary word](../../../core/shader/recompiler/Optimization/src/ConstantFolder.cpp) gives a program that reads `ANCILLARY` sample index 0, inferred for a pixel the guest does not iterate, and that is the sample the one-sample draw keeps. Points and alpha-to-coverage throw as for a multisampled target. With a one-sample depth target, `DB_EQAA.INTERPOLATE_COMP_Z` is accepted: with every sample at the pixel center, Z interpolated to any sample position is the center's Z, which the one-sample draw tests. That rests on the same inference; neither [PAL](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/hw/gfxip/gfx9/gfx9MsaaState.cpp#L117-L118) nor [radeonsi](https://gitlab.freedesktop.org/mesa/mesa/-/blob/761d5c8a54f93b3d43608f718910ed742983dd0b/src/gallium/drivers/radeonsi/si_state.c#L3314-L3316) sets the bit, and its meaning is not documented. Line and point lists (`VGT_PRIMITIVE_TYPE` 1-3, even when a geometry program turns them into triangles), `INTERPOLATE_SRC_Z` and overrasterization (`OVERRASTERIZATION_AMOUNT`, `ENABLE_POSTZ_OVERRASTERIZATION`) still 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. Gran Turismo 7 (PPSA01317) draws its main pass with a one-sample depth target, `DB_EQAA` 0x00050000 and a 4x configuration. - [One-fragment multisampled color target](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - a color target with `CB_COLOR_ATTRIB` 4 samples and 1 fragment is decoded as one sample that keeps its coverage sample count and draws sample 0's coverage. Measured only on radeonsi GL (Radeon 610M and 680M); console behaviour is not measured. It requires static `DB_EQAA` anchors at the depth target's sample count and CMASK code 0 or 0xc-0xf. More than one but fewer fragments than samples, more fragments than samples, other CMASK codes and any guest read of the FMASK of a target the host drew still throw. @@ -82,6 +87,7 @@ - [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 +- [sceAgcDriverUserDataWritePacket, sceAgcDriverUserDataWritePushMarker, sceAgcDriverUserDataWriteSetMarker, sceAgcDriverUserDataWritePopMarker](../../../core/libs/prx/libSceAgcDriver/Submit/src/Dcb.cpp) (libSceAgcDriver) - the packets follow firmware 13.42's libSceAgcDriver, read from disassembly, not measured on a console: a SET_UCONFIG_REG of register 0x342 (and 0x343 with up to four data bytes), then a WRITE_DATA of the remaining bytes to register 0xC343. The second argument is named `asyncCompute` because firmware libSceAgc passes 1 from its `sceAgcAcb*` marker calls and 0 from `sceAgcDcbPopMarker`; 0 sets header bit 2 in both packets. The marker kinds (0xC0000000 set, 0xC1000000 push, 0xC2000000 pop) and the colour packing are copied without names - [Barycentrics without mesh shaders](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp) (AGC driver) - a pixel shader that reads barycentrics on the mesh path is emulated only on a device with `VK_EXT_mesh_shader`; a device with neither it nor `VK_KHR_fragment_shader_barycentric` (reported on Linux RADV with a GCN 5 GPU) throws that the device has neither the barycentric extension nor mesh shaders, because a geometry stage cannot follow a mesh stage and no other route is implemented. `VK_AMD_shader_explicit_vertex_parameter`, which a GCN 5 device on RADV exposes, is an unexplored option. - [NGG vertex programs on the mesh path](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - a vertex program with `PRIMGEN_EN` that uses LDS or `s_barrier` runs on the geometry (mesh) path. Each input primitive is assumed to yield at most one output primitive, the subgroup is bounded by `GE_MAX_OUTPUT_PER_SUBGROUP` as a vertex budget, and the GS/ES VGPR count checks are skipped, because the merged inputs keep their positions. Taken from the code Mesa's ACO emits for NGG culling and from PPSA59322 and PPSA99010 draws, not measured on hardware. - [VGT_SHADER_STAGES_EN 0x02002008](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - this one value, `ES_EN` = 1 without `HS_EN` on a non-patch draw, is drawn as the vertex path. AMD's CIK register reference calls `ES_EN` = 1 `ES_STAGE_DS` (the ES stage runs the tessellation evaluation shader), which does not explain it on a non-patch draw. Seen only on PPSA29413 and not verified on the console; other vertex-path values with `ES_EN` set still throw. @@ -111,7 +117,7 @@ - [PS4 (GCN) 2D PRT tiling](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp) (libSceAgcDriver, PS4) - not checked on a console. GB_TILE_MODE 6, 7, 12 and 17 take the bank from the coordinates inside the macro tile, as shadPS4 does; AMD's addrlib (Mesa 393bf0d1467c) and fpPS4 (04cefd43e6fd) use the whole coordinates for the bank of these 2D PRT modes and take it inside the macro tile only for modes 11 and 16, which fpPS4 documents as the ones that can alias virtual pages to one physical page. - [PS4 (GCN) thick and layered surfaces](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp) (libSceAgcDriver, PS4) - 3D and thick modes (GB_TILE_MODE 15 and 19 to 26) throw, and so does a storage image or a depth plane of a layered surface with several mip levels. A sampled layered surface with several mip levels is stored level by level, which is not checked on a console; [GcnDetile.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/GcnDetile.cpp) runs the GPU detiler over every mode that does not throw. The bank of each layer of a tiled 2D array is rotated by (banks / 2 - 1) times the layer index outside the PRT modes, XORed in as AMD's address library and shadPS4 (`tiling.comp`, 0614ce324be0) do; fpPS4 (`ps4_tiling.pas`, 04cefd43e6fd) adds it to the bank instead. Not checked on a console. - [PS4 (GCN) multisampled color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver, PS4) - a GCN color target with 2, 4 or 8 samples and as many fragments (CB_COLOR_ATTRIB NUM_SAMPLES and NUM_FRAGMENTS) becomes a host-resident multisampled target, as on the AGC path: its samples stay on the host. With FAST_CLEAR or COMPRESSION set, its CMASK (CB_COLOR_CMASK, CMASK_SLICE tile max + 1 times 128 bytes) is taken for fast clears; FMASK contents are not modeled. Fewer fragments than samples (EQAA), more than 8 samples and a single-sample fast clear throw. A CB metadata pass (FMASK decompress or resolve) over a GCN target may cover only its visible rows and columns: the extent the viewport and scissor cover counts when the tile mode pads it to the target's size. None of it is checked on a console. -- [PS4 render targets and display buffers](../../../core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp) (libSceAgcDriver, PS4) - a tiled PS4 display buffer is read as Display2DThin with rows padded to 128 elements, as shadPS4's `image_info.cpp` does. A PS4 render target spans its surface padded to whole macro tiles (1920x1080 at 32 bits is 1920x1088), the display buffer is its top-left corner, and a texture over that corner with the same pitch, tiling and texel size is a [GPU copy](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) of it. A PS4 `DB_Z_INFO` names a GB_TILE_MODE index; the depth image stays on the GPU and HTILE is kept, as for the PS5. shadPS4 (`image_info.cpp`, 0614ce324be0) rounds the width of a tiled display buffer up to a multiple of 128 itself, whereas `DisplayBufferSize` requires the pitch to be one already. +- [PS4 render targets and display buffers](../../../core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp) (libSceAgcDriver, PS4) - a tiled PS4 display buffer is read as Display2DThin with rows padded to 128 elements, as shadPS4's `image_info.cpp` does. A PS4 render target spans its surface padded to whole macro tiles (1920x1080 at 32 bits is 1920x1088), the display buffer is its top-left corner, and a texture over that corner with the same pitch, tiling and texel size is a [GPU copy](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) of it. A PS4 `DB_Z_INFO` names a GB_TILE_MODE index; the depth image stays on the GPU and HTILE is kept, as for the PS5. shadPS4 (`image_info.cpp`, 0614ce324be0) rounds the width of a tiled display buffer up to a multiple of 128 itself, whereas `DisplayBufferSize` requires the pitch to be one already. A PS4 depth target spans `DB_DEPTH_SIZE`; a smaller level-0 depth view reads a plane copy of its corner, a stencil one guest memory. - [PS4 pattern fill](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp) (libSceAgcDriver, PS4) - the PS4 build of the pattern-fill compute kernel seen in Limbo (CUSA01664), with user data s[4:7] a 32-bit UINT source, s[8:11] the 32-bit UINT destination and s[12:15] a control record of count and index mask, where thread i stores source[i & mask], runs as a GPU fill when the mask is 0, a single repeated dword; other masks run the kernel, since running Limbo's two-dword clears of a 64-bit target as fills made its gameplay advance more slowly per frame. The kernel is recognized by its code, not by an SDK symbol. ### Functional @@ -123,12 +129,14 @@ - [Frame capture](../../../core/libs/prx/libSceAgcDriver/Execution/src/FrameRecorder.cpp) (libSceAgcDriver) throws on macOS. Section views, aliases of one direct memory block and file views replay as separate private memory, so a store through one alias is not seen through another. Only write-watched memory records CPU stores during the frames: private memory on Windows, where a store through one alias is recorded only at that alias; registered ranges on Linux, including direct memory unless mapped more than once (libkernel leaves those views unwatched, so no store through any of them is recorded). Title image pages are recorded only when changed and holding a shader header or its arrays. Linux omits no-access pages (Windows records them unreadable) and records file mapping pages past the file's end as unreadable. A capture starts only at a frame boundary where the driver idles within 10 s (five tries), and stops when GPU work in flight at a delta waits on a later CPU store. - [Frame replay](../../../core/libs/prx/libSceAgcDriver/tools/FrameReplay.cpp) (libSceAgcDriver) orders the calls of different captured threads only by their start (a call starts once every earlier call has started), so the driver sees a schedule the title could have produced, not necessarily the one it did. CPU stores a title made from threads that never call the driver are known only by the next recorded call, so the replay applies them early or late when a GPU wait needs them and derives a value from the wait's reference when no recorded store satisfies it (`of_them_synthesized`). Pacing compares the number of submissions in flight, not what they hold. Prologue registrations of shaders whose memory the title reused after registering them fail and are counted (`stale_registrations`). The title's image range must be free in the replaying process, so the tool is linked at a high image base on Windows. - [Host-only multisample memory in frame capture](../../../core/libs/prx/libSceAgcDriver/Execution/src/FrameRecorder.cpp) (libSceAgcDriver) - memory under multisampled color targets, their FMASK and native multisample depth whose samples are only on the host is zero in the snapshot and in deltas (`host_only_sample_bytes`, with a warning), so replay starts those targets without the contents earlier frames left in them. +- [Oracle runner](../../../core/libs/prx/libSceAgcDriver/tools/OracleRunner.cpp) (libSceAgcDriver) accepts only the hardware oracle template's ABI (kernarg pointer in `s[0:1]`, no scratch, 16-dword output) and runs one workgroup of whole waves. NaN results are compared bit for bit although the recompiler canonicalizes NaNs on hosts that do, so a NaN payload difference from the hardware is a finding about the recompiler, not the runner. - [Workgroup count split](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (libSceAgcDriver) - a direct dispatch over `maxComputeWorkGroupCount` runs as `vkCmdDispatch` chunks, each bound to a pipeline variant whose workgroup offset specialization constants add the chunk's base to `WorkgroupId` and `GlobalInvocationId`. A variant is built when its chunk base is first recorded, not when the dispatch is prepared. A split of a shader with device-memory LDS throws: its slot base reads the chunk's `NumWorkgroups`, not the dispatch's. Chunks are ordered by a compute barrier, which the console does not have between workgroups of one dispatch. - [Experimental submission progress](../../../core/libs/prx/libSceAgcDriver/Execution/include/Driver/Queues/SubmissionProgress.hpp) (AGC driver) - the opt-in two-millisecond pacing budget is not derived from console hardware. Cross-title performance remains unvalidated; host scheduling can overshoot the timeout and pacing can reduce CPU/GPU overlap. - [GPU allocation recovery](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - recovery may free too little and still throw: caller-held sampled images, pending storage results and recorder references stay alive. Budgets remain floors rather than hard device limits. Image and buffer allocation failures trim idle buffers, then sampled and clean storage cache entries. A held cache lock is reused only for a new image's allocation after lookup iterators are no longer used; hit refreshes and flushes skip that cache. Other cache, resource and device locks are tried without waiting. -- [UINT color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - what the console's CB does with a raw 32-bit export into an 8- or 16-bit UINT channel (clamp or truncate) and whether it blends into integer targets at all is not measured. Here a raw 32-bit export (`32_R`, `32_GR`, `32_AR`, `32_ABGR`) into a UINT target is passed as the exported bits through the `Uint` export packing and the target's blend is dropped; the execution test draws `32_R` into `R32_UINT`, `32_GR` into `R32G32_UINT` and `32_AR` and `32_ABGR` into `R32G32B32A32_UINT` (tiled and linear) and reads every written channel back bit for bit, and draws `32_R` values from 0 to 255 into `R8_UINT`; a value an 8- or 16-bit channel cannot hold is left to the Vulkan attachment's conversion, which Vulkan leaves undefined, and is not tested. On lavapipe (Mesa, LLVM 20) a constant export of exactly 1.0 in the first channel and -0.0 in the second stores +0.0 in the second although the module declares `SignedZeroInfNanPreserve`, so the test leaves that pair out. The blend is dropped since Vulkan's framebuffer chapter says blending is only defined for floating-point, UNORM, SNORM and sRGB formats and Metal refuses both the float output and the blend on integer attachments (#9783). `FP16_ABGR`, `UNORM16_ABGR`, `SNORM16_ABGR` and `SINT16_ABGR` exports into UINT targets throw. +- [UINT color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - what the console's CB does with a raw 32-bit export into an 8- or 16-bit UINT channel (clamp or truncate) and whether it blends into integer targets at all is not measured. Here a raw 32-bit export (`32_R`, `32_GR`, `32_AR`, `32_ABGR`) into a UINT target is passed as the exported bits through the `Uint` export packing and the target's blend is dropped; the execution test draws `32_R` into `R32_UINT`, `32_GR` into `R32G32_UINT` and `32_AR` and `32_ABGR` into `R32G32B32A32_UINT` (tiled and linear) and reads every written channel back bit for bit, and draws `32_R` values from 0 to 255 into `R8_UINT`; a value an 8- or 16-bit channel cannot hold is left to the Vulkan attachment's conversion, which Vulkan leaves undefined, and is not tested. On lavapipe (Mesa, LLVM 20) a constant export of exactly 1.0 in the first channel and -0.0 in the second stores +0.0 in the second although the module declares `SignedZeroInfNanPreserve`, so the test leaves that pair out. The blend is dropped since Vulkan's framebuffer chapter says blending is only defined for floating-point, UNORM, SNORM and sRGB formats and Metal refuses both the float output and the blend on integer attachments (#9783). An `FP16_ABGR` export into a UINT target stores each half widened to a 32-bit float (a NaN with its quiet bit set) and read as an unsigned integer, clamped to the channel, so an 8- or 16-bit channel holds 0 for +0.0 and its maximum for any other half; two Navi21 GPUs (RDNA2) store this with RADV forced to `FP16_ABGR` (#11513), the console is not measured. The shader widens the half bits with integer operations and the `UintClamp8` and `UintClamp16` packings clamp them; the execution test draws halves read at run time into 32-, 16- and 8-bit UINT targets, and through each clamping packing into `R32_UINT`. An uncompressed `FP16_ABGR` export into a UINT target keeps its 32-bit words without a conversion to half, which is not measured. `UNORM16_ABGR`, `SNORM16_ABGR` and `SINT16_ABGR` exports into UINT targets throw. - [Scan-resistant sampled texture admission](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - while the sampled texture cache is over its budget but within a quarter over it, the first use of a texture it does not hold in a present is refused (used for its draw, then released, never cached) when its least recently used entry was used in the current or previous present, and a later use in the same present is refused when that entry was used in the current present. So a working set larger than the budget keeps a stable subset: a texture used more than once in a present still evicts an entry not yet used in that present, so a working set with interleaved multi-use textures rotates as on main, and the textures that arrive after the cache fills are created again at each use for as long as the least recently used entry is from the current present, including in the transition present after a scene change. Past a quarter over the budget the entries are evicted as before, so after the budget falls the cache can sit at up to 125% of the reduced budget until entries stop being used. Entries unused for two presents are evicted as before. A later use can evict an entry from the previous present and then be refused when the next least recently used entry is from the current present, so that entry is created again in the next present. The "current or previous present" window is an unmeasured threshold: it was chosen, not derived. Measured once, as one A-B-A series on one title (loading phase about 33000 textures per frame against a 5.4 GiB budget) on one RTX 4090, 3600 s each: 932 / 968 / 941 frames, evictions after frame 620 2.72M -> 1.17M, texture creations unchanged (about 5.8M). Nothing is measured on other titles or GPUs, and a title whose per-present working set only slightly exceeds the budget now re-creates the excess textures at every use. - [Texture cache budgets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - the storage image cache gets what the reported heap budget leaves after the heap's other use, the live sampled textures and an eighth of the budget, capped at half of the budget and of the heap, at least 512 MiB; the sampled cache reserves the larger of `TextureCacheBudget` and the storage images held, and keeps at least 2 GiB; above the floors neither budget plus the other cache's bytes exceeds the budget less its eighth. Both read the heap at most once a second and trim at their next insert, so a burst can exceed the budget. The storage budget gives back the part of the sampled floor not in use. The eighth and the floors were checked only on PPSA11386 (10 GB GPU) and PPSA16064 (8 GB GPU); the halves come from PPSA25636 on one 8 GB GPU (a 3.5-4 GiB storage working set). A title whose storage working set exceeds its budget re-creates storage images at each use. +- [Staging shadow headroom](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp) (libSceAgcDriver) - a device-local staging shadow (`stagingBuffer` in `GuestBufferMemory.cpp`) that needs a new allocation is refused when that allocation would end within the last eighth of the budget `VK_EXT_memory_budget` reports for the heap of the memory type it would be allocated from; the region then takes its unstaged path. A shadow the buffer pool already retains is reused without the check. The eighth comes from one title on one GPU (PPSA02530 on a 16 GiB RX 9070 XT, RADV), not from a measured rule. Budget readings are the driver's estimates, and the check reserves nothing: allocations made concurrently by other threads or by the caches can still take the heap past the eighth. Devices without `VK_EXT_memory_budget`, or whose heap reports no budget, make every shadow as before. - [Polygon primitive type](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - `VGT_PRIMITIVE_TYPE` 21 (`PrimitiveType::Polygon`) decodes to `VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN` and is treated as inactive for vertex point size, like triangle fans and strips. PPSA01317 (Gran Turismo 7) submits polygon draws; whether the console performs polygon-mode decomposition or custom vertex routing is unverified. - [Native multisampled DCC fast clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) take uniform 0000/0001/1110/1111 keys and the register-clear key 0x20, or a whole-range DCC fill of one, into a single-layer, level-zero SW_64KB_R_X image of 2, 4 or 8 fragments. The register-clear key uses `CB_COLOR_CLEAR_WORD0/1` through the existing exact texel-to-host clear conversion; formats over 64 bits or without an exact conversion throw. Register-clear semantics follow [Mesa ac_descriptors.h](https://chromium.googlesource.com/external/gitlab.freedesktop.org/mesa/mesa/+/418c4cfa6708a0e0b1175e72fb8fd27d3ca1615a/src/amd/common/ac_descriptors.h) (`GFX8_DCC_CLEAR_REG`); the console result has not been measured. Other initial DCC, register CMASK clears, compressed payloads, color transforms, lossy modes, partial fills, a CMASK changed without a whole fill, a range compared only after its import unless a whole fill follows, guest publication and writes over a live owner throw. Color and metadata must stay write-watched or lie in a host import the Linux probe compares, where a same-value CPU store is not seen; compared Windows imports throw. The metadata extent follows [AMD Addrlib](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp#L347-L454) for GFX10.1 with 16 pipes; console geometry is unverified. @@ -150,6 +158,7 @@ - [Depth surfaces over compared shared memory](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver): a depth surface over Windows direct memory that is compared once it is shared sees CPU writes made after the sharing no more than one over unwatched memory does. Its generations come from refused collects (0) or `TrackerGeneration()`, and nothing stamps compared memory after the one report at the sharing. - [Unmapped depth planes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver): a drawn depth plane whose guest memory the title unmapped before it was stored is not stored; its pending depth is dropped with a one-time warning. What the console does with such a store is not known. Readable planes without write permission retain the unsupported-store failure and their pending results. - [Depth plane copies](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver): a depth plane sampled as a 2D array, a cube or a cube array, or a D16 plane read as `R16_UINT`, is copied into an image of its own (`DepthPlaneCopy`), kept per base address, extent, format and slice count; a slice comes from the live depth surface when one is bound at the slice's address and from guest memory (detiled) otherwise. A draw or dispatch that binds the plane copies again only the slices that changed and records nothing when none did: a surface slice when its image or write serial differs from the one copied (the serial is bumped when the surface is created, when a draw binds it as its depth target, when a pending fast clear is applied and when a written storage view of the plane is copied back), a guest-memory slice when its bytes were written since they were last read (`GuestMemory::UnchangedSince`), a snapshot write overlaps them, or its address, size or tile mode changed. The surface of the latest depth draw is copied at every use until another depth target is bound, since that draw may still be recorded after the copy. A slice's address is the base plus a whole 64 KiB-block depth slice per layer (`DepthSliceBytes`), and the cube face order is the array order; both are inferred from the titles' use, not measured on hardware. A 2D-array T# whose `BASE_ARRAY` is past slice 0 reads slices `BASE_ARRAY` to `LAST_ARRAY` of the copy, which holds slices 0 to `LAST_ARRAY`, and a 2D binding of it reads slice `BASE_ARRAY`; that follows the colour 2D-array semantics and the titles that hit it (PPSA01556, PPSA03912), and was not measured on hardware for depth surfaces. Planes with mips, a cube or a 2D T# with a base slice other than 0, a `BASE_ARRAY` past `LAST_ARRAY`, a cube whose last array slice is neither 0 nor 6n - 1, and other views of the planes still throw, and so do these copies of a 16-bit plane with stencil that the device stores as D24 or D32 (16-bit depth with stencil, below). +- [Depth plane read in a compute pass](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) - a D32 plane is read into its storage image by `DepthToStorage.comp` (`OpImageFetch` on a sampled view, bits stored unchanged), where `main`'s copy moves the bits without a shader read. The shader declares no `DenormPreserve` or `SignedZeroInfNanPreserve` mode and the spec defines no conversion for the read, so subnormals, signed zeros and NaN payloads are measured bit for bit only on an RTX 2080 Ti (driver 617.14), with and without `VK_EXT_depth_range_unrestricted`; another GPU may flush or canonicalize them. `APS5_NO_DEPTH_TRANSFER_PASS=1` restores the exact copy. D16, stencil, multisampled, layered and `TakeStorage` copies and every copy back into the depth plane keep the copy. - [New depth surface from storage writes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) - `TakeStorage` discards the depth aspect (`UNDEFINED` to `TRANSFER_DST_OPTIMAL`) before copying the storage image over the whole surface. On AMD's Windows driver 32.0.31041.1004 (RX 7800 XT) a copy into the depth image after its creation clear left one or two 8x8 blocks testing as the clear value in about 1 of 10 runs, although a copy back read the copied values (#6630); not seen on a Radeon 860M or RX 6700 XT. A surface with a stencil plane keeps the clear before the copy, since its depth aspect is not discarded alone. - [Repacked format-43 copies](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver): a sampled texture of format 43 (10-bit R, 11-bit G and B unsigned floats from bit 0) is uploaded into a `B10G11R11_UFLOAT` image with its R and B fields swapped (`RepackTextureWord`) and viewed with R and B swapped back. A texture whose results a storage image still holds is copied from it again, whole, whenever the image changed (`RefreshDepthCopy`); a 3D one is flushed to guest memory and uploaded instead. Storage reads and writes of format 43 keep the packed `R32_UINT` path. - [SW_64KB_Z_X color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp) (libSceAgcDriver) use the texture equations and 64 KiB block geometry of `SW_64KB_Z_X`, whose addresses are checked against [AMD addrlib GFX10](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp) in the texture tiling tests. Any 256-byte pipe/bank XOR below the block size is accepted, as for `SW_64KB_R_X` targets and Z_X textures; which of those bits the console honours, and the render results, are not measured. @@ -183,6 +192,7 @@ - [Label read traps](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver, macOS): at a label, a pending linear or small storage image that shares no page with another image a label stores or one being stored, and is not direct memory mapped twice, gets a [read trap](../../../core/libs/prx/libc/src/GuestWriteWatch.cpp) instead of its store. The first access to its pages stores it whole through the host-read hook; a store of another image over it stores it too, and an eviction or explicit write-back stores it first. Other stores release the trap once their GPU copy is recorded, so a read before that copy lands sees the old bytes; a CPU store releases the traps on its pages first, leaving those images untrapped until the next label. A thread holding `GpuMutex` touching a trapped image waits forever, a system call that reads one without the hook fails with `EFAULT`, and other double mappings go undetected. `APS5_NO_LABEL_READ_TRAP=1` stores at the label. - [Resident linear color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver): a CPU store inside a drawn scissor rectangle, on a pixel no primitive wrote, made before the target's store is overwritten by it, where the console keeps it. Recorded draws' results are stored before the graphics queue's next flip, label, end-of-pipe interrupt, drain or device wait, as the union of their scissor rectangles; a draw that brings a 17th rectangle that does not merge stores the target at once. A target with an attachment proxy, several mips or layers, a 3D or DCC surface, or results of a dispatch, fill or copy is stored after each draw. One that a fill or copy writes after its draws is stored whole at the next of those points, and GPU results in its CPU-written 64 KiB blocks are dropped. A store the draw pipeline's committer makes (label, end-of-pipe interrupt, device wait) skips a target whose range a queued draw still reads (`DrawPipeline::HeldReads`). - [Direct writes over a drawn linear target](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver): a CPU store into a 64 KiB block of a drawn resident linear target that lands while the completion of a direct write recorded before the draws checks and stamps that block (`StampKeepingNewerResults`) drops the draws' results in that block, where the console keeps them: the stamp's own collect records the store per page only, so the store reads the block as written by the driver. +- [Tiled images read after a label](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver): a larger tiled image, a write after its one store with no gap, and an image rewritten every 8 flips or fewer can leave stale bytes behind the label. Before a label, a tiled image over 64 KiB is stored only if it is at most 1 MiB and its first write or a write more than 8 flips after the one before armed it, and that takes one store until the next such write. Writes are compared by flip serial, so writes within a flip count as one. The 1 MiB and 8 flip limits are a heuristic from PPSA06866, not measured console behaviour. - [Linear color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) with no host import are drawn synchronously (copied in, drawn, waited for, copied back), where the console renders into memory in stream order, unless the draw can make an import that keeps them write-watched. It makes one for a target in registered, writable memory only when the whole import run is write-watched and the import stays watched: a separate alias of shared memory, a direct import of 1 MiB or more of private memory that settles on Windows (when the import probe keeps the watch), or one the probe keeps watched elsewhere. Smaller private ranges on Windows, runs with memory outside the write watch and devices whose imports are compared stay synchronous: a compared import makes every refresh of the resident image compare the whole target. A target over an existing import is resident, compared or not. `APS5_NO_RESIDENT_LINEAR=1` keeps every linear target synchronous. - [`APS5_SKIP_REFUSED_DRAWS`](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp) (libSceAgcDriver) drops a draw whose graphics stage the recompiler refuses instead of failing the queue, so the frame lacks that draw, and the stage is compiled and refused again at each such draw. Compute refusals still fail. - [Pending storage-image results over memory outside the write watch](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) leave to the CPU the 64 KiB blocks whose bytes differ from the ones saved at the upload, the fill, the image copy, the previous store or the moment their range left the watch (`Unwatch`, or a host import that does not stay watched or whose page walk fails), and the blocks the watch saw the CPU write before that moment. A store made on the GPU saves its bytes through a host-visible copy that becomes the saved bytes when its batch completes; a store or a refresh that needs them earlier waits for that batch. A unit without a write generation saves the guest bytes when results land in it outside the watch. One whose range leaves the watch while its results are pending saves its bytes through the unwatch hook, which also runs when a failed collect takes a range out of the watch. @@ -210,6 +220,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. - A [label](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) of 8 bytes or less recorded while a copied writer is listed, or while an earlier completion store is pending, is stored by the CPU when its batch is reaped, after the write-backs, so it becomes visible up to one reap later than on the console, also after GPU writes recorded behind it. Larger stores (DUMP_CONST_RAM, COPY_DATA, DMA_DATA) and DMA_DATA copies the GPU makes stay on the GPU unless they cover a copied writer's range or a pending completion store, so they can be visible before an unrelated earlier write-back. The order also breaks when a write-back's guest store syncs a pending storage image from inside the completion: that nested sync finishes later batches, whose completion stores land first. `APS5_DRAIN_COMPLETION_LABELS=1` drains instead (reason 2). +- A [label](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp) stored on the GPU is ordered behind the batch's earlier writes, for a host that polls it, only by driver behavior: Vulkan makes device writes visible to the host through a fence or event wait. The store run recorded at `Submit` takes every earlier write (`MEMORY_WRITE`) as its barrier's source access. With only shader, transfer and color attachment writes, Intel's Windows driver (Arc Graphics, 101.9150) let a polling host see the label before an earlier copy's bytes (#10199). A run forced in place (`closeStoreRun(false)`) keeps the narrower `LabelLeadingBarrier`, or the pass end it merges into. On that driver, such a label can still become visible before the writes recorded ahead of it. - [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, 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. @@ -221,7 +232,7 @@ - [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 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. - [11_11_10 float CB resolve](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a CB resolve whose source is a multisampled 11_11_10 float color target is refused. The console's averaging and rounding for that resolve are not measured and no known source describes them. PPSA12361 reaches it: a local diagnostic print showed a two-sample R32_UINT source (packing 11_11_10 float) of 512x512 resolving into one sample, and the title stops there (issue #11362). -- [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. Without `VK_EXT_provoking_vertex`, last mode throws only when a pixel shader input could show which vertex provokes (a flat input, a P10 flat read, a per-vertex or custom per-vertex input, or a `Flat` or `PerVertexKHR` decoration in its SPIR-V, ignoring built-ins the pixel stage generates) or another stage's SPIR-V decorates a `Layer` or `ViewportIndex` built-in that is not `PerPrimitiveEXT`; otherwise the draw uses first mode, since the provoking vertex then selects nothing it reads or writes. With the extension, last mode throws 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). +- [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 selects Vulkan's last-vertex mode per opengnm's `flatshade_first`; rect-list flat inputs come from vertex 0, or 2 in last mode; a geometry program gets strip and fan triangles in an RX 6700 XT's order per mode, and in last mode its primitives take flat inputs and layer from their last index, keeping per-vertex order; a flat input without pass-through read only via `v_interp_mov_f32` P10 (P20) is the provoking vertex's (#1413; first (last) mode throws). Barycentric tessellation stages give each corner the last vertex's flat values. Without `VK_EXT_provoking_vertex`, last mode throws only when a pixel shader input could show which vertex provokes (a flat input, a P10 flat read, a per-vertex or custom per-vertex input, or a `Flat` or `PerVertexKHR` decoration in its SPIR-V, ignoring built-ins the pixel stage generates) or another stage's SPIR-V decorates a `Layer` or `ViewportIndex` built-in that is not `PerPrimitiveEXT`; otherwise the draw uses first mode, since the provoking vertex then selects nothing it reads or writes. With the extension, last mode throws for tessellation, explicit per-vertex flat inputs, and triangle strips whose pixel shader reads a vertex slot (`v_interp_mov_f32`, or 16-bit interpolation until checked), as odd triangle order varies by device. - [Triangle strips through the barycentric tessellation stages](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - without `geometryShader`, a triangle strip 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. A non-indexed strip ignores the primitive restart bit, which has no index to match; indexed strips with primitive restart and 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). @@ -246,7 +257,7 @@ - [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 0xcccccccc, 0xdddddddd, 0xeeeeeeee or 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 DCC CMASK without fast clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - the initial CMASK of a multisampled DCC target is not checked when CB_COLOR_INFO enables COMPRESSION but not FAST_CLEAR: it then holds only FMASK compression, and a uniform DCC clear sets every fragment, so the target imports as that clear. What the console reads where such an FMASK names no fragment, as an uninitialized CMASK can, is unverified. - [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, Z64KBX, D64KBX 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. +- [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; other RenderTarget and Z64KBX contents are uploaded sample by sample through the native color initialization. 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. Non-uniform Standard4KB, Standard64KB and D64KBX memory, other tile modes, 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. - [Sample 0 of a multisampled color target](../../../core/libs/prx/libSceAgcDriver/Graphics/src/FirstSampleCopy.cpp) (libSceAgcDriver) - a 2D image read of a multisampled color target returns its float, UNORM, SNORM or sRGB sample 0 exactly only where the driver keeps those values through a shader and a color attachment, which Vulkan does not require, and one of a multisampled depth surface throws. The read binds a one-sample copy that a fragment pass (`texelFetch` of sample 0) draws before every such binding; integer formats copy exactly. - [Multisampled targets in CB passes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a CB eliminate or DCC pass over a multisampled target throws, since its texels are on the host and only a CB resolve and an FMASK decompress (`CB_COLOR_CONTROL` mode 5) are modeled. The FMASK decompress only applies the pending CMASK fast clear to the host image; it requires a CMASK address, and the target's FMASK contents are not interpreted. In that pass as elsewhere, multisampled DCC is accepted only as a native multisampled DCC fast clear or comp-to-single clear. A CB resolve into a fast-clear destination throws: the resolve writes the destination's texels but not its CMASK, which would still read as cleared. - [Multisampled target guest access](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - the memory a multisampled color target throws on (CPU read, index buffer read, fixed-function vertex buffer read, DMA copy, texture or storage image read) uses fragment-aware 64 KiB blocks for the RenderTarget, Z64KBX and D64KBX tile modes. [AMD Addrlib's GFX10 thin-block rule](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/core/addrlib2.cpp#L1680-L1712) gives the block width and height from element bytes and fragment count; its [surface-size calculation](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp#L4000-L4005) rounds both extents to those blocks and multiplies by element bytes and fragments. Thus a 1920x1080 4x RGBA8 surface occupies 0x1fe0000 bytes, leaving the next allocation at +0x2000000 outside the guard. The 2x and 8x block orientations differ from treating the fragments as a larger element. Console address-layout equivalence has not been measured. Other tile modes retain the conservative `max(CB bytes * samples, width * height * element bytes * samples)` bound, which may still reject a distinct allocation. Live targets remain cached per device, surface address, format, extent and sample count. Retired cache owners are removed on the next target, sampled or storage lookup; outstanding recorded work and sampled views retain their images. Ordinary sampled and storage bindings check the color-target guard before resource reuse, including precollected sampled lookups. Reused image tables do not invalidate on multisample color-target registration yet; their per-element fault handling and native multisample sampling require a separate reuse proof. @@ -257,7 +268,7 @@ - [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. +- [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 told apart from no store only in a whole 64 KiB block of the image: a CPU stamp newer than the write generation of the layer's last GPU results keeps the CPU's bytes there. In an edge block, whose stamp can come from bytes beside the image, such a store is not told apart from no store and the GPU result wins. A block a write-back kept for the CPU stays the CPU's until its unit is uploaded again: results written into the unit before that are dropped in that block, also after the memory left the write watch (the unit's guest bytes are saved when it is written again 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. - [Queue-idle storage write-back](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/QueueWorker.cpp) (libSceAgcDriver) - storage-image results that nothing flushes, waits for or overwrites reach guest memory only after every queue has been idle for 50 ms, where the console's GPU writes them to memory as it runs. The idle store goes 4 MiB at a time and stops as soon as queue 0 has queued work or another queue is running, leaving the rest for the next idle period. A title whose frame gaps pass 50 ms still starts an idle store between two frames, and its next frame waits for the piece in progress: a title's movies at about 25 fps (frame gaps up to 65 ms) show frames of up to about 100 ms there. - [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. - [BT.2100 PQ display buffers](../../../core/libs/prx/libSceAgcDriver/Execution/include/PqToSdr.hpp) (libSceAgcDriver, libSceVideoOut) - pixel formats `0x8100070422000000` and `0x8100070400000000` are taken as 10:10:10:2 texels with BT.2020 primaries and the SMPTE ST 2084 transfer, red or blue in the low bits, as [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/VideoOut/VideoOutTypes.cs) names them (`Rgb10A2Bt2100Pq`, `Bgr10A2Bt2100Pq`) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/59183dbfa81ed7efdff6069177fed50de0552725/src/graphics/host_gpu/renderer/image/imageInfo.h) maps the first; PPSA01317 registers display buffers of the first when its output reports HDR. With HDR output they are presented unchanged through an HDR10 swapchain (`VK_COLOR_SPACE_HDR10_ST2084_EXT`, 10-bit, with `VK_EXT_hdr_metadata`); otherwise they are converted to 8-bit sRGB: ST 2084 EOTF, BT.2020 to BT.709 primaries with negative components clipped, a luminance tone map and the sRGB encoding. The tone map is ours: min(203 nits, white level) is SDR white (the BT.2408 HDR reference white), luminance passes unchanged up to 0.75 of it and an extended Reinhard shoulder above reaches 1.0 at the white level, the peak sceSystemServiceGetHdrToneMapLuminance last reported to the title (or the current one when it never asked), since titles read it once. How a console shows such a buffer on an SDR display is unknown. Alpha is opaque. @@ -317,7 +328,8 @@ - [Heap mirror reference storage](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ReferenceStorage.cpp) (libSceAgcDriver): the 8-buffer/2 GiB idle cap and 2 s reuse window are unmeasured choices. Idle release is polled by a take, an eligible return, a heap mirror sweep or a present; without another poll, expired buffers remain until process shutdown. A title taking references less often than every 2 s reallocates them. The cap covers idle storage only, not buffers still held by active references. - [Host import windows and budget](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): a registered range larger than 64 MiB is imported one 64 MiB window at a time for buffer and fill accesses; an access across window edges imports the cells it touches, grown to the union of the range's imports it overlaps. An address-based build (`AcquireRegistered`) imports the range whole and replaces its windows. Live imports stay under a budget: `APS5_HOST_IMPORT_MIB`, else the larger of 6 GiB and half the physical memory, at most the largest host heap less half that heap, kept between 2 and 4 GiB (imports, type-3 mirrors and buffers and textures placed in host memory share it; on a 16 GiB machine whose host heap is half the memory, a 2 GiB reserve left too little for the rest, and on a 32 GiB machine a reserve of half its 16 GiB heap left imports 8 GiB, less than the 10466 MiB one title holds at once). Where the device enables `VK_EXT_memory_budget`, that host heap's reported budget less its other usage and an eighth, in 64 MiB steps, read at most once a second, also bounds the default budget once an import's `vkAllocateMemory` has failed; a set `APS5_HOST_IMPORT_MIB` is never lowered by the report. The report's other usage includes the host-visible copies and heap mirrors that stand in for refused imports, so it grows with the refusals; the first time the report would lower the budget, `[gpu] host import budget: host heap ...` prints the heap's reported budget and usage and the live imports. An import's `vkAllocateMemory` failure within an eighth of the budget, or the third in a row, lowers the budget to the live imports until the device or the configured budget changes; an isolated failure further below only marks its range failed. Live imports are not capped by `maxMemoryAllocationCount`: NVIDIA's Windows driver reports 4096, while Village held 27,167 live imports without a failure. A new import beyond it retires the least recently used imports not used in the current or the previous frame (a frame ends at a flip packet or a CPU flip), so a set used in every frame keeps its imports and what does not fit is copied, as below the budget; until two frames have ended imports age by lookups, an import being evictable 256 lookups after its last use; after that, without flips (a loading stretch, compute-only work) no frame ends and nothing more becomes evictable, so imports that do not fit are copied instead of evicting by recency; one that would not fit even then is refused before anything is retired, and imports made when a range is mapped never evict. An address-based build does not evict either, except for a range the budget refused (or remembers refusing) and no mirror serves whose copy would take the build's copies past `APS5_ADDRESS_COPY_MAX_MIB`; that eviction also takes imports the recency rule protects whose single range is releasable and write-watched, so a heap mirror can serve it, as long as the build has not taken them yet. A heap mirror, sparse or not, whose range has left the write watch is refreshed in full at each use, every block counted as written, unless the build can import its range first, evicting for it as the copy cap does, with the imports of earlier builds no longer held; a build plans its range again only once the import path takes the range. A request that may not evict (an address-based build, an import when a range is mapped) remembers a refused range or block, with the live bytes and the budget at the refusal, and is answered from that memory, still counted as refused, until live imports drop below those bytes, the budget changes or one of its ranges changes. A request that may evict remembers its refusal too, answering later requests for the same run from memory with the room the refusal left (a request whose range fits in that room steps down to a smaller block without scanning the imports again, and a refusal that stepped down is remembered for each block it tried), while no import was made or retired, the budget has not grown, the import window and the run are unchanged, and no import can have become evictable since: no frame has ended (or, before two frames have ended, 256 lookups have not passed), and no build holding an import two frames old has raised its floor. A join remembers its refusal the same way, apart from other requests for the same run, so a buffer bound across refused ranges does not try the join again at every upload. The shutdown line `[gpu] host imports at shutdown` gives the live, made, refused (and remembered) and failed counts per requester. The 64 MiB window, the two-frame protection, the 2 GiB reserve, the eighth, the 64 MiB steps and the three failures are choices, not measured. - [Piece-split ranges over the host import budget](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): an address-based build imports a registered range larger than the device's largest allocation in pieces. If the budget refuses a piece, the range is mirrored or copied without the evicting retry other refused ranges get when their copy would pass `APS5_ADDRESS_COPY_MAX_MIB`, so such a copy can still exceed that cap and end the build. -- [Device-local staging](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver) - a written or atomic buffer element inside a host import, including one an address-based compute build reaches through its pointers, is copied to device memory before the dispatch and copied back whole after it, so a CPU store into that range in between is rolled back. +- [Device-local staging](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver) - a written or atomic buffer element inside a host import, including one an address-based compute build reaches through its pointers, is copied to device memory before the dispatch and copied back after it. The copy-back stores only the bytes the shader changed and the import still holds at their copy-in value, so a CPU store in between survives; where both changed the same byte, the CPU's byte is kept, whichever the console would store last. The whole range is still copied back, rolling CPU stores back, at the unaligned ends of a range, for an element or import offset that is not 4-byte aligned, without a shadow copy or buffer device addresses, with `APS5_NO_CHANGED_COPY_BACK`, and when a synchronous draw stores a GPU-copied region from the CPU (no copy-back recorded, no copy-in snapshot). A CPU store made while the copy-back runs can still be overwritten by it. +- [Write-back conflicts with CPU stores](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) (libSceAgcDriver) - a buffer region the CPU copies for a dispatch is stored back with `WriteChangedKeepingGuest`: a byte takes the shader's value only where the shader changed it and guest memory still holds the uploaded value, so a CPU store made after the upload wins where both changed the same byte. A CPU store that restores the uploaded value is indistinguishable from no store and takes the shader's byte. Writable image mirrors, heap mirrors, storage image write-backs and tiled colour targets use `WriteChanged`, which stores every byte the GPU changed: a CPU store into the same byte between the upload and the write-back is overwritten there, as before; a CPU store into a byte the GPU left alone survives on every path. Which side the console would store last is not measured. - [Committed reads of released pages](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) (libSceAgcDriver) - `ReadCommitted` and `EqualsCommitted` find the readable pages of a range first and read them afterwards. On Windows they read through `ReadProcessMemory`, so a page released in between reads as zero and is left out of the comparison instead of faulting: Demon's Souls (PPSA01342) samples a texture at 0x38400 of 352 MiB while it loads the tutorial level, a range that holds no guest allocation and on Windows covers host heap pages other threads release, which stopped it with an access violation. A texture outside every registered guest allocation still takes the host bytes found there, as it did before; what the console returns for it is not measured. On Linux the pages are copied directly, so a page released between the check and the copy still faults. - [Periodic pipeline cache saves skip busy intervals](../../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp) (libSceAgcDriver) - the saver thread snapshots the Vulkan pipeline cache only when no pipeline creation holds it (`TryLockForSnapshot`); otherwise it skips that 10 s interval. A blocking snapshot queued behind a long creation (an NVIDIA compile of a 1.5M-word compute pipeline runs for minutes) makes the writer-preferring `shared_mutex` hold every later pipeline creation on every queue until that compile ends, which froze Yōtei's (PPSA26344) loading at frame 797-799 for the whole compile. While creations overlap without a gap, no periodic save happens, so a crash in that stretch loses the pipelines built since the last save; `SaveNow` and the teardown save still wait for the lock. How often busy stretches skip saves in practice is not measured. - [Registration preparation at submit](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) (libSceAgcDriver) - `Driver::Submit` waits for every registration-time shader preparation queued before the call, not only those of the shaders the commands use, so a submission behind a long queue of unrelated preparations waits for all of them on the submitting thread. Waiting there instead of in the queue worker keeps the GPU side of a submission short: a title that sees a submission take seconds treats the GPU as hung (Goat Simulator 3, PPSA04159, then stops submitting and its garbage collector fails a 10 s timeout). On the console shaders are compiled ahead of time, so neither wait exists; how long a title tolerates a slow submit call is not known. @@ -335,6 +347,7 @@ - [Benchmark rewrite failure test off Windows](../../../core/libs/prx/libSceAgcDriver/tests/PerformanceTrace.cpp) (libSceAgcDriver) - `ReplaceGzipFile` checks that a write failing at `.tmp` keeps the previous `aps5-bench.json.gz` only on non-Windows builds: the `std::ios_base::failure` thrown inside the MinGW ofstream did not unwind to the test's catch, so Windows CI covers only the failed move. - [Sampled array windows](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp) (libSceAgcDriver) - a sampled 2D array, 1D array or cube view past slice 0 is built from its viewed slices only (`ViewedArrayWindow`). The window needs a thin surface without DCC metadata, or with keys that resolve over the whole surface to Uncompressed (the window then drops the metadata; keys are rescanned at each lookup). For a 64 KiB XOR swizzle it starts at the last slice at or before the base whose swizzle slice bits are zero (R_X 1-byte and Z_X 4-byte: a multiple of 16); when that is slice 0 the view builds slices 0 to its last slice. A window and a whole-array view of the same surface are separate images over overlapping memory, kept coherent by the existing overlap rules, at the cost of both. PPSA26344 samples a cube array at slices 4299..7232 at 1467 MiB instead of about 3.8 GB; PPSA01341 samples slice 636 of a DCC 2D array at 52 MiB instead of 2.5 GiB. +- [1D arrays past maxImageArrayLayers](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) - a 1D array view of more than `maxImageArrayLayers` slices, sampled only by `image_sample_lz` or `image_sample_l` without depth comparison or a 128-bit T#, is a 2D image holding each viewed slice in two adjacent rows; a view of exactly the limit stays a 1D array. A shader is prepared with this mode only once such a view reaches it. The layer is rounded half to even and clamped to the view, as [Vulkan's array layer selection](https://registry.khronos.org/vulkan/specs/latest/html/vkspec.html#textures-layer-selection) does; RDNA's rounding of a halfway layer is unmeasured; a NaN layer reads slice 0. Views of other mip levels, block-compressed or RGBA8-expanded formats, native depth surfaces, other uses of the view and views needing more than `maxImageDimension2D` rows throw, as do `ShaderReplay` requests (no limit recorded). PPSA08329 samples 5,120 slices. - [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. - [dma-buf imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver) - the udmabuf of a guest range lists the runs of the direct memory backing file behind it (`SharedBackingRuns`, one per physical allocation the range was mapped from, up to udmabuf's 1024), where it took the first run's offset for the whole range. The start-up probe also stores to the import from the GPU after CPU stores to the same pages and counts pages the CPU still reads stale: i915 maps a dma-buf without snooping the CPU caches (4 of 4 pages stale on Meteor Lake with Mesa 26.2.3 and main), so such a device does not fall back to dma-buf imports and copies instead. The memory type the import reports is host-coherent for the driver's own mappings; the guest's mapping of the same pages is what goes stale. @@ -343,11 +356,15 @@ - [Depth view mip on a single-level surface](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a `DB_DEPTH_VIEW.MIPID` above zero on a surface whose `DB_Z_INFO.MAXMIP` is zero selects the first level. In Flashback (PPSA08546) the depth view follows a mipmapped colour target from mip 1 to 6 over six slices of a 128x128 Z_X depth array; the depth allocation is six 64 KiB slices (the next allocation starts 0x61000 bytes later, too small for a mip tail) and every view uses the same base and extent, so the field has no other level to select. The console was not measured. A depth copy from such a view still throws - [Long Windows pipeline-cache paths](../../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp) (libSceAgcDriver) - whether retail console filesystem APIs normalize non-canonical or relative cache paths beyond MAX_PATH without truncation remains unmeasured against console diagnostics. On Windows, filesystem journal normalization and shared content readers do not cover non-canonical or relative pipeline cache paths beyond MAX_PATH. +- [Widened epoch collects on Windows](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) (libSceAgcDriver) - a collect of at most one 64 KiB block within a collect epoch walks the whole block, as on Linux, so the epoch's memo serves the block's other ranges. Windows coverage does not know which pages of the block are committed or inaccessible, and a walk refuses such a range before it collects anything: the requested pages are then walked instead, so that collect costs two calls and its block's other ranges are walked on their own. - [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 (`vk_rasterizer.cpp`, `StencilValid`) and KytyPS5 (PS5, `depthRenderTarget.cpp`, `has_stencil`) drop it the same way; fpPS4 sets the clear from the bit alone, but its host depth format then has no stencil aspect to clear. No source states what the hardware does with the bit then; the console was not measured. A read-only stencil plane still throws - [Swept write-only staging](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver): on a discrete GPU a write-only element whose V# stride is wider than the widest buffer store (16 bytes) and that a direct dispatch sweeps is staged in a device-local shadow. The shadow still takes a copy-in, because the copy-back covers the whole element and the stores may leave part of each record unwritten. A sweep proven to write every byte of the element (from the offsets and widths of its stores within the record) could skip the copy-in, about 10 ms per frame in Diablo II: Resurrected; that proof is not made. Store widths are not recorded per instruction either, so a record of 16 bytes or less stays in place even when its stores are narrower. - [Adaptive staging](../../../core/libs/prx/libSceAgcDriver/Graphics/include/AdaptiveStaging.hpp) (libSceAgcDriver): whether a staged element pays for its two copies is decided by probing the frame GPU time, not from the element's accesses. The probe is measured on one title (PPSA03912, NVIDIA on Linux). It judges by the whole frame's GPU time, so a scene whose GPU load moves by itself can keep the slower mode until the next probe, 20 to 160 s later, and each probe runs a few frames in the slower mode. - [WriteChanged](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) (AGC driver) - a partly changed 16-byte write-back chunk is stored with one byte store per changed byte when at most `ChangedByteStoreLimit()` bytes changed, and with `MASKMOVDQU` otherwise. The limit is 16 (byte stores at every density) on a CPU whose vendor string is `AuthenticAMD` and 4 on any other. The cost of `MASKMOVDQU` is measured only on a Ryzen 9 7950X, where byte stores win up to about 10 changed bytes in a microbenchmark but PPSA16064 lost its gain when `MASKMOVDQU` was kept from 10 changed bytes on, and on a Core i9-14900KF, where byte stores win only at 3 or 4 changed bytes and are up to 2.6 times slower for dense chunks (#8930). Other AMD and Intel generations and other vendors are not measured. - [Host report](../../../core/libs/prx/libSceAgcDriver/Execution/src/HostReportVulkan.cpp) (libSceAgcDriver, `ANYPS5_HOST_REPORT=1`): run on Linux with a software Vulkan device only. Its Windows branches (`RtlGetVersion`, `GlobalMemoryStatusEx`, `USERNAME`, `COMPUTERNAME`, `USERPROFILE`) and its macOS branches are not run, and no hardware GPU's output is checked. The list of required Vulkan items in `HostReportVulkan.cpp` mirrors the checks of `VulkanDevice.cpp` by hand: a new hard requirement added there must be added to the list, or the start fails with the older single-item message. Closes when: the report is read on Windows, macOS and a hardware GPU. +- [Compute scratch pool size](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) - each compute program that uses scratch gets one device buffer of per-workgroup slots, capped at `APS5_SCRATCH_POOL_MIB` (default 64 MiB, never below one slot) and sized from the group count of the first dispatch that needs it (an indirect dispatch asks for the cap); a later dispatch with more groups replaces it with a larger one, up to the cap. The cap is not derived from the device's resident workgroups, so workgroups beyond the slots spin on the slot locks and serialize. One pool is also held under 2 GiB: on an RTX 4090 (driver 617.42) a pool of 2049 MiB returned wrong scratch values while 2047 MiB was correct, though `maxStorageBufferRange` allows more; whether other drivers share that limit is unmeasured. A single slot over that limit or `maxStorageBufferRange` throws at the first dispatch. +- [Compute scratch pool budget](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) - all scratch pools together are held under `APS5_SCRATCH_POOL_TOTAL_MIB` (default 256 MiB): a new or enlarged pool drops the least recently used ones first, and a dropped pool lives until the batches that use it complete. The default is a guess, not derived from the device's memory or from a title's working set. A title whose scratch programs need more than the budget at once recreates a pool, and zeroes its header with a queue round trip, each time one comes back. +- [Compute scratch pool header clear](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) - a new scratch pool's slot-lock header is zeroed by its own one-off command batch, submitted and waited on inside the dispatch record path before the dispatch's batch opens (it also submits any batch recorded so far). Every new or enlarged pool therefore stalls the dispatching thread for one queue round trip; it was measured only in the focused tests, not on the game traces. A pool buffer is never zeroed again, so a shader that leaves a slot lock set (a device loss mid-dispatch) is not repaired. - [Pixel interlock on a device without atomic buffer device addresses](../../../core/libs/prx/libSceAgcDriver/tests/execution/PixelInterlock.cpp) (libSceAgcDriver) - `agc_driver_pixel_interlock` skips where `BdaAtomicsUsable` is false, because it is the only test that counts overlapping updates to one address and that count cannot be right without the guarantee. On MoltenVK it also fails before it reaches the count, at `vkCreateGraphicsPipelines` with `Vulkan result -3`: MoltenVK's own SPIRV-Cross refuses `raster_order_group` in an argument buffer, so `RasterOrderGroupUsable` covers that failure first and the skip names it. With the argument buffers off (`MVK_CONFIG_USE_METAL_ARGUMENT_BUFFERS=0`) the pipeline compiles and the plain draws pass, and only then does the missing atomic show as lost updates. Neither is fixed here: a descriptor-bound atomic would change the BDA table ABI, whose ranges carry a flat device address - [Draw](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) partitions layered mesh color draws into attachment windows within the host `maxMeshOutputLayers` limit, remaps output layers and reconstructs the guest layer for fragment ancillary input. Replay requires shaders without guest memory writes, clocks, GDS access or unproven side effects; those draws still throw until mesh output can be captured once and rasterized separately. Layered depth rendering remains unsupported. Vulkan layer-limit semantics follow [Layer](https://docs.vulkan.org/refpages/latest/refpages/source/Layer.html); PS5 behavior outside Vulkan’s defined layer range is unmeasured. Layered 2D array views remain unsupported by the existing color-view decoder. diff --git a/docs/dev/debt/libSceAjmi.md b/docs/dev/debt/libSceAjmi.md index 3d21dfd34d..4d7d648aa4 100644 --- a/docs/dev/debt/libSceAjmi.md +++ b/docs/dev/debt/libSceAjmi.md @@ -1,5 +1,34 @@ # libSceAjmi technical debt +### Additional callable exports + +- [Additional callable exports](../../../core/libs/prx/libSceAjmi/Export.cpp) - 22 entries present in both version-labelled images are import-resolution declarations only. None is listed in the pinned public 3.20 catalogue for this library. Neither label independently authenticates retail firmware provenance. Parameter lists, return types and behavior are unverified; parameterless `int` declarations are placeholders, not verified ABI contracts. Every entry throws through `NotImplemented_nid_no_patch`. Unidentified names use literal NID aliases. No runtime functionality is implemented. Data exports require separate layout and ownership analysis. Identified candidate names match their NIDs in the [public symbol database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv) or [supplemental catalog](https://github.com/claimore22/ps5rs/blob/2304be5131221bae0dac24b9767cabffda33f747/data/nids.csv). + +| Name | NID | +| --- | --- | +| `sceAjmiBatchCancel` | `6lWAGxD8NLs` | +| `sceAjmiBatchWait` | `JJ9g04D344Q` | +| `Unknown` | `4MsUK9kAnKg` | +| `Unknown` | `6fY-ci3au2U` | +| `Unknown` | `9zA4YMlkR9M` | +| `Unknown` | `BD002VBfhuE` | +| `Unknown` | `EYIibEpcHuw` | +| `Unknown` | `J3UwQ5e6sNU` | +| `Unknown` | `LESEGgm133w` | +| `Unknown` | `MTdd8ApwO50` | +| `Unknown` | `QhA7rTjJbro` | +| `Unknown` | `RbLGSlJB6dc` | +| `Unknown` | `Uqtj4RSP-JQ` | +| `Unknown` | `X8sT-xi-dVE` | +| `Unknown` | `YtubBgSeNH0` | +| `Unknown` | `cWZnCK7na5Y` | +| `Unknown` | `dTsNpEt1AqQ` | +| `Unknown` | `dmwcwCeItmE` | +| `Unknown` | `m0xisYC6U2w` | +| `Unknown` | `ofRX8Vq0ss0` | +| `Unknown` | `pfPwiigOE3A` | +| `Unknown` | `r7NMFYcJi08` | + ### Unknown function info - [libSceAjmi pending exports](../../../core/libs/prx/libSceAjmi/Export.cpp) - of the 5 named callable exports identified in the PS5 3.20 export table and cross-checked against firmware 13.60, 2 are declared here, as `sceAjmBatchJobInlineBuffer_nid_disambig1` and `sceAjmBatchJobRunBufferRa_nid_disambig1`, which the NID patcher exports under the names' NIDs, because libSceAjm implements the same names for PS4 titles. Both are firmware 13.42's routines, which write the same bytes as libSceAjm's in an emulator ([sceAjmBatchJobInlineBuffer](https://github.com/boykopovar/AnyPS5/pull/11238#issuecomment-6418132093), [sceAjmBatchJobRunBufferRa](https://github.com/boykopovar/AnyPS5/pull/11238#issuecomment-6442306780)); neither was run on a console. Evidence: [3.20 export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAjmi.c). diff --git a/docs/dev/debt/libSceAppContent.md b/docs/dev/debt/libSceAppContent.md index 9ea6e297ba..18f5fc7272 100644 --- a/docs/dev/debt/libSceAppContent.md +++ b/docs/dev/debt/libSceAppContent.md @@ -11,7 +11,8 @@ - [sceAppContentDownloadDataGetAvailableSpaceKb](../../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) - a title whose `param.json` declares `downloadDataSize` 0 gets 0 KB and OK; the console's behaviour for a title without a download data area is unverified (it may return an error instead). KytyPS5 does not special-case it, and shadPS4 stubs 1048576 KB. One disassembled title (PPSA15246) calls it with the `/download0` mount point and a zeroed 64-bit output, ignores the return value and multiplies the output by 1024, so for that title an error that leaves the output untouched and an OK with 0 KB look the same - [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 - +- [sceAppContentDownload2Expand, sceAppContentDownload2Shrink](../../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) - callable exports imported by Call of Duty: Modern Warfare 3 (PPSA24263), declared in [dlc_emu](https://github.com/drakmor/dlc_emu/blob/fcf69b16d46a9850578bce19dc2c1b4dc867cbb5/src/dlc_modules/sceappcontent_exports.cpp#L42-L43) as taking one `const void* downloadHandle`. Their signatures and behaviour on hardware are unverified; all calls throw through `NotImplemented_nid_no_patch`. +- [sceAppContentUnknown_SWVxsi_ZBlw](../../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent, NID SWVxsi-ZBlw) - master data identifier query (UnknownMdid in [dlc_emu](https://github.com/drakmor/dlc_emu/blob/fcf69b16d46a9850578bce19dc2c1b4dc867cbb5/src/dlc_modules/dlc_content.cpp#L2258-L2262)). Reconstructed signature and return values on hardware are unverified; all calls throw through `NotImplemented_nid_no_patch`. - [libSceAppContent pending exports](../../../core/libs/prx/libSceAppContent/Export.cpp) - 8 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAppContent.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. ### Functional diff --git a/docs/dev/debt/libSceAppInstUtil.md b/docs/dev/debt/libSceAppInstUtil.md index 5c10aa50ea..bbd5cd18b8 100644 --- a/docs/dev/debt/libSceAppInstUtil.md +++ b/docs/dev/debt/libSceAppInstUtil.md @@ -139,3 +139,8 @@ - [sceAppInstUtilInstallAddons](../../../core/libs/prx/libSceAppInstUtil/Export.cpp) (libSceAppInstUtil) - unknown signature - [sceAppInstUtilAppConvertAppDiscToDownload, sceAppInstUtilAppGetCustomThemeInfo, sceAppInstUtilAppGetInstallAddcontFromDiscStatus and sceAppInstUtilAppInstallCloudGame](../../../core/libs/prx/libSceAppInstUtil/Export.cpp) (libSceAppInstUtil) - The list is the intersection of the export table of the console's libSceAppInstUtil (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo. Names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAppInstUtil.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. - [sceAppInstUtilAppInstallMediaPlayer, sceAppInstUtilAppInstallRequestAddcontFromDisc and sceAppInstUtilAppSetSavedataUploadStatus](../../../core/libs/prx/libSceAppInstUtil/Export.cpp) (libSceAppInstUtil) - The list is the intersection of the export table of the console's libSceAppInstUtil (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo. Names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAppInstUtil.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. +- [sceAppInstUtilAppIsInInstalling](../../../core/libs/prx/libSceAppInstUtil/Export.cpp) (libSceAppInstUtil) - throws, as its prototype is disputed: SharpProspero's [`AppInstUtil.cs`](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/AppInstUtil/AppInstUtil.cs#L79-L88) passes an `int*` that receives the answer, while its [`PackageInstaller.cs`](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Platform/PackageInstaller.cs#L484-L498) passes a NUL-terminated id and reads the return value, as the PS4 headers declare `bool sceAppInstUtilAppIsInInstalling(const char* contentId)` ([flatz](https://github.com/flatz/ps4_stub_lib_maker_v2/blob/a8d044c12ea2dc3fa190ce1769eeb48b30d28ed4/include/appinst_util.h#L35)). Writing through the argument would overwrite a caller's id under the second form + +### Functional + +- [sceAppInstUtilInitialize, sceAppInstUtilTerminate, sceAppInstUtilAppExists](../../../core/libs/prx/libSceAppInstUtil/Export.cpp) (libSceAppInstUtil) - `sceAppInstUtilAppExists` writes 1 only for the running title's id, as another title cannot be launched (`sceSystemServiceLaunchApp` throws). [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/AppInstUtil/AppInstUtil.cs#L18-L88) gives the meanings but no error codes, so calls outside an `sceAppInstUtilInitialize`/`sceAppInstUtilTerminate` pair, a second initialization and null pointers throw. The non-zero value a console writes for an installed title is unmeasured diff --git a/docs/dev/debt/libSceAudioSystem.md b/docs/dev/debt/libSceAudioSystem.md index b572e26258..3b704b2db0 100644 --- a/docs/dev/debt/libSceAudioSystem.md +++ b/docs/dev/debt/libSceAudioSystem.md @@ -4,3 +4,42 @@ - [sceAudioSystemInit, sceAudioSystemProcessAdd, sceAudioSystemProcessRemove, sceAudioSystemProcessResume, sceAudioSystemProcessSuspend, sceAudioSystemTerminate](../../../core/libs/prx/libSceAudioSystem/Export.cpp) have unknown signatures and behaviour. The parameterless `int` declarations are import-resolution placeholders, not confirmed ABI signatures, and throw through `NotImplemented_nid_no_patch`. Only the names and NIDs are established by the [13.60 export inventory](https://github.com/xdCloudy/PS5-13.60-Function-Stub-Library/blob/bfec40deb4c47a17d7603ee2bc79aa6862c0c56b/data/firmware-13.60/libsceaudiosystem-bfe74a18cc.jsonl). - [sceAudioSystemSystemResume and sceAudioSystemSystemSuspend](../../../core/libs/prx/libSceAudioSystem/Export.cpp) (libSceAudioSystem) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAudioSystem.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified + +### Additional callable exports + +- [Additional callable exports](../../../core/libs/prx/libSceAudioSystem/Export.cpp) - 30 entries present in both version-labelled images are import-resolution declarations only. None is listed in the pinned public 3.20 catalogue for this library. Neither label independently authenticates retail firmware provenance. Parameter lists, return types and behavior are unverified; parameterless `int` declarations are placeholders, not verified ABI contracts. Every entry throws through `NotImplemented_nid_no_patch`. Unidentified names use literal NID aliases. No runtime functionality is implemented. Identified candidate names match their NIDs in the [public symbol database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv) or [supplemental catalog](https://github.com/claimore22/ps5rs/blob/2304be5131221bae0dac24b9767cabffda33f747/data/nids.csv). + +[Version-pinned selected ELF export records](https://gist.github.com/kyon-eth/3fbb1a4a6f95a936b4cd58e9c3d8c50c/5e3af709224ed8235af11337309af0725e5685cb) list every NID below with its image label, decoded export module and library, raw qualified symbol, ELF function type, value and size for each inspected version-labelled image. The extract SHA-256 is `02ab27f12bcd0cc9fa03fa33f55fc201110f0d54a86bbbaa093d55d0a8cf262d`; its full-module sorted-function-NID digests and raw module-parameter words permit independent comparison. This is an analyst-produced metadata record, not independent authentication of the original images or proof of prototypes or behavior. + +| Name | NID | +| --- | --- | +| `sceAudioOutServerBusGetIoParam` | `jhrJaWSScXs` | +| `sceAudioOutServerGetOption` | `D95bo7YrrGM` | +| `Unknown` | `-e5aJEqL34Y` | +| `Unknown` | `17nOsxccH5o` | +| `Unknown` | `2bKJUeywpeY` | +| `Unknown` | `3HOelmA2vcw` | +| `Unknown` | `5SM0uQwcgTQ` | +| `Unknown` | `5oOYYvwIb0k` | +| `Unknown` | `6PGbkl6OnvE` | +| `Unknown` | `9QaKJdd7jKk` | +| `Unknown` | `B-JyTVr90kc` | +| `Unknown` | `Fd9u7-OKW+Q` | +| `Unknown` | `FkFcs7NTFDo` | +| `Unknown` | `GUtNdBeMrcM` | +| `Unknown` | `HxLyu3KUv5U` | +| `Unknown` | `IiO+DdVz-4E` | +| `Unknown` | `J2Fx9JLKcqU` | +| `Unknown` | `Met2nIMg+Ng` | +| `Unknown` | `WRDd2feDYzg` | +| `Unknown` | `c4m3OSO0lj8` | +| `Unknown` | `ds20PmJfSSk` | +| `Unknown` | `hjlhfF8Voz0` | +| `Unknown` | `jLjHQbmK9+A` | +| `Unknown` | `jZPcy9MG4w0` | +| `Unknown` | `lSatNsWSmps` | +| `Unknown` | `ltR0PMUM270` | +| `Unknown` | `mS9gpe47Eio` | +| `Unknown` | `tHodLaUv04w` | +| `Unknown` | `uoPBJTVWhrg` | +| `Unknown` | `xG7GUeOKtBM` | diff --git a/docs/dev/debt/libSceAudiodec.md b/docs/dev/debt/libSceAudiodec.md index 77b269ef83..b166062918 100644 --- a/docs/dev/debt/libSceAudiodec.md +++ b/docs/dev/debt/libSceAudiodec.md @@ -7,6 +7,7 @@ - [sceAudiodecDecode](../../../core/libs/prx/libSceAudiodec/src/Codecs.cpp) (libSceAudiodec) - a raw M4AAC stream carries no channel configuration, so the decoder tries the mono/stereo configuration and then configurations 3 to 7 within `ui_max_channels` until one decodes the first access unit, and keeps that one. No reference documents how the library picks the layout (shadPS4 maps the channel count straight to the configuration; SharpProspero only describes the maximum). With the stereo configuration a mono raw stream decodes as 2 channels, so 2 is reported, not 1 - [sceAudiodecDecode](../../../core/libs/prx/libSceAudiodec/src/Codecs.cpp) (libSceAudiodec) - the HE-AAC handling follows one console measurement of an HE-AAC v2 ADTS stream: SBR output at 48 kHz in one channel with `ui_enable_heaac` 1, the 24 kHz mono core with 0. Not measured: whether the console, like FFmpeg, decodes a stream without SBR data in its first access unit as AAC-LC with `ui_enable_heaac` 1, and what it reports in `ui_sampling_freq` and `ui_heaac` with `ui_enable_heaac` 0 (here the rate of the returned PCM and 0) +- [sceAudiodecDecode](../../../core/libs/prx/libSceAudiodec/src/Codecs.cpp) (libSceAudiodec) - the console's answer to an M4AAC layout its decoder cannot use is not measured. An ADTS header whose layout FFmpeg cannot open (for example the reserved sampling index 13) is answered with `M4AAC_RESULT_DECODE_ERROR` (-4) and `ERROR_API_FAIL` with sizes 0, as an access unit FFmpeg cannot decode is. The decoder then has no FFmpeg context and answers the same for every access unit until a header names a layout that opens; the raw layout search skips a layout FFmpeg refuses ### Functional diff --git a/docs/dev/debt/libSceAvSetting.md b/docs/dev/debt/libSceAvSetting.md index 1171471326..2b200a5c7d 100644 --- a/docs/dev/debt/libSceAvSetting.md +++ b/docs/dev/debt/libSceAvSetting.md @@ -1,12 +1,22 @@ # libSceAvSetting technical debt +### Recovered behavior + +- [`sceAvControlIsColorimetryHdr`](../../../core/libs/prx/libSceAvSetting/Export.cpp) (NID `3jnRsjnzIjI`) maps colorimetry values 9 through 20 to a 0/1 table; value 18 and values outside that range return false. +- [`sceAvControlIsDeepColorSupportedByMonitorInfo`](../../../core/libs/prx/libSceAvSetting/Export.cpp) (NID `fnqFpsRrsg4`) checks masks `0x0a` at offsets `0x74` and `0x75`, then mask `0x03` at offset `0x352`. +- [`sceAvControlIsHdrSupportedByMonitorInfo`](../../../core/libs/prx/libSceAvSetting/Export.cpp) (NID `5wP7bivaX7c`) requires mask `0x04` at offset `0x358` and mask `0x30` at offset `0x78`. +- [`sceAvControlIsModeGen1Vr`](../../../core/libs/prx/libSceAvSetting/Export.cpp) (NID `C81K5tny9CE`) returns bit 1 of the 32-bit field at offset `0x30`. +- [`sceAvControlIsModeSystemDefault`](../../../core/libs/prx/libSceAvSetting/Export.cpp) (NID `rVK3QqurCkg`) returns true when bit 0 of the 32-bit field at offset `0x30` is clear. + +These behaviors are recovered from static disassembly of the firmware 13.60 `libSceAvSetting.sprx` using Ghidra 12.1.4. The opaque monitor-info and video-mode layouts are represented by byte offsets because no project structure definition was found. These implementations have not been verified on a console. + ### Unknown function info - [sceAvControlChangeOutputLayout, sceAvControlChangeOutputMode, sceAvControlChangeOutputModeForDiag, sceAvControlChangeProcessAttribute, sceAvControlChangeScanInMode](../../../core/libs/prx/libSceAvSetting/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAvSetting.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. - [sceAvControlGetCurrentDeviceId, sceAvControlGetCurrentOutputMode, sceAvControlGetCurrentOutputModeWithSync, sceAvControlGetDetailHdcpStatus, sceAvControlGetDeviceInfo](../../../core/libs/prx/libSceAvSetting/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAvSetting.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. - [sceAvControlGetHdcpStatus, sceAvControlGetMonitorInfo, sceAvControlGetNativeMonitorInfo, sceAvControlGetPuStatus, sceAvControlGetPuType](../../../core/libs/prx/libSceAvSetting/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAvSetting.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. -- [sceAvControlInit, sceAvControlIsAudioOutModeSupportedByMonitorInfo, sceAvControlIsColorimetryHdr, sceAvControlIsDeepColorSupportedByMonitorInfo, sceAvControlIsDeviceConnected](../../../core/libs/prx/libSceAvSetting/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAvSetting.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. -- [sceAvControlIsHdrSupportedByMonitorInfo, sceAvControlIsModeGen1Vr, sceAvControlIsModeSystemDefault, sceAvControlIsSuspendedProcessOutputModeAvailable, sceAvControlIsVideoOutModeSupportedByMonitorInfo](../../../core/libs/prx/libSceAvSetting/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAvSetting.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [sceAvControlInit, sceAvControlIsAudioOutModeSupportedByMonitorInfo, sceAvControlIsDeviceConnected](../../../core/libs/prx/libSceAvSetting/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAvSetting.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [sceAvControlIsSuspendedProcessOutputModeAvailable, sceAvControlIsVideoOutModeSupportedByMonitorInfo](../../../core/libs/prx/libSceAvSetting/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAvSetting.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. - [sceAvControlSet2dVrMode, sceAvControlSetAudioMuteForShutdown, sceAvControlSetAvOutputMode, sceAvControlSetColorEffect, sceAvControlSetDefaultAudioOutMode](../../../core/libs/prx/libSceAvSetting/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAvSetting.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. - [sceAvControlSetDefaultVideoOutMode, sceAvControlSetDispclk, sceAvControlSetGamutMetadata, sceAvControlSetHdcpEncryption, sceAvControlSetHdcpStatus](../../../core/libs/prx/libSceAvSetting/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAvSetting.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. - [sceAvControlSetHdmiGamutMetadata, sceAvControlSetHdrMetadata, sceAvControlSetVideoOutSource, sceAvControlSimulateProcessOutputModeArbitration, sceAvSettingAddCallbacks](../../../core/libs/prx/libSceAvSetting/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAvSetting.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. diff --git a/docs/dev/debt/libSceAvcap2.md b/docs/dev/debt/libSceAvcap2.md index 5b45837431..a73fec5a9e 100644 --- a/docs/dev/debt/libSceAvcap2.md +++ b/docs/dev/debt/libSceAvcap2.md @@ -19,3 +19,24 @@ The 7 original exports listed below in [Export.cpp](../../../core/libs/prx/libSc - [sceAvcap2Start](../../../core/libs/prx/libSceAvcap2/Export.cpp) (libSceAvcap2) - unknown signature (NID 8CGNCwBsItI) - [sceAvcap2Stop](../../../core/libs/prx/libSceAvcap2/Export.cpp) (libSceAvcap2) - unknown signature (NID Z5dKK0xnQ8g) - [sceAvcap2ReadAudio](../../../core/libs/prx/libSceAvcap2/Export.cpp) (libSceAvcap2) - unknown signature (NID WhsHggqdwcg) + +### Additional callable exports + +- [Additional callable exports](../../../core/libs/prx/libSceAvcap2/Export.cpp) - 12 entries present in both version-labelled images are import-resolution declarations only. None is listed in the pinned public 3.20 catalogue for this library. Neither label independently authenticates retail firmware provenance. Parameter lists, return types and behavior are unverified; parameterless `int` declarations are placeholders, not verified ABI contracts. Every entry throws through `NotImplemented_nid_no_patch`. Unidentified names use literal NID aliases. No runtime functionality is implemented. Identified candidate names match their NIDs in the [public symbol database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv) or [supplemental catalog](https://github.com/claimore22/ps5rs/blob/2304be5131221bae0dac24b9767cabffda33f747/data/nids.csv). + +[Version-pinned selected ELF export records](https://gist.github.com/kyon-eth/3fbb1a4a6f95a936b4cd58e9c3d8c50c/5e3af709224ed8235af11337309af0725e5685cb) list every NID below with its image label, decoded export module and library, raw qualified symbol, ELF function type, value and size for each inspected version-labelled image. The extract SHA-256 is `02ab27f12bcd0cc9fa03fa33f55fc201110f0d54a86bbbaa093d55d0a8cf262d`; its full-module sorted-function-NID digests and raw module-parameter words permit independent comparison. This is an analyst-produced metadata record, not independent authentication of the original images or proof of prototypes or behavior. + +| Name | NID | +| --- | --- | +| `Unknown` | `+OspdnZ-8mc` | +| `Unknown` | `1Xp85wUaVTE` | +| `Unknown` | `32gujAUoIMc` | +| `Unknown` | `4hkuDvkGu7U` | +| `Unknown` | `7KBaem5GyBQ` | +| `Unknown` | `88YEF+XHfRY` | +| `Unknown` | `HAeczEM-m7c` | +| `Unknown` | `HrGLkn9o4MY` | +| `Unknown` | `Tzk7-W5mp0A` | +| `Unknown` | `VNRuuZQx7gE` | +| `Unknown` | `iy6ZcsdeoMk` | +| `Unknown` | `k7gz608olKM` | diff --git a/docs/dev/debt/libSceCdlgPlayerReview.md b/docs/dev/debt/libSceCdlgPlayerReview.md index 0e97a3d72d..45cde68133 100644 --- a/docs/dev/debt/libSceCdlgPlayerReview.md +++ b/docs/dev/debt/libSceCdlgPlayerReview.md @@ -6,4 +6,4 @@ ### Functional -- [Player review dialog](../../../core/libs/prx/libSceCdlgPlayerReview/Export.cpp) (libSceCdlgPlayerReview) shows nothing: `scePlayerReviewDialogOpen` finishes at once and the result reports that the user canceled, as `sceWebBrowserDialogOpen` does +- [Player review dialog](../../../core/libs/prx/libSceCdlgPlayerReview/Export.cpp) (libSceCdlgPlayerReview) shows nothing: `scePlayerReviewDialogOpen` finishes at once and the result reports that the user canceled, as `sceWebBrowserDialogOpen` does; returning `COMMON_DIALOG_ERROR_INVALID_ARGUMENT` for an unwritable result is a host-safety fallback as console behavior for unwritable memory has not been measured diff --git a/docs/dev/debt/libSceCloudMessaging.md b/docs/dev/debt/libSceCloudMessaging.md new file mode 100644 index 0000000000..0a38a1baa1 --- /dev/null +++ b/docs/dev/debt/libSceCloudMessaging.md @@ -0,0 +1,5 @@ +# libSceCloudMessaging technical debt + +### Unknown function info + +- [libSceCloudMessaging inventory exports](../../../core/libs/prx/libSceCloudMessaging/Export.cpp) - all six names and NIDs are present in the complete [PS5 13.60 export inventory](https://github.com/xdCloudy/PS5-13.60-Function-Stub-Library/blob/bfec40deb4c47a17d7603ee2bc79aa6862c0c56b/data/firmware-13.60/libscecloudmessaging-1d0153bc23.jsonl), and the pinned [module map](https://github.com/boykopovar/ps5-nid-map/blob/cc1d586fddab576a520f226da6b2632b031e6d09/data/Modules/libSceCloudMessaging.inc) assigns them to the physical `libSceCloudMessaging.sprx` provider. The mangled names establish export identities only; their encoded types are not treated as verified signatures. Parameter lists, return types and behavior are unverified; the parameterless `int` declarations are import-resolution placeholders, not verified ABI contracts. Every entry throws through `NotImplemented_nid_no_patch`. No cloud-messaging behavior is implemented. diff --git a/docs/dev/debt/libSceComposite.md b/docs/dev/debt/libSceComposite.md index aa5a59ec25..d30c4bb904 100644 --- a/docs/dev/debt/libSceComposite.md +++ b/docs/dev/debt/libSceComposite.md @@ -6,10 +6,47 @@ ### Unknown function info -- [sceCompositorGetNullCanvasHandle, sceCompositorIsVirtualCanvasHandle, sceCompositorSetMainPortPixelFormat, sceCompositorGetVirtualCanvasHandle, sceCompositorSetCanvasIdToCanvasHandle, sceCompositorSetMetaIdToCanvasHandle, sceCompositorSetMainPort and sceCompositorSetVirtualCanvasRenderTargetCommand](../../../core/libs/prx/libSceComposite/Export.cpp) - signatures and behavior remain unknown. The parameterless `int` declarations only resolve imports, make no ABI claim and every call throws through `NotImplemented_nid_no_patch`. Names and NIDs are recorded in the [13.60 export inventory](https://github.com/xdCloudy/PS5-13.60-Function-Stub-Library/blob/bfec40deb4c47a17d7603ee2bc79aa6862c0c56b/data/firmware-13.60/libscecomposite-23841193cf.jsonl); not measured on a console. +- [sceCompositorGetNullCanvasHandle, sceCompositorSetMainPortPixelFormat, sceCompositorGetVirtualCanvasHandle, sceCompositorSetCanvasIdToCanvasHandle, sceCompositorSetMetaIdToCanvasHandle, sceCompositorSetMainPort and sceCompositorSetVirtualCanvasRenderTargetCommand](../../../core/libs/prx/libSceComposite/Export.cpp) - signatures and behavior remain unknown. The parameterless `int` declarations only resolve imports, make no ABI claim and every call throws through `NotImplemented_nid_no_patch`. Names and NIDs are recorded in the [13.60 export inventory](https://github.com/xdCloudy/PS5-13.60-Function-Stub-Library/blob/bfec40deb4c47a17d7603ee2bc79aa6862c0c56b/data/firmware-13.60/libscecomposite-23841193cf.jsonl); not measured on a console. +- [sceCompositorIsVirtualCanvasHandle](../../../core/libs/prx/libSceComposite/Export.cpp) - what a virtual canvas handle is and which calls make one are unknown. It follows firmware 13.42's libSceComposite.sprx (the copy in libSceCompositeExt.sprx has the same code), read from its disassembly and run natively on x86-64 (not a console): it takes a 32-bit handle and returns true exactly when bits 8 to 15 are all set, without reading any library state - [sceCompositorMemoryPoolCommit, sceCompositorGetSecondaryMemorySize, sceCompositorGetMemoryPoolAddress, sceCompositorGetSecondaryMemoryAddress, sceCompositorSecondaryMemoryCreate, sceCompositorGetMemoryPoolSize, sceCompositorPermanentMemoryCreate and sceCompositorMemoryPoolCreate](../../../core/libs/prx/libSceComposite/Export.cpp) - signatures and behavior remain unknown. The parameterless `int` declarations only resolve imports, make no ABI claim and every call throws through `NotImplemented_nid_no_patch`. Names and NIDs are recorded in the [13.60 export inventory](https://github.com/xdCloudy/PS5-13.60-Function-Stub-Library/blob/bfec40deb4c47a17d7603ee2bc79aa6862c0c56b/data/firmware-13.60/libscecomposite-23841193cf.jsonl); not measured on a console. - [sceCompositorGetTFRing, sceCompositorStopSeeThroughBuffer, sceCompositorSetNativeVrVideoOutMode, sceCompositorWaitEndOfRenderingIfGpuWorking, sceCompositorSetFlipCommand3, sceCompositorSetTFRing and sceCompositorStartSeeThroughBuffer](../../../core/libs/prx/libSceComposite/Export.cpp) - signatures and behavior remain unknown. The parameterless `int` declarations only resolve imports, make no ABI claim and every call throws through `NotImplemented_nid_no_patch`. Names and NIDs are recorded in the [13.60 export inventory](https://github.com/xdCloudy/PS5-13.60-Function-Stub-Library/blob/bfec40deb4c47a17d7603ee2bc79aa6862c0c56b/data/firmware-13.60/libscecomposite-23841193cf.jsonl); not measured on a console. - [sceCompositorAllocateIndex, sceCompositorCheckCrash, sceCompositorCommandGpuPerfBegin, sceCompositorCommandGpuPerfEnd, sceCompositorFlush, sceCompositorFlushWithRepeat, sceCompositorGetAnotherProcessSystemAddress, sceCompositorGetAnotherProcessSystemSize, sceCompositorGetAnotherProcessVideoAddress, sceCompositorGetAnotherProcessVideoSize, sceCompositorGetCanvasHandle, sceCompositorGetClientList, sceCompositorGetDmemOffset, sceCompositorGetDummyAddress, sceCompositorGetDummySize, sceCompositorGetRemainedCommandNum, sceCompositorGetRenderTargetResolution, sceCompositorGetSystemAddress, sceCompositorGetSystemSize, sceCompositorGetVideoAddress, sceCompositorGetVideoSize, sceCompositorHandleProcessEvents, sceCompositorInit, sceCompositorInitWithProcessOrder, sceCompositorInsertThreadTraceMarker, sceCompositorIsDebugCaptureEnabled, sceCompositorIsReady4K, sceCompositorLockCommandBuffer, sceCompositorMakeCanvasHandle, sceCompositorMapAnotherProcess, sceCompositorMemoryPoolDecommit, sceCompositorPrivacyMaskPushArea, sceCompositorQuit, sceCompositorReleaseCommandBuffer, sceCompositorReleaseIndex, sceCompositorReleaseLocalStall, sceCompositorReserveCommandBuffer, sceCompositorResetZoomCommand, sceCompositorSetAgcCompositeCanvasCommand, sceCompositorSetAgcContextCommand, sceCompositorSetCompositeCanvasCommandInC, sceCompositorSetCursorImageAddress, sceCompositorSetDebugPositionCommand, sceCompositorSetEndOfFrameCommand, sceCompositorSetEventCommand, sceCompositorSetFilterType, sceCompositorSetFlipCommand2, sceCompositorSetGameSufaceControlCommand, sceCompositorSetGnmContextCommand, sceCompositorSetInvisibleCanvasCommand, sceCompositorSetKeepDisplayBufferCommand, sceCompositorSetLocalStallCommand, sceCompositorSetMemoryCommand, sceCompositorSetPatchCommand, sceCompositorSetPostEventCommand, sceCompositorSetRepeatCommand, sceCompositorSetResolutionCommand, sceCompositorSetScalingLayoutInfo, sceCompositorSetSeparateContextCommand, sceCompositorSetSystemConfigWithMask, sceCompositorSetVideoOutMode, sceCompositorSetVirtualCanvasAgcContextCommand, sceCompositorSetVirtualCanvasCommandFooter, sceCompositorSetVirtualCanvasPatchCommand, sceCompositorSetVrMode, sceCompositorSetZoomCommand, sceCompositorSystemConfig, sceCompositorWaitEndOfRendering, sceCompositorWaitEvent, sceCompositorWaitPostEvent, sceCompsoitorGetGpuClock, sceCompsoitorGetProcessRenderingTime and sceCompsoitorGetRenderingTime](../../../core/libs/prx/libSceComposite/Export.cpp) (libSceComposite) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceComposite.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified + +### Additional callable exports + +- [Additional callable exports](../../../core/libs/prx/libSceComposite/Export.cpp) - 29 entries present in both version-labelled images are import-resolution declarations only. None is listed in the pinned public 3.20 catalogue for this library. Neither label independently authenticates retail firmware provenance. Parameter lists, return types and behavior are unverified; parameterless `int` declarations are placeholders, not verified ABI contracts. Every entry throws through `NotImplemented_nid_no_patch`. Unidentified names use literal NID aliases. No runtime functionality is implemented. Data exports require separate layout and ownership analysis. Identified candidate names match their NIDs in the [public symbol database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv) or [supplemental catalog](https://github.com/claimore22/ps5rs/blob/2304be5131221bae0dac24b9767cabffda33f747/data/nids.csv). + +| Name | NID | +| --- | --- | +| `sceCompositorSecondaryMemoryRelease` | `gEME-XiMfm0` | +| `Unknown` | `+A5zYfomrKo` | +| `Unknown` | `+DMKcAbpcrQ` | +| `Unknown` | `0hVj7ycuNjY` | +| `Unknown` | `3znAWbdvzNo` | +| `Unknown` | `4iL4YvcEKdU` | +| `Unknown` | `5vAisOgay3M` | +| `Unknown` | `8l316W539Zw` | +| `Unknown` | `FDHuP+z8KnM` | +| `Unknown` | `HXxq9giIX-I` | +| `Unknown` | `KpuS3CIcckw` | +| `Unknown` | `NMGq0MJ2iGo` | +| `Unknown` | `RPdjyM2WKcw` | +| `Unknown` | `SpfMcnnpfDA` | +| `Unknown` | `UEW01n5qkdA` | +| `Unknown` | `XZzyQX4T5ts` | +| `Unknown` | `Yck3L7ExyVc` | +| `Unknown` | `Z25ikyBUYy0` | +| `Unknown` | `aczOzib8mzI` | +| `Unknown` | `asXqPHp3O5g` | +| `Unknown` | `bwXAoMVv94M` | +| `Unknown` | `dTPMTtYqPX4` | +| `Unknown` | `gLAJaS1VbsA` | +| `Unknown` | `inplO6BVf5k` | +| `Unknown` | `j2CEWhcK0gc` | +| `Unknown` | `lvaCIMhYNh0` | +| `Unknown` | `mzPjqJ+unSw` | +| `Unknown` | `va+6TiqnE1Y` | +| `Unknown` | `yZcGEBwnDAM` | diff --git a/docs/dev/debt/libSceContentExport.md b/docs/dev/debt/libSceContentExport.md index 0e70333be7..ba4d78b3fa 100644 --- a/docs/dev/debt/libSceContentExport.md +++ b/docs/dev/debt/libSceContentExport.md @@ -2,9 +2,9 @@ ### Functional -- [Content export sessions](../../../core/libs/prx/libSceContentExport/Export.cpp): the host has no media library, so `sceContentExportFromDataWithThumbnail` goes through `NotImplemented_nid_no_patch` for a valid session without invoking data callbacks. Session allocation does not reproduce guest allocator calls, worker creation or their failure paths. `sceContentExportGetProgress` goes through `NotImplemented_nid_no_patch` because an idle session returns either zero or an invalid-state error according to the caller's SDK compatibility mode, which is not available here. Initialization and session lookup still throw for some invalid inputs instead of reproducing the console's error precedence and legacy SDK behavior. Session IDs now use the lowest free slot in 0–9; exhaustion, termination with live sessions and cancellation before an export return the confirmed errors. +- [Content export sessions](../../../core/libs/prx/libSceContentExport/Export.cpp): the host has no media library, so `sceContentExportFromData`, `sceContentExportFromFile`, `sceContentExportFromFileWithThumbnail` and `sceContentExportFromDataWithThumbnail` validate the export session before calling `NotImplemented_nid_no_patch` without invoking data callbacks. Session allocation does not reproduce guest allocator calls, worker creation or failure paths. `sceContentExportGetProgress` calls `NotImplemented_nid_no_patch` because an idle session returns either zero or invalid-state according to caller SDK compatibility mode. Initialization and session lookup throw for some invalid inputs instead of reproducing console error precedence and legacy SDK behavior. Session IDs use the lowest free slot in 0–9; exhaustion, termination with live sessions and cancellation before export return confirmed errors. -- [sceContentExportFromData](../../../core/libs/prx/libSceContentExport/Export.cpp) (libSceContentExport) - throws as unimplemented. Two titles disassembled for this entry call it once each with 7 arguments: PPSA01746 (0xba0e14) between `sceContentExportStart` and `sceContentExportFinish`, treating any result other than 0 as failure, and the ContentManagement plugin of PPSA06465 (0x11f9), treating a negative result as failure and any other as success. What the arguments and the callback mean is not shown. +- [sceContentExportFromData](../../../core/libs/prx/libSceContentExport/Export.cpp) (libSceContentExport) - validates the export session and throws as unimplemented. Two titles disassembled for this entry call it once each with 7 arguments: PPSA01746 (0xba0e14) between `sceContentExportStart` and `sceContentExportFinish`, treating any result other than 0 as failure, and the ContentManagement plugin of PPSA06465 (0x11f9), treating a negative result as failure and any other as success. What the arguments and the callback mean is not shown. ### Unknown function info diff --git a/docs/dev/debt/libSceConvertKeycode.md b/docs/dev/debt/libSceConvertKeycode.md index ad1c428747..6b2929fc28 100644 --- a/docs/dev/debt/libSceConvertKeycode.md +++ b/docs/dev/debt/libSceConvertKeycode.md @@ -3,5 +3,5 @@ ### Functional - [sceConvertKeycodeGetVirtualKeycode](../../../core/libs/prx/libSceConvertKeycode/Export.cpp), `sceConvertKeycodeGetCharacter` and `sceConvertKeycodeGetCharacterFromKeyboardData`: layouts 31 and up need a PS5 title or a PS4 SDK of 0x05000000, 38 and 39 a PS5 SDK of 0x06000000 or a PS4 SDK of 0x10000000, and 39 a PS5 title with SDK 0x08000000; other titles get `0x80BC001D`. `GetVirtualKeycode` swaps the Y and Z keys of layouts 2, 3 and 12 below PS5 SDK 0x04000000 or PS4 SDK 0x09000000. These rules come from firmware 13.42's module, run natively, not from a console; the SDK version is the one in the process parameters. Three titles disassembled for this entry call `sceConvertKeycodeGetVirtualKeycode` at four sites (PPSA24779 0xa9199a, PPSA29866 0x2d8bdd, PPSA30601 0x15e4624 and 0x15e47da): the keyboard type is the constant 4 at 0xa9199a and 0x15e47da and a run-time value at the other two, and no value read is 2, 3, 12 or in 31 to 39. -- [sceConvertKeycodeGetCharacterFromKeyboardData](../../../core/libs/prx/libSceConvertKeycode/Export.cpp): an index outside `keyData->length` or `KEYBOARD_MAX_KEYCODES` throws; no reference gives the real result or validation order. +- [sceConvertKeycodeGetCharacterFromKeyboardData](../../../core/libs/prx/libSceConvertKeycode/Export.cpp): returns `0x80BC0030` for a `length` above 16 (or negative), an `led` of 8 or more, a `modifier_key` of 0x100 or more, a `state` bit outside 0x1C000080, or an index outside the key data, before the layout checks, and otherwise converts `key_code[index]` with `state | modifier_key << 8 | led << 16`. From firmware 13.42's module, run natively, not from a console. - [sceConvertKeycodeGetImeKeyboardType](../../../core/libs/prx/libSceConvertKeycode/Export.cpp): implemented exactly as the repository owner (boykopovar) specified the UserService keyboard type (plus the Japanese variant for type 37 and the Traditional Chinese variant for type 39) to IME keyboard type table, the SDK compatibility rules (38 becomes 4 for PS5 SDK below 0x06000000 or PS4 SDK below 0x10000000; 39 becomes 29 for PS5 SDK below 0x08000000 and for every PS4 client), the error codes and the unmatched-type result (success, IME type 0). The PS4/PS5 client rule reads the title ID prefix from `param.sfo` or `param.json` (`CUSA` is PS4); a missing, malformed or oversized ID or title, as in homebrew, is treated as PS5. Reliable detection inspects the original `eboot.bin`, which only the relinker reads, so it is not done. UserService reports the keyboard type of the configured system language, so the function returns the owner's IME type for that language (see libSceUserService.md). diff --git a/docs/dev/debt/libSceCustomMusicSysCallWrapper.md b/docs/dev/debt/libSceCustomMusicSysCallWrapper.md index f7379ba3fc..525a5deb5f 100644 --- a/docs/dev/debt/libSceCustomMusicSysCallWrapper.md +++ b/docs/dev/debt/libSceCustomMusicSysCallWrapper.md @@ -1,5 +1,121 @@ # libSceCustomMusicSysCallWrapper technical debt +### Additional callable exports + +- [Additional callable exports](../../../core/libs/prx/libSceCustomMusicSysCallWrapper/Export.cpp) - 101 entries present in both version-labelled images are import-resolution declarations only. None is listed in the pinned public 3.20 catalogue for this library. Neither label independently authenticates retail firmware provenance. Parameter lists, return types and behavior are unverified; parameterless `int` declarations are placeholders, not verified ABI contracts. Every entry throws through `NotImplemented_nid_no_patch`. Unidentified names use literal NID aliases. No runtime functionality is implemented. Identified candidate names match their NIDs in the [public symbol database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv) or [supplemental catalog](https://github.com/claimore22/ps5rs/blob/2304be5131221bae0dac24b9767cabffda33f747/data/nids.csv). + +[Version-pinned selected ELF export records](https://gist.github.com/kyon-eth/3fbb1a4a6f95a936b4cd58e9c3d8c50c/5e3af709224ed8235af11337309af0725e5685cb) list every NID below with its image label, decoded export module and library, raw qualified symbol, ELF function type, value and size for each inspected version-labelled image. The extract SHA-256 is `02ab27f12bcd0cc9fa03fa33f55fc201110f0d54a86bbbaa093d55d0a8cf262d`; its full-module sorted-function-NID digests and raw module-parameter words permit independent comparison. This is an analyst-produced metadata record, not independent authentication of the original images or proof of prototypes or behavior. + +| Name | NID | +| --- | --- | +| `sceCustomMusicAudiodecClearContext` | `WsJoyOz47OE` | +| `sceCustomMusicAudiodecCpuDecode` | `mz62R6Qjmf0` | +| `sceCustomMusicAudiodecCreateDecoder` | `QCuN-9Dd1bg` | +| `sceCustomMusicAudiodecDecode` | `qqFUG6MJKcU` | +| `sceCustomMusicAudiodecDeleteDecoder` | `4rOCTBP87dQ` | +| `sceCustomMusicFileClose` | `E0KbSONVDcU` | +| `sceCustomMusicFileLseek` | `go-ecEub9P0` | +| `sceCustomMusicFileOpenRead` | `V4A7LcYAdgA` | +| `sceCustomMusicFileRead` | `xrrLysMOFr4` | +| `sceCustomMusicKernelGettimeofday` | `GlUEBpsSb2E` | +| `sceCustomMusicKernelNanosleep` | `SsMe-3HXEHE` | +| `sceCustomMusicNetAccept` | `DELu3JawLrk` | +| `sceCustomMusicNetBind` | `EOO+r1P-9Nc` | +| `sceCustomMusicNetConnect` | `wvD8sZ7XghA` | +| `sceCustomMusicNetCtlInit` | `jORbkQLFfRM` | +| `sceCustomMusicNetCtlTerm` | `ZqJXAMKrqus` | +| `sceCustomMusicNetEpollAbort` | `Injn5VOe4Gg` | +| `sceCustomMusicNetEpollControl` | `H466Vy6VBLk` | +| `sceCustomMusicNetEpollCreate` | `5TsLV1cUWTc` | +| `sceCustomMusicNetEpollDestroy` | `XcF5UiPx-pc` | +| `sceCustomMusicNetEpollWait` | `FQ3hKAacgR8` | +| `sceCustomMusicNetErrnoLoc` | `E62C8Og0N5E` | +| `sceCustomMusicNetInetPton` | `DRqtzRNDa7s` | +| `sceCustomMusicNetListen` | `m2UfRq+Z1NU` | +| `sceCustomMusicNetPoolCreate` | `D62ZUAXrLTM` | +| `sceCustomMusicNetPoolDestroy` | `4cT5gPSCILs` | +| `sceCustomMusicNetRecv` | `Plv2m81jN84` | +| `sceCustomMusicNetResolverAbort` | `nvJMgVPsXUo` | +| `sceCustomMusicNetResolverCreate` | `lWQRcz0JroQ` | +| `sceCustomMusicNetResolverDestroy` | `NwCCxIIlm6I` | +| `sceCustomMusicNetSend` | `3+ODHrZgJRc` | +| `sceCustomMusicNetSetsockopt` | `2rJROSjbRZw` | +| `sceCustomMusicNetSocketClose` | `uAM3M24ikfU` | +| `sceCustomMusicNetSocket` | `bl5G0iE1jE4` | +| `sceCustomMusicPthreadAttrDestroy` | `UKFhulDRkuo` | +| `sceCustomMusicPthreadAttrInit` | `rKSYoakH9zg` | +| `sceCustomMusicPthreadCondDestroy` | `364AWhMvihw` | +| `sceCustomMusicPthreadCondInit` | `u0ACyDAN+gk` | +| `sceCustomMusicPthreadCondWait` | `34KbODwYX1s` | +| `sceCustomMusicPthreadCreate` | `BPtJo-zdOJI` | +| `sceCustomMusicPthreadDetach` | `iZNSjGzrgBc` | +| `sceCustomMusicPthreadExit` | `0cnReUY0C94` | +| `sceCustomMusicPthreadJoin` | `EtoqDoZoLFQ` | +| `sceCustomMusicPthreadMutexDestroy` | `gFxwjwKPvzc` | +| `sceCustomMusicPthreadMutexInit` | `7cqf8yfv218` | +| `sceCustomMusicPthreadMutexLock` | `HCNWPzFo2ak` | +| `sceCustomMusicPthreadSetprio` | `L7x6JPdSMtQ` | +| `sceCustomMusicPthreadYield` | `4LwHgkQnNSY` | +| `Unknown` | `+LdekyVYugM` | +| `Unknown` | `-f8PcMdF8L0` | +| `Unknown` | `0WBrHbI33Ec` | +| `Unknown` | `0d4Aw8u3xzE` | +| `Unknown` | `1auNQHxva3I` | +| `Unknown` | `24ZHuc5b0RA` | +| `Unknown` | `2yE7PehS6iI` | +| `Unknown` | `3DyCYcJudI4` | +| `Unknown` | `5I2IqwB0Hes` | +| `Unknown` | `5p7ZpbyUKuo` | +| `Unknown` | `6PNhaGAw6Ho` | +| `Unknown` | `70NNIr-nfFo` | +| `Unknown` | `7uOVo0lL97g` | +| `Unknown` | `8t8UK2XkJ1Y` | +| `Unknown` | `CQv51NIK3i4` | +| `Unknown` | `DABJz+GrAWg` | +| `Unknown` | `EaC0JWOBvy4` | +| `Unknown` | `HNx-bHhat18` | +| `Unknown` | `J4+14VzlTHM` | +| `Unknown` | `JSZXadyt-nk` | +| `Unknown` | `Kxq7i4lQjGg` | +| `Unknown` | `L0MmfM0N-y8` | +| `Unknown` | `Mk6mbbOvaBM` | +| `Unknown` | `NbAJfB19zUk` | +| `Unknown` | `QEVVJH35Fis` | +| `Unknown` | `ROnMWfJ56t8` | +| `Unknown` | `SneVIJUdrLw` | +| `Unknown` | `Tr-kghWgcoI` | +| `Unknown` | `VDAd516qud8` | +| `Unknown` | `VTS+zlRk-wo` | +| `Unknown` | `Xkm5tRxA0UQ` | +| `Unknown` | `Y3l3L6A4OYE` | +| `Unknown` | `ZiO9aSV1ZKc` | +| `Unknown` | `bzddXxOnr30` | +| `Unknown` | `eTHTXNt0B2k` | +| `Unknown` | `fsQhNY+RJf8` | +| `Unknown` | `gxew9cxe+xk` | +| `Unknown` | `hd0MG4ZvfRA` | +| `Unknown` | `jETJjJLQtas` | +| `Unknown` | `jVD2jFJAQ28` | +| `Unknown` | `kso0cI0kq1U` | +| `Unknown` | `lgRL6Oblmyg` | +| `Unknown` | `mgSEL99WSwA` | +| `Unknown` | `n0aVoomYuOM` | +| `Unknown` | `n4jZ2HjzXUo` | +| `Unknown` | `oaaW7CuQLV0` | +| `Unknown` | `qqNVevi1LlY` | +| `Unknown` | `sWVPeXELNss` | +| `Unknown` | `sak+IpFmBkk` | +| `Unknown` | `y2Fu+vFwEXo` | +| `Unknown` | `yM-etuyf-2M` | +| `Unknown` | `zRDEHOFDvzQ` | +| `Unknown` | `zUgaEYLIysQ` | + ### Unknown function info -- [ and sceCustomMusicSelect](../../../core/libs/prx/libSceCustomMusicSysCallWrapper/Export.cpp) (libSceCustomMusicSysCallWrapper) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCustomMusicSysCallWrapper.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified +- [sceCustomMusicSelect](../../../core/libs/prx/libSceCustomMusicSysCallWrapper/Export.cpp) (libSceCustomMusicSysCallWrapper) - the name and NID are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCustomMusicSysCallWrapper.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified + +- [sceCustomMusicNetInetNtop](../../../core/libs/prx/libSceCustomMusicSysCallWrapper/Export.cpp) - signature and behavior are unknown; the parameterless int declaration is an import-resolution placeholder and throws. +- [sceCustomMusicNetShutdown](../../../core/libs/prx/libSceCustomMusicSysCallWrapper/Export.cpp) - signature and behavior are unknown; the parameterless int declaration is an import-resolution placeholder and throws. +- [sceCustomMusicNetSocketAbort](../../../core/libs/prx/libSceCustomMusicSysCallWrapper/Export.cpp) - signature and behavior are unknown; the parameterless int declaration is an import-resolution placeholder and throws. +- [sceCustomMusicPthreadAttrGet](../../../core/libs/prx/libSceCustomMusicSysCallWrapper/Export.cpp) - signature and behavior are unknown; the parameterless int declaration is an import-resolution placeholder and throws. +- [sceCustomMusicPthreadSelf](../../../core/libs/prx/libSceCustomMusicSysCallWrapper/Export.cpp) - signature and behavior are unknown; the parameterless int declaration is an import-resolution placeholder and throws. diff --git a/docs/dev/debt/libSceEsvm.md b/docs/dev/debt/libSceEsvm.md index ff5986e112..ed4ba70116 100644 --- a/docs/dev/debt/libSceEsvm.md +++ b/docs/dev/debt/libSceEsvm.md @@ -53,3 +53,16 @@ | `sceEsvmVideoOutSetContentType` | `oUGLWJNfVwM` | - [sceEsvmAppAudioSyncCreateContext, sceEsvmAppAudioSyncFlush and sceEsvmSyncVideo](../../../core/libs/prx/libSceEsvm/Export.cpp) (libSceEsvm) - signatures and behavior are unverified; these parameterless declarations only resolve imports and throw through `NotImplemented_nid_no_patch`. + +### Additional callable exports + +- [Additional callable exports](../../../core/libs/prx/libSceEsvm/Export.cpp) - 4 entries present in both version-labelled images are import-resolution declarations only. None is listed in the pinned public 3.20 catalogue for this library. Neither label independently authenticates retail firmware provenance. Parameter lists, return types and behavior are unverified; parameterless `int` declarations are placeholders, not verified ABI contracts. Every entry throws through `NotImplemented_nid_no_patch`. Unidentified names use literal NID aliases. No runtime functionality is implemented. Identified candidate names match their NIDs in the [public symbol database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv) or [supplemental catalog](https://github.com/claimore22/ps5rs/blob/2304be5131221bae0dac24b9767cabffda33f747/data/nids.csv). + +[Version-pinned selected ELF export records](https://gist.github.com/kyon-eth/3fbb1a4a6f95a936b4cd58e9c3d8c50c/5e3af709224ed8235af11337309af0725e5685cb) list every NID below with its image label, decoded export module and library, raw qualified symbol, ELF function type, value and size for each inspected version-labelled image. The extract SHA-256 is `02ab27f12bcd0cc9fa03fa33f55fc201110f0d54a86bbbaa093d55d0a8cf262d`; its full-module sorted-function-NID digests and raw module-parameter words permit independent comparison. This is an analyst-produced metadata record, not independent authentication of the original images or proof of prototypes or behavior. + +| Name | NID | +| --- | --- | +| `sceEsvmInitializePlayReady` | `9FyuJdx5q9s` | +| `sceEsvmInitializeWidevine` | `9+Iejj-syHc` | +| `sceEsvmRequestMediaKeySystemAccessPlayReady` | `UOTAc1GJCLI` | +| `sceEsvmRequestMediaKeySystemAccessWidevine` | `bYyMqndqumU` | diff --git a/docs/dev/debt/libSceFios2.md b/docs/dev/debt/libSceFios2.md index 7b43eb0959..f52986776b 100644 --- a/docs/dev/debt/libSceFios2.md +++ b/docs/dev/debt/libSceFios2.md @@ -1,7 +1,23 @@ # libSceFios2 technical debt +### Functional + +- [libSceFios2 prototypes](../../../core/libs/prx/libSceFios2/include/Api.hpp) (libSceFios2) - the implemented exports take the prototypes of RPCS3's Vita FIOS2 module (`Emu/ARMv7/Modules/sceFios.h` and `sceFios.cpp`, jacquesjk/RPCS-DX12 at `9b8b7c9eea446095f24af19182032013837e6ca5`, 64-bit types substituted), checked against the PS4 headers and call sites of kalaposfos13/fios2 (`src/fios2.h`, `ae6ca1d78e7a822c2528f7660959167b9b3d8b63`), sonny-tel/libSceFios2 (`sceFios2.h`, `5d44f9e7a80ae00557af80fad335e4c74ef35d22`), Inori/GPCS4 (`SceFios2/sce_fios2.cpp`) and SonicMastr/SceLibcPosix (`src/internal/fios2.h`, Vita, `c2cd5e880a8770bdbfb58de761de11f396c1a2e7`). +- [Layouts](../../../core/libs/prx/libSceFios2/include/Api.hpp) (libSceFios2) - The `SceFiosOpAttr`, `SceFiosOpenParams`, `SceFiosStat` and `SceFiosDirEntry` layouts and the `SCE_FIOS_ERROR_*` values (`0x80820000` plus the enumeration order) come from the same sources; vascon-es, a PC port of a PS4 title (CUSA01176 and CUSA00935 version 01.24), at `57c3c202975ea48e574df2d1e4a620b4e18b7701`, confirms the `SceFiosStat` and `SceFiosDirEntry` offsets and the `fileSize`, `statFlags` bit 0 (directory) and `offsetToName` reads from the call sites of a PS4 title. None of it was measured on a PS5. The `SceFiosOpenParams` buffer sits at offset 8 in kalaposfos13 and sonny-tel and at offset 0x10 in vascon-es; only `openFlags` is read +- [Operations](../../../core/libs/prx/libSceFios2/src/Runtime.cpp) (libSceFios2) - there are no I/O or decompressor threads. An operation runs on the calling thread and is complete when the export returns, and its callback fires there with `SCE_FIOS_OPEVENT_COMPLETE`; while `sceFiosSuspend` is in effect new operations queue and run on the thread whose `sceFiosResume` ends the suspension. `deadline`, `priority`, `opflags`, `userTag`, `userPtr` and the scheduler fields are stored and returned but have no effect, and `SCE_FIOS_OPEVENT_START` is never sent. The `*Sync` exports are the asynchronous export followed by `sceFiosOpSyncWait`, so they also fire the completion and `SCE_FIOS_OPEVENT_DELETE` callbacks. +- [Completion values and parameters](../../../core/libs/prx/libSceFios2/src/Runtime.cpp) (libSceFios2) - `sceFiosOpSyncWait` returns the error or the low 32 bits of the result (1 or 0 for the existence queries, as in kalaposfos13/fios2), `sceFiosOpGetActualCount` returns the result (the size for `sceFiosFileGetSize`) also for a failed operation, and `sceFiosOpGetError` returns 1 while an operation is queued; none of these is measured. Handles of operations, files and directories share one number space that starts at 1. Nothing requires `sceFiosInitialize` first, and `SceFiosParams` is stored as `paramsSize` bytes (the 15 bits at bit 1 of the first word) without interpretation, so the storage buffers, `pathMax` (1024 is used), thread and scheduler settings are ignored +- [sceFiosExists, sceFiosFileExists, sceFiosDirectoryExists, sceFiosFileGetSize, sceFiosStat and their Sync forms](../../../core/libs/prx/libSceFios2/src/PathQueries.cpp) (libSceFios2) - paths go to `sceKernelStat`, so archive and alias mapping is the kernel's. A missing path is not an error for the existence queries. `SceFiosStat` carries only the sizes, the nanosecond dates as seconds since 1970 times 10^9 plus nanoseconds (the epoch and unit are not measured), `statFlags` bit 0 for a directory, and the kernel `st_uid`, `st_gid`, `st_dev`, `st_ino` and `st_mode`; other `statFlags` bits are 0. `sceFiosFileExists` and `sceFiosDirectoryExists` take the `pOutExists` argument of RPCS3's prototype, which kalaposfos13/fios2 omits, and `sceFiosFileGetSize` has no output pointer as in kalaposfos13/fios2 and sonny-tel (RPCS3 declares one) +- [sceFiosDelete, sceFiosFileDelete, sceFiosDirectoryCreate, sceFiosDirectoryDelete, sceFiosRename, sceFiosFileRead, sceFiosFileWrite, sceFiosFileTruncate and their Sync forms](../../../core/libs/prx/libSceFios2/src/PathOperations.cpp) (libSceFios2) - behaviour is inferred from the export names; none of it is measured. They run on `sceKernelStat`, `sceKernelOpen`, `sceKernelPread`, `sceKernelPwrite`, `sceKernelFtruncate`, `sceKernelMkdir`, `sceKernelRmdir`, `sceKernelUnlink` and `sceKernelRename`, so guest path mapping and the read-only application directory are the kernel's. `sceFiosFileWrite` opens the file write-only with creation and no truncation, `sceFiosFileTruncate` does not create it, directories are created with mode 0777 and files with 0666, `sceFiosDelete` removes a file or an empty directory, and a negative length returns `SCE_FIOS_ERROR_BAD_SIZE`, a negative offset `SCE_FIOS_ERROR_BAD_OFFSET`. +- [Error mapping](../../../core/libs/prx/libSceFios2/src/PathOperations.cpp) (libSceFios2) - A read stops at the end of the file and returns the bytes read. The mapping of kernel errors to `SCE_FIOS_ERROR_*` is chosen here: `ENOENT` and `ENOTDIR` to `BAD_PATH`, `EPERM`, `EACCES` and `ENOTEMPTY` to `ACCESS`, `EROFS` to `READ_ONLY`, `EEXIST` to `ALREADY_EXISTS`, `EISDIR` to `NOT_A_FILE`, `EINVAL` to `BAD_OFFSET`, `ENFILE` and `EMFILE` to `CANT_ALLOCATE_FH`, `ENAMETOOLONG` to `PATH_TOO_LONG`, `EFAULT` to `BAD_PTR`, and anything else to `UNKNOWN`. The prototypes follow RPCS3 (`sceFios.cpp` `sceFiosFileWrite`, `sceFiosFileTruncate`, `sceFiosRename`, `sceFiosDelete`) and kalaposfos13/fios2 (`sceFiosFileRead`) +- [sceFiosFHOpen, sceFiosFHOpenWithMode, sceFiosFHClose, sceFiosFHStat, sceFiosFHTruncate, sceFiosFHSync, sceFiosFHRead, sceFiosFHWrite, sceFiosFHPread, sceFiosFHPwrite, the vectored forms with their Sync forms, sceFiosFHSeek, sceFiosFHTell, sceFiosFHGetSize, sceFiosFHGetPath and sceFiosFHGetOpenParams](../../../core/libs/prx/libSceFios2/src/FileHandles.cpp) (libSceFios2) - a file handle owns one kernel descriptor, and `sceFiosFHRead`, `sceFiosFHWrite`, `sceFiosFHSeek` and `sceFiosFHTell` use its kernel position. +- [File handle errors and lifetime](../../../core/libs/prx/libSceFios2/src/FileHandles.cpp) (libSceFios2) - `openFlags` map as `SCE_FIOS_O_READ` 1, `SCE_FIOS_O_WRITE` 2, `SCE_FIOS_O_APPEND` 4, `SCE_FIOS_O_CREAT` 8 and `SCE_FIOS_O_TRUNC` 16 (sonny-tel/libSceFios2, SonicMastr/SceLibcPosix); `SCE_FIOS_O_DIRECT` (1 << 12) is ignored, no flags or null parameters open read-only, and the handle returns `SCE_FIOS_ERROR_WRITE_ONLY` or `SCE_FIOS_ERROR_READ_ONLY` for a transfer its flags exclude (the console result is not measured). The creation mode is 0777 unless `sceFiosFHOpenWithMode` is given one (`-1` means the default, as in kalaposfos13/fios2). +- [Open flags and offsets](../../../core/libs/prx/libSceFios2/src/FileHandles.cpp) (libSceFios2) - Opening a directory returns `SCE_FIOS_ERROR_NOT_A_FILE`. An invalid `whence` or a negative resulting position returns `SCE_FIOS_ERROR_BAD_OFFSET`, a vector count outside 1 to 1024 `SCE_FIOS_ERROR_BAD_IOVCNT`. The Readv and Writev exports transfer the vectors in order and stop at the first short one. `sceFiosFHGetPath` and `sceFiosFHGetOpenParams` return pointers that stay valid until the handle is closed; the stored `SceFiosOpenParams` has no buffer. The handle prototypes follow RPCS3's `sceFios.cpp` and kalaposfos13/fios2; `sceFiosFHToFileno` and `sceFiosFilenoToFH` have no source and keep throwing +- [sceFiosDHOpen, sceFiosDHRead, sceFiosDHClose, their Sync forms and sceFiosDHGetPath](../../../core/libs/prx/libSceFios2/src/DirectoryHandles.cpp) (libSceFios2) - the prototypes follow RPCS3 and kalaposfos13/fios2, with the `SceFiosBuffer` passed by value in two registers as in vascon-es. Opening reads the whole listing through `sceKernelGetdirentries` into host memory; the caller's buffer is never written. A title that opens with a null or too small buffer gets `SCE_FIOS_ERROR_BAD_SIZE` and the required size from `sceFiosOpGetActualCount`, the probe that vascon-es observes at the call sites of a PS4 title. +- [Required directory buffer size](../../../core/libs/prx/libSceFios2/src/DirectoryHandles.cpp) (libSceFios2) - The required size, `16` plus the name lengths plus one each, is chosen here and is not what the console reports, so a title that sizes its buffer from another source can get a false `BAD_SIZE`. `.` and `..` are not listed (unverified), entries come in kernel order, the end of the listing returns `SCE_FIOS_ERROR_EOF` and `sceFiosDHRead` fills `fileSize`, `statFlags` bit 0, `nameLength`, `fullPathLength`, `offsetToName` and `fullPath` as vascon-es reads them, with `fullPath` the opened path, a slash and the name +- [sceFiosDateGetCurrent, sceFiosDateFromComponents, sceFiosDateToComponents, sceFiosTimeIntervalFromNanoseconds and sceFiosTimeIntervalToNanoseconds](../../../core/libs/prx/libSceFios2/src/Time.cpp) (libSceFios2) - a `SceFiosDate` is taken as nanoseconds since 1970-01-01 UTC and a `SceFiosTimeInterval` as nanoseconds (identity conversions), as in kalaposfos13/fios2 (`src/fios2.cpp`, `time(nullptr) * 1000000000`) and RPCS3 (`sceFios.cpp`, 64-bit unsigned date); neither is measured, and both may descend from one Vita SDK. kalaposfos13/fios2 converts components through local time, here they are UTC (`sceFiosDateFromComponents` normalizes out-of-range fields like `timegm` and returns 0 before 1970). Only the nine leading `int` fields of `struct tm` are read and written, so `tm_gmtoff` and `tm_zone` are left untouched. `sceFiosTimeGetCurrent` has no source for its clock and keeps throwing +- [Operation threading](../../../core/libs/prx/libSceFios2/src/Runtime.cpp) (libSceFios2) - an operation and its `SCE_FIOS_OPEVENT_COMPLETE` callback run inside the call that issues the operation, whereas on the console the callback runs on a FIOS I/O thread. A title that issues an operation while holding a lock its own callback takes deadlocks on its own thread, and a title that expects the output pointers (`pOutFH`, `pOutExists`, `pOutStatus`) to be written only after `sceFiosOpWait` is not tested. A worker thread was not added: guest callbacks need a guest thread (`scePthreadCreate`, as libSceJobManager uses) with its TLS and stack, the `*Sync` forms must stay synchronous on the caller, and moving completion to another thread changes every output-pointer, callback and teardown ordering in the library and its tests, which could not be validated here against a title or a console +- [Guest inputs](../../../core/libs/prx/libSceFios2/src/Runtime.cpp) (libSceFios2) - `SceFiosOpAttr`, `SceFiosOpenParams`, `SceFiosParams`, the vector arrays and `struct tm` are copied with `GuestCopyIn_nid_no_patch`; a failed copy returns `SCE_FIOS_ERROR_BAD_PTR` (the operation handle value itself for an unreadable `SceFiosOpAttr`), or is ignored by the `void` exports. That the console returns `SCE_FIOS_ERROR_BAD_PTR` for an unreadable pointer is not measured + ### Unknown function info -- [sceFiosArchiveGetDecompressorThreadCount, sceFiosArchiveGetMountBufferSize, sceFiosArchiveGetMountBufferSizeSync, sceFiosArchiveMount, sceFiosArchiveMountSync, sceFiosArchiveMountWithOrder, sceFiosArchiveMountWithOrderSync, sceFiosArchiveSetDecompressorThreadCount, sceFiosArchiveUnmount, sceFiosArchiveUnmountSync, sceFiosCacheContainsFileRangeSync, sceFiosCacheContainsFileSync, sceFiosCacheFlushFileRangeSync, sceFiosCacheFlushFileSync, sceFiosCacheFlushSync, sceFiosCachePrefetchFH, sceFiosCachePrefetchFHRange, sceFiosCachePrefetchFHRangeSync, sceFiosCachePrefetchFHSync, sceFiosCachePrefetchFile, sceFiosCachePrefetchFileRange, sceFiosCachePrefetchFileRangeSync, sceFiosCachePrefetchFileSync, sceFiosCancelAllOps, sceFiosClearTimeStamps, sceFiosCloseAllFiles, sceFiosDHClose, sceFiosDHCloseSync, sceFiosDHGetPath, sceFiosDHOpen, sceFiosDHOpenSync, sceFiosDHRead, sceFiosDHReadSync, sceFiosDLLInitialize, sceFiosDLLTerminate, sceFiosDeallocatePassthruFH, sceFiosDebugDumpDH, sceFiosDebugDumpDate, sceFiosDebugDumpError, sceFiosDebugDumpFH, sceFiosDebugDumpOp, sceFiosDelete, sceFiosDeleteSync, sceFiosDirectoryCreate, sceFiosDirectoryCreateSync, sceFiosDirectoryCreateWithMode, sceFiosDirectoryCreateWithModeSync, sceFiosDirectoryDelete, sceFiosDirectoryDeleteSync, sceFiosDirectoryExists, sceFiosDirectoryExistsSync, sceFiosExists, sceFiosExistsSync, sceFiosFHClose, sceFiosFHCloseSync, sceFiosFHGetOpenParams, sceFiosFHGetPath, sceFiosFHGetSize, sceFiosFHOpen, sceFiosFHOpenSync, sceFiosFHOpenWithMode, sceFiosFHOpenWithModeSync, sceFiosFHPread, sceFiosFHPreadSync, sceFiosFHPreadv, sceFiosFHPreadvSync, sceFiosFHPwrite, sceFiosFHPwriteSync, sceFiosFHPwritev, sceFiosFHPwritevSync, sceFiosFHRead, sceFiosFHReadSync, sceFiosFHReadv, sceFiosFHReadvSync, sceFiosFHSeek, sceFiosFHStat, sceFiosFHStatSync, sceFiosFHSync, sceFiosFHSyncSync, sceFiosFHTell, sceFiosFHToFileno, sceFiosFHTruncate, sceFiosFHTruncateSync, sceFiosFHWrite, sceFiosFHWriteSync, sceFiosFHWritev, sceFiosFHWritevSync, sceFiosFileDelete, sceFiosFileDeleteSync, sceFiosFileExists, sceFiosFileExistsSync, sceFiosFileGetSize, sceFiosFileGetSizeSync, sceFiosFileRead, sceFiosFileReadSync, sceFiosFileTruncate, sceFiosFileTruncateSync, sceFiosFileWrite, sceFiosFileWriteSync, sceFiosFilenoToFH, sceFiosGetAllDHs, sceFiosGetAllFHs, sceFiosGetAllOps, sceFiosGetDefaultOpAttr, sceFiosGetGlobalDefaultOpAttr, sceFiosGetSuspendCount, sceFiosGetThreadDefaultOpAttr, sceFiosIOFilterAdd, sceFiosIOFilterCache, sceFiosIOFilterGetInfo, sceFiosIOFilterPsarcDearchiver, sceFiosIOFilterRemove, sceFiosInitialize, sceFiosIsIdle, sceFiosIsInitialized, sceFiosIsSuspended, sceFiosOpCancel, sceFiosOpDelete, sceFiosOpGetActualCount, sceFiosOpGetAttr, sceFiosOpGetBuffer, sceFiosOpGetError, sceFiosOpGetOffset, sceFiosOpGetPath, sceFiosOpGetRequestCount, sceFiosOpIsCancelled, sceFiosOpIsDone, sceFiosOpReschedule, sceFiosOpRescheduleWithPriority, sceFiosOpSetBuffer, sceFiosOpSyncWait, sceFiosOpSyncWaitForIO, sceFiosOpWait, sceFiosOpWaitUntil, sceFiosOverlayAdd, sceFiosOverlayGetInfo, sceFiosOverlayGetList, sceFiosOverlayModify, sceFiosOverlayRemove, sceFiosOverlayResolveSync, sceFiosPathNormalize, sceFiosPathcmp, sceFiosPathncmp, sceFiosPrintTimeStamps, sceFiosPrintf, sceFiosRename, sceFiosRenameSync, sceFiosResolve, sceFiosResolveSync, sceFiosResume, sceFiosSaveTimeStamp, sceFiosSetGlobalDefaultOpAttr, sceFiosSetThreadDefaultOpAttr, sceFiosShutdownAndCancelOps, sceFiosStat, sceFiosStatSync, sceFiosSuspend, sceFiosTerminate, sceFiosTimeGetCurrent, sceFiosTimeIntervalFromNanoseconds, sceFiosTimeIntervalToNanoseconds, sceFiosTraceTimestamp, sceFiosUpdateParameters and sceFiosVprintf](../../../core/libs/prx/libSceFios2/Export.cpp) (libSceFios2) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceFios2.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified - -- [sceFiosDateFromComponents, sceFiosDateGetCurrent, sceFiosDateToComponents and sceFiosIsValidHandle](../../../core/libs/prx/libSceFios2/Export.cpp) (libSceFios2) - the date packing and the handle validity rule are unverified and the declarations in Export.cpp still carry no parameters. kalaposfos13/fios2 (`src/fios2.h`, PS4) and RPCS3 (`Emu/PSP2/Modules/sceFios.cpp`) independently declare `sceFiosDateFromComponents(tm*)` and `sceFiosDateGetCurrent()` returning a 64-bit date, `sceFiosDateToComponents(int64_t, tm*)` returning the `tm` pointer and `sceFiosIsValidHandle(int)` returning a bool, but both may descend from one leaked Vita SDK and kalaposfos13's bodies are guesses (`mktime` times 1e9). sonny-tel/libSceFios2 (a header read from a Fortnite build) also declares `sceFiosIsValidHandle(int)` and gives `SceFiosParams`, `SceFiosOpAttr`, `SceFiosOpenParams` and `SceFiosStat` layouts, unverified against firmware +- [sceFiosArchiveGetDecompressorThreadCount, sceFiosArchiveGetMountBufferSize, sceFiosArchiveGetMountBufferSizeSync, sceFiosArchiveMount, sceFiosArchiveMountSync, sceFiosArchiveMountWithOrder, sceFiosArchiveMountWithOrderSync, sceFiosArchiveSetDecompressorThreadCount, sceFiosArchiveUnmount, sceFiosArchiveUnmountSync, sceFiosCacheContainsFileRangeSync, sceFiosCacheContainsFileSync, sceFiosCacheFlushFileRangeSync, sceFiosCacheFlushFileSync, sceFiosCacheFlushSync, sceFiosCachePrefetchFH, sceFiosCachePrefetchFHRange, sceFiosCachePrefetchFHRangeSync, sceFiosCachePrefetchFHSync, sceFiosCachePrefetchFile, sceFiosCachePrefetchFileRange, sceFiosCachePrefetchFileRangeSync, sceFiosCachePrefetchFileSync, sceFiosClearTimeStamps, sceFiosDLLInitialize, sceFiosDLLTerminate, sceFiosDeallocatePassthruFH, sceFiosDebugDumpDH, sceFiosDebugDumpDate, sceFiosDebugDumpError, sceFiosDebugDumpFH, sceFiosDebugDumpOp, sceFiosDirectoryCreateWithMode, sceFiosDirectoryCreateWithModeSync, sceFiosFHToFileno, sceFiosFilenoToFH, sceFiosIOFilterAdd, sceFiosIOFilterCache, sceFiosIOFilterGetInfo, sceFiosIOFilterPsarcDearchiver, sceFiosIOFilterRemove, sceFiosOpReschedule, sceFiosOpRescheduleWithPriority, sceFiosOpSetBuffer, sceFiosOpWaitUntil, sceFiosOverlayAdd, sceFiosOverlayGetInfo, sceFiosOverlayGetList, sceFiosOverlayModify, sceFiosOverlayRemove, sceFiosOverlayResolveSync, sceFiosPathNormalize, sceFiosPathcmp, sceFiosPathncmp, sceFiosPrintTimeStamps, sceFiosPrintf, sceFiosResolve, sceFiosResolveSync, sceFiosSaveTimeStamp, sceFiosTimeGetCurrent, sceFiosTraceTimestamp and sceFiosVprintf](../../../core/libs/prx/libSceFios2/Export.cpp) (libSceFios2) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceFios2.c), but no credible prototypes were found. The declarations have no 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/libSceFont.md b/docs/dev/debt/libSceFont.md index e06d138e19..4fb91133ae 100644 --- a/docs/dev/debt/libSceFont.md +++ b/docs/dev/debt/libSceFont.md @@ -1,5 +1,9 @@ # libSceFont technical debt +### Silent stubs + +- [sceFontSetFontsOpenMode](../../../core/libs/prx/libSceFont/src/Library.cpp) (libSceFont) - stores nothing: the mode does not change how fonts are opened. It rejects a null or non-library handle and a mode above 2, as the [shadPS4 port](https://github.com/shadps4-emu/shadPS4/blob/b6f0c58dba9ee75b93477703032ae4f1e0de8f1c/src/core/libraries/font/font.cpp#L5796-L5821) does; what each mode does is not measured. Closes when: measured on a PS5 + ### Unknown function info - [Render surface rows](../../../core/libs/prx/libSceFont/src/Layout.cpp): negative row strides return `SCE_FONT_ERROR_NO_SUPPORT_SURFACE` instead of writing before the buffer as the firmware 13.42 modules did in [BunnyFl's native x86-64 harness](https://github.com/boykopovar/AnyPS5/pull/8250#issuecomment-6417381846), not on console hardware. Nonnegative strides address each row at `row * stride`, including overlapping rows and stride 0; `(height - 1) * stride + width * pixelSize` bounds the addressable range of a nonempty surface, but the API supplies no allocation length to verify the caller's storage. @@ -31,10 +35,12 @@ The 40 additional exports below have unknown signatures and behavior. Their para - [sceFontGetFontGlyphsCount, sceFontGetCharGlyphCode](../../../core/libs/prx/libSceFont/src/Open.cpp): signatures, error precedence, failed-output clearing and external-font glyph-code encoding are confirmed by the PS5 library. Multi-face system-font glyph counts, face-selection bits and typographic glyph substitutions are not modeled. - [sceFontGetFontResolution](../../../core/libs/prx/libSceFont/src/Open.cpp): the 32-bit resolution output, float scale and nullable outputs are confirmed by the PS5 library and FontFt driver. Native system font sets return resolution 1000; the host still uses its selected face resolution because multi-face system font sets are not modeled. - [Writing-line ABI and allocation](../../../core/libs/prx/libSceFont/src/WritingLine.cpp): PS5 layouts, creation forms, memory validation, allocator callbacks and error handling are recovered. The 2208-byte line and 64-byte orders use caller memory. Host registry bookkeeping still allocates separately; memory lifetime and concurrent destruction require caller synchronization. +- [sceFontWritingGetRenderMetrics, sceFontWritingSetMaskInvisible](../../../core/libs/prx/libSceFont/src/Text.cpp) (libSceFont) - not measured on a console, and what masking format characters changes when a writing is rendered is unknown: the metrics zeroed for a null writing, the accepted masks 0 and 1 and `SCE_FONT_ERROR_INVALID_PARAMETER` for other masks follow the shadPS4 port ([metrics](https://github.com/shadps4-emu/shadPS4/blob/b6f0c58dba9ee75b93477703032ae4f1e0de8f1c/src/core/libraries/font/font.cpp#L6943-L6973), [mask](https://github.com/shadps4-emu/shadPS4/blob/b6f0c58dba9ee75b93477703032ae4f1e0de8f1c/src/core/libraries/font/font.cpp#L7157-L7182)). Closes when: measured on a PS5 - [sceFontWritingRefersRenderStepCharacter](../../../core/libs/prx/libSceFont/src/Text.cpp): PS5 traversal and multi-character shaping remain unverified; the host has one character and glyph per step. PS4 11.00 measurements establish an 8-byte pointer output into the writing, null for a null writing, and traversal to null after the current step is exhausted. The pointer ABI agrees with [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/d2413fc2ebd91d1b7234197c2dd5bd48e1cb6a51/src/libs/libFont.cpp#L1882-L1896) and SharpProspero; shadPS4 and prosper instead treat the output as a struct. The internal letter step remains valid until writing destruction and is rebuilt when a character is returned. - [sceFontWritingLineGetOrderingSpace](../../../core/libs/prx/libSceFont/src/WritingLine.cpp): PS5 confirms output order, calculations and zeroed outputs on invalid handles. A non-null head output with a null tail output causes an unchecked native write; the host throws instead. With no head output, the tail output remains untouched. - [sceFontCharacterGetSyllableStringState](../../../core/libs/prx/libSceFont/src/Text.cpp) (libSceFont) - the signature `(textCharacter, int* syllableStringState)` follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFont.Engine.cs). Measured on PS4 firmware 11.00 `libSceFont.sprx` (not on a PS5): the state is 0 for Latin, CJK, Hangul, Arabic, Hebrew, Devanagari and the other scripts tried, and the library sets 1, 3 or 4 (never 2 on real text) for Thai while it creates the string; a null character or a null result returns `SCE_FONT_ERROR_INVALID_PARAMETER`, and a null character writes 0 to a non-null result. This returns 0 for every character outside the Thai block (U+0E00 to U+0E7F), as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/d2413fc2ebd91d1b7234197c2dd5bd48e1cb6a51/src/libs/libFont.cpp#L1805-L1815) does, and throws for Thai characters, whose cluster states are not modelled. SharpProspero's `SceFontSyllableString` names 0 NonDetection, 1 Starting, 2 Following, 3 Depending and 4 Connecting. PPSA06699 (Hot Shots Golf 2) calls it. - [sceFontCharactersRefersTextCodes, sceFontTextCodesStepNext and sceFontTextCodesStepBack](../../../core/libs/prx/libSceFont/src/Text.cpp) (libSceFont) - the signatures follow the [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json#L679) IDA type database and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Font/SceFont.Engine.cs#L395-L406), and the 64-byte `FontTextCodes` with the order pointer at 0 and the code at 8 follows SharpProspero's [SceFontTextCodes](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Font/SceFontStructs.cs#L216-L228). The traversal follows the PS4 11.00 `libSceFont.sprx` disassembly posted in [#4050](https://github.com/boykopovar/AnyPS5/pull/4050#issuecomment-6114504202), not a PS5 measurement: the cursor keeps the current, start and term characters at +0x28, +0x30 and +0x38; `Refers` never compares the character with the term and returns null for a clustered or synthetic character; `StepNext` returns null at the term after visiting it; `StepBack` returns null when the current character is the start; both steps skip synthetic characters. Assumed and not covered by the report, which says `StepNext` returns null only at the term: `StepNext` also returns null at the end of the chain (for example with a null term), and `StepBack` at the head of the chain. A null cursor or output returns null as in [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/d2413fc2ebd91d1b7234197c2dd5bd48e1cb6a51/src/libs/libFont.cpp#L1696-L1740); a null text character throws, as its console result is unknown +- [sceFontCharacterGetTextFontCode, sceFontCharacterGetTextOrder](../../../core/libs/prx/libSceFont/src/Text.cpp) (libSceFont) - not measured on a console: the null-argument errors, the outputs cleared for a null character and the returned font, code and order follow the [shadPS4 port](https://github.com/shadps4-emu/shadPS4/blob/b6f0c58dba9ee75b93477703032ae4f1e0de8f1c/src/core/libraries/font/font.cpp#L793-L828). Closes when: measured on a PS5 - [sceFontGraphicsDrawupFillTextureImageObject, sceFontGraphicsDrawupFillTexturePatternObject, sceFontGraphicsDrawupFillFlatColor, sceFontGraphicsDrawupFillLinearGradient, sceFontGraphicsDrawupFillRadialGradient](../../../core/libs/prx/libSceFont/src/Graphics.cpp) (libSceFont) - disassembled from PS5 4.03 `libSceFont.sprx` (SHA-256 `3c67846f50ed3bf844efdc274f83c7f0043581b607154f3946499b72a3bfa086`): bodies `0x1f940` (image), `0x20f50` (pattern), `0x20cb0` (flat), gradient producers `0x1fc10`/`0x20080`, allocator `0x1bcc0`, pool init `0x1bc40` (sole `scan` write, `=nodes`), texture callers in PPSA20397 `libSceFontGsm.prx`; not console-measured. - Disassembled: magic `0xf53`, errors `0x80460084`/`0x804600a9`/`0x80460002`/`0x80460001`/`0x8046005c`/`0x804600a2`, error paths storing at frame `+4` and invoking the registered callback (sealed included), pool/bitmap/queue mechanics, record layouts and tables, `mode` as `memcpy` size, `dimension-1` bytes, `option1` flags `{0x1000, 0x3000, 0x4000, 0x6000, 0x2000}` (jump table `0x37fcc`), allocator marking slots `start..start+count` but returning `memory + completingNodeIndex*slotSize`, the fills' packed-color and float fields. @@ -46,6 +52,7 @@ The 40 additional exports below have unknown signatures and behavior. Their para - [sceFontDefineAttribute, sceFontGetAttribute](../../../core/libs/prx/libSceFont/src/Style.cpp), [sceFontGlyphDefineAttribute, sceFontGlyphGetAttribute](../../../core/libs/prx/libSceFont/src/Render.cpp) (libSceFont) - the signatures `(handle, int attribute, int* oldAttribute)` and `(handle, int attribute, int* nowAttribute)`, and the same shape for a glyph, follow the [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFont.Engine.cs) bindings, matching the IDA type database of [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/d2413fc2ebd91d1b7234197c2dd5bd48e1cb6a51/src/libs/libFont.cpp#L1249-L1264), not confirmed on a title. Every handle keeps one attribute, an opaque int whose meaning and valid values are unknown: define stores the new value and returns the previous one, and get returns the stored value with its `attribute` argument unused, as in KytyPS5. An attribute never set is 0. A glyph keeps one bit instead: its define accepts only 0x10 and 0x11, returning `SCE_FONT_ERROR_INVALID_PARAMETER` for any other value, stores the value and returns the previous 0x10 or 0x11, which is 0x10 when never set; its get returns 0x10 or 0x11 and ignores its `attribute` argument. The null checks and the invalid-object errors follow the other libSceFont functions and diverge from [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/d2413fc2ebd91d1b7234197c2dd5bd48e1cb6a51/src/libs/libFont.cpp#L1251-L1265), which returns `OK` for a null handle or glyph after writing 0 to the old-attribute pointer: every other handle and glyph entry point in this library reports the invalid object instead, and a title that passes one is already failing. shadPS4 leaves both exports unimplemented, so there is no second implementation to compare. PPSA01325 calls the glyph define with 0x11 on a glyph from `sceFontGenerateCharGlyph` that it deletes unused and draws with `sceFontRenderCharGlyphImageHorizontal`; the three bindings all read the third argument as an out pointer, not a value, so this call sets the glyph's attribute to 0x11. The attribute changes neither shaping nor rendering. - [sceFontGlyphRenderImage, sceFontGlyphRenderImageHorizontal, sceFontGlyphRenderImageVertical](../../../core/libs/prx/libSceFont/src/Render.cpp) (libSceFont) - follow firmware 13.42's module as BunnyFl ran it natively ([comment](https://github.com/boykopovar/AnyPS5/pull/10892#issuecomment-6415399922); one font, 'A', one pixel format, not a console): the glyph's character is drawn as `sceFontRenderCharGlyphImage` and its directional forms draw it, at the glyph's generation scale; the surface's style frame wins over the argument, and a frame that sets no scale keeps that scale. A valid `sceFontGlyphRenderImageVertical` call, and `sceFontGlyphRenderImage` on a vertical font, throw: the module reports advance and stride 40/40 there where the char call gives 90/90 at 40 px, and one case doesn't name the rule. A pointer that isn't a renderer, and `SCE_FONT_ERROR_INVALID_FONT_HANDLE` for a closed owner font, are not measured - [sceFontRebindRenderer](../../../core/libs/prx/libSceFont/src/Open.cpp): follows the shadPS4 port and its return codes are unmeasured: `SCE_FONT_ERROR_INVALID_FONT_HANDLE` for a null handle or a block with a zero handle magic, `SCE_FONT_ERROR_NOT_BOUND_RENDERER` for a font without a renderer, `SCE_FONT_ERROR_FATAL` for a renderer with a wrong magic, and `SCE_FONT_OK` on a bound font, repeated calls included. The `guest_font_rebind_renderer` test pins the invalid handle, unbound and bound cases; the wrong-magic case is not covered. +- [sceFontGetLibrary](../../../core/libs/prx/libSceFont/src/Open.cpp) (libSceFont) - the null-output error and the returned owning library follow the [shadPS4 port](https://github.com/shadps4-emu/shadPS4/blob/b6f0c58dba9ee75b93477703032ae4f1e0de8f1c/src/core/libraries/font/font.cpp#L2039-L2046). The invalid-handle error and the output cleared on it are not in shadPS4 and not measured. Closes when: measured on a PS5 ### Functional diff --git a/docs/dev/debt/libSceFontGs.native.md b/docs/dev/debt/libSceFontGs.native.md index 8fafc9ce1c..08e92e0bda 100644 --- a/docs/dev/debt/libSceFontGs.native.md +++ b/docs/dev/debt/libSceFontGs.native.md @@ -2,4 +2,13 @@ ### Unknown function info -- [6 exports](../../../core/libs/prx/libSceFontGs.native/Export.cpp) (libSceFontGs.native) - every function export of `libSceFontGs.native.sprx` from the firmware 13.60 export table, names from aerolib.csv only where `tools/nid_names.py` `compute_nid` reproduces the NID. Signatures, parameter layouts and behaviour are unknown; every export throws through `NotImplemented_nid_no_patch` +- [sceFontGraphicsAgcSurfaceInit](../../../core/libs/prx/libSceFontGs.native/Export.cpp) (libSceFontGs.native) - still throws through `NotImplemented_nid_no_patch`. The libSceFontGsm.prx body (PPSA13197 unwrapped ELF, VA 0x39e0, size 0x324) validates surface format/attributes, writes a large AGC surface descriptor through a deep helper tree and returns 0x8a6cXXXX family errors; the full object layout it produces is not yet mapped, so no general implementation exists. +- [sceFontSelectGraphicsAgc](../../../core/libs/prx/libSceFontGs.native/Export.cpp) (libSceFontGs.native) - throws for index 0 and returns `nullptr` for other indices. Verified bodies disagree on index 0: the 4.03 `libSceFontGs.native.sprx` body is `xor eax,eax; ret` (VA 0x250; SHA-256 `6c72c3ff5785d6bc68984de885d19a812f98626d45d2909794c31f489926101c`), while the newer `libSceFontGsm.prx` body returns a backend descriptor pointer for index 0 (`3890: 31 c0 / xor eax,eax; 85 ff / test edi,edi; 48 8d 0d 85 07 03 00 / lea rcx,[rip->0x34020]; 48 0f 44 c1 / cmove rax,rcx; c3 / ret`). The repository targets the 13.60 export table, and no 13.60 native body proving `nullptr` is available, so index 0 stays unimplemented until the descriptor table is mapped. The `nullptr` for index != 0 is verified by both bodies. +- [FontAgcSurface producer](../../../core/libs/prx/libSceFontGs.native/Export.cpp) (libSceFontGs.native) - the object consumed by `sceFontGraphicsAgcDrawupFillTextureImage`/`sceFontGraphicsAgcDrawupFillTexturePattern` (tag byte 0x58 at +0x0d, mode at +0x0f, busy flag at +0x1c, fill object at +0x28, initialized flag at +0x38) is produced by an export outside the six bridge functions (the FontGsm `sceFontGraphicsAgcSurfaceInit` path does not set +0x38), so its producer is not yet identified; the layout is firmware-verified from the libSceFontGsm.prx bridge bodies. +- [FontAgcSequence producer](../../../core/libs/prx/libSceFontGs.native/Export.cpp) (libSceFontGs.native) - the sequence object consumed by `sceFontGraphicsSequenceGetAgcCommandsForJump`/`...ForSubmit` (tag 0xf80 at +0x00, lock bit at +0x1b, commands descriptor at +0x28) is produced by `sceFontGraphicsStructureCanvasSequence` in libSceFont, which is itself still a stub; the descriptor layout (+0x28 first stream, +0x30 first size, +0x34 second count, +0x38 second stream) is firmware-verified. + +### Notes + +- The four drawup/sequence [bridge exports](../../../core/libs/prx/libSceFontGs.native/Export.cpp) are transliterated from the `libSceFontGsm.prx` bodies (PPSA13197 `sce_module/`, SHA-256 `0c7ebf7f36aba76df09cba3509bf91e4a716c58963ce3f70198eeadec3527cd9`): `XKfh4naaANE` @0x3890 (select), `PwZEP8ZBK2Y` @0x38a0 (image), `z-m061OoEqQ` @0x3950 (pattern), `qEGeDXvL3fc` @0x39e0 (surface-init), `6zxuBlsGF0I` @0x3d10 (submit), `CeSI7C4ihjo` @0x3dc0 (jump). The image bridge head — `38a0: mov eax,0x80460002; test rdx,rdx; je; movq [rdx],0`, then `cmpb [rsi+0xd],0x58`, `cmpq [rsi],0`, `cmpl [rsi+0x38],1` — matches the implemented tag/head/initialized ordering. +- The drawup [bridges](../../../core/libs/prx/libSceFontGs.native/Export.cpp) forward to `sceFontGraphicsDrawupFillTextureImageObject`/`...PatternObject` in `core/libs/prx/libSceFont/src/Graphics.cpp` through the same export boundary firmware uses (FontGsm PLT imports `CfkpBe2CqBQ`/`FL0unhGcFvI` from `libSceFont-module`). +- Nothing in PPSA13197 imports the six [bridge](../../../core/libs/prx/libSceFontGs.native/Export.cpp) NIDs: they appear only in the bundled `libSceFontGsm.prx`'s own export table (module tag `#F` = `libSceFontGsm` itself; its import table lists only `libSceLibcInternal`, `libSceFont-module`, `libSceAgcDriver`, `libSceAgc`). No guest module binds them against `libSceFontGs.native` in this title; the exports are implemented here because `libSceFontGs.native` is the canonical provider module for these NIDs in the 13.60 inventory. diff --git a/docs/dev/debt/libSceFreeType.md b/docs/dev/debt/libSceFreeType.md index be3af2e4db..33eeb2f08f 100644 --- a/docs/dev/debt/libSceFreeType.md +++ b/docs/dev/debt/libSceFreeType.md @@ -1,5 +1,8 @@ # libSceFreeType technical debt +- [FT_Face_SetUnpatentedHinting](../../../core/libs/prx/libSceFreeType/src/FaceCompat.cpp) (libSceFreeType) - console signature is unverified; the adapter forwards to bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftpatent.c), where the call is deprecated, ignores the value and always returns false +- [FT_Face_CheckTrueTypePatents](../../../core/libs/prx/libSceFreeType/src/FaceCompat.cpp) (libSceFreeType) - console signature is unverified; the adapter forwards to bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftpatent.c), where the call is deprecated and always returns false +- [FT_Get_Font_Format](../../../core/libs/prx/libSceFreeType/src/FaceCompat.cpp) (libSceFreeType) - console signature is unverified; the adapter forwards to bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftobjs.c). The returned string is static native storage, valid for the life of the process ### Build - [FT_Library_Version and FT_Get_TrueType_Engine_Type](../../../core/libs/prx/libSceFreeType/src/BuildInfo.cpp) have no console ABI or build-option verification. They query the native library backing the guest handle, following bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftobjs.c); the version therefore describes that backend, not an identified console release. A null library yields version 0.0.0 and engine type NONE. @@ -31,6 +34,8 @@ - [FT_Cos, FT_Sin, FT_Tan, FT_Vector_Unit and FT_Angle_Diff](../../../core/libs/prx/libSceFreeType/src/Trigonometry.cpp) (libSceFreeType) - console ABI/version and CORDIC equivalence are unverified; the LP64 adapter follows bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/fttrigon.c). Tangent retains the 64-bit quotient near poles instead of Windows FT_Long truncation. Angle differences whose signed 64-bit subtraction overflows throw; the bundled source's subtraction is undefined there. +- [FT_New_Memory and FT_Done_Memory](../../../core/libs/prx/libSceFreeType/src/MemoryRecord.cpp) (libSceFreeType) - console behaviour is unverified. The argument-less creation, callback wiring and record destruction follow [FreeType 2.4.12](https://github.com/freetype/freetype/blob/cecf93ef90c660a8b0b45e5adbbfb5ea443fb6b9/src/base/ftsystem.c#L286-L317) and bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftsystem.c), which agree on this interface; the callbacks wrap the host allocator instead of the console's system layer. Records not created by this adapter are destroyed the same way the bundled source destroys them and their lifetime remains the caller's contract. The matching inventory helper in the distinct physical `libSceFreeTypeOt` provider still throws. + - [FT_Outline_Copy](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - compatibility with the console FreeType build and its errors is unverified. The LP64 guest adapter follows the unsigned counts and copy contract of bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftoutln.c). - [FT_Outline_Decompose](../../../core/libs/prx/libSceFreeType/src/OutlineDecompose.cpp) (libSceFreeType) - console callback ABI and FreeType-version equivalence are unverified; the LP64 adapter follows bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftoutln.c). Nonempty outlines with shifts outside 0-62, contour endpoints at or beyond `n_points` or signed 64-bit overflow in scaling, translation or midpoint sums throw; native behavior there is undefined. Callers must supply valid point/tag/contour arrays and the callbacks their contours use. diff --git a/docs/dev/debt/libSceFreeTypeOt.md b/docs/dev/debt/libSceFreeTypeOt.md index 0048f987cd..c8deef6008 100644 --- a/docs/dev/debt/libSceFreeTypeOt.md +++ b/docs/dev/debt/libSceFreeTypeOt.md @@ -1,7 +1,7 @@ # libSceFreeTypeOt technical debt - The FT_Glyph_Copy, FT_Outline_Get_Orientation, FT_Request_Size, FT_Sin, FT_Tan, FT_Vector_Unit, FT_New_Size, FT_Outline_Copy, FT_Outline_Decompose, FT_Stroker_New, FT_Stroker_Done, FT_Stroker_GetCounts, FT_Stroker_BeginSubPath, FT_Stroker_LineTo, FT_Stroker_EndSubPath, FT_Stroker_Set, FT_Stroker_Rewind, FT_Stroker_GetBorderCounts, FT_Stroker_ConicTo and FT_Stroker_CubicTo NIDs in [Export.cpp](../../../core/libs/prx/libSceFreeTypeOt/Export.cpp) are identified, but their provider-specific prototypes and behavior remain unverified. Their named helpers retain the explicit inventory aliases and throw; the separate libSceFreeType implementations do not supply this provider. -- The 72 `sceFreeTypeOtUnknown_*_nid_no_patch` exports in [Export.cpp](../../../core/libs/prx/libSceFreeTypeOt/Export.cpp) are present in the PS5 3.20 export inventory from [PS5-3.20_Libs](https://github.com/DNNDHH/PS5-3.20_Libs). Their prototypes and behavior are unverified. Each uses the no-parameter import-resolution convention and throws through `NotImplemented_nid_no_patch`; these declarations make no ABI or behavior claim. +- The 68 `sceFreeTypeOtUnknown_*_nid_no_patch` exports in [Export.cpp](../../../core/libs/prx/libSceFreeTypeOt/Export.cpp) are present in the PS5 3.20 export inventory from [PS5-3.20_Libs](https://github.com/DNNDHH/PS5-3.20_Libs). Their prototypes and behavior are unverified. Each uses the no-parameter import-resolution convention and throws through `NotImplemented_nid_no_patch`; these declarations make no ABI or behavior claim. ### Unknown function info - [FT_Outline_GetInsideBorder, FT_Outline_GetOutsideBorder](../../../core/libs/prx/libSceFreeTypeOt/CMakeLists.txt) (libSceFreeTypeOl, libSceFreeTypeOt) - console prototypes, enum/layout equivalence, FreeType configuration and module/library versions are unverified. The existing LP64 outline layout and unsigned 16-bit counts follow bundled FreeType 2.13.3; border selection follows [ftstroke.c](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftstroke.c), with the shifted polygon area from [ftoutln.c](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftoutln.c) accumulated in 64 bits. Null positive-count arrays and out-of-range or non-increasing contour endpoints throw when reached; their console behavior is unknown. Tags and flags are not read, and input arrays remain caller-owned. diff --git a/docs/dev/debt/libSceFsInternalForVsh.md b/docs/dev/debt/libSceFsInternalForVsh.md index 5ef46ad555..5d42f8c25e 100644 --- a/docs/dev/debt/libSceFsInternalForVsh.md +++ b/docs/dev/debt/libSceFsInternalForVsh.md @@ -7,3 +7,173 @@ ### Unknown function info - [sceBdSchedCancelBackgroundCopyRequest, sceBdSchedCancelPrioritizedBackgroundCopyRequest, sceBdSchedConfigure, sceBdSchedConfigure2, sceBdSchedDeconfigure, sceBdSchedGetBackgroundCopyRequest, sceBdSchedGetExtentMap, sceBdSchedGetPrioritizedBackgroundCopyRequest, sceBdSchedGetState, sceBdSchedSetBackgroundCopyRequest, sceBdSchedSetDiscIdentity, sceBdSchedSetPrioritizedBackgroundCopyRequest, sceBdSchedStartGame, sceBdSchedStartRipping, sceBdSchedStopScheduling, sceFsBfsCheckCleanFlag, sceFsBindApplication, sceFsBindPackages, sceFsCheckFormat, sceFsCheckSingleUser, sceFsCreateExfatPartition, sceFsCreatePfsSaveDataImage, sceFsCreatePfsTrophyDataImage, sceFsCreatePprPfsTrophyDataImage, sceFsDeviceAlignedPread, sceFsDeviceAlignedPwrite, sceFsDevpfsA53IocGetBuildMetadataStatus, sceFsDevpfsCancelWaitEvent, sceFsDevpfsCreateForVSH, sceFsDevpfsDeleteForVSH, sceFsDevpfsDeregisterEvent, sceFsDevpfsEventComplete, sceFsDevpfsEventResponseComplete, sceFsDevpfsFetchMetaEntry, sceFsDevpfsFsck, sceFsDevpfsInitFetchMetaEntryCtx, sceFsDevpfsMountForFSMP, sceFsDevpfsRegisterEvent, sceFsDevpfsWaitEvent, sceFsDrfsRegisterSocket, sceFsDrfsResumeSocket, sceFsExfatMkfsExternalHdd, sceFsExfatfsMkfs, sceFsExtUSBSchedDestroy, sceFsExtUSBSchedInsert, sceFsExtUSBSchedInsertForHDD, sceFsExternalStorageDeconfigure, sceFsExternalStorageGetRawDevice, sceFsExternalStorageInitialize, sceFsExternalStorageVerifyMetadata, sceFsGetAttribute, sceFsGetClusterStatistics, sceFsGetDeviceSectorsize, sceFsGetFsMetadata, sceFsGetFsMetadataWithProgress, sceFsGetSmrInfo, sceFsGschedCtl, sceFsISSchedConfigCurrentThread, sceFsISSchedConfigDescriptor, sceFsISSchedConfigThread, sceFsISSchedMapStat, sceFsISSchedResultToPriority, sceFsISSchedResultToSlot, sceFsISSchedStatCurrentThread, sceFsISSchedStatDescriptor, sceFsISSchedStatThread, sceFsISSchedUnmapStat, sceFsInitCreatePfsSaveDataOpt, sceFsInitCreatePfsTrophyDataOpt, sceFsInitMountDownloadDataOpt, sceFsInitMountGamePkgOpt, sceFsInitMountLwfsCompatOpt, sceFsInitMountLwfsOpt, sceFsInitMountPprPkgOpt, sceFsInitMountSaveDataOpt, sceFsInitMountTrophyDataOpt, sceFsInitUmountDownloadDataOpt, sceFsInitUmountGamePkgOpt, sceFsInitUmountLwfsCompatOpt, sceFsInitUmountLwfsOpt, sceFsInitUmountPprPkgOpt, sceFsInitUmountSaveDataOpt, sceFsInitUmountTrophyDataOpt, sceFsInitWorkspaceFileMetadataParam, sceFsLvdAttach, sceFsLvdAttach2, sceFsLvdAttachPartiallyCryptedDownloadData, sceFsLvdAttachSingleDefaultImage, sceFsLvdAttachSingleImage, sceFsLvdDetach, sceFsLvdDisableLayer, sceFsLvdEnableLayer, sceFsLvdSetDiscIdentity, sceFsLvdWaitResumed, sceFsMlfsMount, sceFsMlfsMountWithFlags, sceFsMlfsUmount, sceFsMountDownloadData, sceFsMountGamePkg, sceFsMountLwfs, sceFsMountLwfsCompat, sceFsMountPartiallyCryptedDownloadData, sceFsMountPprPkg, sceFsMountSaveData, sceFsMountTrophyData, sceFsNsfsAddOverlayToNamespace, sceFsNsfsMountWithFlags, sceFsNsfsSetGlobalExclude, sceFsNsfsUmount, sceFsPartitionTrim, sceFsReserveCluster, sceFsSetAttribute, sceFsSetClusterCacheSize, sceFsSetFileReservedTotal, sceFsSetSystemReserveSize, sceFsSetWorkspaceFileHash, sceFsStatWorkspace, sceFsSysStorageGetStatus, sceFsTransactionCancel, sceFsTransactionSaveDataInitMountOpt, sceFsTransactionSaveDataMount, sceFsTransactionStart, sceFsTrophyImageError, sceFsUfsAllocateAdditionalContents, sceFsUfsAllocateAligned, sceFsUfsAllocateDownloadData, sceFsUfsAllocateGameImage, sceFsUfsAllocatePatchImage, sceFsUfsAllocateSaveData, sceFsUfsAllocateTrophyData, sceFsUfsCheckCleanFlag, sceFsUfsCheckFixedCylinderGroupSize, sceFsUfsCollectTelemetry, sceFsUfsCollectTelemetry2, sceFsUfsDirectWrite, sceFsUfsEstimateZoneCapacityForUser, sceFsUfsExtendFile, sceFsUfsFsck, sceFsUfsFsckWithMM, sceFsUfsGetExtentMap, sceFsUfsGrowLwfs, sceFsUfsGrowfs, sceFsUfsHideBlocksFromUser, sceFsUfsInitalizeSparseFile, sceFsUfsLargewriteFsck, sceFsUfsLargewriteFsckWithMM, sceFsUfsMkLwfs, sceFsUfsMkfs, sceFsUfsMkfsForExternalHDD, sceFsUfsMkfsForSystem, sceFsUfsMkfsWithFixedCylinderGroupSize, sceFsUfsReallocateSparseFile, sceFsUfsRecryptBlocks, sceFsUfsRecryptGetState, sceFsUfsRecryptIsUnfinished, sceFsUfsRecryptSyncMetadata, sceFsUfsSeekHoleInFile, sceFsUfsStopOperation, sceFsUmountDownloadData, sceFsUmountGamePkg, sceFsUmountLwfs, sceFsUmountLwfsCompat, sceFsUmountSaveData, sceFsUmountTrophyData, sceFsUmountWorkspace, sceFsUnbindApplication, sceFsUnbindPackages, sceFsWsCreate, sceFsWsDelete, sceFsWsFetchFileMetadata, sceFsWsFsck, sceFsWsGetStatus, sceFsWsMount and sceFsWsUmount](../../../core/libs/prx/libSceFsInternalForVsh/Export.cpp) (libSceFsInternalForVsh) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceFsInternalForVsh.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified + +### Additional callable exports + +- [Additional callable exports](../../../core/libs/prx/libSceFsInternalForVsh/Export.cpp) - 163 entries present in both version-labelled images are import-resolution declarations only. None is listed in the pinned public 3.20 catalogue for this library. Neither label independently authenticates retail firmware provenance. Parameter lists, return types and behavior are unverified; parameterless `int` declarations are placeholders, not verified ABI contracts. Every entry throws through `NotImplemented_nid_no_patch`. Unidentified names use literal NID aliases. No runtime functionality is implemented. Data exports require separate layout and ownership analysis. Identified candidate names match their NIDs in the [public symbol database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv) or [supplemental catalog](https://github.com/claimore22/ps5rs/blob/2304be5131221bae0dac24b9767cabffda33f747/data/nids.csv). + +| Name | NID | +| --- | --- | +| `sceFsBfsAllocateAdditionalContents` | `gyqYJnZGUeg` | +| `sceFsBfsAllocateGameImage` | `Hwr6oTQrHYA` | +| `sceFsBfsAllocatePatchImage` | `dRB3R2dK7lM` | +| `sceFsBfsAllocateSaveData` | `eLrSG2GpWfU` | +| `sceFsBfsAllocateTrophyData` | `pG3cfObys9w` | +| `sceFsBfsApplyPatch` | `qtWlhnEzWqs` | +| `sceFsBfsCancelPatch` | `VOFWv+ZZgpc` | +| `sceFsBfsCommitPatch` | `OCV7fw1WFHY` | +| `sceFsBfsContainerBind` | `U0Kb1beVNHk` | +| `sceFsBfsContainerCreate` | `Zr6b1pGvea0` | +| `sceFsBfsContainerQuery` | `zR4vUK1nJxM` | +| `sceFsBfsContainerUnbind` | `pGk-HIsJgPc` | +| `sceFsBfsContainerUpdate` | `7WUxqTxbHH8` | +| `sceFsBfsExtendFile` | `eVZX55IE8Qw` | +| `sceFsBfsFormat` | `NdmkyL4E+6A` | +| `sceFsBfsFsck` | `YaWCv3JUFdM` | +| `sceFsBfsGcGetStatus` | `j8aGZfdbhc8` | +| `sceFsBfsGcMove` | `hLLe9yb4CI8` | +| `sceFsBfsGetAvailableSize` | `MSQ1n+tzSEA` | +| `sceFsBfsGetBindStatus` | `MqHkPiPliiQ` | +| `sceFsBfsGetContainers` | `mw0lr-KcAVM` | +| `sceFsBfsGetFileSize` | `07w8uSyyDgo` | +| `sceFsBfsGetMapping` | `fOZZcF1L-4I` | +| `sceFsBfsGetSCFStatus` | `EV39PBp42ss` | +| `sceFsBfsGetStatus` | `A--9Ydn8BOU` | +| `sceFsBfsM2Format` | `rPXZMK+AG0I` | +| `sceFsBfsMount` | `geq37pht+aU` | +| `sceFsBfsTrim` | `B2Waw07963I` | +| `sceFsBfsUnprotect` | `oNxcT+bbrd8` | +| `sceFsCreatePprPfsSaveDataImage` | `koGUBftreVg` | +| `sceFsCreateWorkspace` | `vjXtNys5x6M` | +| `sceFsDeleteWorkspace` | `Yola1NbNquE` | +| `sceFsDevpfsAllocateFile` | `A8RiikwWk-U` | +| `sceFsDevpfsAllocateSparseFile` | `OD0v8qAhYWo` | +| `sceFsDevpfsMountForVSH` | `nLkSa8Hcik0` | +| `sceFsDevpfsSystemPowerEvent` | `d+s2iU87ZzI` | +| `sceFsDevpfsTermFetchMetaEntryCtx` | `73RHx-09jRA` | +| `sceFsDevpfsUmountForFSMP` | `8x7XaKzCy-A` | +| `sceFsDevpfsUmountForVSH` | `bMPogEwPp+g` | +| `sceFsDevpfsWriteChunk` | `sCqqQQYUiK8` | +| `sceFsDrfsDestroyContext` | `UUgb-PXhk2Q` | +| `sceFsDrfsDisconnect` | `KNujEf-sWXY` | +| `sceFsDrfsInitContext` | `ajihfYkZJ6I` | +| `sceFsDrfsMount` | `igEsGj+Ys5o` | +| `sceFsDrfsSuspendSocket` | `G+qmQeVtF9Q` | +| `sceFsDrfsUmount` | `1UNNe8ym0-A` | +| `sceFsDrfsUnregisterSocket` | `f-AL1VFNUAo` | +| `sceFsDrfsWaitEvent` | `9yYkQ0ZT4+s` | +| `sceFsFetchWorkspaceFileMetadata` | `pf-lA0kywn4` | +| `sceFsInitCreatePprPfsSaveDataOpt` | `ns-+qa8Pd5c` | +| `sceFsInitCreateWorkspaceOpt` | `G+lXWFmyIrc` | +| `sceFsInitDeleteWorkspaceOpt` | `tAnh6QH6lR0` | +| `sceFsInitMountWorkspaceOpt` | `vjXmWuIa46I` | +| `sceFsInitUmountWorkspaceOpt` | `HSLfRvvCFJ8` | +| `sceFsIsActiveBankSystemEx` | `ZIXa1o3EEao` | +| `sceFsIsActiveBankSystem` | `d3BVR4cnRxo` | +| `sceFsM2StorageCheckCleanFlag` | `nnQ-729CENw` | +| `sceFsM2StorageDeconfigure` | `vYKMA1Eyvks` | +| `sceFsM2StorageFsckWithMM` | `Q3Dx5zpjZds` | +| `sceFsM2StorageGetM2UniqueId` | `e6RvxzRHg0g` | +| `sceFsM2StorageInitialize` | `afZqNcRuBfg` | +| `sceFsM2StorageMount` | `3gF9wsB0078` | +| `sceFsM2StorageUnmount` | `5QhQmY1NL-Y` | +| `sceFsM2StorageVerifyMetadata` | `V6i6yMSwqo4` | +| `sceFsMountPprSaveData` | `rFjyPw4dCiw` | +| `sceFsMountWorkspace` | `WB69f40fP8s` | +| `sceFsNewfsDefault` | `Ggp-Q5xJa20` | +| `sceFsNsfsMount` | `Qjoa0WfIKfs` | +| `sceFsPartitionSwitchBank` | `90NDXvGAt2E` | +| `sceFsPathMappingResolve` | `LRhP2sMzWKA` | +| `sceFsPlaygoDisable` | `WvQjGuJgbDQ` | +| `sceFsPlaygoEnable` | `ELZxyxIOpuM` | +| `sceFsPlaygoInitializeParams` | `5WGkFjeeTy0` | +| `sceFsPlaygoSetParams` | `JB6h0wNjfXE` | +| `sceFsTransactionCommit` | `sSqVfkgfjFs` | +| `sceFsTransactionPprNoCowSaveDataMount` | `aOFarTYiGKg` | +| `sceFsTransactionPprSaveDataMount` | `hdK6CFu85Yk` | +| `sceFsTransactionSaveDataReadParam` | `3ebHTSYhPZY` | +| `sceFsTransactionSaveDataUmount` | `8X8J-jiHTn0` | +| `sceFsTransactionSaveDataWriteParam` | `CBUzOsP0eS8` | +| `sceFsUfsCoerceCryptMode` | `6HcKx4a0alU` | +| `sceFsUfsMkfsProsperoApplicationTemporaryData` | `pMwPp537VNk` | +| `sceFsUfsMkfsProsperoDownloadData` | `VFD9HXJT5PY` | +| `sceFsUmountPprPkg` | `S5+bpaH0TWk` | +| `sceFsUpdatePackageMetadataApp0` | `IEepZ-CjqQQ` | +| `sceFsWorkspaceCoreDump` | `XLW5WuNYVfo` | +| `sceFsWsWriteFile` | `DnuQ4XKoF+s` | +| `sceFsWsWriteFileExtdata` | `JTQE83+A0rM` | +| `Unknown` | `+gVdjPejLwc` | +| `Unknown` | `-HW6neCER+A` | +| `Unknown` | `0JhonchgIa8` | +| `Unknown` | `0XlyVxRNeIw` | +| `Unknown` | `1lLh9mukbz0` | +| `Unknown` | `2UgTmJt2dsc` | +| `Unknown` | `3TFUof4RLWM` | +| `Unknown` | `41PqBeun5cU` | +| `Unknown` | `4uqQOvVpISA` | +| `Unknown` | `68F1C7m2pO4` | +| `Unknown` | `6Bku+Im1Pew` | +| `Unknown` | `89zs8+EWQYQ` | +| `Unknown` | `9X3ydcDhbhw` | +| `Unknown` | `AnMWzsLhsYk` | +| `Unknown` | `CFlYeKz+HYs` | +| `Unknown` | `DEHwNbsX2BA` | +| `Unknown` | `DNjr3qYr-lY` | +| `Unknown` | `DSmBrz8enws` | +| `Unknown` | `Elqj7jkJd5s` | +| `Unknown` | `GNeCtmqY-58` | +| `Unknown` | `GsHxp1IXkfA` | +| `Unknown` | `KesAmkrIb9Y` | +| `Unknown` | `KrZEB+TsZ5k` | +| `Unknown` | `LGPQXeAAFdc` | +| `Unknown` | `LrpsS5hGlyY` | +| `Unknown` | `M9342PuS+y0` | +| `Unknown` | `MQs7RB-KfYM` | +| `Unknown` | `N7SnQKgSGA4` | +| `Unknown` | `NHH7572Mh30` | +| `Unknown` | `NUfgeY29b9E` | +| `Unknown` | `NdL2fJF8e78` | +| `Unknown` | `NrSqjlYMetU` | +| `Unknown` | `O6akvoAE6k0` | +| `Unknown` | `P8F9U9uTPqA` | +| `Unknown` | `Rwk7z377rco` | +| `Unknown` | `SyNglOzKCV8` | +| `Unknown` | `T3LiDkpiS7s` | +| `Unknown` | `UD35isX9168` | +| `Unknown` | `W8CmA+wZ9+w` | +| `Unknown` | `WwEWgYYuAmo` | +| `Unknown` | `XwPXO+lxKy0` | +| `Unknown` | `Y-AXyBKGn8I` | +| `Unknown` | `Z9o0nKENoCQ` | +| `Unknown` | `ZcrLUvsmmrU` | +| `Unknown` | `a5ex0HGaufY` | +| `Unknown` | `b5kZJiwZPT8` | +| `Unknown` | `bGBEeJ2B6XE` | +| `Unknown` | `cDZDkhZeK4o` | +| `Unknown` | `cUyUz68FKBc` | +| `Unknown` | `czWRa7s1ogE` | +| `Unknown` | `dKQBYtuMOWA` | +| `Unknown` | `eV9ctY+OiOo` | +| `Unknown` | `g3Lf8TiVEBM` | +| `Unknown` | `gcOITai35ls` | +| `Unknown` | `gerE9e47NoU` | +| `Unknown` | `gtzIEP+e2jo` | +| `Unknown` | `hfb5Y906dYA` | +| `Unknown` | `hhPrQak7oFg` | +| `Unknown` | `iJQ7aolqS1A` | +| `Unknown` | `juY2ugdopus` | +| `Unknown` | `k-WaWvM74jI` | +| `Unknown` | `lZ+oSeAyI8Q` | +| `Unknown` | `lxyJjALNmp4` | +| `Unknown` | `mvNIgoNVeWA` | +| `Unknown` | `nIMoeyl3SRY` | +| `Unknown` | `noVGZAYgjI8` | +| `Unknown` | `oP09BdVEs7o` | +| `Unknown` | `pt+umHHWgvM` | +| `Unknown` | `rOoqs69ugy8` | +| `Unknown` | `rakMRjDRGS4` | +| `Unknown` | `s7X-JxrF650` | +| `Unknown` | `v3jcGxw1mR8` | +| `Unknown` | `v9uqOhwlhjs` | +| `Unknown` | `yadgQseaDKA` | +| `Unknown` | `ydRWehw26Kk` | diff --git a/docs/dev/debt/libSceGnmDriverForNeoMode.md b/docs/dev/debt/libSceGnmDriverForNeoMode.md index 730bcb08d3..ad90e7dffa 100644 --- a/docs/dev/debt/libSceGnmDriverForNeoMode.md +++ b/docs/dev/debt/libSceGnmDriverForNeoMode.md @@ -4,13 +4,14 @@ - [sceGnmIsDebugResetAvailable (`xMMot887QXE`)](../../../core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp) - the PS5 firmware 13.60 export table and public NID map identify the export, but its signature and behaviour are unknown; its placeholder uses the no-parameter unknown-signature convention and throws through `NotImplemented_nid_no_patch`. - [sceGnmDingDongForWorkload, sceGnmRegisterOwner](../../../core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp) - the first throws through `NotImplemented_nid_no_patch` and `sceGnmRegisterOwner` returns `Failure` (0x8EEE00FF) without registering anything; behaviour and the PS5 prototypes are unmeasured. The only source of the parameter lists is shadPS4's PS4-derived prototypes (GPLv2, used as a reference only), with no second reference; structure pointers are `void*` because no layout is known. -- [libSceGnmDriverForNeoMode pending exports](../../../core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp) - 34 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) - 33 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`. ### 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 - [default state packets](../../../core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp) (libSceGnmDriverForNeoMode) - `sceGnmDrawInitDefaultHardwareState`, `sceGnmDrawInitDefaultHardwareState200`, `sceGnmDrawInitDefaultHardwareState350`, `sceGnmDrawInitToDefaultContextState` and `sceGnmDrawInitToDefaultContextState400` write the sequences in [InitSequences.hpp](../../../core/libs/prx/libSceGnmDriverForNeoMode/include/InitSequences.hpp), taken from [shadPS4's gnmdriver_init.h](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/gnmdriver/gnmdriver_init.h): its Neo sequences, except the plain hardware state, which shadPS4 also writes unchanged in Neo mode. The size checks and returned sizes are shadPS4's. Not compared with console output, and nothing in AnyPS5 consumes a GNM command buffer yet - [sceGnmDrawInitToDefaultContextStateInternalCommand](../../../core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp) (libSceGnmDriverForNeoMode) - answers as `sceGnmDrawInitToDefaultContextState` when the compiled SDK version in the process parameters is below 0x04000000 and as `sceGnmDrawInitToDefaultContextState400` from it, as firmware 13.42's module does, run natively with its imports stubbed, not on a console. The module reads the version at init through `sceKernelGetCompiledSdkVersion`. From 0x04000000 its result and output equal `...State400` for every size tried ([run](https://github.com/boykopovar/AnyPS5/pull/10701#issuecomment-6403613914)). Below it, size 0x20 writes 19 words byte-identical to the module's own `sceGnmDrawInitToDefaultContextState`, from a copy of that sequence, and other sizes write nothing ([@BunnyFl's comparison](https://github.com/boykopovar/AnyPS5/pull/13712#issuecomment-6543609746)). The version is read at each call here, not cached at init +- [sceGnmDrawInitToDefaultContextStateInternalSize](../../../core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp) (libSceGnmDriverForNeoMode) - not called in the native firmware 13.42 runs and not measured on a console; returning `0x20` below compiled SDK `0x04000000` and `0x100` from `0x04000000` follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/2183807e592b36d4440384878165b43d930c1dee/src/core/libraries/gnmdriver/gnmdriver.cpp#L2986-L2992) and matches the command sizes of `sceGnmDrawInitToDefaultContextStateInternalCommand`, reading the compiled SDK version on each call rather than caching at init - [draw, dispatch, wait and VGT packets](../../../core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp) (libSceGnmDriverForNeoMode) - the signatures, size checks, PM4 words and refusals (-1) of the 17 command writers follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/gnmdriver/gnmdriver.cpp) on its Neo-mode path, since this module is the Neo-mode driver: draw initiators keep flag bits 29 to 31, and `sceGnmSetVgtControl` and `sceGnmResetVgtControl` write `IA_MULTI_VGT_PARAM` through `SET_UCONFIG_REG` (shadPS4's `UseNeoCompatSequences` is false). The packets are not compared with console output, and nothing in AnyPS5 consumes a GNM command buffer yet - [embedded shaders](../../../core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp) (libSceGnmDriverForNeoMode) - `sceGnmSetEmbeddedVsShader` (id 0, a fullscreen triangle) and `sceGnmSetEmbeddedPsShader` (id 0 writes black, id 1 exports depth only) write the register packets of [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/gnmdriver/gnmdriver.cpp), pointing at the shader code in [EmbeddedShaders.hpp](../../../core/libs/prx/libSceGnmDriverForNeoMode/include/EmbeddedShaders.hpp) in this module's memory instead of the console's `0xfe000f00` area. That code is shadPS4's instruction listing assembled with LLVM 19 `llvm-mc -arch=amdgcn -mcpu=bonaire`, without the binary header and register block that follow it on the console, since nothing here reads them; another id returns `0x8EEE00FF`. The size and modifier arguments are not checked, as in shadPS4. Not compared with console output, and nothing in AnyPS5 consumes a GNM command buffer yet - [shader register packets](../../../core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp) (libSceGnmDriverForNeoMode) - `sceGnmUpdateVsShader`, `sceGnmUpdatePsShader`, `sceGnmUpdatePsShader350`, `sceGnmSetPsShader350`, `sceGnmUpdateGsShader`, `sceGnmUpdateHsShader` and `sceGnmSetCsShaderWithModifier` write the packets, size checks and -1 refusals of [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/gnmdriver/gnmdriver.cpp); the update forms carry context registers inside NOP packets as there. Not compared with console output, and nothing in AnyPS5 consumes a GNM command buffer yet diff --git a/docs/dev/debt/libSceHmd2.md b/docs/dev/debt/libSceHmd2.md index fb15e06839..48662531da 100644 --- a/docs/dev/debt/libSceHmd2.md +++ b/docs/dev/debt/libSceHmd2.md @@ -62,7 +62,7 @@ - [Hmd2BeforeInitializeParameterNull](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` to these 16 exports both with every argument null and with every argument pointing at a zeroed 16 KiB buffer, writing nothing: `MNtiJTDOPAU`, `RRy9WmqZfgU` (sceHmd2InternalDecryptData), `uobR4m9lgwc` (sceHmd2InternalGetMirroringMode), `-XPk5KWBYdo` (sceHmd2InternalRelocalizerSetResultData), `J44idh9izZM`, `Ps0wAvYaa1E`, `kLnMR8oQ1MY`, `kMz4rbBxvjo`, `DIU1sFRRaiE` (sceHmd2GazeGetCalibrationData), `ZQvU4vDzZGQ` (sceHmd2GazeSetUserCalibrationData), `TJpyGGP9YEY`, `Dih+Odk+RC0`, `oaqdSJw+HVY`, `m35gtHuanXU`, `CTM7QPLJF9Q` (sceHmd2InternalGetFlipInfo), `+rExHeC9lP0` (sceHmd2GazeObjectSelectionInitialize). Measured from a payload process on firmware 13.60, so a game process before initialization is assumed to answer the same; the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded - [Hmd2BeforeInitializeParameterInvalid](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720009` to these 9 exports both with every argument null and with every argument pointing at a zeroed 16 KiB buffer, writing nothing: `bBh5bCp9coo` (sceHmd2InternalSetPositionResetIcon), `G3PNDE2VjfY` (sceHmd2InternalSetPowerOff), `nScCT0OPU0o`, `6Q0Ng7OTN9o`, `-fJKh4SnFR0` (sceHmd2InternalSetBudgetMode), `jUA2s-l0npY` (sceHmd2InternalSetForceIcon), `+9XhyAIJVj4`, `eBGK-o6E4RQ` (sceHmd2InternalBindDeviceWithUserId), `MTuFeL9184E`. Measured from a payload process on firmware 13.60, so a game process before initialization is assumed to answer the same; the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded - [Hmd2BeforeInitializeReprojectionNotInitialized](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a72000b` to these 10 exports both with every argument null and with every argument pointing at a zeroed 16 KiB buffer, writing nothing: `d3SEIYTP6wc`, `EVECbTvBezo`, `ciusPdJKjSU`, `DIwqTkcZiJo` (sceHmd2ReprojectionSetUserEventStart), `64ERZWVFBTE` (sceHmd2ReprojectionSetUserEventEnd), `z0+0gXUJWs8` (sceHmd2ReprojectionClearUserEventStart), `rjAd4nCY4DM` (sceHmd2ReprojectionClearUserEventEnd), `Acyg4vTdGm0` (sceHmd2ReprojectionGetCurrentOutput), `aGOWaEeg04I`, `9up+EPhw1No`. Measured from a payload process on firmware 13.60, so a game process before initialization is assumed to answer the same; the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded -- [sceHmd2ReprojectionEnableMirroring, sceHmd2ReprojectionGetPredictedDisplayTime and sceHmd2ReprojectionSetRenderConfig](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - the answers inside a reprojection session are not implemented (for the last two [measured](https://github.com/boykopovar/AnyPS5/pull/7658#issuecomment-6408246816) in a PPSA01317 session on firmware 13.40), so a session has to gate these calls once `sceHmd2ReprojectionInitialize` can create one. Without a session each returns 0x8a72000b for every argument and writes nothing: the firmware 13.40 module tests the session before its arguments, and a retail PS5 answered so from a payload before `sceHmd2Initialize` with null arguments and with zeroed buffers ([firmware 13.60](https://github.com/boykopovar/AnyPS5/pull/7798#issuecomment-6262489180)) and in a PPSA23479 game process whose arguments were not recorded ([firmware 13.40](https://github.com/boykopovar/AnyPS5/pull/8645#issuecomment-6307024384)) +- [sceHmd2ReprojectionEnableMirroring, sceHmd2ReprojectionGetPredictedDisplayTime and sceHmd2ReprojectionSetRenderConfig](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - inside a reprojection session they throw, except the answers [measured](https://github.com/boykopovar/AnyPS5/pull/7658#issuecomment-6408246816) in a PPSA01317 session on firmware 13.40: GetPredictedDisplayTime with a null output 0x8a720008, SetRenderConfig with a bit outside bits 0 and 32 or a nonzero second or third argument 0x8a720009. The 0x8a720019 GetPredictedDisplayTime answered there without a headset is not returned, because the answer with one is unknown. Without a session each returns 0x8a72000b for every argument and writes nothing, as a retail PS5 answered from a payload before `sceHmd2Initialize` ([firmware 13.60](https://github.com/boykopovar/AnyPS5/pull/7798#issuecomment-6262489180)) - [Hmd2BeforeInitializeError002C](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a72002c` to these 1 exports both with every argument null and with every argument pointing at a zeroed 16 KiB buffer, writing nothing: `V1kLQmP-17U` (sceHmd2GazeObjectSelectionTerminate). Measured from a payload process on firmware 13.60, so a game process before initialization is assumed to answer the same; the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded - [Hmd2BeforeInitializeError1002](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a721002` to these 5 exports both with every argument null and with every argument pointing at a zeroed 16 KiB buffer, writing nothing: `2LVD1EC5SYE`, `Bf87AL6zWxU`, `XKt7w1MODFY`, `Y5s22M-qUDA`, `JJKY-BtQJLg`. Measured from a payload process on firmware 13.60, so a game process before initialization is assumed to answer the same; the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded - [Hmd2BeforeInitializeError1003](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a721003` to these 1 exports both with every argument null and with every argument pointing at a zeroed 16 KiB buffer, writing nothing: `9kj9vw8UShQ`. Measured from a payload process on firmware 13.60, so a game process before initialization is assumed to answer the same; the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded diff --git a/docs/dev/debt/libSceHttp2.md b/docs/dev/debt/libSceHttp2.md index 8072c04755..9b00f5c685 100644 --- a/docs/dev/debt/libSceHttp2.md +++ b/docs/dev/debt/libSceHttp2.md @@ -37,6 +37,7 @@ - [libSceHttp2 gzip responses](../../../core/libs/prx/libSceHttp2/Transport.cpp) - `sceHttp2SetInflateGZIPEnabled` stores the flag. When it is set and the response carries `content-encoding: gzip`, the transport inflates the body with the vendored zlib before `sceHttp2ReadData` and `sceHttp2GetResponseContentLength` see it, and reports the inflated length; both calls wait for END_STREAM while the enabled response is gzip. The transport neither adds nor inspects `Accept-Encoding`, so the guest supplies that request header and an unsolicited gzip response is inflated too. Whether a console rewrites the header block (Content-Encoding and Content-Length) is unverified, so the serialized headers keep the fields the server sent; other encodings are not inflated, a malformed or truncated stream, a bad checksum or size and concatenated members throw, and the optional gzip header CRC is not verified. - [sceHttp2GetAllResponseHeaders](../../../core/libs/prx/libSceHttp2/Export.cpp) throws: the serialization, size and lifetime of a decoded HTTP/2 header block have not been established. Status and Content-Length fields are decoded; absent Content-Length reports `NotFound`, following SharpProspero. - [libSceHttp2 asynchronous requests](../../../core/libs/prx/libSceHttp2/Export.cpp) - send/read submission and `sceHttp2WaitAsync` throw. The previous offline implementation queued a fabricated network failure and used event type zero; public sources do not establish the completion event types, wait timeout updates, cancellation results or ownership of async buffers. PPSA17221's kqueue layout and PPSA15508's 24-byte wait result establish caller ABI only. No completion or equeue event is manufactured. +- [Host request header](../../../core/libs/prx/libSceHttp2/Transport.cpp) (libSceHttp2) - a `Host` header set with `sceHttp2AddRequestHeader` becomes the request's `:authority` pseudo-header and is not sent as a `Host` field, following the HTTP/1.1-to-HTTP/2 translation in [RFC 9113 section 8.3.1](https://www.rfc-editor.org/rfc/rfc9113.html#section-8.3.1). The last value set wins. The console's handling of a `Host` that differs from the URL authority and of whitespace in the value is not measured; here whitespace throws. - [WebSocket ping settings](../../../core/libs/prx/libSceHttp2/Export.cpp): both setters take a 32-bit request ID and a 32-bit interval/timeout in microseconds, confirmed by the timer consumer and its clock conversion. The protected implementations remain unchanged. - [sceHttp2Init](../../../core/libs/prx/libSceHttp2/Export.cpp) - a negative maximum concurrent request count throws, and the pool size is not checked. In a payload process on a PS5 (firmware 12.70), with a 64 KiB pool, a count of 0 returned `0x817B11FE` (invalid value) and a count of 1 returned `0x817B121D` (pool too small). A count of -1 with that pool, and a count of 1 with a 1 MiB pool in a second run, produced no result before the runner's timeout, so their return values and the smallest accepted pool are not measured. Every call with a constant count in the titles checked passes 1 to 32, and every call with a constant pool size passes 256 KiB to 2.75 MiB, the range above the measured 64 KiB pool; the value of the other calls is not known. diff --git a/docs/dev/debt/libSceIcu.md b/docs/dev/debt/libSceIcu.md index 5931e66156..8ee68e53ff 100644 --- a/docs/dev/debt/libSceIcu.md +++ b/docs/dev/debt/libSceIcu.md @@ -2,6 +2,8 @@ ### Unknown function info +- The 134 [recovered inventory exports](../../../core/libs/prx/libSceIcu/RecoveredInventory.cpp) have unverified parameters, return types, callback ABIs and behavior. Every entry immediately throws through `NotImplemented_nid_no_patch`; the parameterless `int` declarations are import-resolution placeholders. The [13.60 inventory](https://github.com/xdCloudy/PS5-13.60-Function-Stub-Library/blob/226383a7fa4b1275d6cebf61eb1c1e7ca7b58bcb/data/firmware-13.60/libsceicu-9790ee85e3.jsonl) establishes only name/NID bindings. The [module map](https://github.com/boykopovar/ps5-nid-map/blob/cc1d586fddab576a520f226da6b2632b031e6d09/data/Modules/libSceIcu.inc) assigns all 134 NIDs to logical `libSceIcu` in `libSceIcu.sprx`. Recovered `ucfpos`, `uset` and `uiter` groups are excluded. Console ABI, behavior and title compatibility remain unverified. + - [ICU export availability](../../../core/libs/prx/libSceIcu/Unescape.cpp) across firmware versions remains unproven. The [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceIcu.c) includes the implemented escape, invariant-character and version helpers. [BunnyFl’s firmware 13.42 report](https://github.com/boykopovar/AnyPS5/pull/10961#issuecomment-6400071863) records `u_getVersion` returning `78.1.0.0` under an emulator, but neither escape function is in the checked native `system_ex/common_ex/lib/libSceIcu.sprx` export table. Only 104 of the 661 inventory NIDs appear among that module’s 299 exports. The exports for compatibility with older firmware are retained; availability and routing across firmware versions remain unproven. ### Older dispatch exports @@ -37,6 +39,12 @@ - [UTF-16 set searches and tokenization](../../../core/libs/prx/libSceIcu/Tokenize.cpp) have no console verification; ICU versions on other firmware are unverified. Signatures and behavior follow ICU 78.1's [ustring.h](https://github.com/unicode-org/icu/blob/049e0d6a420629ac7db77256987d083a563287b5/icu4c/source/common/unicode/ustring.h) and [ustring.cpp](https://github.com/unicode-org/icu/blob/049e0d6a420629ac7db77256987d083a563287b5/icu4c/source/common/ustring.cpp). `u_strtok_r` preserves that source's one-code-unit delimiter overwrite, including the trailing surrogate left after splitting at a supplementary delimiter. Invalid null inputs and rejected guest-write scopes throw; an initial continuation assignment is retained if a later delimiter write is rejected. Set-search offsets beyond `INT32_MAX` throw. +- [Numeric platform helpers](../../../core/libs/prx/libSceIcu/Numeric.cpp) are unverified on a console. The 24 `uprv_` IEEE 754, rounding, extreme, overflow and pointer-limit exports follow ICU 78.1 [putil.cpp](https://github.com/unicode-org/icu/blob/release-78.1/icu4c/source/common/putil.cpp) and [umath.cpp](https://github.com/unicode-org/icu/blob/release-78.1/icu4c/source/common/umath.cpp), unchanged since 65.1, assuming the default `IEEE_754` and `U_MAX_PTR` configuration. The 3.20 table has them; firmware 13.42 exports none ([#11013](https://github.com/boykopovar/AnyPS5/pull/11013#issuecomment-6401310351)). The libm wrappers call the guest libc, so their precision is libc's. ICU's quirks are kept: `uprv_trunc` of negative infinity is positive infinity, NaN results are the canonical quiet NaN, `uprv_fmax(0.0, -0.0)` is `-0.0`, and `uprv_round` floors `x + 0.5`. Null `uprv_modf` and overflow result pointers throw; ICU dereferences them. Closes when: compared with the console. + +- [u_getVersion and u_errorName](../../../core/libs/prx/libSceIcu/ErrorName.cpp) follow ICU 78.1, the version firmware 13.42's module reports and exports both for ([#11013](https://github.com/boykopovar/AnyPS5/pull/11013#issuecomment-6401310351)); only `u_getVersion`'s 78.1.0.0 was measured there. `u_errorName` returns the names in 78.1's [utypes.cpp](https://github.com/unicode-org/icu/blob/release-78.1/icu4c/source/common/utypes.cpp), including its `U_BRK_VARIABLE_REDFINITION` spelling, and `[BOGUS UErrorCode]` for every other code; the names were not compared with the console. Both report 78.1 whatever firmware is emulated, although the 3.20 module was ICU 65 to 68 and has no names for the codes added since. Closes when: compared with the console. + +- [Constrained field positions](../../../core/libs/prx/libSceIcu/ConstrainedFieldPosition.cpp) are unverified on a console. The seven `ucfpos_` exports firmware 13.42 has ([#11013](https://github.com/boykopovar/AnyPS5/pull/11013#issuecomment-6401310351)) follow ICU 78.1 [formattedvalue.cpp](https://github.com/unicode-org/icu/blob/release-78.1/icu4c/source/i18n/formattedvalue.cpp) and [capi_helper.h](https://github.com/unicode-org/icu/blob/release-78.1/icu4c/source/common/capi_helper.h). A handle is a host object whose first four bytes hold ICU's magic `0x55434600`; the rest of its layout differs from ICU's. A set error changes nothing, a null handle sets `U_ILLEGAL_ARGUMENT_ERROR` and other memory `U_INVALID_FORMAT_ERROR`; `ucfpos_open` ignores the error unless allocation fails. No export fills a position yet, so the indexes stay 0. Null error pointers and index outputs throw where ICU dereferences them. Closes when: compared with the console. + ### Known export names; ABI prototypes unverified - The 93 `sceIcuUnknown_*_nid_no_patch` and named `*_nid_postfix` stubs in [Export.cpp](../../../core/libs/prx/libSceIcu/Export.cpp) are present in the PS5 3.20 export inventory from [PS5-3.20_Libs](https://github.com/DNNDHH/PS5-3.20_Libs). Their prototypes and behavior are unverified. Each uses the no-parameter import-resolution convention and throws through `NotImplemented_nid_no_patch`; these declarations make no ABI or behavior claim. diff --git a/docs/dev/debt/libSceIme.md b/docs/dev/debt/libSceIme.md index d05f886ca9..53e8e0b915 100644 --- a/docs/dev/debt/libSceIme.md +++ b/docs/dev/debt/libSceIme.md @@ -36,3 +36,5 @@ - [sceImeOpen text filter](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) runs on the text edits of host input - typing, backspace, forward delete and a multiline Enter - following [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/1211958b4eb38256cc0c5c7ba92d668820c05389/src/libs/ime.cpp), not confirmed on a PS5. The host input thread only queues the edit under the IME mutex; `sceImeUpdate` applies it on the title's thread, where the filter also runs with the mutex released - like the event handler, so a filter that calls back into `sceIme*` cannot deadlock - and the structural edit and the filtered commit are serialized under the mutex, with a commit after the session closed or reopened dropped by a session id. The filter is guest code and `ImeTextFilter` and `ImeExtKeyboardFilter` are declared `APS5_VABI` like `EventHandler`. The filter is called with the edited text, a buffer of `maxTextLength` UTF-16 units and its length; a nonzero return, an output longer than the capacity, an unpaired surrogate or, without the multiline option, a line break keeps the edit unfiltered. An accepted output replaces the text, the caret stays where the edit put it and is clamped to the text and normalized past a surrogate pair, and one UpdateText is sent with the minimal edit delta of the whole old and new text, as KytyPS5 computes it; an output equal to the previous text sends no event. Caret moves, PressEnter without multiline, PressClose, `sceImeSetText` and `sceImeSetCaret` do not run the filter, as in KytyPS5; a `sceImeSetText` or `sceImeSetCaret` that lands while the filter runs bumps a revision beside the session id, and the filtered commit is dropped when either changed, as KytyPS5 drops the commit when its revision changed, so the title's own text or caret setting wins, the edit is discarded and sends no event. An external keyboard filter is accepted and stored on the session: it is called only for keyboard events that carry key data, which the host input path does not produce - it delivers text and actions, as KytyPS5's host API queues external inputs with a zeroed key and skips the filter for them - so it is never called - [sceImeVshNotifyCurrentTextInfo and sceImeVshNotifyCurrentTextInfoForReconvertText](../../../core/libs/prx/libSceIme/Export.cpp): assembly confirms seven and eight integer/pointer arguments, including the stack arguments, with no incoming SIMD parameter. Both take a context, UTF-16 string, signed 32-bit caret index and two pairs of 32-bit preedit/selection values. Pair endpoint-versus-length semantics and the reconversion option remain unresolved. Protected bodies are unchanged. +- [sceImeOpen](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) tracks a host composition (SDL_TEXTEDITING, forwarded by [ImeTextInput](../../../core/libs/prx/libSceVideoOut/src/ImeTextInput.cpp)) and never delivers it to the title: the preedit is not in the text, the buffers, the text filter or the events, and the console's delivery of a preedit to a title (a text area of mode Preedit) is not measured. While a composition is active Enter and Escape only discard it, and Backspace, Delete, the arrow keys, Home, End and paste are ignored, assuming an input method takes those keys itself; no real input method was tried on any platform. Closes when: measured with a CJK input method on Linux, Windows and macOS +- Pasting (Ctrl+V, Shift+Insert, Command+V on macOS) into [sceImeOpen](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) and [sceImeDialogInit](../../../core/libs/prx/libSceImeDialog/Export.cpp) is a host feature with no console counterpart measured: the clipboard is read only on the key press, converted to UTF-16 with invalid bytes as U+FFFD, controls stripped, line breaks dropped (kept as LF for a multi-line `sceImeOpen` session) and cut to 64 KiB after SDL has already copied the whole text. The panel type and the password option are not modelled by the host, so a numeric or password field accepts what a typed key would; only the title's text filter shapes a paste. Copy and cut are not implemented, as there is no selection diff --git a/docs/dev/debt/libSceImeDialog.md b/docs/dev/debt/libSceImeDialog.md index 1a5297436d..0c2709c190 100644 --- a/docs/dev/debt/libSceImeDialog.md +++ b/docs/dev/debt/libSceImeDialog.md @@ -9,7 +9,7 @@ - [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 when the text is accepted, 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 it is accepted +- [sceImeDialogInit, sceImeDialogGetStatus, sceImeDialogGetResult, sceImeDialogAbort, sceImeDialogTerm](../../../core/libs/prx/libSceImeDialog/Export.cpp) (libSceImeDialog) - statuses (0-2), end statuses (0-2) and `sceImeDialogInit` errors (`0x80bc0001`, `0x80bc0031`, `0x80bc0016`, `0x80bc0017`) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/b2d7ec7d31f9ad9426db93afe079db8679b3e1e0/src/core/libraries/ime/ime_dialog.cpp); unmeasured on PS5. In `sceImeDialogGetResult`, returning `0x80bc0002` for null differs from shadPS4 (`0x80bc0031`), and for non-null unmapped pointer is an unmeasured host-safety fallback. Abort or term on a non-finished dialog returns `0x80bc0002` (shadPS4: `0x80bc0105`-`0x80bc0107`), and `sceImeDialogTerm` closes running dialogs. Accepted text writes to buffer once - [Pad capture](../../../core/libs/prx/libSceImeDialog/Export.cpp) (libSceImeDialog) - while the dialog's keyboard holds the controller, every pad read of every port ([PadState](../../../core/libs/prx/libScePad/src/PadState.cpp)) returns a neutral sample: buttons `0x80000000` and nothing else, sticks centred, triggers released, no touch and no motion, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/30f4170896ce974353a6f6486d0ecec179aec54b/src/core/libraries/pad/pad.cpp#L376-L389) (PS4) reports its `Intercepted` bit. Neither the bit nor the rest of the sample is measured on a PS5. Buttons still held when the keyboard closes stay hidden from the title until each is released. Closes when: measured on a PS5 - [sceImeDialogGetResult, sceImeDialogTerm](../../../core/libs/prx/libSceImeDialog/Export.cpp) (libSceImeDialog) - while the keyboard is open, `sceImeDialogGetResult` returns `0x80bc0002` and `sceImeDialogTerm` closes the keyboard and returns 0. [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/30f4170896ce974353a6f6486d0ecec179aec54b/src/core/libraries/ime/ime_dialog.cpp#L933-L939) (PS4) returns `DIALOG_NOT_FINISHED` (`0x80bc0106`) from both and keeps its keyboard open ([Term](https://github.com/shadps4-emu/shadPS4/blob/30f4170896ce974353a6f6486d0ecec179aec54b/src/core/libraries/ime/ime_dialog.cpp#L1331-L1334)). Neither is measured on a PS5. Closes when: measured on a PS5 @@ -21,3 +21,4 @@ - [Keyboard redraw](../../../core/libs/prx/libSceImeDialog/Export.cpp) (libSceImeDialog) - the keyboard is drawn into the frames the title flips, from the dialog's state at each flip ([VideoOutDriver](../../../core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp)). A title that stops flipping while the dialog runs shows the last keyboard it flipped, and typing changes nothing on screen until it flips again. Closes when: a title is seen to wait on the dialog without flipping - [Held-button repeat](../../../core/libs/prx/libSceImeDialog/src/PadKeyboard.cpp) (libSceImeDialog) - held Cross (on a character, Space, Backspace or arrow key), D-pad, Square, L1 and R1 repeat after 0.275 s and then every 0.050 s, Dear ImGui's `ImGuiIO` defaults `KeyRepeatDelay` and `KeyRepeatRate`, which [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/30f4170896ce974353a6f6486d0ecec179aec54b/src/core/libraries/ime/ime_ui_shared.cpp#L756-L757) (PS4) reads. The left stick repeats on shadPS4's own tilt-scaled timings. None of these is measured on a PS5. Closes when: measured on a PS5 - [Keyboard extras](../../../core/libs/prx/libSceImeDialog/src/PadKeyboard.cpp) (libSceImeDialog) - the language switch, `...` menu and R3 settings keys of [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/30f4170896ce974353a6f6486d0ecec179aec54b/src/core/libraries/ime/ime_kb_layout.cpp#L490-L492) (PS4) are left as empty cells, and there is no R3 control mode, no right-stick panel drag, no prediction pane, no top close panel ([kTopPanelDefaultElements](https://github.com/shadps4-emu/shadPS4/blob/30f4170896ce974353a6f6486d0ecec179aec54b/src/core/libraries/ime/ime_kb_layout.cpp#L966-L969)) and no copy or paste. Glyphs outside the pages, such as CJK text, are drawn as a box +- [sceImeDialogInit](../../../core/libs/prx/libSceImeDialog/Export.cpp) (libSceImeDialog) shows a host composition at the caret in the keyboard text field, underlined with the selected part underlined twice, and keeps it out of the buffer the title reads. The overlay font has no CJK glyphs, so a composition and committed CJK text draw as the replacement box, which shows the length but not the characters. Closes when: the overlay gets a font with CJK coverage diff --git a/docs/dev/debt/libSceIpmi.md b/docs/dev/debt/libSceIpmi.md index e60923e470..fa4b20dfcd 100644 --- a/docs/dev/debt/libSceIpmi.md +++ b/docs/dev/debt/libSceIpmi.md @@ -8,7 +8,7 @@ ### Known export names; ABI prototypes unverified -- 40 pending exports in [Export.cpp](../../../core/libs/prx/libSceIpmi/Export.cpp) are present in the [PS5-3.20_Libs](https://github.com/DNNDHH/PS5-3.20_Libs) inventory. Their prototypes and behavior are unverified. They use the no-parameter import-resolution convention and throw through `NotImplemented_nid_no_patch`; these declarations make no ABI or behavior claim. +- 40 pending exports in [Export.cpp](../../../core/libs/prx/libSceIpmi/Export.cpp) are present in the [PS5-3.20_Libs](https://github.com/DNNDHH/PS5-3.20_Libs) inventory. Each is named after the mangled symbol the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv) gives for its NID, and the NID of that name matches. Their prototypes and behavior are unverified. They use the no-parameter import-resolution convention and throw through `NotImplemented_nid_no_patch`; these declarations make no ABI or behavior claim. ### Additional callable exports diff --git a/docs/dev/debt/libSceLibcInternal.md b/docs/dev/debt/libSceLibcInternal.md index 55edd94988..a8130c2a23 100644 --- a/docs/dev/debt/libSceLibcInternal.md +++ b/docs/dev/debt/libSceLibcInternal.md @@ -6,7 +6,7 @@ The named entries below use provider-specific NID aliases to avoid interposing h - The [revision-pinned identity inventory](https://gist.github.com/kyon-eth/6bd58c3d7a0fa933dd4b524754c43265/e4e5dbc43890a24e35dc1c656135000210e28390) records module, library, symbol type and NIDs for [InventoryExports.cpp](../../../core/libs/prx/libSceLibcInternal/InventoryExports.cpp); it is a metadata summary rather than independent verification. One entry belongs to the logical library `libSceLibcInternalExt` within this module. -- [Additional callable exports](../../../core/libs/prx/libSceLibcInternal/InventoryExports.cpp) - 297 entries observed in a 3.20-labelled image and a later 13.60-labelled image; 291 match the public 3.20 catalogue and 6 do not. These are declarations only; neither image label authenticates a retail firmware version. Signatures and behavior are unverified; parameterless `int` declarations are placeholders. Every entry throws through `NotImplemented_nid_no_patch`. All declarations use literal NID aliases; the table records candidate names where known. No runtime functionality is implemented; data exports need separate analysis. Identified candidate names match their NIDs in the [public symbol database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv) or [supplemental catalog](https://github.com/claimore22/ps5rs/blob/2304be5131221bae0dac24b9767cabffda33f747/data/nids.csv). +- [Additional callable exports](../../../core/libs/prx/libSceLibcInternal/InventoryExports.cpp) - 295 entries observed in a 3.20-labelled image and a later 13.60-labelled image; 289 match the public 3.20 catalogue and 6 do not. These are declarations only; neither image label authenticates a retail firmware version. Signatures and behavior are unverified; parameterless `int` declarations are placeholders. Every entry throws through `NotImplemented_nid_no_patch`. All declarations use literal NID aliases; the table records candidate names where known. No runtime functionality is implemented; data exports need separate analysis. Identified candidate names match their NIDs in the [public symbol database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv) or [supplemental catalog](https://github.com/claimore22/ps5rs/blob/2304be5131221bae0dac24b9767cabffda33f747/data/nids.csv). | Name | NID | | --- | --- | @@ -28,7 +28,6 @@ The named entries below use provider-specific NID aliases to avoid interposing h | `_ZNSt10moneypunctIwLb1EED1Ev` | `OM0FnA7Tldk` | | `_ZNSt10moneypunctIwLb1EED2Ev` | `uVOxy7dQTFc` | | `_ZNSt12future_errorD0Ev` | `rNYLEsL7M0k` | -| `_ZNSt12future_errorD2Ev` | `XFh0C66aEms` | | `_ZNSt12system_errorC2ESt10error_codePKc` | `oly3wSwEJ2A` | | `_ZNSt13bad_exceptionD0Ev` | `lX9M5u0e48k` | | `_ZNSt13bad_exceptionD1Ev` | `t6egllDqQ2M` | @@ -54,14 +53,12 @@ The named entries below use provider-specific NID aliases to avoid interposing h | `_ZNSt13basic_ostreamIwSt11char_traitsIwEE6sentryD2Ev` | `MB1VCygerRU` | | `_ZNSt13basic_ostreamIwSt11char_traitsIwEED0Ev` | `7VRfkz22vPk` | | `_ZNSt13basic_ostreamIwSt11char_traitsIwEED1Ev` | `EYZJsnX58DE` | -| `_ZNSt14overflow_errorD2Ev` | `OypvNf3Uq3c` | | `_ZNSt15basic_streambufIcSt11char_traitsIcEE6xsgetnEPci` | `VpwymQiS4ck` | | `_ZNSt15basic_streambufIcSt11char_traitsIcEE6xsputnEPKci` | `sXaxl1QGorg` | | `_ZNSt15basic_streambufIcSt11char_traitsIcEE9showmanycEv` | `IAEl1Ta7yVU` | | `_ZNSt15basic_streambufIwSt11char_traitsIwEE6xsgetnEPwi` | `lZVehk7yFok` | | `_ZNSt15basic_streambufIwSt11char_traitsIwEE6xsputnEPKwi` | `041c37QfoUc` | | `_ZNSt15basic_streambufIwSt11char_traitsIwEE9showmanycEv` | `olsoiZsezkk` | -| `_ZNSt15underflow_errorD2Ev` | `xiqd+QkuYXc` | | `_ZNSt16nested_exceptionD1Ev` | `+qKS53qzWdA` | | `_ZNSt16nested_exceptionD2Ev` | `8R00hgzXQDY` | | `_ZNSt22_Future_error_categoryD0Ev` | `WDKzMM-uuLE` | @@ -298,8 +295,6 @@ The named entries below use provider-specific NID aliases to avoid interposing h | `_seekdir` | `Do3zPpsXj1o` | | `backtrace_game` | `jFSmlgyyEUg` | | `clock_1700` | `n8onIBR4Qdk` | -| `devname_r` | `BIALMFTZ75I` | -| `devname` | `fqLrWSWcGHw` | | `malloc_initialize` | `owT6zLJxrTs` | | `Unknown` | `GG6441JdYkA` | | `Unknown` | `QZ9YgTk+yrE` | @@ -329,6 +324,8 @@ The named entries below use provider-specific NID aliases to avoid interposing h - [_sceLibcInternalThreadAtexit, _sceLibcInternalThreadDtors](../../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) - FreeBSD `__cxa_thread_atexit` semantics assumed; a null or non-image destructor and an allocation failure throw - [_sceLibcInternalForceTlsDestructor](../../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) - behaviour follows `sceLibcForceTlsDestructor(int)` in a title's `libc.prx` (disassembly): the calling thread's destructors whose dso belongs to the module run newest first, only if the dso's module is loaded and the dso points to itself, in up to 4 passes. The return type is unknown (that function returns nothing), 0 is returned. The module of each dso is looked up when the destructors are forced, not when they are registered as `libc.prx` does +- [Recovered inventory exports](../../../core/libs/prx/libSceLibcInternal/RecoveredInventory.cpp) (libSceLibcInternal) - all 40 parameterless `int` declarations are import-resolution placeholders with unverified parameter lists, return types and behavior; every call throws through `NotImplemented_nid_no_patch`. The names and NIDs are listed in the [PS5 13.60 inventory](https://github.com/xdCloudy/PS5-13.60-Function-Stub-Library/blob/226383a7fa4b1275d6cebf61eb1c1e7ca7b58bcb/data/firmware-13.60/libscelibcinternal-f9a6d6abff.jsonl), and the [physical-module map](https://github.com/boykopovar/ps5-nid-map/blob/cc1d586fddab576a520f226da6b2632b031e6d09/data/Modules/libSceLibcInternal.inc) assigns all 40 NIDs to libSceLibcInternal. Neither source establishes the ABI or behavior. + ### Functional - [makecontext](../../../core/libs/prx/libSceLibcInternal/UserContext.cpp) (libSceLibcInternal) follows FreeBSD 11's `lib/libc/amd64/gen/makecontext.c`: at most six arguments, a stack of at least `MINSIGSTKSZ` (2048) bytes, and `exit(0)` when the function returns with no `uc_link`. The console's version is not verified @@ -337,9 +334,11 @@ The named entries below use provider-specific NID aliases to avoid interposing h - [__opendir2](../../../core/libs/prx/libSceLibcInternal/Directory.cpp) (libSceLibcInternal, NID `KOy7MeQ7OAU`) - the device's answer to any flag is not measured; the flags follow FreeBSD 11.4's [`__opendir2`](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libc/gen/opendir.c#L76-L95) - [fdopendir](../../../core/libs/prx/libSceLibcInternal/Directory.cpp) (libSceLibcInternal) follows FreeBSD 11's [`fdopendir`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/lib/libc/gen/opendir.c#L67-L74): it sets `FD_CLOEXEC` on the descriptor first, and the stream starts at the descriptor's current position instead of the first entry. On Windows the stream skips the entries the descriptor's cursor (`lseek` or `getdirentries`) has passed, which assumes that the descriptor and the stream list a directory in the same host order; on Linux the stream is opened on a duplicate of the descriptor, which shares its position. The Linux path is not run by the tests here. As in FreeBSD, a descriptor that is not open fails with `EBADF`, one that is not a directory with `ENOTDIR` (both leave it open, and `FD_CLOEXEC` stays set on the latter), and `closedir` closes it - [fstatvfs](../../../core/libs/prx/libSceLibcInternal/FilesystemStatistics.cpp) (libSceLibcInternal) converts `_fstatfs` to the 88-byte `struct statvfs` as FreeBSD 9.2's `sfs2svfs` does (`f_bsize` from the transfer size, `f_frsize` from the block size, `f_favail` equal to `f_ffree`, `f_fsid` 0, `ST_RDONLY` and `ST_NOSUID` only). Differences: `f_namemax` is the `statfs` value, not `fpathconf(_PC_NAME_MAX)`; negative `f_bavail` and `f_ffree` wrap to unsigned without `EOVERFLOW`, as the same-width copy in FreeBSD does; inode counts are 0 on Windows, inherited from `_fstatfs`; a null output returns `EFAULT` after the descriptor is validated, where FreeBSD would fault. Layout and conversion are unverified against the PS5 library (PS5MC imports `fstatvfs` but was not run) -- [posix_spawn and posix_spawnp](../../../core/libs/prx/libSceLibcInternal/Spawn.cpp) (libSceLibcInternal) create a native child running the requested program's converted image, which must be under `/app0/` (`/app0/eboot.bin` maps to the running image); `posix_spawnp` searches `PATH` as FreeBSD 11's `_execvpe`, and the FreeBSD 11.4 file actions and attributes are applied to the child. A null `argv` returns `EFAULT`; a null `envp` inherits the parent environment; guest process IDs are the host's. Limits: the child executes the conversion made at relink time; Windows redirects only descriptors 0 to 2 and starts the child on the parent's std handles; Linux applies only `POSIX_SPAWN_RESETIDS` and `POSIX_SPAWN_SETPGROUP`, other flags or a nonzero set on Windows throw. The console sandbox's effect on child creation is unverified, and null or uninitialized objects, which FreeBSD dereferences, throw. PPSA99305 (Dusklight) reaches this when SDL looks for `zenity` +- [posix_spawn and posix_spawnp](../../../core/libs/prx/libSceLibcInternal/Spawn.cpp) (libSceLibcInternal) have unverified console sandbox and error equivalence. POSIX errors missing from the shared translator fall back to `EIO`. Only converted programs under `/app0/` run (`/app0/eboot.bin` uses the running image). Windows open/duplication actions redirect only descriptors 0 to 2; nonzero attribute flags throw. POSIX hosts implement only `POSIX_SPAWN_RESETIDS` and `POSIX_SPAWN_SETPGROUP`; other flags throw. Guest IDs are host IDs, a null `argv` returns `EFAULT`, and null or uninitialized objects throw where FreeBSD dereferences them. A null `envp` inherits the parent environment. `posix_spawnp` follows FreeBSD 11 `_execvpe` path search; PPSA99305 reaches it while SDL searches for `zenity` - [getopt, getopt_long](../../../core/libs/prx/libSceLibcInternal/Getopt.cpp) (libSceLibcInternal) follow FreeBSD 11 [getopt.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdlib/getopt.c) and [getopt_long.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdlib/getopt_long.c) with `GNU_COMPATIBLE`: `getopt` does not permute, `getopt_long` permutes unless `POSIXLY_CORRECT` is set in the guest environment or the option string starts with `+`, and the exported `opterr`, `optind`, `optopt`, `optreset` and `optarg` start at 1, 1, 0, 0 and null. Warnings go to the guest's current `stderr` prefixed with `__progname`, where FreeBSD's `warnx` keeps the `stderr` of its first call. A null `getopt` option string, a null argument vector or argument, a negative `optind` and an `optind` moved inside a pending permutation throw where FreeBSD faults. `getopt_long_only` is not exported. That the console's `libSceLibcInternal` uses this FreeBSD code is unverified +- [_ZNSt4_PadD1Ev](../../../core/libs/prx/libSceLibcInternal/Pad.cpp) (libSceLibcInternal, NID `XyJPhPqpzMw`) is the complete-object destructor of `_Pad`, which has no virtual bases, so the [Itanium C++ ABI](https://itanium-cxx-abi.github.io/cxx-abi/abi.html#definitions) makes it do the same work as the base-object `D2` destructor; it runs the `libc` export of that `D2` name, which unlocks and destroys the pad's mutex and condition and throws from a thread that does not hold the mutex. The console's `libSceLibcInternal.sprx` was not read and no title was seen calling it - [posix_spawnattr_getflags, getpgroup, getschedparam, getschedpolicy, getsigdefault, getsigmask, setpgroup, setschedparam, setschedpolicy and setsigmask](../../../core/libs/prx/libSceLibcInternal/Spawn.cpp) (libSceLibcInternal) read and write the 0x30-byte attribute object at the offsets of the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x8D860 to 0x8D910: flags at 0, process group at 4, scheduling parameter at 8, policy at 0xC, default and masked signal sets at 0x10 and 0x20), which never fail there and return 0; the layout matched on a PS5 (firmware 13.60). Uninitialized attributes and null pointers, which the console dereferences, throw as the existing setters do +- [Optional PID outputs in posix_spawn and posix_spawnp](../../../core/libs/prx/libSceLibcInternal/Spawn.cpp) (libSceLibcInternal) are unverified on the console; null PID outputs follow [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/posix_spawn.c#L279-L287), creating a child that can still be reaped with `waitpid` - [sys_siglist and sys_nsig](../../../core/libs/prx/libSceLibcInternal/SignalList.cpp) (libSceLibcInternal) hold the 32 descriptions and the count of [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/siglist.c#L73-L107) `siglist.c` (`NSIG` 32, signal 0 to `SIGUSR2`); the console's table was not read - [_err, _warn](../../../core/libs/prx/libSceLibcInternal/Err.cpp) and [_getprogname](../../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) are assumed to be the same functions as `err`, `warn` and `getprogname` and run their code; the console's module was not read. FreeBSD 11.4 defines the underscore names and makes the public names weak references to them ([`_err`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/err.c#L70-L79), [`_warn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/err.c#L135-L144), [`_getprogname`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/getprogname.c#L10-L17)) - [inet_aton, inet_addr, inet_ntoa and inet_ntoa_r](../../../core/libs/prx/libSceLibcInternal/Inet.cpp) (libSceLibcInternal) follow the FreeBSD code in the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x8ADE0 to 0x8B3D0) and matched it on a PS5 (firmware 13.60) for 20 `inet_aton` inputs; a null text, which the console dereferences, throws. `inet_ntoa_r` formats through libkernel's `__inet_ntop`, so a null buffer with room for the text throws and a null buffer that is too small sets `ENOSPC`, as `__inet_ntop` does for a null destination; the console's `inet_ntoa_r` was not measured with a null buffer @@ -497,6 +496,7 @@ The named entries below use provider-specific NID aliases to avoid interposing h - [_ZNKSt7num_putIwSt19ostreambuf_iteratorIwSt11char_traitsIwEEE6do_putES3_RSt8ios_basewx](../../../core/libs/prx/libSceLibcInternal/Err.cpp) (libSceLibcInternal) - NID `9SSHrlIamto` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. - [_ZNKSt7num_putIwSt19ostreambuf_iteratorIwSt11char_traitsIwEEE6do_putES3_RSt8ios_basewy](../../../core/libs/prx/libSceLibcInternal/Err.cpp) (libSceLibcInternal) - NID `uX0nKsUo8gc` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. - [_ZnamSt11align_val_tRKSt9nothrow_t and _ZdaPvSt11align_val_t](../../../core/libs/prx/libSceLibcInternal/AlignedNew.cpp) (libSceLibcInternal, NIDs `s2eGsgUF9vk` and `v09ZcAhZzSc`) take the standard C++17 signatures their mangled names give, the aligned `nothrow` array `operator new` and the aligned array `operator delete`, and run the same code as the `libc` exports of those names, on the same application heap and new handler. The console's `libSceLibcInternal.sprx` was not read and no title was seen calling them, so whether it allocates from the same heap as `libc.prx` is unverified +- [_ZNSt12future_errorD2Ev, _ZNSt14overflow_errorD2Ev and _ZNSt15underflow_errorD2Ev](../../../core/libs/prx/libSceLibcInternal/ExceptionDestructors.cpp) (libSceLibcInternal, NIDs `XFh0C66aEms`, `OypvNf3Uq3c` and `xiqd+QkuYXc`) are the base-object destructors of classes without virtual bases, which the [Itanium C++ ABI](https://itanium-cxx-abi.github.io/cxx-abi/abi.html#definitions) makes do the same work as the complete-object `D1` destructors, so they run the `libc` exports of those `D1` names. The console's `libSceLibcInternal.sprx` was not read and no title was seen calling them, so whether it frees the message from the same heap as `libc.prx` is unverified - [_Mtxinit, _Mtxlock, _Mtxunlock, _Mtxdst](../../../core/libs/prx/libSceLibcInternal/Mutex.cpp) (libSceLibcInternal) follow the PS4 code of [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/860d73b7e99559106a3ffa3d3d69f0f9237e115e/src/core/libraries/libc_internal/libc_internal_threads.cpp#L10-L62) and [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/src/libcinternal/ps4_mtx_internal.pas#L47-L99): the argument points to a pointer-sized libkernel mutex handle, `_Mtxinit` creates it recursive, a zeroed handle becomes an error-checking mutex on its first lock as with the POSIX calls, and each returns 0 on success and 1 on any failure. A null handle pointer throws in libkernel. The `SceLibcI_` name goes to `scePthreadMutexInit`, which drops it. For PS5 only the names are known (NID hashes, ps5sdk stubs); layout, signatures, mutex type and results are not verified on a PS5 ### Additional PS5 3.20 imports (xdCloudy #9498) @@ -598,7 +598,6 @@ The named entries below use provider-specific NID aliases to avoid interposing h - [_ZSt11setiosflagsNSt5_IosbIiE9_FmtflagsE](../../../core/libs/prx/libSceLibcInternal/InventoryExports6.cpp) (libSceLibcInternal, NID `L7Vnk06zC2c`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibcInternal.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [__atomic_load_n](../../../core/libs/prx/libSceLibcInternal/InventoryExports6.cpp) (libSceLibcInternal, NID `Q6oqEnefZQ8`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibcInternal.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [_ZNSt8ios_base4InitC2Ev](../../../core/libs/prx/libSceLibcInternal/InventoryExports6.cpp) (libSceLibcInternal, NID `bTQcNwRc8hE`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibcInternal.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` -- [_ZNSt4_PadD1Ev](../../../core/libs/prx/libSceLibcInternal/InventoryExports6.cpp) (libSceLibcInternal, NID `XyJPhPqpzMw`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibcInternal.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [_malloc_thread_cleanup](../../../core/libs/prx/libSceLibcInternal/InventoryExports6.cpp) (libSceLibcInternal, NID `Sopthb9ztZI`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibcInternal.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [_ZNSt8time_putIcSt19ostreambuf_iteratorIcSt11char_traitsIcEEED1Ev](../../../core/libs/prx/libSceLibcInternal/InventoryExports6.cpp) (libSceLibcInternal, NID `h+c9OSfCAEg`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibcInternal.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` - [_ZNSt8ios_baseD1Ev](../../../core/libs/prx/libSceLibcInternal/InventoryExports6.cpp) (libSceLibcInternal, NID `X9D8WWSG3As`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibcInternal.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` diff --git a/docs/dev/debt/libSceLibreSsl.md b/docs/dev/debt/libSceLibreSsl.md index 47857b9aa7..296026380b 100644 --- a/docs/dev/debt/libSceLibreSsl.md +++ b/docs/dev/debt/libSceLibreSsl.md @@ -7,16 +7,17 @@ ### Unknown function info -- [libSceLibreSsl pending exports](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - 287 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) - 231 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibreSsl.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. - [MD4, MD5, SHA1, SHA224, SHA256, SHA384 and SHA512 Init/Update/Final and SHA256](../../../core/libs/prx/libSceLibreSsl/src/Digest.cpp) (libSceLibreSsl) - signatures and context layouts follow the [LibreSSL headers](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/sha/sha.h), and buffering and `Final` follow its [sources](https://github.com/libressl/openbsd/tree/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/sha); nothing is checked on the console. Null contexts, null input with a nonzero length, null digest buffers in the 32-bit `Final` functions and a buffered byte count that is not below the block size, which LibreSSL does not check, throw. SHA-384 and SHA-512 initialization retains the block buffer, following [LibreSSL 2.5.4 `sha512.c`](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/sha/sha512.c) - [DES_set_key, DES_ecb_encrypt, DES_set_odd_parity](../../../core/libs/prx/libSceLibreSsl/Export.cpp) (libSceLibreSsl) - the 128-byte `DES_key_schedule` holds the 8-byte key followed by zeros instead of LibreSSL's precomputed subkeys, so a title reading its contents sees different bytes; `DES_ecb_encrypt` derives the subkeys through mbedtls on each call. Not compared with the console. Results follow LibreSSL [des_key.c](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/des/des_key.c) and [des.c](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/des/des.c): `DES_set_key` skips the parity and weak-key checks and returns 0, as it does while `DES_check_key` is 0 (that variable is not in the PS5 export inventory), a nonzero `enc` encrypts, and `DES_set_odd_parity` sets each byte's low bit for odd parity. Null pointers throw where LibreSSL faults - [X509_verify_cert_error_string](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - no console call is verified. The 64-bit argument, low-32-bit lookup, messages and shared numeric fallback follow [LibreSSL 2.5.4](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/x509/x509_txt.c), the version identified in [published firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214). Console modifications and other firmware versions remain unverified. Unknown-code results share an overwritten static buffer and are not safe for concurrent use, as in that release; code 1 and codes above 54 use the numeric fallback. - [CRYPTO_num_locks, CRYPTO_set_locking_callback, CRYPTO_set_add_lock_callback, CRYPTO_THREADID_set_callback, CRYPTO_THREADID_set_numeric](../../../core/libs/prx/libSceLibreSsl/Export.cpp) (libSceLibreSsl) - the locking, add-lock and thread id callbacks are stored but never called: LibreSSL calls them from `CRYPTO_lock`, `CRYPTO_add_lock` and `CRYPTO_THREADID_current`, which the module does not export, around its own shared state. Nothing is checked on the console. Results follow [LibreSSL 2.5.4](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/cryptlib.c), the version in the [firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214): 41 locks, the first non-null thread id callback is kept and later ones return 0, and `CRYPTO_THREADID_set_numeric` clears the 16-byte `{ptr, val}` structure; a null structure throws, as LibreSSL writes through it unchecked - [RAND_add, RAND_bytes, RAND_status, RAND_load_file, RAND_file_name, CRYPTO_malloc, CRYPTO_free, CRYPTO_set_mem_functions](../../../core/libs/prx/libSceLibreSsl/src/Runtime.cpp) (libSceLibreSsl) - follow LibreSSL's [rand_lib.c, randfile.c](https://github.com/libressl/openbsd/tree/0f0b16dc977f6ca499f0f4540078d992c96da597/src/lib/libcrypto/rand) and [malloc-wrapper.c](https://github.com/libressl/openbsd/blob/0f0b16dc977f6ca499f0f4540078d992c96da597/src/lib/libcrypto/malloc-wrapper.c), which are the same in 2.6.5 (3.4.3 linked); nothing is checked on the console. `RAND_bytes` reads host entropy and `CRYPTO_malloc` and `CRYPTO_free` use the guest heap. `CRYPTO_malloc` returns null for a size of zero or less, as LibreSSL 2.5.4 to 3.4 do; later versions take a `size_t` size and call `malloc`. A null buffer with a positive length throws - [AES_set_encrypt_key, AES_set_decrypt_key, AES_encrypt, AES_decrypt, AES_ecb_encrypt, AES_cbc_encrypt, AES_cfb128_encrypt, AES_ctr128_encrypt](../../../core/libs/prx/libSceLibreSsl/src/Aes.cpp) (libSceLibreSsl) - follow LibreSSL's [aes.h](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/aes/aes.h) and [mode code](https://github.com/libressl/openbsd/tree/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/modes). Nothing is checked on the console. Key setup follows [LibreSSL 2.5.4's aes_core.c](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/aes/aes_core.c), the version the `X509_verify_cert_error_string` entry names: a null pointer returns -1 and a key length other than 128, 192 or 256 bits returns -2 before the key is written, and only the 44, 52 or 60 words the key length needs are written. The `AES_KEY` schedule uses that file's generic C layout (big-endian words, `rounds` 10, 12 or 14). The 2.5.4 amd64 build uses [aes-x86_64](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/aes/asm/aes-x86_64.pl) instead, which copies the key bytes without swapping, so each word is byte-reversed compared with the C layout (OpenSSL 3.0.13's x86-64 code gives the same schedules byte-reversed); the console's build options are unknown, so a schedule the title reads or fills is unverified. CTR leaves the last keystream block in `ecount_buf` even at a block boundary. Null pointers in the cipher and mode functions, other `rounds` values and a CFB or CTR position outside 0-15 throw -- [ERR_get_error, ERR_peek_error, ERR_clear_error, ERR_remove_thread_state](../../../core/libs/prx/libSceLibreSsl/src/Error.cpp) have no console execution verification or production error producers. The 16-slot calling-thread queue retains 15 codes following LibreSSL err.c and err.h. [Firmware 13.42 module comparisons on a PC](https://github.com/boykopovar/AnyPS5/pull/9535#issuecomment-6407060914) matched libSceLibreSsl3 queue ordering and overflow for 0-20 publications; whether the two firmware libraries share a queue was not tested. Explicit thread IDs throw because guest thread identity mapping is unverified; null clears the calling thread. File/line metadata is stored by the native producer helper but no guest export exposes it. +- [ERR_get_error, ERR_peek_error, ERR_clear_error, ERR_remove_thread_state](../../../core/libs/prx/libSceLibreSsl/src/Error.cpp) have no console execution verification. Memory BIO and CMAC failures publish to this queue. The 16-slot calling-thread queue retains 15 codes following LibreSSL err.c and err.h. [Firmware 13.42 module comparisons on a PC](https://github.com/boykopovar/AnyPS5/pull/9535#issuecomment-6407060914) matched libSceLibreSsl3 queue ordering and overflow for 0-20 publications; whether the two firmware libraries share a queue was not tested. Explicit thread IDs throw because guest thread identity mapping is unverified; null clears the calling thread. File/line metadata is stored by the native producer helper but no guest export exposes it. - [OCSP_response_status_str, OCSP_cert_status_str and OCSP_crl_reason_str](../../../core/libs/prx/libSceLibreSsl/src/Ocsp.cpp) (libSceLibreSsl) - the module's strings are not measured on the console. The tables follow LibreSSL [ocsp_prn.c](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/ocsp/ocsp_prn.c), unchanged from revision 1.8 (2015) to 1.12 (2025), with codes from `ocsp.h`; the argument is a 64-bit `long` and every value not in a table returns `(UNKNOWN)` - [SSL_get_ex_new_index, SSL_CTX_get_ex_new_index, SSL_get_ex_data_X509_STORE_CTX_idx and CRYPTO_cleanup_all_ex_data](../../../core/libs/prx/libSceLibreSsl/src/ExData.cpp) (libSceLibreSsl) - unverified on a PS5. The stored `argl`, `argp` and callbacks are never called, because no export creates an `SSL` or `SSL_CTX` and `SSL_get_ex_data`/`SSL_set_ex_data` still throw. Index numbering follows LibreSSL 2.5.4's [ex_data.c](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/ex_data.c#L310-L406), [ssl_lib.c](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libssl/ssl_lib.c#L2828-L2854) and [ssl_cert.c](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libssl/ssl_cert.c#L133-L160): per class from 0, the verify-callback index allocated once and kept across `CRYPTO_cleanup_all_ex_data`, which restarts every class at 0. +- [EVP digest and HMAC functions](../../../core/libs/prx/libSceLibreSsl/src/Evp.cpp) (libSceLibreSsl) - follow LibreSSL 2.5.4's [digest.c](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/evp/digest.c) and [hmac.c](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/hmac/hmac.c). They and the `EVP_MD`, `EVP_MD_CTX` and `HMAC_CTX` layouts behave the same up to 3.4.1, so this holds for 2.5.4 (the firmware 13.40 measurements) and for 2.7 or later (its `SSL_get0_param` export). Nothing is checked on the console. `sign` and `verify` in the `EVP_MD` tables are null as `RSA_sign` and `RSA_verify` still throw, failures add nothing to the error queue, and a failed allocation leaves the context without a digest, where LibreSSL keeps it. An `ENGINE`, an `EVP_PKEY_CTX`, a null context, `EVP_DigestInit_ex` without a type on a context that has a digest, and an update or final without a digest throw - [SSL_CIPHER_get_bits, SSL_CIPHER_get_name and SSL_CIPHER_get_version](../../../core/libs/prx/libSceLibreSsl/src/Cipher.cpp) - the 88-byte guest cipher layout and these calls are not console-verified. They follow [LibreSSL 2.5.4 ssl.h](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libssl/ssl.h) and [ssl_ciph.c](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libssl/ssl_ciph.c), matching the version in [published firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214). Console modifications and other firmware layouts remain unverified. Protocol labels use the full 64-bit cipher ID, not the protocol flags; TLS 1.2 ciphers still report `TLSv1/SSLv3`, as in that release. - [sk_num, sk_value, sk_pop, sk_pop_free](../../../core/libs/prx/libSceLibreSsl/src/Stack.cpp) (libSceLibreSsl) - the 32-byte `_STACK` layout (`num`, `data`, `sorted`, `num_alloc`, `comp`) and the results follow [LibreSSL 2.5.4](https://github.com/libressl/openbsd/tree/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/stack) `stack.h` and `stack.c`, the version identified in [published firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214); nothing is checked on the console. `sk_pop_free` releases the element array and the stack with the guest `free`, as the module would through libc. No export of this module creates a stack yet, so only stacks the caller builds reach them. A null element array that LibreSSL would read through, and a null free function with a non-null element, throw where LibreSSL faults @@ -30,6 +31,16 @@ ### Functional +- [Error publication](../../../core/libs/prx/libSceLibreSsl/ErrorQueue.hpp) is integrated with memory BIO and CMAC cipher errors. The digest, low-level AES, allocator/RAND, DES, BIGNUM and EVP/HMAC implementations require separate error-publication integration. `LibreSslPutError_nid_no_patch(int library, int function, int reason, const char* file, int line)` has C linkage and the native host calling convention, accepts upstream numeric fields and publishes to the calling thread's queue. Three-argument source calls default file/line to null/zero. It stores file/line without taking ownership; producers must supply a filename with static lifetime. Guest ERR entry points use `APS5_VABI`. Unsupported producer exports continue throwing; the local tests publish through this helper. + +- [CMAC and AES-CBC descriptors](../../../core/libs/prx/libSceLibreSsl/src/Cmac.cpp) have no PS5 equivalence verification. Signatures, the 88-byte cipher method and 168-byte cipher context follow [LibreSSL 3.4.1](https://github.com/libressl/openbsd/blob/078b3d1df8ca2011c1605cd40418c58fa0f5b143/src/lib/libcrypto/cmac/cmac.c); the console version remains unknown. AES cipher data uses Mbed TLS storage instead of LibreSSL's private key schedule. Guest-defined cipher methods run through guest-ABI callbacks. Other built-in cipher selectors and the general EVP cipher exports still throw. ENGINE-backed methods throw. Null contexts, null nonempty update buffers, null initialized-final length pointers, negative cipher dimensions and missing required callbacks throw where upstream dereferences unchecked pointers. Error metadata names host source locations. + +- [BN_new, BN_free, BN_CTX_new, BN_CTX_free, BN_CTX_start, BN_CTX_end, BN_bin2bn, BN_bn2bin, BN_hex2bn, BN_num_bits, BN_set_word, BN_add_word, BN_sub_word, BN_mul and BN_mod_inverse](../../../core/libs/prx/libSceLibreSsl/src/Bignum.cpp) (libSceLibreSsl) - the `BIGNUM` layout (24 bytes, 64-bit limbs, `top`, `dmax`, `neg`, `flags`) and the documented semantics follow the public LibreSSL 2.x API; the module's LibreSSL version is unknown and nothing is checked on the console. `BN_CTX` is opaque and only counts `BN_CTX_start` and `BN_CTX_end`, since `BN_CTX_get` is not implemented, and `BN_mul` and `BN_mod_inverse` ignore it. `BN_mod_inverse` treats a negative modulus by its magnitude and returns 0 for a modulus of 1, which is not compared with LibreSSL. `BN_hex2bn` rejects a string without a hex digit, such as `-`, which older LibreSSL versions accept. Null `BIGNUM` pointers, which LibreSSL does not check, throw. `BN_print` still throws because it needs a `BIO`. + +- [Memory BIOs](../../../core/libs/prx/libSceLibreSsl/src/Bio.cpp) have no console execution verification. [Firmware 13.42 module comparisons on a PC](https://github.com/boykopovar/AnyPS5/pull/9536#issuecomment-6407192928) cover 1,048 sequences with stand-in allocators, including null/zero writes, callbacks and growth wiping. They support dispatching nonnegative writes to the method even with null source or zero length; LibreSSL 3.4.1 instead guards those calls. Negative public lengths retain that source's early return because the module report did not measure them. The 104-byte BIO, 80-byte method and 24-byte buffer use LP64 layouts. Allocator padding is zeroed rather than retaining the module allocator's fill. Other firmware and console imports remain unverified. CRYPTO ex-data, file, socket and SSL BIOs remain unsupported. + +- [Memory BIO unchecked inputs](../../../core/libs/prx/libSceLibreSsl/src/Bio.cpp) throw for null method or raw line-buffer pointers, negative raw method write sizes, an overflowing raw line-buffer size, inconsistent read-only buffer lengths, and memory write lengths that overflow size_t. Upstream does not validate these inputs. An unrepresentable string length throws instead of narrowing into an unchecked write. + - [ERR string formatting](../../../core/libs/prx/libSceLibreSsl/ErrorQueue.cpp) is not console-verified. [Firmware 13.42 modules run natively on a PC](https://github.com/boykopovar/AnyPS5/pull/9535#issuecomment-6407060914) produced numeric-only strings before and after their loaders. Formatting, truncation and caller/static buffers matched with a standard-C snprintf stand-in; other firmware, console imports and concurrent static-buffer use remain unverified. Loading and freeing do not enable named strings or alter queued errors. Historical tables are generated only as regression-test inputs. - [ASN1_INTEGER_get](../../../core/libs/prx/libSceLibreSsl/src/Asn1.cpp) has no console verification for nonpositive lengths. It follows [LibreSSL 2.5.4](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/asn1/a_int.c#L363-L389): valid integer types return zero without reading data when length is zero or negative; invalid types return -1 first. Later LibreSSL versions reject negative lengths through `ASN1_INTEGER_get_int64`; that later contract is not used for this library. diff --git a/docs/dev/debt/libSceLibreSsl3.md b/docs/dev/debt/libSceLibreSsl3.md index fcdcf46879..67eef01d34 100644 --- a/docs/dev/debt/libSceLibreSsl3.md +++ b/docs/dev/debt/libSceLibreSsl3.md @@ -2,11 +2,13 @@ ### Functional +- [OpenSSL_version](../../../core/libs/prx/libSceLibreSsl3/Export.cpp) - other firmware revisions are unverified. Selectors 0 through 5 follow the public firmware 13.40 measurements in [discussion #8981](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214); unknown selectors follow LibreSSL 4.0.0 and return `not available`. No independent console test was run. + - [ERR_load_crypto_strings and ERR_peek_last_error](../../../core/libs/prx/libSceLibreSsl3/Export.cpp) have no console execution verification. [Firmware 13.42 modules run on a PC](https://github.com/boykopovar/AnyPS5/pull/9535#issuecomment-6407060914) retained numeric-only output after loaders and matched queue order/overflow; whether the two firmware modules share a queue was not tested. This implementation reads libSceLibreSsl's calling-thread queue. `ERR_print_errors_cb` remains throwing because guest thread identifiers and diagnostic metadata are unverified. ### Unknown function info -- [libSceLibreSsl3 pending exports](../../../core/libs/prx/libSceLibreSsl3/Export.cpp) - 77 named callable exports, excluding `CRYPTO_memcmp`, `MD5`, `SHA1`, `d2i_ASN1_INTEGER`, `EVP_PKEY_up_ref`, `EC_KEY_up_ref`, `RSA_up_ref`, `EVP_PKEY_get0_RSA`, `EVP_PKEY_get0_DSA` and `EVP_PKEY_get0_DH`, remain unimplemented from the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibreSsl3.c), cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. +- [libSceLibreSsl3 pending exports](../../../core/libs/prx/libSceLibreSsl3/Export.cpp) - 76 named callable exports, excluding `CRYPTO_memcmp`, `MD5`, `SHA1`, `d2i_ASN1_INTEGER`, `EVP_PKEY_up_ref`, `EC_KEY_up_ref`, `RSA_up_ref`, `EVP_PKEY_get0_RSA`, `EVP_PKEY_get0_DSA`, `EVP_PKEY_get0_DH` and `OpenSSL_version`, 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 @@ -22,9 +24,11 @@ - [X509_VERIFY_PARAM_set_flags](../../../core/libs/prx/libSceLibreSsl3/src/X509VerifyParam.cpp) (libSceLibreSsl3) - nothing is checked on the console. It ORs the flags into `flags` and returns 1, as both [LibreSSL 2.5.4](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/x509/x509_vpm.c) and [4.0.0](https://github.com/libressl/openbsd/blob/ce488b78e0bd994780569df718596b9776ff681a/src/lib/libcrypto/x509/x509_vpm.c) `x509_vpm.c` do. They differ when `EXPLICIT_POLICY`, `INHIBIT_ANY` or `INHIBIT_MAP` is set while `POLICY_CHECK` is neither set nor requested: 2.5.4 also adds `POLICY_CHECK` and 4.0.0 does not. The console's choice is unknown, so that case throws through `NotImplemented_nid_no_patch` and leaves the parameters unchanged. It writes libSceLibreSsl's 2.5.4 `LibreSslX509VerifyParam`, whose `flags` is at offset 24 as in 4.0.0. Null parameters throw. Closes when: the console's policy behaviour is measured. - [sk_new, sk_push and sk_free](../../../core/libs/prx/libSceLibreSsl3/src/Stack.cpp) (libSceLibreSsl3) - follow [LibreSSL 4.0.0](https://github.com/libressl/openbsd/blob/ce488b78e0bd994780569df718596b9776ff681a/src/lib/libcrypto/stack/stack.c) `stack.c` with libSceLibreSsl's 32-byte `_STACK` layout, which `stack_local.h` keeps there. Stacks and element arrays come from the guest heap, so the forwarded `sk_pop_free` can release them. Nothing is checked on the console. Where LibreSSL faults or overflows, they throw: `sk_push` on a null stack, on a negative element count, on a null element array it does not regrow, or when the element array cannot double without overflowing `int` +- [ASN1_TIME_diff](../../../core/libs/prx/libSceLibreSsl3/src/Time.cpp) has no console ABI or behavior verification. Its signature and date rules follow LibreSSL 4.0.0 [a_time.c](https://github.com/libressl/openbsd/blob/ce488b78e0bd994780569df718596b9776ff681a/src/lib/libcrypto/asn1/a_time.c), [a_time_tm.c](https://github.com/libressl/openbsd/blob/ce488b78e0bd994780569df718596b9776ff681a/src/lib/libcrypto/asn1/a_time_tm.c) and [a_time_posix.c](https://github.com/libressl/openbsd/blob/ce488b78e0bd994780569df718596b9776ff681a/src/lib/libcrypto/asn1/a_time_posix.c), using the existing 24-byte string layout. Null inputs use the host wall clock; the console clock relationship is unverified. Null output pointers throw after valid input parsing, where the reference dereferences them. Closes when: measured on a PS5. + ### Additional callable export inventory -- [Additional exports](../../../core/libs/prx/libSceLibreSsl3/AdditionalExports.cpp) declare 176 callable exports: 50 named and 126 known only by NID. Every entry throws. Parameterless `int` declarations are unknown-signature placeholders, not verified ABI contracts. Names/NIDs match the 13.60 export table. No functional implementation or title unblock is claimed. +- [Additional exports](../../../core/libs/prx/libSceLibreSsl3/AdditionalExports.cpp) declare 175 callable exports: 165 named and 10 known only by NID. Every entry throws. Parameterless `int` declarations are unknown-signature placeholders, not verified ABI contracts. Names/NIDs match the 13.60 export table. No functional implementation or title unblock is claimed. | Name | NID | | --- | --- | @@ -195,7 +199,6 @@ | `tls_config_insecure_noverifyname` | `wNQT6Zn4UG4` | | `X509_EXTENSION_free` | `wbJDMMsje6s` | | `ASN1_TYPE_free` | `whL1IRBxFZA` | -| `ASN1_TIME_diff` | `xBkcXV1OqWQ` | | `ECDSA_SIG_set0` | `xVv3qlSRldc` | | `OBJ_obj2txt` | `xZ1BME5qDv0` | | `SSL_set_quic_transport_params` | `xf6anoZpWRM` | @@ -205,4 +208,4 @@ | `ECDSA_SIG_get0` | `z8fsXF8JuL0` | | `EVP_MD_block_size` | `zCa75KLWWn4` | -- 116 of the 126 exports the 13.60 table lists only by NID carry, in the table above and in [Additional exports](../../../core/libs/prx/libSceLibreSsl3/AdditionalExports.cpp), the LibreSSL names whose `tools/nid_names.py` hash equals the NID; the names were taken from the identifiers of the LibreSSL 3.8.4 and 4.0.0 `include`, `crypto`, `ssl` and `tls` sources. A name match does not show that the console function has LibreSSL's prototype or behavior; each keeps the no-parameter import-resolution convention and throws through `NotImplemented_nid_no_patch`. `AES_wrap_key`, `ECDSA_SIG_free`, `ECDSA_do_verify`, `ERR_load_ERR_strings`, `ERR_print_errors_fp` and `i2d_PUBKEY` also hash to the NIDs of six `unknown` rows, which keep their NID names; `PBYz0kpXwg8` hashes to no identifier of those sources. +- 115 of the 126 exports the 13.60 table lists only by NID carry, in the table above and in [Additional exports](../../../core/libs/prx/libSceLibreSsl3/AdditionalExports.cpp), the LibreSSL names whose `tools/nid_names.py` hash equals the NID; the names were taken from the identifiers of the LibreSSL 3.8.4 and 4.0.0 `include`, `crypto`, `ssl` and `tls` sources. A name match does not show that the console function has LibreSSL's prototype or behavior; each keeps the no-parameter import-resolution convention and throws through `NotImplemented_nid_no_patch`. `AES_wrap_key`, `ECDSA_SIG_free`, `ECDSA_do_verify`, `ERR_load_ERR_strings`, `ERR_print_errors_fp` and `i2d_PUBKEY` also hash to the NIDs of six `unknown` rows, which keep their NID names; `PBYz0kpXwg8` hashes to no identifier of those sources. diff --git a/docs/dev/debt/libSceLoginService.md b/docs/dev/debt/libSceLoginService.md index db043fd9c7..3a3bfa14ce 100644 --- a/docs/dev/debt/libSceLoginService.md +++ b/docs/dev/debt/libSceLoginService.md @@ -2,8 +2,8 @@ ### Silent stubs -- [sceLoginServiceInitialize, sceLoginServiceTerminate](../../../core/libs/prx/libSceLoginService/Export.cpp) (libSceLoginService) - return success and keep no login-service state: AnyPS5 has no PSN login service. EA SPORTS FC 27 (PPSA34015) loads the module with `sceSysmoduleLoadModule(0xe3)` and calls `sceLoginServiceInitialize` with no arguments; a missing `libSceLoginService.prx` stops Windows start-up with `Failed to load module` / `GetLastError: 126` +- [sceLoginServiceInitialize, sceLoginServiceTerminate](../../../core/libs/prx/libSceLoginService/Export.cpp) (libSceLoginService) - return success and track initialization state: AnyPS5 has no PSN login service. EA SPORTS FC 27 (PPSA34015) loads the module with `sceSysmoduleLoadModule(0xe3)` and calls `sceLoginServiceInitialize` with no arguments; a missing `libSceLoginService.prx` stops Windows start-up with `Failed to load module` / `GetLastError: 126` ### Unknown function info -- [sceLoginServiceRequestDevices](../../../core/libs/prx/libSceLoginService/Export.cpp): the native wrapper confirms one read-only request pointer and transfers exactly 64 bytes to the login service. Null input returns 0x813a0003 before the initialization check; an uninitialized service returns 0x813a0001. The request fields are interpreted by the service backend and remain unresolved. The protected export body is unchanged. +- [sceLoginServiceRequestDevices](../../../core/libs/prx/libSceLoginService/Export.cpp): the native wrapper confirms one read-only request pointer and transfers exactly 64 bytes to the login service. Null input returns 0x813a0003 before the initialization check; an uninitialized service returns 0x813a0001. The request fields are interpreted by the service backend and remain unresolved. diff --git a/docs/dev/debt/libSceMbus.md b/docs/dev/debt/libSceMbus.md index 89c4ff5e82..6a51009a7e 100644 --- a/docs/dev/debt/libSceMbus.md +++ b/docs/dev/debt/libSceMbus.md @@ -22,7 +22,7 @@ - [sceMbus debug process and device queries](../../../core/libs/prx/libSceMbus/PendingExports.cpp): process information is transmitted as 16 bytes, startup input/output as eight bytes, control status as 24 bytes, internal information as 2832 bytes and priority information as 3088 bytes. Device enumeration writes a 32-bit count followed by 396-byte entries. These pointee fields and the semantics of unnamed process flags remain unresolved; declarations use distinct incomplete types instead of guessing their fields. -- [sceMbus event subscriptions](../../../core/libs/prx/libSceMbus/PendingExports.cpp): the subscription transports two 32-bit and two 64-bit fields whose service-side meanings remain unresolved. EventCreate uses 80-byte queue records and copies a 16-byte callback table; event fields still require the dispatch path; the overflow callback takes an event handle and user data, and the event callback additionally takes a pointer to the queued event. EventCallbackFuncsInit has no defined return value and its recovered behavior is to clear memory. The second argument is treated here as a byte count, but the source analysis in [merged PR #9137](https://github.com/boykopovar/AnyPS5/pull/9137) does not record firmware, function offset or instructions establishing whether the native routine clamps it to the 16-byte callback table; this must be verified before claiming full parity. A null callback pointer with a nonzero size is not validated and is not measured on a console. Eight dump/BGM wrappers have confirmed zero-argument ABIs. EventBusStatusChangeUnsubscribe ignores its first incoming argument and consumes a 32-bit second argument; its first parameter type remains unresolved. +- [sceMbus event subscriptions](../../../core/libs/prx/libSceMbus/PendingExports.cpp): the subscription transports two 32-bit and two 64-bit fields whose service-side meanings remain unresolved. EventCreate uses 80-byte queue records and copies a 16-byte callback table; event fields still require the dispatch path; the overflow callback takes an event handle and user data, and the event callback additionally takes a pointer to the queued event. EventCallbackFuncsInit has no defined return value and its recovered behavior is to clear memory. The second argument is treated here as a byte count, but the source analysis in [merged PR #9137](https://github.com/boykopovar/AnyPS5/pull/9137) does not record firmware, function offset or instructions establishing whether the native routine clamps it to the 16-byte callback table; this must be verified before claiming full parity. The 11.00 wrapper ignores a null callback pointer before clearing, and the host now does the same; that case is not measured on a console and the 13.42 behaviour remains unverified. Eight dump/BGM wrappers have confirmed zero-argument ABIs. EventBusStatusChangeUnsubscribe ignores its first incoming argument and consumes a 32-bit second argument; its first parameter type remains unresolved. - [sceMbus device and driver payloads](../../../core/libs/prx/libSceMbus/PendingExports.cpp): device-info records occupy 256 bytes, descriptions 264 bytes, simulated-handle status and Spark state 16 bytes each. Their service-side fields remain unresolved. Driver registration copies four callback pointers; driver-event notification transports two 64-bit values without dereferencing them. Init, IsBgmProhibited, ReenableBgmForShellUi and RemoveCameraAppModuleFocus consume no incoming arguments. diff --git a/docs/dev/debt/libSceMouse.md b/docs/dev/debt/libSceMouse.md index df77103a23..3dbb311dfc 100644 --- a/docs/dev/debt/libSceMouse.md +++ b/docs/dev/debt/libSceMouse.md @@ -21,8 +21,9 @@ - [sceMouseDisconnectAll, sceMouseVirtualDeviceAddDevice, sceMouseVirtualDeviceDeleteDevice and sceMouseVirtualDeviceGetCursor](../../../core/libs/prx/libSceMouse/InventoryExports.cpp) - the names and NIDs are recorded in the [13.60 export inventory](https://github.com/xdCloudy/PS5-13.60-Function-Stub-Library/blob/e495d3cf24aea17cefda59decdfda9e3e4e3e5ff/data/firmware-13.60/libscemouse-acdd5b5e23.jsonl). Signatures and behavior remain unknown; the parameterless `int` declarations only resolve imports and every call throws. Not measured on a console. - [sceMouseConnectPort, sceMouseDebugGetDeviceId, sceMouseDeviceOpen, sceMouseDisconnectDevice, sceMouseDisconnectPort, sceMouseGetDeviceInfo, sceMouseMbusInit, sceMouseSetHandType, sceMouseSetPointerSpeed and sceMouseSetProcessPrivilege](../../../core/libs/prx/libSceMouse/Export.cpp) (libSceMouse) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceMouse.c); the no-argument prototypes follow [shadPS4's PS4 stubs](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/mouse/mouse.cpp#L34-L146), not measured on a console. These declarations have no parameters solely to resolve known imports and make no ABI claim. -- [sceMouseReadAbsolutePosition, sceMouseSetAbsoluteHighPrecisionMode and sceMouseSetAbsoluteVirtualScreenSize](../../../core/libs/prx/libSceMouse/Export.cpp) (libSceMouse) - the names match unresolved NIDs reported by a title, but their signatures and behavior are not verified. These no-argument declarations only resolve imports; the exports throw if called. ### Functional -- [sceMouseResetAbsolutePosition](../../../core/libs/prx/libSceMouse/Export.cpp) (libSceMouse) - the position is not stored because the host reports relative motion only. Native input widths and validation are confirmed, including the default 1920 by 1080 bounds; other native coordinate modes and absolute reports remain unimplemented +- [Publish](../../../core/libs/prx/libSceMouse/src/mouse_impl.cpp) (libSceMouse) - consecutive motion-only reports with the same buttons are summed into the newest unread entry, so a title that reads a few entries per frame gets current motion instead of a full queue of old reports. Reports with no motion and no button, wheel, tilt or connection change are dropped. Per-report granularity and the console's own report rate are not reproduced + +- [sceMouseResetAbsolutePosition, sceMouseSetAbsoluteVirtualScreenSize, sceMouseSetAbsoluteHighPrecisionMode, sceMouseReadAbsolutePosition](../../../core/libs/prx/libSceMouse/src/mouse_impl.cpp) (libSceMouse) - follow firmware 13.42 [run natively](https://github.com/boykopovar/AnyPS5/issues/11477#issuecomment-6502712241) with the driver stubbed: checks and order, screen sizes and rescale, the mode byte, the curve with its fraction, the 40-byte record. Host choices, after [XatiX's title-tested version](https://github.com/boykopovar/AnyPS5/issues/11477#issuecomment-6503782633): each host event is one sample with unscaled counts, in its own queue, so `sceMouseRead`'s motion merging does not feed the curve. A read takes all pending samples; past `num`, same-button neighbours share a record, each still moving the pointer, and the last `num` records are kept. Speed is taken as 4, the hand swap and flagged bit never apply, and a disconnected mouse stands in for a zero driver status diff --git a/docs/dev/debt/libSceMsgDialog.native.md b/docs/dev/debt/libSceMsgDialog.native.md index 48eb0c188b..e52604f7c0 100644 --- a/docs/dev/debt/libSceMsgDialog.native.md +++ b/docs/dev/debt/libSceMsgDialog.native.md @@ -6,4 +6,4 @@ ### Unknown function info -- [sceMsgDialogGetResult](../../../core/libs/prx/libSceMsgDialog.native/Export.cpp) (libSceMsgDialog.native) returns the button id the player picked in a shown dialog, and 1 for every other dialog and for one the title closed; the result code is always 0. Which button ids and result codes a PS5 reports after a close or a cancel is not measured. `sceMsgDialogOpen` closes a dialog that is still waiting and shows the new one, instead of failing as libSceMsgDialog does with busy. Closes when: measured on a PS5 +- [sceMsgDialogGetResult](../../../core/libs/prx/libSceMsgDialog.native/Export.cpp) (libSceMsgDialog.native) returns the button id the player picked in a shown dialog, and 1 for every other dialog and for one the title closed; the result code is always 0. Which button ids and result codes a PS5 reports after a close or a cancel is not measured. `sceMsgDialogOpen` closes a dialog that is still waiting and shows the new one, instead of failing as libSceMsgDialog does with busy. Returning `0x80b8000d` (`COMMON_DIALOG_ERROR_ARG_NULL`) for a non-null unwritable result pointer is a host-safety fallback; on a console an unmapped pointer faults and its answer is not measured. Closes when: measured on a PS5 diff --git a/docs/dev/debt/libSceNKWebKit.md b/docs/dev/debt/libSceNKWebKit.md index 3e29741ba1..e181103836 100644 --- a/docs/dev/debt/libSceNKWebKit.md +++ b/docs/dev/debt/libSceNKWebKit.md @@ -1,11 +1,15 @@ # libSceNKWebKit technical debt +Inventory prose retains historical counts; use the [generated progress report](https://boykopovar.github.io/AnyPS5/progress.html) for current totals. + ### Unknown function info - [WebKit API objects](../../../core/libs/prx/libSceNKWebKit/src/Object.hpp) (libSceNKWebKit) - `WK*` objects are host objects numbered with `API::Object::Type` as [APIObject.h](https://github.com/neonmodder123/PS5-WebKit-11.x/blob/80f77e94c63ee7202eec3d4b5b1a021dd89011ba/Source/WebKit/Shared/API/APIObject.h) builds for PlayStation, with ApplicationManifest, InspectorExtension, WebExtension and WebAuthn types off and PopupMenuItem and AccessibilityObject on per [OptionsPlayStation.cmake](https://github.com/neonmodder123/PS5-WebKit-11.x/blob/80f77e94c63ee7202eec3d4b5b1a021dd89011ba/Source/cmake/OptionsPlayStation.cmake); the console module version, its type numbers and its object layouts are unverified, so these objects cannot be passed to a console WebKit module. A null or wrong-type reference throws where WebKit dereferences it. - [WTF and WebCore value layouts](../../../core/libs/prx/libSceNKWebKit/src/WtfStringAbi.hpp) (libSceNKWebKit) - `WTF::String`, `StringImpl`, `StringView`, `CString`, `URL` and the WebCore geometry, transform and `Color` layouts follow the release 64-bit headers at the [pinned source](https://github.com/neonmodder123/PS5-WebKit-11.x/blob/80f77e94c63ee7202eec3d4b5b1a021dd89011ba/Source/WTF/wtf/text/StringImpl.h) and are pinned with `static_assert`; they are tested on Windows, not against console binaries. `StringImpl::destroy`, atom and symbol strings and external buffers still throw, so a string whose last reference guest code drops terminates. - [Media preferences](../../../core/libs/prx/libSceNKWebKit/src/Preferences.cpp) (libSceNKWebKit) - unset `MediaSourceEnabled`, `EncryptedMediaAPIEnabled` and `SourceBufferChangeTypeEnabled` getters throw, and feature-key setters for `EncryptedMediaAPIEnabled`, `SourceBufferChangeTypeEnabled` and `WebMParserEnabled` throw, because the PlayStation media player configuration behind `ENABLE_MEDIA_SOURCE` and `ENABLE_ENCRYPTED_MEDIA` is not in the pinned source. Other defaults evaluate [UnifiedWebPreferences.yaml](https://github.com/neonmodder123/PS5-WebKit-11.x/blob/80f77e94c63ee7202eec3d4b5b1a021dd89011ba/Source/WTF/Scripts/Preferences/UnifiedWebPreferences.yaml) for PlayStation; downstream console overrides are unverified. +- [FastBitVector exports](../../../core/libs/prx/libSceNKWebKit/src/WtfFastBitVector.cpp) have no console ABI or behavior verification. The 16-byte owner (word pointer at 0, 64-bit bit count at 8), range clearing, growth and copying follow the paired header and [PS5 11.x WebKit source](https://github.com/neonmodder123/PS5-WebKit-11.x/blob/80f77e94c63ee7202eec3d4b5b1a021dd89011ba/Source/WTF/wtf/FastBitVector.cpp). `resizeSlow` preserves the stored bit count and unused bits of existing words; new words are zeroed. Allocation uses the module's `fastMalloc` and `fastFree`. Invalid ranges, null accessed storage, shrinking the word count and overflowing the upstream round-up throw instead of reproducing assertion failures or invalid memory access. Closes when: compared with a console module. + ### Functional - [Constant-return exports](../../../core/libs/prx/libSceNKWebKit/src/ConstantExports.cpp) (libSceNKWebKit) - 137 exports answer with a fixed value in the PS5 11.00 module: 111 execute `xor eax,eax; ret`, 13 `mov eax,imm32; ret` and 13 `mov al,1; ret` (addresses and bodies from the 11.00 `libSceNKWebKit.sprx`, SHA-256 `3af992253665387feeb5df90ff27dff683e3aa270cc4d92a0535b46ab18fa20d`; NIDs recomputed from the declared names). The 124 EAX-writing bodies measure the 32-bit return; the 13 `mov al,1` bodies write only the low byte, so for them only AL is measured and the upper bits are unspecified. The declared prototypes stay the no-parameter `int` placeholders, so no other width, pointer or floating-point return is claimed. Only 11.00 was measured. @@ -14,6 +18,7 @@ - [Dictionary key order](../../../core/libs/prx/libSceNKWebKit/src/Dictionary.cpp) (libSceNKWebKit) - `WKDictionaryCopyKeys` returns keys sorted by UTF-16 code unit, not in the `WTF::HashMap` iteration order of [WKDictionary.cpp](https://github.com/neonmodder123/PS5-WebKit-11.x/blob/80f77e94c63ee7202eec3d4b5b1a021dd89011ba/Source/WebKit/Shared/API/c/WKDictionary.cpp). - [Out-of-line colors](../../../core/libs/prx/libSceNKWebKit/src/WebCoreColor.cpp) (libSceNKWebKit) - `WebCore::Color` operations on an out-of-line value (extended color spaces in [Color.h](https://github.com/neonmodder123/PS5-WebKit-11.x/blob/80f77e94c63ee7202eec3d4b5b1a021dd89011ba/Source/WebCore/platform/graphics/Color.h)) throw; only inline 8-bit sRGBA values are handled. - [WTF host services](../../../core/libs/prx/libSceNKWebKit/src/WtfSystem.cpp) (libSceNKWebKit) - `fastMalloc`, `bmalloc` and Gigacage allocations come from the host allocator, and `numberOfProcessorCores`, `ramSize`, the clocks and random values come from the host, not the console; they throw when the host cannot answer. `WTFCrash` and failed assertions abort the process. +- [WebCore::ResourceResponseBase](../../../core/libs/prx/libSceNKWebKit/src/ResourceResponse.cpp) (libSceNKWebKit) - the object layout in [ResourceResponseAbi.hpp](../../../core/libs/prx/libSceNKWebKit/src/ResourceResponseAbi.hpp) comes from [ResourceResponseBase.h](https://github.com/neonmodder123/PS5-WebKit-11.x/blob/80f77e94c63ee7202eec3d4b5b1a021dd89011ba/Source/WebCore/platform/network/ResourceResponseBase.h) and is not checked against the PS5 module: `URL` at 0, four `AtomString` and the 64-bit expected length up to offset 80, then a 32-byte `HTTPHeaderMap` (two vectors of inline capacity 0 as in [HTTPHeaderMap.h](https://github.com/neonmodder123/PS5-WebKit-11.x/blob/80f77e94c63ee7202eec3d4b5b1a021dd89011ba/Source/WebCore/platform/network/HTTPHeaderMap.h)). Only these fields are modelled because `std::optional` follows the `Box` and the size of the PS5 `CertificateInfo` is unknown, so the offsets of `m_httpStatusCode`, `m_isNull`, `m_initLevel`, `m_type`, `m_source` and the flags are not established. The getters (`url`, `mimeType`, `expectedContentLength`, `textEncodingName`, `httpStatusText`, `httpVersion`, `httpHeaderFields`, `isInHTTPFamily`) skip `lazyInit` because the curl [ResourceResponse.h](https://github.com/neonmodder123/PS5-WebKit-11.x/blob/80f77e94c63ee7202eec3d4b5b1a021dd89011ba/Source/WebCore/platform/network/curl/ResourceResponse.h) has no `platformLazyInit`; the PS5 `ResourceResponse` is not known to match. `isInHTTPFamily` reads the valid bit of `URL::bits` and the scheme text, not the cached `m_protocolIsInHTTPFamily` bit, whose position is unknown. The 30 other exports (`ResourceResponseBase` constructors, `setURL`, `setType`, `setExpectedContentLength`, `setHTTPStatusCode`, `httpStatusCode`, `source`, `isSuccessful`, `isRedirection`, `isHTTP09`, `isAttachment*`, `suggestedFilename`, `sanitize*`, `syntheticRedirectResponse`, `cacheControlContains*`, `hasCacheValidatorFields`, `lazyInit`, `httpHeaderField*`, `addHTTPHeaderField*`, `setHTTPHeaderField*`) still throw `NotImplemented_nid_no_patch`: they write `m_isNull`, read fields after `CertificateInfo`, or need `HTTPHeaderMap`, `CertificateInfo`, `Vector` or `ResourceResponse` layouts. ### Known export names; ABI prototypes unverified @@ -27,7 +32,7 @@ - The [revision-pinned identity inventory](https://gist.github.com/kyon-eth/d44995f181e1818977bd38f0e6fc0f34/8afcb14b0707a09b6274ff7f7bcf33ef46cfe2e7) records the module, library, symbol type and NIDs for the declarations in [Export.cpp](../../../core/libs/prx/libSceNKWebKit/Export.cpp); it is a metadata summary rather than independent verification. For the 255 absent from the public catalogue, [per-image ELF records](https://gist.github.com/kyon-eth/147be8dd556e9cced2577ccbed548972/1a659ecfe819e724675991a574a1aebb5fbb48e5) include the raw qualified names, module and library identities, symbol type, values and sizes; the record file SHA-256 is `44bdc4d6073192a8f41b7be359b75364b3d41dbbd11ce649924b8cebdd4ffe51`. Image labels are not authenticated. -- [Additional callable exports](../../../core/libs/prx/libSceNKWebKit/Export.cpp) - 7439 of the 8703 inventory declarations listed below throw through `NotImplemented_nid_no_patch`; signatures and behavior are unverified. The later 13.60-labelled image contains all 8703; 8448 match the public 3.20 catalogue, while 255 are absent and their presence in retail 3.20 is unverified. Per-image records show the latter in a separate 3.20-labelled image, but neither image label is authenticated. Identified names match NIDs in the [public symbol database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv) or [supplemental catalog](https://github.com/claimore22/ps5rs/blob/2304be5131221bae0dac24b9767cabffda33f747/data/nids.csv). Data exports need separate analysis. +- [Additional callable exports](../../../core/libs/prx/libSceNKWebKit/Export.cpp) - 7437 of the 8703 inventory declarations listed below throw through `NotImplemented_nid_no_patch`; signatures and behavior are unverified. The later 13.60-labelled image contains all 8703; 8448 match the public 3.20 catalogue, while 255 are absent and their presence in retail 3.20 is unverified. Per-image records show the latter in a separate 3.20-labelled image, but neither image label is authenticated. Identified names match NIDs in the [public symbol database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv) or [supplemental catalog](https://github.com/claimore22/ps5rs/blob/2304be5131221bae0dac24b9767cabffda33f747/data/nids.csv). Data exports need separate analysis. | Name | NID | | --- | --- | @@ -1599,7 +1604,6 @@ | `_ZN3WTF12CountingLock10unlockSlowEv` | `y9CL7tutRKQ` | | `_ZN3WTF12CountingLock8lockSlowEv` | `QSRjO+sqoFU` | | `_ZN3WTF12isMainThreadEv` | `MKhFWAdCyCw` | -| `_ZN3WTF13FastBitVector10clearRangeEmm` | `q1+R1z2aE0E` | | `_ZN3WTF13MetaAllocator17addFreshFreeSpaceEPvm` | `RPLKRR7vgYE` | | `_ZN3WTF13MetaAllocator17currentStatisticsERKNS_6LockerINS_4LockEEE` | `3SW2Rs+BCYE` | | `_ZN3WTF13MetaAllocator17freeFreeSpaceNodeEPNS0_13FreeSpaceNodeE` | `sIQ2j-9PjgU` | @@ -1760,7 +1764,6 @@ | `_ZN3WTF19ParallelEnvironmentC1EPFvPvEmi` | `Efn-a1tGPqw` | | `_ZN3WTF19ParallelEnvironmentC2EPFvPvEmi` | `ciXM3j2URKk` | | `_ZN3WTF19isCompilationThreadEv` | `qoO6VaMuvG0` | -| `_ZN3WTF19mimeTypeFromDataURLENS_10StringViewE` | `kie+nv-1HCM` | | `_ZN3WTF19numGraphemeClustersENS_10StringViewE` | `K7qAbz7P3rs` | | `_ZN3WTF20ConcurrentPtrHashSet15deleteOldTablesEv` | `K59xDVxbCyM` | | `_ZN3WTF20ConcurrentPtrHashSet5clearEv` | `sDuWnr1d9ko` | @@ -1804,8 +1807,6 @@ | `_ZN3WTF22CrossThreadTaskHandlerD0Ev` | `aMwGXRZ6024` | | `_ZN3WTF22CrossThreadTaskHandlerD1Ev` | `RQGUZHWVCLY` | | `_ZN3WTF22CrossThreadTaskHandlerD2Ev` | `F9bDxOEGeA8` | -| `_ZN3WTF22FastBitVectorWordOwner10resizeSlowEm` | `NetEd5QBLRw` | -| `_ZN3WTF22FastBitVectorWordOwner13setEqualsSlowERKS0_` | `qY895ndHoiI` | | `_ZN3WTF22TextBreakIteratorCache9singletonEv` | `m9nEKMAp80k` | | `_ZN3WTF22isMainThreadOrGCThreadEv` | `UHbf2fZIrxk` | | `_ZN3WTF23callOnMainThreadAndWaitEONS_8FunctionIFvvEEE` | `95FhkfvoGQM` | @@ -6054,7 +6055,6 @@ | `_ZNK3WTF3URL19protocolHostAndPortEv` | `q8fEMeIqd5w` | | `_ZNK3WTF3URL21truncatedForUseAsBaseEv` | `uekVJTVqBGc` | | `_ZNK3WTF3URL24strippedForUseAsReferrerEv` | `CyN41AniueA` | -| `_ZNK3WTF3URL30stringCenterEllipsizedToLengthEj` | `JRPrmfTHt08` | | `_ZNK3WTF3URL4dumpERNS_11PrintStreamE` | `ITwB6pPwmRc` | | `_ZNK3WTF3URL4userEv` | `ETN4mn+-5l0` | | `_ZNK3WTF3URL8passwordEv` | `aCtpeXWclTE` | @@ -6730,29 +6730,21 @@ | `_ZNK7WebCore20PasteboardCustomData18createSharedBufferEv` | `oNgcYv7sg2E` | | `_ZNK7WebCore20PasteboardWriterData7isEmptyEv` | `pCjXnlN6R4c` | | `_ZNK7WebCore20RenderBoxModelObject18inlineContinuationEv` | `nV9al8rbOKg` | -| `_ZNK7WebCore20ResourceResponseBase11httpVersionEv` | `qJpr45shtzs` | | `_ZNK7WebCore20ResourceResponseBase12isAttachmentEv` | `C+morSMfP84` | | `_ZNK7WebCore20ResourceResponseBase12isSuccessfulEv` | `3a6B43jFjX8` | | `_ZNK7WebCore20ResourceResponseBase13isRedirectionEv` | `BrSesfxNNvs` | | `_ZNK7WebCore20ResourceResponseBase14httpStatusCodeEv` | `bFhYDNofRSE` | -| `_ZNK7WebCore20ResourceResponseBase14httpStatusTextEv` | `MYd7F5R6ryw` | -| `_ZNK7WebCore20ResourceResponseBase14isInHTTPFamilyEv` | `wW+Pyy9LiGA` | | `_ZNK7WebCore20ResourceResponseBase15httpHeaderFieldENS_14HTTPHeaderNameE` | `JkEYUFSjoXw` | -| `_ZNK7WebCore20ResourceResponseBase16httpHeaderFieldsEv` | `cots5mYtZpo` | -| `_ZNK7WebCore20ResourceResponseBase16textEncodingNameEv` | `wTRdXzKM3u4` | | `_ZNK7WebCore20ResourceResponseBase17suggestedFilenameEv` | `fBVtigUQ-qk` | -| `_ZNK7WebCore20ResourceResponseBase21expectedContentLengthEv` | `i4zxQPHkqJs` | | `_ZNK7WebCore20ResourceResponseBase23hasCacheValidatorFieldsEv` | `qAv1-ceZ9iQ` | | `_ZNK7WebCore20ResourceResponseBase24isAttachmentWithFilenameEv` | `ClZ8vxvE28U` | | `_ZNK7WebCore20ResourceResponseBase27cacheControlContainsNoCacheEv` | `bTMmuXgrk-4` | | `_ZNK7WebCore20ResourceResponseBase27cacheControlContainsNoStoreEv` | `CchgKVep+h8` | | `_ZNK7WebCore20ResourceResponseBase29cacheControlContainsImmutableEv` | `OKrc5AyAqtM` | | `_ZNK7WebCore20ResourceResponseBase34cacheControlContainsMustRevalidateEv` | `8956F1voabk` | -| `_ZNK7WebCore20ResourceResponseBase3urlEv` | `VljXeIAPJoA` | | `_ZNK7WebCore20ResourceResponseBase6sourceEv` | `Nc4OHQvmIOM` | | `_ZNK7WebCore20ResourceResponseBase8isHTTP09Ev` | `JjPx6bo9fbs` | | `_ZNK7WebCore20ResourceResponseBase8lazyInitENS0_9InitLevelE` | `o+TiqWzvS8s` | -| `_ZNK7WebCore20ResourceResponseBase8mimeTypeEv` | `HDR9zkTEoJ4` | | `_ZNK7WebCore20ScrollingCoordinator36coordinatesScrollingForOverflowLayerERKNS_11RenderLayerE` | `1nxJkpm8wLA` | | `_ZNK7WebCore21BackForwardController12forwardCountEv` | `wmkk8v-vEI0` | | `_ZNK7WebCore21BackForwardController18canGoBackOrForwardEi` | `No2407W5Jes` | diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index 8c9fcdc170..d9c9e689a7 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -53,11 +53,14 @@ ### Functional +- [sceNetGetMacAddress](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the console's result for a non-null unwritable output is unmeasured. AnyPS5 returns `EFAULT` through a checked guest write instead of crashing the host; null output and invalid flags retain `EINVAL`. The synthetic address remains independent of the host adapter. + - [IPV6_V6ONLY in sceNetSetsockopt and sceNetGetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) is unverified on PS5. The IPv6 level (41), option (27), exact integer input size, nonzero enable behavior, boolean readback and rejection after binding follow FreeBSD 11.4's [`ip6_ctloutput`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/netinet6/ip6_output.c). The shared libkernel option implementation applies the host's IPv6-only mode; disabling it permits IPv4-mapped traffic on a socket bound to the IPv6 wildcard address. Host network configuration controls IPv6 availability. Null buffers retain this library's `EINVAL` behavior instead of FreeBSD's fault handling. Crimson Desert (PPSA25286) disables it, passing a 4-byte 0 at both of its two calls; of the other nine titles checked, only Borderlands 4 (PPSA01494) passes an IPv6-level option as a constant, one `IPV6_JOIN_GROUP` (12). - [sceNetRecv/sceNetRecvfrom](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) 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; the console measurement covered only libkernel_sys `recv` and `recvfrom`, not libSceNet. - [sceNetUsleep](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - whether the console prints its `[LIBNET] usleep ` line for a negative count before `sceNetInit` is unmeasured; nothing is printed here. 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) - [sceNetIoctl](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - on a `sceNetSocket` socket only `FIONBIO` and `FIONREAD` are handled and other requests fail with `ENOTTY`, as in libkernel's `ioctl`, though FreeBSD 11.4's [`soo_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_socket.c#L156-L262) handles more; not measured on a console. [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/60c42b925c3d1e00008621b8587819999260ec31/src/core/libraries/net/net.cpp#L1839) (PS4) returns `EINVAL` for other requests. `FIONBIO` sets `SO_NBIO`, assumed to be `soo_ioctl`'s `SS_NBIO`. `FIONREAD` asks the host, which on Linux counts only the next datagram where `soo_ioctl` counts all; on macOS a non-stream socket uses `SO_NREAD` (next datagram), as macOS's `FIONREAD` counts more. A PS4 Pro counts payload plus 16 bytes per datagram (#9932). Libkernel's own sockets go to its `ioctl`; other descriptors get `EBADF`. +- [sceNetResolverGetError](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the console's error for a non-null unwritable status output is unmeasured. AnyPS5 returns `EFAULT` through a checked guest write instead of crashing the host; null output retains `EINVAL` and an unknown resolver retains `EBADF`. - [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 read after the host accept returns; changes between connection creation and accept are not modelled. Not measured on a console. @@ -66,6 +69,7 @@ - [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 - [SO_OOBINLINE](../../../core/libs/prx/libSceNet/Export.cpp) uses native TCP urgent-data handling; its urgent-pointer and signalling behavior has not been compared with PS5. The flag bit (0x100), non-stream flag storage and truncated readback follow FreeBSD 11.4 [sosetopt/sogetopt](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/kern/uipc_socket.c). Null pointers retain the existing libSceNet `EINVAL` result. - [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 +- [sceNetSetsockopt socket-level options](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - a `SOL_SOCKET` option it does not handle returns `EOPNOTSUPP`. Titles checked set four of them, named as in [shadPS4's `net_types.h`](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/net/net_types.h): `SO_POLICY` (`0x1201`) once each in PPSA01494, PPSA21564, PPSA30839 and PPSA31162; `SO_USECRYPTO` (`0x20000`) twice in PPSA02225; `SO_LINGER` (`0x80`) once in PPSA25286, with linger off; and `SO_REUSEPORT` (`0x200`) once each in PPSA18194 and PPSA21564, which sets it instead of `SO_REUSEADDR` when its own socket-type field is 1 or 3. `SO_LINGER` and `SO_REUSEPORT` are FreeBSD 11.4 socket options; what the console does with the other two is unknown - [SO_REUSEADDR and SO_BROADCAST readback](../../../core/libs/prx/libSceNet/Export.cpp) follows FreeBSD 11.4's [sogetopt](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/kern/uipc_socket.c#L2674-L2688): enabled options return their flag bit, and output is truncated to the supplied length. PS5 results are unverified. Values reflect the native socket; host restrictions on setting the options are unchanged. Null pointers retain libSceNet's existing `EINVAL` result. Windows passes `SO_REUSEADDR` directly to Winsock, which permits a second bind to an occupied address and port under its [reuse rules](https://learn.microsoft.com/en-us/windows/win32/winsock/using-so-reuseaddr-and-so-exclusiveaddruse), unlike FreeBSD's separate `SO_REUSEPORT` requirement for duplicate unicast binds. Guest `SO_REUSEPORT` (`0x200`) still fails with `EOPNOTSUPP`. - [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. One title (PPSA02225) disassembled for this entry has one call site (0x1056d34), with level 0xffff, option 0x1007 (FreeBSD's `SOL_SOCKET` and `SO_ERROR`) and a length variable set to 4 beforehand, reached only after a helper called with the value 2 returned true. For a negative result it takes the thread's error code as the value; for a result of 0 or more with a value of 0 it reports the connection as made, and otherwise it turns the value into an error code of its own. The call does not show how often the title reads it or whether it keeps using the socket after an error. - [SO_KEEPALIVE](../../../core/libs/prx/libSceNet/Export.cpp) uses the host TCP keepalive timers and probe count, which can differ from the console. The boolean flag, bit-valued readback and truncated output follow FreeBSD 11.4's [sosetopt and sogetopt](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/kern/uipc_socket.c), without PS5 hardware verification. Non-stream sockets retain the flag without sending probes, as FreeBSD does. Null pointers retain the existing libSceNet `EINVAL` result instead of FreeBSD's `EFAULT`. @@ -76,8 +80,11 @@ - [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 - [sceNetGetSockInfo6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) is not measured on a console; shadPS4 stubs it. It lists only IPv6 sockets and `flags` 0x1000 adds IPv4 sockets, reported with the IPv4 addresses, all-zero IPv6 addresses and no `F_IPV6` (0x400000), which every IPv6 socket sets ([SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Socket.cs#L251-L343), [flags](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Socket.cs#L698-L705), [docs](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/docs/net.md#L301-L304)). The default of leaving IPv4 sockets out, the zero IPv6 addresses of an IPv4 socket and accepting 0x40 are inferred from those names, not documented. An IPv4 socket without 0x1000 fails with `EAFNOSUPPORT`. The other gaps are those of `sceNetGetSockInfo`. +- [sceNetGetSockInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - every direct call to it in the local titles whose executables import it passes the constant 0 as `flags` (disassembly of each direct call), so none of them reaches the `EINVAL` that other bits than 0x40 get here, or passes 0x40; what a console returns for other `flags` bits is not measured. - [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. - [sceNetEpollControl and sceNetEpollWait](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) take sockets made with libkernel's `socket()`. PPSA02429 registers such sockets and retries the failed `ADD` and `DEL` about 1.6 million times in its first seconds. This follows the position taken for PPSA30839 in `sceNetSocketClose` and `sceNetSetsockopt`, that both libraries work on the same kernel sockets; epoll on them is not measured on a console. libkernel's `close()` and `sceKernelClose` retire their registrations at once, so a socket that reuses the number starts unregistered. +- [sceNetEpollCreate](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - a null name or non-zero `flags` fail with `EINVAL`, as [prosper's reading of the libSceNet module](https://github.com/mattias800/prosper/blob/6c09a34a092a00898fcead5f855200ca7c368cd2/prosper/src/hle/net/hle_net.cpp#L443-L449) gives. Every direct call in the titles checked passes the constant 0 as flags, so none reaches the check +- [sceNetEpollControl](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - an `op` other than 1 (add), 2 (modify) and 3 (delete) fails with `EINVAL`. In [prosper's reading of the libSceNet module](https://github.com/mattias800/prosper/blob/6c09a34a092a00898fcead5f855200ca7c368cd2/prosper/src/hle/net/hle_net.cpp#L471-L491) the module checks `op` and the event before the epoll id, and an add or modify whose event has an events mask of 0 also fails with `EINVAL`; here an unknown epoll id fails with `EBADF` before `op` is checked, and a mask of 0 is accepted. Every direct call in the titles checked whose `op` is a constant passes 1, 2 or 3; the `op` of the calls where it is not a constant is not known - [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. - [MSG_NOSIGNAL](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - `sceNetSend`, `sceNetSendto` and `sceNetSendmsg` accept `0x20000`, FreeBSD's `MSG_NOSIGNAL` (`sys/sys/socket.h`), which ProsperoTV (PPSA99003) passes on every phone-remote reply. Firmware 13.42's `libSceNet.sprx` adds `0x40020000` to the flags of every send and refuses no flag itself ([measured in an emulator](https://github.com/boykopovar/AnyPS5/pull/11037#issuecomment-6400759240)), so passing the flag changes nothing on the console. What `0x40000000` means and which flags the kernel refuses are unknown; other flags still fail with `EOPNOTSUPP` here. Host sends outside Windows already pass `MSG_NOSIGNAL` and Windows raises no `SIGPIPE`, so a send on a stream whose write side is shut down fails with `EPIPE`. 35 of 53 local titles call `sceNetSend`/`sceNetSendto` directly from the executable (title libraries, function pointers, 2 tail jumps not read) with flags 0 at 86 sites, 0x80 at 10, non-constant at 2. - [Stream sends on macOS](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - a `MSG_DONTWAIT` transfer also passes XNU's `MSG_NBIO` (`0x20000`, absent from the SDK headers), because XNU's `MSG_DONTWAIT` does not keep a stream send from waiting for buffer space. A blocking stream send goes to the host in sends of 64 KiB, so an error after some progress, such as an abort, reports the bytes already queued, which one host send would drop; the bytes that the failing 64 KiB send had queued are not counted diff --git a/docs/dev/debt/libSceNgs2.native.md b/docs/dev/debt/libSceNgs2.native.md index 3a9844f15b..4d96da6da2 100644 --- a/docs/dev/debt/libSceNgs2.native.md +++ b/docs/dev/debt/libSceNgs2.native.md @@ -2,6 +2,8 @@ ### Unknown function info +- [libSceNgs2.native pending exports](../../../core/libs/prx/libSceNgs2.native/Export.cpp) - 14 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNgs2.native.c). Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. + - [NGS2 PCM sampler playback](../../../core/libs/prx/libSceNgs2.native/src/Pcm.cpp) is not console-verified. Types 0x11, 0x12, 0x14, 0x16, 0x18 and 0x1a follow [prosper's waveform parser analysis](https://github.com/mattias800/prosper/blob/30572df43ae20ff28f995db56149f7af42235741/prosper/src/hle/audio/ngs2_waveform.cpp#L194-L223) and the WAVE representations it selects: unsigned 8-bit, signed little-endian 16/24/32-bit and little-endian IEEE float/double. Integer samples use the existing signed-16 normalization generalized to their width; double samples narrow to host float. Console rounding, float clipping, nonfinite and subnormal handling are unknown: floating input outside -1..1, nonfinite values and nonzero magnitudes below the smallest normal float throw. Other types and nonzero configuration words on the new formats throw. Incomplete streamed blocks and dataless silence blocks remain supported only for signed-16 PCM; the new formats require complete blocks. - [NGS2 voice validation](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) returns `0x804A8203` for an invalid voice in Control, RunCommands, GetState, GetStateFlags, GetPortInfo and QueryInfo, following the type-4 handle error in [prosper's PS5 module analysis](https://github.com/mattias800/prosper/blob/d61eb2c0ad2de879ef74df9c4aa050c512804a18/prosper/src/hle/audio/hle_audio.cpp#L4478). Control checks a null parameter list first (`0x804A8360`); GetState resolves the voice before output validation, and GetState rejects sizes below 8 before a null output. Existing rack-specific exact state sizes, other argument precedence and command layouts are retained; arbitrary-sized state copies and full console behavior remain unverified. These values follow module analysis, not new console execution. @@ -75,7 +77,7 @@ - [sceNgs2VoiceGetMatrixInfo](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - a matrix id past the rack's `max_matrices` throws, as the PS5 code for shadPS4's PS4 [`INVALID_MATRIX_INDEX`](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/ngs2/ngs2_error.h#L92) is unknown. A matrix without levels reports 0 levels, though it mixes through the default channel map. Signature from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/Ngs2.cs#L442), layout (a count and 64 levels) from [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/ngs2/ngs2.h#L263-L266); levels are the stored, clamped ones. Unmeasured: a null info answers `INVALID_OUT_ADDRESS` and a size other than 260 `INVALID_OUT_SIZE` before the voice is checked, as in `sceNgs2VoiceGetPortInfo` - [sceNgs2CustomRackGetModuleInfo](../../../core/libs/prx/libSceNgs2.native/src/Rack.cpp) (libSceNgs2.native) - a size over 32 bytes throws, as the longer per-module info in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/ngs2/ngs2_custom.h#L419-L442) is not kept. A size under 32 and an index past the rack's modules (any on a non-custom rack) also throw: the PS5 codes for shadPS4's PS4 [`INVALID_MODULE_INDEX` and `INVALID_MODULE_INFO_SIZE`](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/ngs2/ngs2_error.h#L117-L118) are unknown. It repeats the ids, state offset and size from rack creation, reserved words 0, as [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/Ngs2.cs#L323-L341) lays it out. Unmeasured: a null info gives `INVALID_OUT_ADDRESS`, then a bad handle `INVALID_RACK_HANDLE` - [NGS2 patch destinations](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) - a rack handle with input 0 resolves to its first voice, matching the Dead Cells call in #4334; selection on multi-voice racks remains unverified. Input 1 is supported only for a voice handle in a two-input custom submixer. Nonzero rack-handle inputs, other unsupported inputs and nonzero inputs on disconnected ports throw because their contracts are unknown. GetPortInfo reports the resolved voice handle and selected input, with zero delay. Changes to a port destination or input while its source is rendered throw before mixing: callback routing order is unverified, and combining the old destination with the new input is invalid. -- [NGS2 PCM waveform parsing](../../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) (libSceNgs2.native) - `sceNgs2ParseWaveformData` reads integer PCM (tag 1, or `WAVE_FORMAT_EXTENSIBLE` with the PCM GUID) of 8, 16, 24 and 32 bits as types 0x11, 0x12, 0x14 and 0x16, and float (tag 3) of 32 and 64 bits as 0x18 and 0x1a, and returns 0x804A8432 for other depths, after [prosper's reading of `libSceNgs2.native.sprx`](https://github.com/mattias800/prosper/blob/30572df43ae20ff28f995db56149f7af42235741/prosper/src/hle/audio/ngs2_waveform.cpp#L194-L223); not measured on a console. Every depth keeps the checks of the 16-bit path (channels, rate, block align, whole frames, a looped `smpl` chunk throws), which that reading does not apply +- [NGS2 PCM waveform parsing](../../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) (libSceNgs2.native) - not measured on a console. Every depth keeps the 16-bit checks (channels, rate, block align, whole frames), which [prosper's reading of the module](https://github.com/mattias800/prosper/blob/40d95db32efe1ad00106a5b9a84e018fffa9ecff/prosper/src/hle/audio/ngs2_waveform.cpp#L194-L343) does not apply. A `smpl` loop ending past the last sample (the module's tail size wraps) and a `smpl` chunk too short for its loop count or first loop (the module reads the bytes after it) throw. Voices play only 16-bit PCM and ATRAC9. Integer PCM (tag 1 or the extensible PCM GUID) of 8, 16, 24 and 32 bits is 0x11, 0x12, 0x14 and 0x16, float (tag 3) of 32 and 64 bits 0x18 and 0x1a, other depths 0x804A8432. As in that reading, only the first loop's start and inclusive end are read, the loop block repeats forever and an end not after the start keeps one block - [sceNgs2ParseWaveformFile](../../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) (libSceNgs2.native) - the signature `(path, offset, info)` is taken from the call site of PPSA21402, which sets only three argument registers; the file is read from `offset` to its end, the data and block offsets are taken to be relative to the start of the file, not of `offset`, and an unreadable file throws. The audio unit fields of a 16-bit PCM waveform are assumed to be one sample frame per unit and per frame; `INVALID_WAVEFORM_DATA` for a `data` chunk that runs past the buffer or ends inside a frame is assumed, and looped PCM and other PCM formats throw - [NGS2 waveform format routing](../../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) (libSceNgs2.native) - an extensible fmt chunk of 0x34 bytes whose GUID is neither PCM nor ATRAC9, which the module parses as ATRAC9 without checking its config, and the GUID of waveform type 0x43 throw. Otherwise a tag other than 1, 3 or 0xFFFE, an extensible fmt chunk under 0x28 bytes and an unknown GUID return `0x804A8432`, per [prosper's reading of the module](https://github.com/mattias800/prosper/blob/8dcb5dfdb9997ede7c76783995010d6abd1222f8/prosper/src/hle/audio/ngs2_waveform.cpp#L190-L208); not measured on a console - [sceNgs2VoiceControl sampler filter](../../../core/libs/prx/libSceNgs2.native/src/Filter.cpp) (libSceNgs2.native) - throws: a filter param sent during a render (PS4 11.00 and PS5 accept it from the next render), PRE_SEND 2+k with another port patched, RunCommands filter commands (not run), a rate change under a filter. PLAY on a playing or paused voice without a filter plays on (PS5 restarts it). Not run: non-DC PAUSE. Measured on the PS5 libSceNgs2.native, firmware 3.20 and 4.03, run on a PC, not a console (ngs2-oracle-m12, m18): its sinf/cosf came from the PC's libm (console libm not run); this takes double sin/cos diff --git a/docs/dev/debt/libSceNpCommon.md b/docs/dev/debt/libSceNpCommon.md index 0902b4e4e7..87850259c5 100644 --- a/docs/dev/debt/libSceNpCommon.md +++ b/docs/dev/debt/libSceNpCommon.md @@ -36,7 +36,7 @@ Pending import declarations are matched to names in the local libSceNpCommon.spr - [Online ID and NpId utilities](../../../core/libs/prx/libSceNpCommon/Export.cpp) (libSceNpCommon) - `sceNpCmpOnlineId`, `sceNpCmpNpIdInOrder`, `sceNpGetPlatformType` and `sceNpIntIsValidOnlineId` are not measured on a console; their signatures, checks and results follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/np/np_common.cpp), including the platform tags read from `opt[4..7]` and the 3 to 16 character online ID rule. - [Mutex, condition and thread wrappers](../../../core/libs/prx/libSceNpCommon/Export.cpp) (libSceNpCommon) - the `sceNpMutex*`, `sceNpLwMutex*`, `sceNpCond*` and `sceNpJoinThread` wrappers forward to the libkernel `scePthread*` calls as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/np/np_common.cpp) and are not measured on a console. Only `EBUSY` from try-lock and `ETIMEDOUT` from a timed wait are mapped to NP errors; every other kernel error is returned unchanged. The `flags` argument of `sceNpCondInit` is ignored. - [sceNpGetSystemClockUsec](../../../core/libs/prx/libSceNpCommon/Export.cpp) (libSceNpCommon) - a null destination returns the invalid-argument error `0x80550003` as a host-safety fallback; shadPS4 writes through the pointer without checking it, and the console behaviour is not measured. -- [sceNpGetSdkVersion](../../../core/libs/prx/libSceNpCommon/Export.cpp) (libSceNpCommon) - writes the libkernel system software version as `major.minor` into an 8-byte buffer without checking it, as shadPS4 does; that version is the fixed `1.00`, not the SDK version a title was built with. +- [sceNpGetSdkVersion](../../../core/libs/prx/libSceNpCommon/Export.cpp) (libSceNpCommon) - clears the 8-byte buffer and then writes the libkernel system software version into it as `major.minor` without checking the pointer, as firmware 13.42's `libSceNpCommon.sprx` does ([read from its disassembly and an emulator run](https://github.com/boykopovar/AnyPS5/pull/13684#issuecomment-6537744884), not measured on a console); that version is the fixed `1.00`, not the SDK version a title was built with. - [The exports in Runtime.cpp](../../../core/libs/prx/libSceNpCommon/Runtime.cpp) and [Strings.cpp](../../../core/libs/prx/libSceNpCommon/Strings.cpp) follow `libSceNpCommon.sprx` from firmware 13.60, read from disassembly and not measured on a console. - [sce::np::Time](../../../core/libs/prx/libSceNpCommon/Runtime.cpp) (libSceNpCommon) - `AddMicroSeconds`, `AddSeconds`, `AddMinutes`, `AddHours`, `AddDays` and both `operator+` throw `std::invalid_argument` when the libSceRtc add fails, where the module asserts and calls `sceNpPanic`. `AddHours` and `AddDays` take a 64-bit count but keep its low 32 bits, because the libSceRtc calls take 32-bit counts; the comparison operators compare the ticks unsigned. These follow `libSceNpCommon.sprx` of firmware 13.60, read from disassembly, not measured on a console. - [The sce::np::Object new and delete operators](../../../core/libs/prx/libSceNpCommon/Runtime.cpp) (libSceNpCommon) - do not check that the size plus their 16-byte header fits, and free through the allocator stored in that header, ignoring the allocator passed to the delete forms, as `libSceNpCommon.sprx` of firmware 13.60 does, read from disassembly, not measured on a console. The extended forms keep the address of the `SceNpAllocatorEx` itself, whose first member is the allocator. @@ -45,9 +45,11 @@ Pending import declarations are matched to names in the local libSceNpCommon.spr - [sce::np::RefObject](../../../core/libs/prx/libSceNpCommon/Runtime.cpp) (libSceNpCommon) - follow `libSceNpCommon.sprx` of firmware 13.60, read from disassembly, not measured on a console: `Release` calls the deleting destructor from the vtable only when the count it leaves is exactly 0, so a count that was already 0 or below goes negative without deleting the object; `AddRef` and `Release` return the new count, wrapping at the 32-bit limits. - [sce::np::NpCommId and sceNpUtilParseTitleId](../../../core/libs/prx/libSceNpCommon/Runtime.cpp) (libSceNpCommon) - follow `libSceNpCommon.sprx` of firmware 13.60, read from disassembly, not measured on a console, with one change: `sceNpUtilParseTitleId` reads `titleId[9]` only when the string is at least 10 characters long, where the module reads it whatever the length, past the end of a shorter string. The parsed number is `strtol` of the text after the underscore, cut to a byte, as in the module. - [sce::np::NpTitleId Parse and ConstructFromTitleId](../../../core/libs/prx/libSceNpCommon/Runtime.cpp) (libSceNpCommon) - drop the parsed `_NN` suffix and store `_00`, and accept only ASCII letters and digits, as `libSceNpCommon.sprx` of firmware 13.60 does, read from disassembly, not measured on a console. The module classifies characters with the C-locale `_Getpctype` table, so a locale with other letters is not covered. Parse takes exactly 12 characters, ConstructFromTitleId exactly 9, and a rejected title id leaves the object unchanged. +- [The sce::np::NpTitleSecret constructor from data](../../../core/libs/prx/libSceNpCommon/Runtime.cpp) (libSceNpCommon) - throws `std::invalid_argument` when the size is not 128, where `libSceNpCommon.sprx` of firmware 13.60 asserts `size == (128)` and panics, read from disassembly, not measured on a console. `SetSecretData` and `Equals` with a size return `0x80559E11` or false instead, and `ParseHex` / `BuildHex` return only the negative results of `sceNpStrnParseHex` / `sceNpStrBuildHex`, as the module does. - [The sce::np::StreamReader and StreamWriter helpers](../../../core/libs/prx/libSceNpCommon/Runtime.cpp) (libSceNpCommon) - follow `libSceNpCommon.sprx` of firmware 13.60, read from disassembly, not measured on a console: `ReadAll` halts through `sceNpPanic` when the stream reports more bytes than it was asked for; `WriteData` and `WriteFilledData` loop for as long as the stream accepts 0 bytes; `SkipAll` and `SkipData` add the reported count even when the read fails; positive stream and context results count as success and are returned as 0. - [sceNpCreateThread](../../../core/libs/prx/libSceNpCommon/Runtime.cpp) always applies an explicit priority when one is given; the firmware picks between explicit and inherited scheduling from the title's compiled SDK version, which is not available here. - [sceNpCalloutStartOnCtx and sceNpCalloutStartOnCtx64](../../../core/libs/prx/libSceNpCommon/Runtime.cpp) look for the callout in the queue before writing it and return `0x80558006` with the entry and the queue unchanged. Firmware 13.60, read from disassembly and not measured on a console, clears and fills the entry before it takes the lock and checks the queue, so restarting a queued callout returns the same error but drops every callout queued after it. +- [sceNpCalloutTermCtx](../../../core/libs/prx/libSceNpCommon/Runtime.cpp) (libSceNpCommon) throws when a callout of the context calls it on that same context, because the worker thread cannot join itself and tearing the context down under it leaves the worker reading freed memory. What the console does is unknown and not measured. Closes when: measured on a PS5. - [sceNpArchInit and sceNpArchTerm](../../../core/libs/prx/libSceNpCommon/Runtime.cpp) keep a reference count under a lock and create the global heap once. Firmware 13.60, read from disassembly and not measured on a console, uses no lock and never sets its count on the first call, so every call made while the count is zero creates the heap again, and a call to sceNpArchTerm at zero destroys the heap and leaves the count negative. - [sceNpGetRandom](../../../core/libs/prx/libSceNpCommon/Runtime.cpp) draws from a 32-bit Mersenne Twister seeded once per process; the console uses SFMT-19937 with a seed set inside the module, so the sequences differ. - [sceNpHeapShowStat](../../../core/libs/prx/libSceNpCommon/Runtime.cpp) and [sceNpDbgDumpBinary](../../../core/libs/prx/libSceNpCommon/Runtime.cpp) print the same information as the console in a different layout. @@ -59,9 +61,8 @@ Pending import declarations are matched to names in the local libSceNpCommon.spr - [sceNpJsonParse, sceNpJsonParseBuf and sceNpJsonParseEx](../../../core/libs/prx/libSceNpCommon/Json.cpp) (libSceNpCommon) - decode escapes with this library's `sceNpUtilJsonGetOneChar`, so they differ from firmware 13.42 only where that function does. The firmware's version also unescapes the whole window it is given (six bytes from the character, or the twelve bytes of a surrogate pair) and returns that error without writing its outputs, so an invalid `\u` escape there makes the firmware's parser use the lengths left from the previous character. It also reads past the given length to finish a `\u` escape, so in the firmware an escape split between two calls depends on the bytes that follow the first part. Well-formed escapes that are not split decode the same - [JSON parser exports](../../../core/libs/prx/libSceNpCommon/Json.cpp) (libSceNpCommon) - the 11 `sceNpJsonParse*` and `sceNpServerErrorJson*` exports follow firmware 13.42's libSceNpCommon.sprx, read from disassembly and compared by running the firmware's functions under a CPU emulator, not measured on a console and not checked against 13.60. As in that firmware, the type of the 16th nested container shares its context slot with the saved parser state, so when a call ends inside a 16th-level object, the bracket that closes it closes nothing and the outermost object is left open; the firmware does the same for every split position inside that object - [_ZN3sce2np10CancelableC2Ev](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `vqekW3s-eFg`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpUtilXmlEscape](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `5bBPLZV49kY`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpUtilXmlEscape and sceNpUtilXmlUnescape](../../../core/libs/prx/libSceNpCommon/Strings.cpp) (libSceNpCommon) - read from `libSceNpCommon.sprx` of firmware 13.60 disassembly, not measured on a console. Neither bounds an entity or a UTF-8 sequence by the source length: a sequence whose lead byte is inside the source is copied whole and an entity whose `&` is inside it is matched whole, reading the bytes after the source, as the module does. Escaping refuses NUL, the other control characters except tab, LF and CR, DEL and malformed UTF-8 (lead bytes below 0xC2 or above 0xF7, missing continuation bytes) with `0x80550608`; 3-byte sequences are not checked for overlong forms or surrogates. Unescaping knows only the five predefined entities and copies every other byte, NUL included - [_ZN3sce2np4CondD0Ev](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `AKiHGWhC2KU`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [_sceNpAllocatorExRealloc](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `kKF3w-XkCWA`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [_ZN3sce2np8WorkItem6CancelEi](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `Oq5aepLkEWg`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [_ZN3sce2np9WorkQueue11GetItemByIdEi](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `F2umEBpQFHc`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [_ZN3sce2np14JsonDocBuilderC2ERKNS0_9JsonValueE](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `dhJGQPKLmn0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. @@ -73,7 +74,6 @@ Pending import declarations are matched to names in the local libSceNpCommon.spr - [_ZN3sce2np16StreamReadBufferD1Ev](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `ZufKqNXItD0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [_ZNK3sce2np9EventFlag6IsInitEv](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `st-oQvV7HVI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [_ZN3sce2np10CancelLockC2Ev](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `MecB8wAHCfE`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [_ZN3sce2np13NpTitleSecretC2ERK16SceNpTitleSecret](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `9QN7g5mQgCU`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [_ZN3sce2np8WorkItem10SetRunningEv](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `XUCjhejJvPc`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [_ZN3sce2np3ipc17ServiceIpmiClient21CreateRequestForAsyncEPiiPKvm](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `v5Z2LAKua28`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [_ZN3sce2np18HttpConnectionPool13InvalidateAllEv](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `et05S+nkWG8`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. @@ -100,7 +100,6 @@ Pending import declarations are matched to names in the local libSceNpCommon.spr - [_ZN3sce2np3ipc10IpmiClient11IsConnectedEv](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `prj9aMR74bA`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [_ZN3sce2np12HttpTemplate19SetAuthInfoCallbackEPFii15SceHttpAuthTypePKcPcS5_iPPhPmPiPvESA_](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `ezJnmv7hkAg`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [_ZN3sce2np9Semaphore6CreateEiiPKc](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `wcOCedFKan4`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpUtilXmlUnescape](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `+0rj9KhmYb0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpXmlParse](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `fEcrs9UPPyo`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [_ZN3sce2np9WorkQueue15GetFinishedItemENS0_14WorkItemStatusE](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `wM4q1JMisvA`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [_ZN3sce2np9EventFlag4PollEmjPm](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `37Rd2JS+FCM`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. @@ -138,12 +137,10 @@ Pending import declarations are matched to names in the local libSceNpCommon.spr - [_ZThn8_N3sce2np6HandleD1Ev](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `lUsG1QfgVN4`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [_ZN3sce2np6Handle4InitEv](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `ifqJb-V1QZw`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [_ZThn16_N3sce2np10MemoryFileD1Ev](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `8S1mWU-N9kM`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [_sceNpAllocatorExFree](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `Zh23aSLeeZo`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [_ZN3sce2np3ipc17ServiceIpmiClient7ConnectEPKvm](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `NTPZ5GZIA6U`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [_ZN3sce2np7RingBufC1Ev](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `L5BnZpuQImk`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [_ZN3sce2np10MemoryFileD1Ev](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `ZUf92uPkRuA`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [_ZN3sce2np9HttpTrans21GetResponseStatusCodeEPNS0_6HandleEPi](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `WoFp77mNyw0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [_ZN3sce2np13NpTitleSecretC2ERKS1_](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `JHG9CTmkdQw`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [_ZN3sce2np14CalloutContextD1Ev](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `S+a+rgnGX8A`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [_ZN3sce2np5Mutex7DestroyEv](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `RgGW4f0ox1g`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [_ZN3sce2np14JsonDocBuilderC1ERKNS0_9JsonValueE](../../../core/libs/prx/libSceNpCommon/PendingExportsRound7.cpp) (NID `ikFI73f3hP4`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. diff --git a/docs/dev/debt/libSceNpCppWebApi.md b/docs/dev/debt/libSceNpCppWebApi.md index 97064c7277..3ef2e6814c 100644 --- a/docs/dev/debt/libSceNpCppWebApi.md +++ b/docs/dev/debt/libSceNpCppWebApi.md @@ -8,6 +8,7 @@ - [sce::Np::CppWebApi::SessionManager::V1](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - `RequestPlayerSession::set{MaxSpectators,InvitableUserType,JoinableUserType,ExclusiveLeaderPrivileges}` and `RequestPlayerSessionPlayer::setPushContexts` succeed without storing state. The `PlayerSessionsApi` parameters `ParameterToCreatePlayerSessions`, `ParameterToGetPlayerSessionInvitations`, `ParameterToJoinPlayerSessionAsPlayer` and `ParameterToLeavePlayerSession` keep only whether they are initialized, as the TitleCloudStorage ones do: `initialize`, `terminate` and `setinvitationInvalidFilter` return the codes of the module bundled with PPSA21564, plus 0x80553512 when the request body of `ParameterToCreatePlayerSessions` or `ParameterToJoinPlayerSessionAsPlayer` is empty. They store none of the strings or bodies, and `setfields` stores nothing either: only a request would read them - [sce::Np::CppWebApi::TitleCloudStorage::V1 and InGameCatalog::V5](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - `DataApi::ParameterToDownloadData`, `DataApi::ParameterToGetMultiDataStatusesByUser` and `ContainerApi::ParameterToGetContainer` keep only whether they are initialized. `initialize`, `terminate` and the string setters return the codes of the module bundled with PPSA21564: 0x80553503 for a null `LibContext` or string, 0x80553502 for a `LibContext` that `Common::initialize` did not set or a parameter not yet initialized, 0x8055350f for a second `initialize` and 0x80553510 for `terminate` without one; an empty `SetDataInfo` body returns 0x80553512. `ParameterToUploadData`, `ParameterToSetDataInfo`, `ParameterToDeleteMultiDataByUser` and `ParameterWithBinaryRequestBodyToUploadData` add state-less setters and keep only the initialized flag. None store values; only a request would read them. `DataStatus::{getLastUpdatedDateTime,getInfo}` and `UploadDataResponseHeaders::{getXPsnAtomicOperationId,hasXPsnAtomicOperationId}` throw, as a failed request never produces those objects. The console behaviour is not measured. - [sce::Np::CppWebApi::UserProfile::V1, ProfanityFilter::V2, CommunicationRestrictionStatus::V3](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - `FriendsApi::ParameterToGetFriends`, `PresenceApi::ParameterToGetBasicPresences`, `BlocksApi::ParameterToGetBlockingUsers`, `ProfanityApi::ParameterToFilterProfanity` and `CommunicationRestrictionStatusApi::ParameterToGetCommunicationRestrictionStatus` keep only whether they are initialized, as the SessionManager::V1 parameters do: `initialize` and `terminate` return the codes of the module bundled with PPSA21564, 0x80553512 for an empty `WebApiFilterRequest` included. They store none of the arguments: only a request would read them +- [sce::Np::CppWebApi::UserProfile::V1 public profiles](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - `BasicProfileApi::ParameterToGetPublicProfile` `{ctor,dtor,initialize,terminate}` and `TransactionBase>::setResponseInformationOption` return success without keeping state, as the plural `ParameterToGetPublicProfiles` does, and the constructor and destructor of `Transaction>` do the same: `start` fails, so none of them is ever used - [sce::Np::CppWebApi::Activities::V1](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - `ParameterToGetUserActivities::initialize` ignores the account-id vector, `setlimit` and `setnpServiceLabel` store their value in a host block that nothing reads, and `getUserActivities` returns `SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN`; the response accessors throw because an offline request never produces a response object. The 0x10-byte handle and the lifecycle codes come from the module bundled with PPSA21564/PPSA23732 (builds `J03093737`/`J03341606`, addresses `0x3e7a10`, `0x3e7a70`, `0x3e7cb0`, `0x3e7e50`, `0x3e7ec0`, `0x3e7f20`); the module's setters write no return value, so they stay `void`. Nothing here is measured on a console. PPSA22408 (Hot Wheels Infinite Rush) imports the 15 NIDs @@ -15,16 +16,18 @@ - [sce::Np::CppWebApi](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - every request entry point (API calls, transaction `start`/`getResponse`/`readData`, the `*Factory::create`s) returns `SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN` offline. The constant's name and value follow the reference tables in shadPS4 `np_error.h` and fpPS4 `np_error.pas`, where `0x80552907` is `ORBIS_NP_WEBAPI_ERROR_NOT_SIGNED_IN` and `0x80552901` is `OUT_OF_MEMORY` (no `UNAVAILABLE` exists in the `0x80552901`-`0x8055291F` range); it is the code a signed-out console returns from these entry points. The earlier `0x80552901` value came from the module the previous implementation shipped, which used the same incorrect constant - [sce::Np::CppWebApi::Common::IntrusivePtr](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - the 0x18-byte layout (object, deleter, `LibContext*`) and the offset of the 32-bit reference count in the pointee (0x00; in a `Vector`, 0x20 for `unsigned long`, 0x30 for `IntrusivePtr` and 0x38 for `String` elements) come from the module bundled with one title (PPSA21564); other SDK versions are not checked. The destructor throws on the last reference of a pointer without a deleter: destroying the object needs its destructor and the `LibContext` allocator, which are not implemented -- [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. The pointer/deleter constructor adopts a title-provided `Vector`, counting the reference at 0x38, which the `Vector` constructor zeroes, as the module bundled with PPSA21564 does, and releases it through the stored deleter; `pushBack` on such a body throws, since its element layout is unknown. `begin`, `end`, iterators and `copyFrom` throw: the only `Vector`s the title reads back come from failed transactions, which never produce one. `empty`, `size` and `operator[]` read the host-created body honestly and throw on title-provided bodies +- [sce::Np::CppWebApi::Common::String](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - the 0x20-byte layout (32-bit reference count, buffer at 0x08, used size including the terminator at 0x10, `LibContext*` at 0x18), the `+0x10` semantics and the `0x80553503`/`0x80553504` results follow the module bundled with PPSA21564 and PPSA23732 (builds J03093737/J03341606): `append`, `copyFrom`, `clear`, `size`, `length`, `operator==`, `setContext` and the intrusive reference operations match it. Allocation and release still use the host allocator instead of the `LibContext` allocator, so a title-provided buffer released through this destructor would use the wrong allocator; `find`, `replace` and `npos` remain unimplemented. `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 layout with the reference count at 0x30. The pointer/deleter constructor adopts a title-provided `Vector`, counting the reference at 0x38, which the `Vector` constructor zeroes, as the module bundled with PPSA21564 does, and releases it through the stored deleter; `pushBack` on such a body throws, as its element layout is unknown. `Vector::begin`/`end` and `Iterator` operators walk host-created bodies and throw on title-provided ones; other `begin`, `end` and iterators, and `copyFrom`, throw: the only `Vector`s the title reads back come from failed transactions, which never produce one. `empty`, `size` and `operator[]` read host-created bodies and throw on title-provided ones - [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::Matchmaking::V1](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - imported by PPSA21564 (86 of 92) and PPSA05510, PPSA25256, PPSA17226 and PPSA08395 for the other six; behaviour comes from the module bundled with PPSA21564/PPSA23732 (builds J03093737/J03341606), not from a console. Offline, the four `Api` calls and four factories return `SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN`. The four `ParameterTo*` objects keep only an initialized flag: `initialize` returns 0x80553503 for a null context or string, 0x80553502 for an unset context, 0x8055350f when already initialized and 0x80553512 for an empty `SubmitTicketRequestBody`; `terminate` returns 0x80553510. `ParameterToGetTicket::setview` returns 0x80553502 uninitialized and 0x80553503 for a null string. Other setters succeed without keeping state. Response accessors throw: a failed request produces no response. 0x80553504 is never returned; the stub stores a static pointer. -- [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 +- [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 `unsigned long` and those over `IntrusivePtr` other than `size` throw: a failed request never produces a response - [libSceNpCppWebApi](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - unimplemented stubs have their parameter lists, const qualifiers and return types aligned with the demangled symbols confirmed by NID matching in [#8143](https://github.com/boykopovar/AnyPS5/issues/8143). They cover response-body accessors, iterators, and uninstantiated transactions in Leaderboards::V1, SessionManager::V1, InGameCatalog::V5, TitleCloudStorage::V1, and Common templates: each mangled name gives the method and parameter types, but the runtime layout of response objects and errors are unverified on console, so each call continues to throw NotImplemented_nid_no_patch. No call site is recorded for them -- [sce::Np::CppWebApi::SessionManager2V1 and sce::Np::CppWebApi::InGameCatalog2V5](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - 198 further exports imported by PPSA20397, named from aerolib. API calls, factories and transaction `start`/`getResponse*` fail `SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN`; parameter bookkeeping succeeds without state; `IntrusivePtr`/`Vector` helpers follow the layouts above, with `Vector::empty`/`size`/`operator[]` reading host-created bodies and throwing on title-provided ones. Response accessors, iterators, `Vector::begin`/`end`/`copyFrom` and transaction `operator=` throw: a failed request produces no response object. As `start` never succeeds here, `TransactionBase::finish` and `abort` return 0x80553508 and `getId` returns 0, which is what the module bundled with PPSA21564 returns while the two words its `Transaction` constructor zeroes are still unset +- [sce::Np::CppWebApi::SessionManager2V1 and sce::Np::CppWebApi::InGameCatalog2V5](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - 198 further exports imported by PPSA20397, named from aerolib. API calls, factories and transaction `start`/`getResponse*` fail `SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN`; parameter bookkeeping succeeds without state, except the `PlayerSessionsApi` parameters, which track initialization per the SessionManager::V1 entry; `IntrusivePtr`/`Vector` helpers follow the layouts above, with `Vector::empty`/`size`/`operator[]` reading host bodies and throwing on title-provided ones. Response accessors, iterators, `Vector::begin`/`end`/`copyFrom` and transaction `operator=` throw: a failed request produces no response object. As `start` never succeeds here, `TransactionBase::finish` and `abort` return 0x80553508 and `getId` returns 0, as the module bundled with PPSA21564 does while the two words its `Transaction` constructor zeroes are unset - [sce::Np::CppWebApi::UserProfile::V1, sce::Np::CppWebApi::CommunicationRestrictionStatus::V3 and the remaining sce::Np::CppWebApi::Common instantiations](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - 87 further exports imported by PPSA20397, plus the previously-throwing `ContainerApi::ParameterToGetContainer` lifecycle and `getContainer`. API verbs and transaction `start`/`getResponse` fail `SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN`; parameter and model bookkeeping (`initialize`, `terminate`, `set*`, constructors, destructors) succeeds without state. `BasicProfile`/`BasicPresence`/`PersonalDetail` response accessors, `*IsSet` queries, `Get*Response` getters, vector `begin`/`end`, iterator dereference/compare and transaction `abort`/`getId`/`operator=` throw: no response object exists offline and the model layouts are unverified - [sce::Np::CppWebApi title-imported exports](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - exports imported by the `sce_module` copies shipped with PPSA20397 and PPSA03530 that main did not declare. NIDs and signatures come only from the titles' import tables; const and return types are inferred from the demangled name. Layouts and offline behaviour follow the landed same-template implementations sourced from the module bundled with PPSA21564. Response-body accessors, `IsSet` queries, `Vector` begin/end, `Iterator` dereference/compare and transaction `operator=` throw: a failed request produces no response object and the layouts are unverified. `SessionManager::V1` `GameSessionsApi`, `Matches::V1`, `Matchmaking::V1`, `UserProfile::V1`, `TitleCloudStorage::V1` and `InGameCatalog::V5` accessors have no other source, so behaviour is only what the signatures admit. + +- [sce::Np::CppWebApi::Matches::V1](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - imported by PPSA21564/PPSA23732 (builds `J03093737`/`J03341606`). The 34 request/model setters store the value and the is-set flag at the measured offsets; the 10 vector setters copy the `IntrusivePtr` handle and increment the body reference count at +0x30 instead of deep-copying the elements, which is a host choice. A constructor-created empty vector (null backing pointer) is a valid value: those setters copy the empty handle and set the flag instead of returning 0x80553512. The six `ParameterTo*` objects allocate a host implementation of the module's layout (0x40 bytes with the `Common::String` at +0x0 and the request body at +0x20; `ParameterToCreateMatch` uses 0x20 with the body at +0x0): `initialize` takes the actual string and body arguments and returns 0x80553503 for a null context or text, 0x80553502 for an unset context, 0x8055350f when already initialized, 0x80553504 on allocation failure and 0x80553512 for an empty request body; `terminate` returns 0x80553510. The host allocator stands in for the module's `LibContext` allocator. The 22 factories are unsupported and throw `NotImplemented_nid_no_patch` rather than returning a signed-out result; only the six API verbs keep `SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN`. The three accessors read +0x30/+0x48/+0x28. Response layouts beyond those fields are unverified. diff --git a/docs/dev/debt/libSceNpFriendListDialog.md b/docs/dev/debt/libSceNpFriendListDialog.md index 20d29a05ed..c921345af1 100644 --- a/docs/dev/debt/libSceNpFriendListDialog.md +++ b/docs/dev/debt/libSceNpFriendListDialog.md @@ -3,7 +3,7 @@ ### Silent stubs - [sceNpFriendListDialogOpenA](../../../core/libs/prx/libSceNpFriendListDialog/Export.cpp) (libSceNpFriendListDialog) - with `ANYPS5_PSN_DIALOGS=show`, a successful open shows AnyPS5's notice that the feature needs PlayStation Network; the console's own display without PlayStation Network is not measured, and the notice wording is AnyPS5's. -- [libSceNpFriendListDialog](../../../core/libs/prx/libSceNpFriendListDialog/Export.cpp) - the friend list dialog (`sceNpFriendListDialogOpenA` and `sceNpFriendListDialogGetResultA`) runs the common-dialog state machine but shows nothing and reports the user as having cancelled, as there is no PSN friend list. The signatures are from [Orbital's type database](https://github.com/AlexAltea/orbital/blob/b321fcdbd700cc02da1cb1ad25412781d5ec73c6/tools/ida/db_types.json); the result layout beyond the leading `int32_t` result code is not known, so nothing else is written. +- [libSceNpFriendListDialog](../../../core/libs/prx/libSceNpFriendListDialog/Export.cpp) - the friend list dialog (`sceNpFriendListDialogOpenA` and `sceNpFriendListDialogGetResultA`) runs the common-dialog state machine but shows nothing and reports the user as having cancelled, as there is no PSN friend list. In `sceNpFriendListDialogGetResultA`, checking for a null result pointer (`COMMON_DIALOG_ERROR_ARG_NULL`) takes precedence over unfinished state (`COMMON_DIALOG_ERROR_NOT_FINISHED`), matching native PS5 firmware 13.42 module `libSceNpFriendListDialog.sprx` disassembly (offset `0xa10`) and native export execution data. The signatures are from [Orbital's type database](https://github.com/AlexAltea/orbital/blob/b321fcdbd700cc02da1cb1ad25412781d5ec73c6/tools/ida/db_types.json); the result layout beyond the leading `int32_t` result code is not known, so nothing else is written. ### Unknown function info diff --git a/docs/dev/debt/libSceNpGameIntent.md b/docs/dev/debt/libSceNpGameIntent.md index ce2836c2a0..ee5a11894b 100644 --- a/docs/dev/debt/libSceNpGameIntent.md +++ b/docs/dev/debt/libSceNpGameIntent.md @@ -7,6 +7,18 @@ ### Unknown function info +- [libSceNpGameIntent additional exports](../../../core/libs/prx/libSceNpGameIntent/Export.cpp) - these name/NID pairs are present in the inspected export inventory. Parameter lists, return types and behavior are unverified. Parameterless `int` declarations are import-resolution placeholders, not verified ABI prototypes; every entry throws through `NotImplemented_nid_no_patch`. No runtime functionality is implemented. + +The inventory label is not authenticated retail-firmware provenance. The inspected image contains raw module-parameter words `0x13590001` at +0x10 and `0x13600007` at +0x14. Using the [published static ELF membership procedure](https://github.com/boykopovar/AnyPS5/pull/12367#issuecomment-6470603298), its 20 unique function NIDs have sorted-LF SHA-256 `04952ce9705980433d9f6f51c9e20d2f6b354a102c085dd3b545ed0b96f5b6f7`; all 5 entries below are present. This digest identifies the function-NID set, not the whole binary or its ABI. An independent review on [this PR](https://github.com/boykopovar/AnyPS5/pull/11956#pullrequestreview) corroborated these identities using a separately reported 13.42 reference; that does not establish matching binaries, prototypes or behavior. + +| Name | NID | +| --- | --- | +| `sceNpGameIntentGetPermittedIntentTypes` | `+u0P5feg100` | +| `sceNpGameIntentLaunchAppForPinvoke` | `4hjt8Rxl9CY` | +| `sceNpGameIntentLaunchAppRequestToGkp` | `hSVuTA3vJuQ` | +| `sceNpGameIntentRemoveLaunchCallback` | `RQPgtaOJdTk` | +| `sceNpGameIntentRequest` | `aScB-WDl+L0` | + - [sceNpGameIntentReceiveIntent, sceNpGameIntentGetPropertyValueString](../../../core/libs/prx/libSceNpGameIntent/Export.cpp) (libSceNpGameIntent) - firmware 13.42 traces report `0x80553802` before `sceNpGameIntentInitialize` (precedence over argument validity), and `0x80553804` for invalid arguments (null pointer, empty key, or zero buffer size); `sceNpGameIntentTerminate` resetting initialized state is modeled lifecycle behavior rather than hardware-verified. `sceNpGameIntentReceiveIntent` reportedly requires `info->size == 0x4140` on firmware 13.42, while AnyPS5 `NpGameIntentInfo` is `0x560` bytes (matching fpPS4) and set by `sceNpGameIntentInfoInit`, so initialized objects are rejected by the receiver's measured size guard. Complete retail layout, output field offsets, and retail behavior of `sceNpGameIntentInfoInit` are unverified; expanding the type with unverified reserved bytes is not an evidence-backed solution. - [sceNpAppLauncherLaunchApp](../../../core/libs/prx/libSceNpGameIntent/Export.cpp) (libSceNpGameIntent, NID `GbayQ7DO8jA`) - its recovered signature is `(int32_t, const char*, const char*)`: an unhandled first argument, a required non-empty string and an optional string rejected past 0x2000 bytes, returning `0x80553A02` before `sceNpAppLauncherInitialize` and `0x80553A04` on invalid arguments. The launch itself is not implemented and throws; the firmware tail calls an internal launch through an extra library context and remaps its `0x805538xx` errors into `0x80553Axx` - [sceNpGameIntentGetUdsNpComId, sceNpGameIntentLaunchApp, sceNpGameIntentLaunchApp2, sceNpGameIntentNotifyAppLaunched, sceNpSessionServiceGetJoinedPartySession, sceNpSessionServiceGetRunningGamePlayerSession and sceNpSessionServiceSendPlayerSessionInvitations](../../../core/libs/prx/libSceNpGameIntent/Export.cpp) (libSceNpGameIntent) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpGameIntent.c). They are declared without parameters solely to resolve known imports and make no ABI claim. diff --git a/docs/dev/debt/libSceNpManager.md b/docs/dev/debt/libSceNpManager.md index 9628a10204..ff962a54dd 100644 --- a/docs/dev/debt/libSceNpManager.md +++ b/docs/dev/debt/libSceNpManager.md @@ -174,7 +174,9 @@ - [sceNpSetTimeout](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - the signature follows [orbital](https://github.com/AlexAltea/orbital/blob/b321fcdbd700cc02da1cb1ad25412781d5ec73c6/tools/ida/db_types.json), [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/np/np_manager.cpp) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Np/NpAccount.cs); the rejected values (all five 0, negative retries, a resolve timeout under 1 s or another timeout under 10 s, 0 meaning the default) are shadPS4's, not checked on a console. An id that was never created or was deleted returns `REQUEST_NOT_FOUND`, as in shadPS4; there is no network, so the timeouts are not stored - [sceNpGetUserIdByAccountId](../../../core/libs/prx/libSceNpManager/Export.cpp) - the `(account id, user id output)` signature and its errors follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/np/np_manager.cpp): account id 0 or a null output returns `SCE_NP_ERROR_INVALID_ARGUMENT`, and a signed-out NP returns `SCE_NP_ERROR_SIGNED_OUT` without writing the output. NP is always signed out here, so no account id resolves to a user. PPSA30839 01.003.000 (at 0x447C995 and 0x448C72C) and PPSA34502 01.010.000 (0x712FE2), disassembled for this entry, pass a 64-bit account id and the address of a 32-bit user id on the stack, matching that signature; PPSA30839 sets the user id to -1 before the call, still reads it after a failure, and after a success passes it to its next call - [sceNpNotifyPlusFeature](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - PS4 titles import it; its parameter block and what the console does with it for a signed-out user are unknown ([shadPS4](https://github.com/shadps4-emu/shadPS4/blob/81be949167d0060b08f26dc3e055a0b640d129c8/src/core/libraries/np/np_manager.cpp#L1170) returns 0 without reading it), so every call throws. -- [libSceNpManager pending exports](../../../core/libs/prx/libSceNpManager/PendingExports.cpp) - 189 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpManager.c). Their signatures and behavior are unverified; the no-parameter declarations exist for import resolution and throw through `NotImplemented_nid_no_patch`. +- [libSceNpManager pending exports](../../../core/libs/prx/libSceNpManager/PendingExports.cpp) - 188 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpManager.c). Their signatures and behavior are unverified; the no-parameter declarations exist for import resolution and throw through `NotImplemented_nid_no_patch`. + +- [sceNpInGameMessageInitialize](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - this, `Terminate`, `CreateHandle`, `DeleteHandle` and `AbortHandle` are not measured on a console: signatures follow [Orbital](https://github.com/AlexAltea/orbital/blob/b321fcdbd700cc02da1cb1ad25412781d5ec73c6/tools/ida/db_types.json), error codes [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/d1d0b58cf94e5bfb6a2be406e140dcf1e314853f/src/core/libraries/np/np_error.h#L483-L491). The pool is not allocated (size and option unchecked, counts unlimited) and check order is unknown. Ids are never reused; handing out id 2147483647 throws, as the console's limit is unknown. `AbortHandle` only checks that the handle exists. `PrepareA`, `SendDataA`, `GetMemoryPoolStatistics`, `CheckCallback`, `Prepare` and `SendData` throw, as the preparation rule is unknown. ### Functional @@ -184,6 +186,7 @@ - [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 +- [sceNpSetGamePresenceOnlineA](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - not measured on a console: answers as if no PSN account is signed in, returning `SCE_NP_ERROR_SIGNED_OUT` for any valid request and user, whereas [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/2183807e592b36d4440384878165b43d930c1dee/src/core/libraries/np/np_manager.cpp#L1092-L1119) returns 0 under its offline service. The `(request id, user id, presence)` signature and argument validation follow shadPS4: user id -1 or request id 0 returns `SCE_NP_ERROR_INVALID_ARGUMENT` (user checked first), and an unknown or deleted request id returns `SCE_NP_ERROR_REQUEST_NOT_FOUND`. - [NP reachability callbacks](../../../core/libs/prx/libSceNpManager/Export.cpp) retain one callback and its user data; duplicate registration and unregistering an absent callback return the errors used by [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/945dbc3cc3eee80ac3e053b438502ed936fa6bb2/src/core/libraries/np/np_manager.cpp). No callback is delivered because the existing NP model remains signed out with reachability unavailable. - [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)); the unregister errors are in the next entry; 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 diff --git a/docs/dev/debt/libSceNpMatching2.md b/docs/dev/debt/libSceNpMatching2.md index 396f6d4f0d..b9856e5a2b 100644 --- a/docs/dev/debt/libSceNpMatching2.md +++ b/docs/dev/debt/libSceNpMatching2.md @@ -2,13 +2,18 @@ ### Unknown function info -- [sceNpMatching2CreateContextA, sceNpMatching2CreateContextInternal, sceNpMatching2CreateJoinRoomA, sceNpMatching2GetLobbyInfoList, sceNpMatching2GetLobbyMemberDataInternal, sceNpMatching2GetLobbyMemberDataInternalList, sceNpMatching2GetMemoryInfo, sceNpMatching2GetRoomJoinedSlotMaskLocal, sceNpMatching2GetRoomMemberDataExternalList, sceNpMatching2GetRoomMemberIdListLocal, sceNpMatching2GetRoomPasswordLocal, sceNpMatching2GetSignalingOptParamLocal](../../../core/libs/prx/libSceNpMatching2/Export.cpp) (libSceNpMatching2) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpMatching2.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified -- [sceNpMatching2GetSslMemoryInfo, sceNpMatching2GetUserInfoListA, sceNpMatching2GetWorldIdArrayForAllServers, sceNpMatching2GrantRoomOwner, sceNpMatching2JoinLobby, sceNpMatching2JoinRoomA, sceNpMatching2LeaveLobby, sceNpMatching2RegisterLobbyEventCallback, sceNpMatching2RegisterLobbyMessageCallback, sceNpMatching2RegisterManualUdpSignalingCallback, sceNpMatching2SendLobbyChatMessage, sceNpMatching2SetExtraInitParam, sceNpMatching2SetLobbyMemberDataInternal, sceNpMatching2SetRoomDataInternalExt](../../../core/libs/prx/libSceNpMatching2/Export.cpp) (libSceNpMatching2) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpMatching2.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified -- [sceNpMatching2SetSignalingOptParam, sceNpMatching2SignalingAbortConnection, sceNpMatching2SignalingCancelPeerNetInfo, sceNpMatching2SignalingEnableManualUdpMode, sceNpMatching2SignalingEstablishConnection, sceNpMatching2SignalingGetConnectionInfoA, sceNpMatching2SignalingGetLocalNetInfo, sceNpMatching2SignalingGetPeerNetInfo, sceNpMatching2SignalingGetPeerNetInfoResult, sceNpMatching2SignalingGetPingInfo, sceNpMatching2SignalingGetPort](../../../core/libs/prx/libSceNpMatching2/Export.cpp) (libSceNpMatching2) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpMatching2.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified -- [sceNpMatching2SignalingSetPort](../../../core/libs/prx/libSceNpMatching2/Export.cpp) (libSceNpMatching2) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpMatching2.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified +- [sceNpMatching2CreateContextInternal, sceNpMatching2CreateJoinRoomA, sceNpMatching2GetLobbyInfoList, sceNpMatching2GetLobbyMemberDataInternal, sceNpMatching2GetLobbyMemberDataInternalList, sceNpMatching2GetMemoryInfo, sceNpMatching2GetRoomMemberDataExternalList, sceNpMatching2GetRoomMemberIdListLocal, sceNpMatching2GetRoomPasswordLocal, sceNpMatching2GetSignalingOptParamLocal](../../../core/libs/prx/libSceNpMatching2/Export.cpp) (libSceNpMatching2) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpMatching2.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified +- [sceNpMatching2GetSslMemoryInfo, sceNpMatching2GetUserInfoListA, sceNpMatching2GrantRoomOwner, sceNpMatching2JoinLobby, sceNpMatching2JoinRoomA, sceNpMatching2LeaveLobby, sceNpMatching2RegisterManualUdpSignalingCallback, sceNpMatching2SendLobbyChatMessage, sceNpMatching2SetExtraInitParam, sceNpMatching2SetLobbyMemberDataInternal, sceNpMatching2SetRoomDataInternalExt](../../../core/libs/prx/libSceNpMatching2/Export.cpp) (libSceNpMatching2) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpMatching2.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified +- [sceNpMatching2SetSignalingOptParam, sceNpMatching2SignalingAbortConnection, sceNpMatching2SignalingCancelPeerNetInfo, sceNpMatching2SignalingEnableManualUdpMode, sceNpMatching2SignalingEstablishConnection, sceNpMatching2SignalingGetLocalNetInfo, sceNpMatching2SignalingGetPeerNetInfo, sceNpMatching2SignalingGetPeerNetInfoResult](../../../core/libs/prx/libSceNpMatching2/Export.cpp) (libSceNpMatching2) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpMatching2.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified ### Functional - [sceNpMatching2Initialize, sceNpMatching2Terminate](../../../core/libs/prx/libSceNpMatching2/Export.cpp) (libSceNpMatching2) - only the library state is kept: initialization checks the parameter's `size` field (0x28, or 0x30 with an SSL pool) as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/81be949167d0060b08f26dc3e055a0b640d129c8/src/core/libraries/np/np_matching2/np_matching2.cpp) does and starts no thread; a second initialization returns `ALREADY_INITIALIZED` and a termination without one `NOT_INITIALIZED`. The pool, thread and SSL settings are not used: no matching server is reached. Not measured on a console - [sceNpMatching2CreateContext and the context calls](../../../core/libs/prx/libSceNpMatching2/Export.cpp) (libSceNpMatching2) - offline only, not measured on a PS5: the checks follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/91e6afa69b0a64b0afaa0281b80f8fbb0cd88136/src/core/libraries/np/np_matching2/np_matching2.cpp#L26-L349) and its [context manager](https://github.com/shadps4-emu/shadPS4/blob/91e6afa69b0a64b0afaa0281b80f8fbb0cd88136/src/core/libraries/np/np_matching2/np_matching2_internal.cpp#L1348-L1457) (a `0x28`-byte parameter, ten ids handed out round-robin). `sceNpMatching2ContextStart` returns `0x804101E2` (`SCE_NET_ERROR_RESOLVER_ETIMEDOUT`), which shadPS4 says was confirmed on a console disconnected from the internet, so no context starts and `sceNpMatching2ContextStop` answers `CONTEXT_NOT_STARTED`. Callbacks and the default request option are checked but never used. The room, user, world, server-id and signaling-info calls still throw +- [sceNpMatching2CreateContextA](../../../core/libs/prx/libSceNpMatching2/Export.cpp) (libSceNpMatching2) - offline only: the A parameter (user id, service label, then a `size` of 0x10) and its checks follow a [later shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0614ce324be092791b758d79f93d9f1ca5b10a81/src/core/libraries/np/np_matching2/np_matching2.cpp#L48-L71), and the user id is unused. The context takes the next of the same ten slots with 0x100 added: firmware 13.42, [run natively](https://github.com/boykopovar/AnyPS5/pull/12623#issuecomment-6501291543), gave context 1 to `sceNpMatching2CreateContext` and 258 (0x0102) to the `sceNpMatching2CreateContextA` after it. That the 0x100 marks A contexts, and that slot 2 alone does not name the A context, are read from that single pair +- [sceNpMatching2RegisterLobbyEventCallback, sceNpMatching2RegisterLobbyMessageCallback](../../../core/libs/prx/libSceNpMatching2/Export.cpp) (libSceNpMatching2) - offline only, not measured on a PS5: checked like the room callbacks, as shadPS4 does ([event](https://github.com/shadps4-emu/shadPS4/blob/0614ce324be092791b758d79f93d9f1ca5b10a81/src/core/libraries/np/np_matching2/np_matching2.cpp#L166-L183), [message](https://github.com/shadps4-emu/shadPS4/blob/0614ce324be092791b758d79f93d9f1ca5b10a81/src/core/libraries/np/np_matching2/np_matching2.cpp#L654-L672)), and never called: no lobby is joined offline - [sceNpMatching2SignalingGetConnectionStatus, sceNpMatching2AbortContextStart](../../../core/libs/prx/libSceNpMatching2/Export.cpp) (libSceNpMatching2) - not measured on a console. With no room joined offline, the status call writes 0 (inactive) and returns 0 for a known context, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/3eecea94ddaca360b19f4b800523b609a8b2ffcc/src/core/libraries/np/np_matching2/np_matching2.cpp#L569-L637) does; the PS3 module in [RPCS3](https://github.com/RPCS3/rpcs3/blob/c365ea76a552961ff078ea1a46da9d4b07729674/rpcs3/Emu/Cell/Modules/sceNp2.cpp#L449-L952) also writes inactive but returns `INVALID_ROOM_ID`. A null status pointer throws (shadPS4 skips the write, RPCS3 returns `INVALID_ARGUMENT`); peer address and port are never written. The abort returns `NOT_INITIALIZED` before initialization, as RPCS3 does, and 0 for any context id, as both do +- [sceNpMatching2GetWorldInfoList, sceNpMatching2SetUserInfo, sceNpMatching2SetRoomDataExternal, sceNpMatching2SetRoomDataInternal, sceNpMatching2GetRoomDataInternal, sceNpMatching2SignalingGetPingInfo](../../../core/libs/prx/libSceNpMatching2/Export.cpp) (libSceNpMatching2) - follow firmware 13.42 [run natively](https://github.com/boykopovar/AnyPS5/pull/12623#issuecomment-6501291543) on never-started contexts: initialization, a null parameter, then its leading server id (`INVALID_SERVER_ID`) or room id (`INVALID_ROOM_ID`) come before the context (`CONTEXT_NOT_FOUND`). A request that passes these checks throws: the run accepted it and handed out a request id counted from 1, but with its worker threads stopped it could not show how the module completes, calls back or cancels the request offline. Not reproduced or measured: the checks on the rest of each parameter and a null request id (refused with the parameter, as in shadPS4) +- [sceNpMatching2LeaveRoom](../../../core/libs/prx/libSceNpMatching2/Export.cpp) (libSceNpMatching2) - checked like the room requests above, then returns `SESSION_NOT_FOUND` (0x80550C3A) on a known context and `CONTEXT_NOT_FOUND` on an unknown one, as firmware 13.42 [did](https://github.com/boykopovar/AnyPS5/pull/12623#issuecomment-6501700641), and room 0 gives `INVALID_ROOM_ID` on any context +- [sceNpMatching2GetServerId, sceNpMatching2GetRoomJoinedSlotMaskLocal, sceNpMatching2GetWorldIdArrayForAllServers](../../../core/libs/prx/libSceNpMatching2/Export.cpp) (libSceNpMatching2) - no context starts offline, so a known context gets what firmware 13.42 [returned](https://github.com/boykopovar/AnyPS5/pull/12623#issuecomment-6501291543) for a created, never-started one, writing nothing: `CONTEXT_NOT_STARTED`, `CONTEXT_NOT_STARTED` and `SERVER_NOT_AVAILABLE` (0x80550C28). An unknown context gives `CONTEXT_NOT_FOUND`. The first two refuse a null output before the context; room 0 gives 0x80550C0C on a known context ([measured](https://github.com/boykopovar/AnyPS5/pull/12623#issuecomment-6501700641)). The third checks the context first, then its pointers. Assumed: that initialization is checked first, as in the request calls, the widths of the third call's arguments and which of its pointers it checks +- [sceNpMatching2SignalingGetConnectionInfo, sceNpMatching2SignalingGetConnectionInfoA, sceNpMatching2SignalingGetPort, sceNpMatching2SignalingSetPort](../../../core/libs/prx/libSceNpMatching2/Export.cpp) (libSceNpMatching2) - as firmware 13.42 [did](https://github.com/boykopovar/AnyPS5/pull/12623#issuecomment-6501291543) with no context started: the connection info calls refuse a null `info` with 0x80550E15, then answer 0x80550E19 (no peer) for a known context and 0x80550E05 for an unknown one; the null check of the non-A form and the answer before initialization are not measured. The signaling port starts at 0 and `SetPort` stores what `GetPort` reports. Neither checks initialization here, and `GetPort` writes through its pointer with no null check, as the module does per its disassembly diff --git a/docs/dev/debt/libSceNpProfileDialog.md b/docs/dev/debt/libSceNpProfileDialog.md index 5d274e8544..6216a429d1 100644 --- a/docs/dev/debt/libSceNpProfileDialog.md +++ b/docs/dev/debt/libSceNpProfileDialog.md @@ -3,7 +3,7 @@ ### Silent stubs - [sceNpProfileDialogOpen, sceNpProfileDialogOpenA](../../../core/libs/prx/libSceNpProfileDialog/Export.cpp) (libSceNpProfileDialog) - with `ANYPS5_PSN_DIALOGS=show`, a successful open shows AnyPS5's notice that the feature needs PlayStation Network; the console's own display for this call without PlayStation Network is not measured, and the notice wording is AnyPS5's. -- [sceNpProfileDialogOpen, sceNpProfileDialogOpenA, sceNpProfileDialogGetResult](../../../core/libs/prx/libSceNpProfileDialog/Export.cpp) (libSceNpProfileDialog) - no profile is shown: `Open` and `OpenA` move the dialog straight to `FINISHED`, and `GetResult` reports `result` 0, `userAction` `USER_CANCELED` (1) and the caller's `userData`, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/c2814041e546bf75c12756dbec9da911c721ce1c/src/core/libraries/np/np_profile_dialog/np_profile_dialog.cpp#L21-L180) does when its window is dismissed. Not measured on a console. +- [sceNpProfileDialogOpen, sceNpProfileDialogOpenA, sceNpProfileDialogGetResult](../../../core/libs/prx/libSceNpProfileDialog/Export.cpp) (libSceNpProfileDialog) - no profile is shown: `Open` and `OpenA` move the dialog straight to `FINISHED`, and `GetResult` reports `result` 0, `userAction` `USER_CANCELED` (1) and the caller's `userData`, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/c2814041e546bf75c12756dbec9da911c721ce1c/src/core/libraries/np/np_profile_dialog/np_profile_dialog.cpp#L21-L180) does when its window is dismissed. Not measured on a console; returning `COMMON_DIALOG_ERROR_INVALID_ARGUMENT` for an unwritable result is a host-safety fallback as console behavior for unwritable memory has not been measured. ### Unknown function info diff --git a/docs/dev/debt/libSceNpScoreRanking.md b/docs/dev/debt/libSceNpScoreRanking.md index f7d1f09b94..7486cc6e12 100644 --- a/docs/dev/debt/libSceNpScoreRanking.md +++ b/docs/dev/debt/libSceNpScoreRanking.md @@ -1,11 +1,14 @@ # libSceNpScoreRanking technical debt -### Unknown function info +### Silent stubs -- [sceNpScoreCensorComment, sceNpScoreCensorCommentAsync, sceNpScoreChangeModeForOtherSaveDataOwners, sceNpScoreCreateNpTitleCtxA, sceNpScoreCreateTitleCtx, sceNpScoreGetBoardInfoAsync, sceNpScoreGetFriendsRankingA, sceNpScoreGetFriendsRankingAAsync, sceNpScoreGetFriendsRankingForCrossSave, sceNpScoreGetFriendsRankingForCrossSaveAsync, sceNpScoreGetGameData, sceNpScoreGetGameDataAsync, sceNpScoreGetGameDataByAccountId, sceNpScoreGetGameDataByAccountIdAsync, sceNpScoreGetRankingByAccountId](../../../core/libs/prx/libSceNpScoreRanking/Export.cpp) (libSceNpScoreRanking) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpScoreRanking.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified -- [sceNpScoreGetRankingByAccountIdAsync, sceNpScoreGetRankingByAccountIdForCrossSave, sceNpScoreGetRankingByAccountIdForCrossSaveAsync, sceNpScoreGetRankingByAccountIdPcId, sceNpScoreGetRankingByAccountIdPcIdAsync, sceNpScoreGetRankingByAccountIdPcIdForCrossSave, sceNpScoreGetRankingByAccountIdPcIdForCrossSaveAsync, sceNpScoreGetRankingByNpIdPcId, sceNpScoreGetRankingByNpIdPcIdAsync, sceNpScoreGetRankingByRangeA, sceNpScoreGetRankingByRangeAAsync, sceNpScoreGetRankingByRangeForCrossSave](../../../core/libs/prx/libSceNpScoreRanking/Export.cpp) (libSceNpScoreRanking) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpScoreRanking.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified -- [sceNpScoreGetRankingByRangeForCrossSaveAsync, sceNpScoreRecordGameData, sceNpScoreRecordGameDataAsync, sceNpScoreSanitizeComment, sceNpScoreSanitizeCommentAsync, sceNpScoreSetPlayerCharacterId, sceNpScoreSetThreadParam, sceNpScoreSetTimeout, sceNpScoreWaitAsync](../../../core/libs/prx/libSceNpScoreRanking/Export.cpp) (libSceNpScoreRanking) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpScoreRanking.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified +- [sceNpScoreSetThreadParam](../../../core/libs/prx/libSceNpScoreRanking/Export.cpp) (libSceNpScoreRanking) - answers 0 and changes nothing: the module [recreates its `SceNpScoreWorker` thread](https://github.com/boykopovar/AnyPS5/pull/12611#issuecomment-6494084502) with the given priority and affinity, and requests here finish without one +- [sceNpScoreChangeModeForOtherSaveDataOwners](../../../core/libs/prx/libSceNpScoreRanking/Export.cpp) (libSceNpScoreRanking) - checks the mode and the id as firmware 13.42 does ([disassembly](https://github.com/boykopovar/AnyPS5/pull/12611#issuecomment-6507773030)) and answers 0, but does not keep the mode: the module stores it in the title context or request for the service, and no request reaches a service here ### Functional - [libSceNpScoreRanking](../../../core/libs/prx/libSceNpScoreRanking/Export.cpp) (libSceNpScoreRanking) - offline only, not measured on a PS5: the argument checks, their order, the limit of 32 title contexts and 32 requests and the array sizes follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/91e6afa69b0a64b0afaa0281b80f8fbb0cd88136/src/core/libraries/np/np_score/np_score.cpp#L64-L1210), and a valid call then returns `SCE_NP_ERROR_SIGNED_OUT` and finishes its request with it, as shadPS4 does when the user [has no connection](https://github.com/shadps4-emu/shadPS4/blob/91e6afa69b0a64b0afaa0281b80f8fbb0cd88136/src/core/libraries/np/np_handler/np_handler_score.cpp#L18-L26). No output is written, where shadPS4 first clears the rank, comment and info arrays. A null `boardInfo` throws: shadPS4 answers it by the title's SDK version +- [the A, account id, character id, game data and board info variants](../../../core/libs/prx/libSceNpScoreRanking/Export.cpp) (libSceNpScoreRanking) - offline only, not measured on a PS5: signatures, checks, their order and element sizes follow a [later shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0614ce324be092791b758d79f93d9f1ca5b10a81/src/core/libraries/np/np_score/np_score.cpp#L85-L1589), which also answers `SCE_NP_ERROR_SIGNED_OUT` for a user with [no connection](https://github.com/shadps4-emu/shadPS4/blob/0614ce324be092791b758d79f93d9f1ca5b10a81/src/core/libraries/np/np_handler/np_handler_score.cpp#L529-L544). `sceNpScoreCreateNpTitleCtxA` checks the sign-in first, so it always returns `SCE_NP_ERROR_SIGNED_OUT`; `sceNpScoreGetGameData` finishes the request with `INVALID_ARGUMENT` for an empty online id; `sceNpScoreSetPlayerCharacterId` only checks its arguments, since no request reaches a server +- [the ForCrossSave calls, sceNpScoreSetTimeout, sceNpScoreSetThreadParam, sceNpScoreCreateTitleCtx](../../../core/libs/prx/libSceNpScoreRanking/Export.cpp) (libSceNpScoreRanking) - follow firmware 13.42's module as [read from its disassembly](https://github.com/boykopovar/AnyPS5/pull/12611#issuecomment-6494084502), not run: each ForCrossSave call is its regular call under another name, and `sceNpScoreGetRankingByRange` takes 0x80 or 0x90 bytes per rank. `sceNpScoreSetTimeout` checks the retry count and the minimum timeouts in the module's order; the module picks a title context or a request by the id range, and the ids here overlap, so both are looked up. `sceNpScoreCreateTitleCtx` prints its three warnings and takes a context from the same table. The module's `NOT_INITIALIZED` checks never apply here +- [sceNpScoreWaitAsync](../../../core/libs/prx/libSceNpScoreRanking/Export.cpp) (libSceNpScoreRanking) - throws for a request with no operation started. Firmware 13.42 [blocks](https://github.com/boykopovar/AnyPS5/pull/12611#issuecomment-6507773030) there on the request's semaphore, which only the worker's completion signals, as shadPS4 waits forever; neither `sceNpScoreAbortRequest` nor `sceNpScoreDeleteRequest` [releases it](https://github.com/boykopovar/AnyPS5/pull/12611#issuecomment-6520890176), so such a wait never returns +- [sceNpScoreCensorComment, sceNpScoreSanitizeComment, sceNpScoreRecordGameData and their async forms](../../../core/libs/prx/libSceNpScoreRanking/Export.cpp) (libSceNpScoreRanking) - make the checks firmware 13.42 makes before queueing to its worker ([disassembly](https://github.com/boykopovar/AnyPS5/pull/12611#issuecomment-6507773030), not run): an option, then a null comment or data, then the request, then a comment longer than 255 characters. Valid calls then answer offline like the rest of the library. The module copies the score and sizes unchecked and leaves the chunk checks to the service, `sanitizedComment` is never written, and the async forms finish the request here instead of returning 0 for the worker to finish diff --git a/docs/dev/debt/libSceNpTcs.md b/docs/dev/debt/libSceNpTcs.md index f22ccbfae8..57ae1442e8 100644 --- a/docs/dev/debt/libSceNpTcs.md +++ b/docs/dev/debt/libSceNpTcs.md @@ -4,4 +4,4 @@ - [sceNpTcsIntDownloadData, sceNpTcsIntUploadDataToNewSlotAbort and sceNpTcsIntUploadDataToNewSlotBegin](../../../core/libs/prx/libSceNpTcs/Export.cpp) (libSceNpTcs) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpTcs.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified -- [Older callable NIDs](../../../core/libs/prx/libSceNpTcs/Export.cpp): 8 unidentified exports have unverified names, signatures and behavior. These are throwing import-resolution placeholders; no functionality is implemented. No matching NIDs were found in the later module inventory. +- [Older callable NIDs](../../../core/libs/prx/libSceNpTcs/Export.cpp): The [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv) names these exports `sceNpTcsIntAbortRequest`, `sceNpTcsIntCreateContext`, `sceNpTcsIntCreateRequest`, `sceNpTcsIntDestroyContext`, `sceNpTcsIntDestroyRequest`, `sceNpTcsIntUploadDataToNewSlot`, `sceNpTcsIntUploadDataToNewSlotEnd` and `sceNpTcsIntUploadDataToNewSlotWrite`, and the NID of each name matches. Signatures and behavior are unverified. These are throwing import-resolution placeholders; no functionality is implemented. No matching NIDs were found in the later module inventory. diff --git a/docs/dev/debt/libSceNpTrophy.md b/docs/dev/debt/libSceNpTrophy.md index c5a1f5bb5a..324848d8ce 100644 --- a/docs/dev/debt/libSceNpTrophy.md +++ b/docs/dev/debt/libSceNpTrophy.md @@ -8,8 +8,8 @@ - [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`. 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. - [sceNpTrophyAbortHandle](../../../core/libs/prx/libSceNpTrophy/Export.cpp) (libSceNpTrophy, PS4) - measured on a PS4 Pro (firmware 10.71): after an abort, `sceNpTrophyGetTrophyUnlockState` and `sceNpTrophyGetGameIcon` through that handle answer 0x80551607, before the not-registered check, and `sceNpTrophyRegisterContext` answers 0x80551607 before the missing trophy package (0x80551626 for a live handle); aborting again answers 0. The other calls that take the handle are assumed to answer 0x80551607 the same way, as `libSceNpTrophy.sprx` of firmware 10.71 maps the service's abort answer to it on the shared request path. On the console `sceNpTrophyRegisterContext` checks the process's NP title id before it reaches the service, so a title without one gets the NP title id error instead; AnyPS5 has no NP title id and does not model that step -- [sceNpTrophyCaptureScreenshot, sceNpTrophyConfigGetTrophyDetails, sceNpTrophyConfigGetTrophyFlagArray, sceNpTrophyConfigGetTrophyGroupArray, sceNpTrophyConfigGetTrophyGroupDetails, sceNpTrophyConfigGetTrophySetInfo, sceNpTrophyConfigGetTrophySetInfoInGroup, sceNpTrophyConfigGetTrophySetVersion, sceNpTrophyConfigGetTrophyTitleDetails, sceNpTrophyConfigHasGroupFeature, sceNpTrophyGroupArrayGetNum, sceNpTrophyIntAbortHandle, sceNpTrophyIntCheckNetSyncTitles, sceNpTrophyIntCreateHandle](../../../core/libs/prx/libSceNpTrophy/Export.cpp) (libSceNpTrophy) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpTrophy.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified -- [sceNpTrophyIntDestroyHandle, sceNpTrophyIntGetLocalTrophySummary, sceNpTrophyIntGetProgress, sceNpTrophyIntGetRunningTitle, sceNpTrophyIntGetRunningTitles, sceNpTrophyIntGetTrpIconByUri, sceNpTrophyIntNetSyncTitle, sceNpTrophyIntNetSyncTitles, sceNpTrophyNumInfoGetTotal, sceNpTrophySetInfoGetTrophyFlagArray, sceNpTrophySetInfoGetTrophyNum, sceNpTrophyShowTrophyList, sceNpTrophySystemAbortHandle, sceNpTrophySystemBuildGroupIconUri, sceNpTrophySystemBuildNetTrophyIconUri, sceNpTrophySystemBuildTitleIconUri](../../../core/libs/prx/libSceNpTrophy/Export.cpp) (libSceNpTrophy) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpTrophy.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified +- [sceNpTrophyCaptureScreenshot, sceNpTrophyConfigGetTrophyDetails, sceNpTrophyConfigGetTrophyFlagArray, sceNpTrophyConfigGetTrophyGroupArray, sceNpTrophyConfigGetTrophyGroupDetails, sceNpTrophyConfigGetTrophySetInfo, sceNpTrophyConfigGetTrophySetInfoInGroup, sceNpTrophyConfigGetTrophySetVersion, sceNpTrophyConfigGetTrophyTitleDetails, sceNpTrophyIntAbortHandle, sceNpTrophyIntCheckNetSyncTitles, sceNpTrophyIntCreateHandle](../../../core/libs/prx/libSceNpTrophy/Export.cpp) (libSceNpTrophy) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpTrophy.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified +- [sceNpTrophyIntDestroyHandle, sceNpTrophyIntGetLocalTrophySummary, sceNpTrophyIntGetProgress, sceNpTrophyIntGetRunningTitle, sceNpTrophyIntGetRunningTitles, sceNpTrophyIntGetTrpIconByUri, sceNpTrophyIntNetSyncTitle, sceNpTrophyIntNetSyncTitles, sceNpTrophyShowTrophyList, sceNpTrophySystemAbortHandle, sceNpTrophySystemBuildGroupIconUri, sceNpTrophySystemBuildNetTrophyIconUri, sceNpTrophySystemBuildTitleIconUri](../../../core/libs/prx/libSceNpTrophy/Export.cpp) (libSceNpTrophy) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpTrophy.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified - [sceNpTrophySystemBuildTrophyIconUri, sceNpTrophySystemCheckCallback, sceNpTrophySystemCheckNetSyncTitles, sceNpTrophySystemCheckRecoveryRequired, sceNpTrophySystemCloseStorage, sceNpTrophySystemCreateContext, sceNpTrophySystemCreateHandle, sceNpTrophySystemDebugLockTrophy, sceNpTrophySystemDebugUnlockTrophy, sceNpTrophySystemDestroyContext, sceNpTrophySystemDestroyHandle, sceNpTrophySystemDestroyTrophyConfig](../../../core/libs/prx/libSceNpTrophy/Export.cpp) (libSceNpTrophy) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpTrophy.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified - [sceNpTrophySystemGetGroupIcon, sceNpTrophySystemGetLocalTrophySummary, sceNpTrophySystemGetNextTitleFileEntryStatus, sceNpTrophySystemGetProgress, sceNpTrophySystemGetTitleFileStatus, sceNpTrophySystemGetTitleIcon, sceNpTrophySystemGetTitleSyncStatus, sceNpTrophySystemGetTrophyConfig, sceNpTrophySystemGetTrophyData, sceNpTrophySystemGetTrophyGroupData, sceNpTrophySystemGetTrophyIcon, sceNpTrophySystemGetTrophyTitleData, sceNpTrophySystemGetUserFileInfo, sceNpTrophySystemGetUserFileStatus](../../../core/libs/prx/libSceNpTrophy/Export.cpp) (libSceNpTrophy) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpTrophy.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified - [sceNpTrophySystemIsServerAvailable, sceNpTrophySystemNetSyncTitle, sceNpTrophySystemNetSyncTitles, sceNpTrophySystemOpenStorage, sceNpTrophySystemPerformRecovery, sceNpTrophySystemRegisterTitleSyncedCallback, sceNpTrophySystemRegisterTitleUpdateCallback, sceNpTrophySystemRemoveAll, sceNpTrophySystemRemoveTitleData, sceNpTrophySystemRemoveUserData, sceNpTrophySystemUnregisterTitleSyncedCallback, sceNpTrophySystemUnregisterTitleUpdateCallback](../../../core/libs/prx/libSceNpTrophy/Export.cpp) (libSceNpTrophy) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpTrophy.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified @@ -19,10 +19,13 @@ ### Functional +- [sceNpTrophyNumInfoGetTotal, sceNpTrophyGroupArrayGetNum, sceNpTrophySetInfoGetTrophyNum, sceNpTrophySetInfoGetTrophyFlagArray and sceNpTrophyConfigHasGroupFeature](../../../core/libs/prx/libSceNpTrophy/Export.cpp) (libSceNpTrophy) - read only their arguments in firmware 13.42 ([disassembly, the first four also run natively](https://github.com/boykopovar/AnyPS5/pull/12957#issuecomment-6507951946)): the wrapping sum of four counts, the set bits of the first group word, the set bits of the four 16-byte trophy flag arrays and their OR into a 16-byte output, and bit 0 of the config byte at +0x90 (0 for a null config). The other four fault on null as the module does. The flag-array call returns the set bits of its output, 0 to 128 ([run and read](https://github.com/boykopovar/AnyPS5/pull/13005#issuecomment-6510415052)) - [sceNpTrophyGetGameInfo](../../../core/libs/prx/libSceNpTrophy/Export.cpp) (libSceNpTrophy) - the trophy configuration `TROPCONF.ESFM` is encrypted and not read, so the game details report the trophy count taken from the `TROPnnn.PNG` icons and the group count taken from the `GRnnn.PNG` icons of the trophy package; the per-grade counts are 0, the title and description are empty, and the progress percentage is unlocked trophies over all trophies instead of the console's grade-weighted points - [sceNpTrophyGetGroupInfo](../../../core/libs/prx/libSceNpTrophy/Export.cpp) (libSceNpTrophy) - only the base game group (id -1) is answered, with the same counts as the game; an id past the `GRnnn.PNG` icons returns `INVALID_GROUP_ID`, and an existing group throws because the trophies of a group are listed only in the encrypted configuration - [sceNpTrophyGetTrophyInfo](../../../core/libs/prx/libSceNpTrophy/Export.cpp) (libSceNpTrophy) - the grade is reported as unknown, the trophy is never hidden, the name and description are empty and the unlock timestamp is 0; a package with groups throws, since a trophy's group is listed only in the encrypted configuration +- [TRP entry bounds](../../../core/libs/prx/libSceNpTrophy/Export.cpp) (libSceNpTrophy, PS4) - returning `0x80551616` for a table or entry range outside the physical trophy file is host-side validation, not console-verified. Bounds checks do not authenticate the package or its contents. + ### Additional callable exports - [Additional callable exports](../../../core/libs/prx/libSceNpTrophy/Export.cpp) - 6 entries present in both version-labelled images are import-resolution declarations only. None is listed in the pinned public 3.20 catalogue for this library. Neither label independently authenticates retail firmware provenance. Parameter lists, return types and behavior are unverified; parameterless `int` declarations are placeholders, not verified ABI contracts. Every entry throws through `NotImplemented_nid_no_patch`. Unidentified names use literal NID aliases. No runtime functionality is implemented. Identified candidate names match their NIDs in the [public symbol database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv) or [supplemental catalog](https://github.com/claimore22/ps5rs/blob/2304be5131221bae0dac24b9767cabffda33f747/data/nids.csv). diff --git a/docs/dev/debt/libSceNpUniversalDataSystem.md b/docs/dev/debt/libSceNpUniversalDataSystem.md index f0e2e47289..65be689e58 100644 --- a/docs/dev/debt/libSceNpUniversalDataSystem.md +++ b/docs/dev/debt/libSceNpUniversalDataSystem.md @@ -21,7 +21,7 @@ | `sceNpUniversalDataSystemIntOpenStreamByUri` | `tkUOy4Ag4wo` | | `sceNpUniversalDataSystemIntReadStream` | `ABFxQ2B50RA` | -- [Events](../../../core/libs/prx/libSceNpUniversalDataSystem/src/Event.cpp): `sceNpUniversalDataSystemCreateEvent` copies `prop` and returns the event's own property object through `prop_ptr`, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/libTrophy.cpp) does; `sceNpUniversalDataSystemEventPropertyObjectSetObject`, `SetArray` and the array setters for objects and arrays store a copy of `value` and return the stored copy through `value_ptr`. Destroying a property object or array that its Create function did not return (an event's property object or a stored copy) does nothing, as its owner frees it. These ownership rules are not verified on a console, nor are the output of `sceNpUniversalDataSystemEventToString` (still `{}`) and `sceNpUniversalDataSystemEventEstimateSize` (still 3). +- [Events](../../../core/libs/prx/libSceNpUniversalDataSystem/src/Event.cpp): `sceNpUniversalDataSystemCreateEvent` copies `prop` and returns the event's own property object through `prop_ptr`, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/libTrophy.cpp) does; `sceNpUniversalDataSystemEventPropertyObjectSetObject`, `SetArray` and the array setters for objects and arrays store a copy of `value` and return the stored copy through `value_ptr`. Destroying a property object or array that its Create function did not return (an event's property object or a stored copy) does nothing, as its owner frees it. These ownership rules are not verified on a console, nor is the text of `sceNpUniversalDataSystemEventToString` (a debug rendering of the event, not the console's format) and the size that `sceNpUniversalDataSystemEventEstimateSize` returns (a MessagePack size model, not measured). - [sceNpUniversalDataSystemCreateContext](../../../core/libs/prx/libSceNpUniversalDataSystem/src/Context.cpp) and [sceNpUniversalDataSystemCreateHandle](../../../core/libs/prx/libSceNpUniversalDataSystem/src/Handle.cpp) (libSceNpUniversalDataSystem) - native return codes for null output pointers (`context == nullptr` and `handle == nullptr`) are unresolved and unverified on a console. The reimplementation in [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/libTrophy.cpp#L620-L640) returns `0x80553102` (`SCE_NP_UNIVERSAL_DATA_SYSTEM_ERROR_INVALID_ARGUMENT`), but this error return is unverified against native hardware; host paths continue to throw `std::invalid_argument` until the native contract is established. No call that could be decoded in the titles checked passes a constant null pointer to either function. - [sceNpUniversalDataSystemIntCreateRecordArray, sceNpUniversalDataSystemIntCreateRecordData and sceNpUniversalDataSystemIntCreateRecordObject](../../../core/libs/prx/libSceNpUniversalDataSystem/PendingExports.cpp) (libSceNpUniversalDataSystem) - The list is the intersection of the export table of the console's libSceNpUniversalDataSystem (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpUniversalDataSystem.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. @@ -36,6 +36,8 @@ - [sceNpUniversalDataSystemIntRecordObjectSetBool, sceNpUniversalDataSystemIntRecordObjectSetFloat32 and sceNpUniversalDataSystemIntRecordObjectSetFloat64](../../../core/libs/prx/libSceNpUniversalDataSystem/PendingExports.cpp) (libSceNpUniversalDataSystem) - The list is the intersection of the export table of the console's libSceNpUniversalDataSystem (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpUniversalDataSystem.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. - [sceNpUniversalDataSystemIntRecordObjectSetInt32, sceNpUniversalDataSystemIntRecordObjectSetInt64 and sceNpUniversalDataSystemIntRecordObjectSetObject](../../../core/libs/prx/libSceNpUniversalDataSystem/PendingExports.cpp) (libSceNpUniversalDataSystem) - The list is the intersection of the export table of the console's libSceNpUniversalDataSystem (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpUniversalDataSystem.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. - [sceNpUniversalDataSystemIntRecordObjectSetString, sceNpUniversalDataSystemIntRecordObjectSetUInt32 and sceNpUniversalDataSystemIntRecordObjectSetUInt64](../../../core/libs/prx/libSceNpUniversalDataSystem/PendingExports.cpp) (libSceNpUniversalDataSystem) - The list is the intersection of the export table of the console's libSceNpUniversalDataSystem (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpUniversalDataSystem.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. +- [sceNpUniversalDataSystemIntAbortHandle, sceNpUniversalDataSystemIntCheckNetSyncTitles, sceNpUniversalDataSystemIntCreateContext, sceNpUniversalDataSystemIntCreateHandle, sceNpUniversalDataSystemIntDeleteAllData and sceNpUniversalDataSystemIntDeleteUserDataWithServerData](../../../core/libs/prx/libSceNpUniversalDataSystem/Export.cpp) (libSceNpUniversalDataSystem) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpUniversalDataSystem.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim, and each throws until its signature is verified +- [sceNpUniversalDataSystemIntDestroyContext, sceNpUniversalDataSystemIntDestroyHandle, sceNpUniversalDataSystemIntInitialize, sceNpUniversalDataSystemIntNetSyncTitles and sceNpUniversalDataSystemIntTerminate](../../../core/libs/prx/libSceNpUniversalDataSystem/Export.cpp) (libSceNpUniversalDataSystem) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpUniversalDataSystem.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim, and each throws until its signature is verified ### Functional diff --git a/docs/dev/debt/libSceNpWebApi2.md b/docs/dev/debt/libSceNpWebApi2.md index f6f1f467a9..5aef569be5 100644 --- a/docs/dev/debt/libSceNpWebApi2.md +++ b/docs/dev/debt/libSceNpWebApi2.md @@ -9,6 +9,15 @@ ### Unknown function info +- [libSceNpWebApi2 additional exports](../../../core/libs/prx/libSceNpWebApi2/Export.cpp) - these name/NID pairs are present in the inspected export inventory. Parameter lists, return types and behavior are unverified. Parameterless `int` declarations are import-resolution placeholders, not verified ABI prototypes; every entry throws through `NotImplemented_nid_no_patch`. No runtime functionality is implemented. + +The inventory label is not authenticated retail-firmware provenance. The inspected image contains raw module-parameter words `0x13590001` at +0x10 and `0x13600007` at +0x14. Using the [published static ELF membership procedure](https://github.com/boykopovar/AnyPS5/pull/12367#issuecomment-6470603298), its 46 unique function NIDs have sorted-LF SHA-256 `c21118be1db21feb9029a685e16abd9ff7d5761c3d31b15d38b742d43c83a330`; all 2 entries below are present. This digest identifies the function-NID set, not the whole binary or its ABI. An independent review on [this PR](https://github.com/boykopovar/AnyPS5/pull/11955#pullrequestreview) corroborated these identities using a separately reported 13.42 reference; that does not establish matching binaries, prototypes or behavior. + +| Name | NID | +| --- | --- | +| `sceNpWebApi2IntGlobalPush2Initialize` | `xIcy2kpLv6M` | +| `sceNpWebApi2IntGlobalPush2Terminate` | `UCOQAasV6gw` | + - [sceNpWebApi2AddMultipartPart, sceNpWebApi2IntCreateRequest, sceNpWebApi2IntPushEventCreateCtxIndFilter and sceNpWebApi2SetMultipartContentType](../../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpWebApi2.c); no-argument prototypes follow [shadPS4's PS4 stubs](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_web_api2/np_web_api2.cpp), not measured on a console. These declarations have no parameters solely to resolve known imports and make no ABI claim. - [sceNpWebApi2InitializeForPresence, sceNpWebApi2IntInitialize and sceNpWebApi2IntInitialize2](../../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2) - create a library context like `sceNpWebApi2Initialize`, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/8d50471b7dc141dd700b51276b8edd1f293bce26/src/core/libraries/np/np_web_api2/np_web_api2.cpp#L174-L222) does on the PS4: the argument structures (32 and 40 bytes) and the `struct_size` check that returns `SCE_NP_WEBAPI2_ERROR_INVALID_ARGUMENT` follow its [declarations](https://github.com/shadps4-emu/shadPS4/blob/8d50471b7dc141dd700b51276b8edd1f293bce26/src/core/libraries/np/np_web_api2/np_web_api2.h#L15-L31). The context type shadPS4 records (2 for the internal initializers, 3 for presence), the context name and the push configuration group are not kept, and the 0x8000 context limit is not applied. Not measured on a PS5 - [sceNpWebApi2CreateMultipartRequest and sceNpWebApi2SendMultipartRequest](../../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2) - signatures and checks follow the PS4 implementations in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/e0845a12dc05f2fde85e7b203c76de03f93a585d/src/core/libraries/np/np_web_api2/np_web_api2.cpp#L52-L72) and [its multipart sender](https://github.com/shadps4-emu/shadPS4/blob/e0845a12dc05f2fde85e7b203c76de03f93a585d/src/core/libraries/np/np_web_api2/np_web_api2.cpp#L388-L399): a null API group, path or method, a part index below 1, a null part or an empty part returns `SCE_NP_WEBAPI2_ERROR_INVALID_ARGUMENT`. As `sceNpWebApi2CreateRequest` and `sceNpWebApi2SendRequest` do here, a create takes the next shared ID (written only for a non-null ID pointer) and a send answers `SCE_NP_WEBAPI2_ERROR_UNAVAILABLE`; user contexts are not checked and parts are not kept. Not measured on a PS5 diff --git a/docs/dev/debt/libScePad.md b/docs/dev/debt/libScePad.md index 46ec699813..4d685d525a 100644 --- a/docs/dev/debt/libScePad.md +++ b/docs/dev/debt/libScePad.md @@ -39,7 +39,8 @@ - [scePadReadState and scePadRead for port 0 while it is withdrawn](../../../core/libs/prx/libScePad/src/PadState.cpp) (libScePad) - under ANYPS5_PAD_PORT0_STATE=controller a port 0 without a controller reads zeroed data (buttons, sticks, triggers and touch zero, orientation identity) with connected false, and scePadRead returns one entry. The shape follows [shadPS4's pad.cpp](https://github.com/shadps4-emu/shadPS4/blob/1d8004a22feb0d5cc58828bac066018e80b6d73f/src/core/libraries/pad/pad.cpp#L369-L374) at commit 1d8004a22feb0d5cc58828bac066018e80b6d73f. What the console returns for a removed controller (zeroed, neutral or an error code) and the connected_count it reports are not measured. - [scePadOpen and scePadOpenExt](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the special port (type 2) opens a handle per user (`PAD_SPECIAL_HANDLE` + slot, 16 slots for the console's 16 accounts, a 17th user gets the unknown-user code 0x809b0081) that every pad call accepts: reads give the rest record (sticks 0x80, orientation w 1, acceleration y 1, `connected` false), information is zeroed, extended information has device class -1, setters succeed without effect. Not measured: the console's own handle values (fresh per open), a PS5, and the calls the PS4 Pro survey (firmware 10.71, no special controller) did not cover; it measured open, `ALREADY_OPENED`, `scePadGetHandle`, both reads, both informations, the five setters above, close, and that two logged-in users get independent handles. `scePadOpenExt` returns `DEVICE_NOT_CONNECTED` (0x80920007) for special ports, unmeasured; PPSA01317 skips a user on any negative result. `scePadOpenExt` throws for other port types - [scePadOpen, scePadGetHandle, scePadRead and scePadReadState for the remote control port](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - whether a console without a media remote opens the system user's remote control port (type 16), returns `DEVICE_NOT_CONNECTED` or fills other fields is not measured. The host has no remote, so the port opens its own handle (`PAD_REMOTE_HANDLE`) that reads one zeroed entry with no buttons and `connected` false; the other pad calls reject it with `INVALID_HANDLE`. SDL's PS5 video driver maps this port's buttons to keys, so a remote that read the DualSense turned Cross into Return (Raptor PPSA94002). -- [scePadOpen](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - port types 3 and 4 throw: a payload process on a PS5 (firmware 13.60) let them past the type check, but it cannot open a port for its own logged-in user (0x809b0081), so what they open is unknown. The argument checks it did reach are followed: types 1, 5, 6, 255 and -1, and 16 for a user other than the system user, fail with `INVALID_ARG` before the user is looked at, user -1 with 0x809b0001 on types 0, 2, 3 and 4 and on index 1, and index 1 of a logged-in user with 0x809b0003; whether index 1 of types 3 and 4 fails the same way was not measured; the payload never tried type 16 with the system user (255), which opens the remote control port as below. Of 21 calls in ten titles checked, all pass index 0 and no param; 15 pass type 0, three type 2 (PPSA01494, PPSA30839, PPSA31162) and three a type from a register or field (PPSA18194, PPSA25286) +- [scePadOpen](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - types 3 and 4 return `INVALID_ARG` for a valid user as a compatibility fallback. The application-info gate and successful VR handles are not modelled. On PS5 firmware 13.40, a closed gate returns `INVALID_ARG` before user and index checks. With the gate open, PPSA01317 opened both types without connected VR controllers, with invalid user and index returning 0x809b0001 and 0x809b0003 ([evidence](https://github.com/boykopovar/AnyPS5/pull/10935#issuecomment-6408526250)). The fallback checks the user first. The relation to `attribute3` needs confirmation. A firmware 13.60 payload passed the type check but failed to open a port (0x809b0081). Of 21 calls in ten titles, 15 use type 0, three type 2 and three a dynamic type. All use index 0 and null param. +- [scePadOpen argument observations](../../../core/libs/prx/libScePad/Export.cpp) - the 13.60 payload observed invalid types 1, 5, 6, 255, -1 and type 16 for a non-system user rejected before user validation. User -1 returned 0x809b0001 on types 0, 2, 3 and 4, and index 1 on type 0. A valid user with index 1 returned 0x809b0003. Index 1 for types 3 and 4 and type 16 for the system user were not measured. Of the type 2 calls above, PPSA01494, PPSA30839 and PPSA31162 each have one. PPSA18194 and PPSA25286 have the dynamic calls. - [scePadVrControllerRead](../../../core/libs/prx/libScePad/Export.cpp) - the handle, output pointer and count arguments and the 104-byte output layout are recovered, but touch-value semantics and kernel report production are not measured, so the returned sample and the success conditions are unknown; the export still throws. - [scePadVrControllerReadState](../../../core/libs/prx/libScePad/Export.cpp) - the 104-byte output layout is recovered, including buttons, stick, triggers, touch fields, motion, connection state and timestamp. Touch-value semantics and kernel report production remain incomplete; the export still throws. - [scePadVrControllerReadCombinedState](../../../core/libs/prx/libScePad/Export.cpp) - the two handle arguments and 128-byte output are recovered; output field semantics remain incomplete. @@ -47,6 +48,8 @@ - [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 +- [scePadGetParticularMode, scePadGetControllerBusType, scePadGetJackState, scePadIsControllerUpdateRequired and scePadGetContainerIdInformation](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the console's answers are not measured; signatures, the 0x2004-byte container record and the bus values (USB 1, Bluetooth 2) come from [duaLib](https://github.com/WujekFoliarz/duaLib/blob/master/src/include/duaLib.h), which reimplements the PC `libScePad.dll`. The emulated pad reports Bluetooth, no headset (state 0), no update required and an empty container id (duaLib fills it on Windows only). A special handle and a disconnected port answer `DEVICE_NOT_CONNECTED`, as in duaLib. `scePadSetParticularMode` stores its argument for the getter and nothing else: duaLib uses it to expose the Share and PS buttons in reads, which is not modelled. The null-pointer check before the handle check follows the library's other pad queries, not duaLib +- [scePadIsSupportedAudioFunction, scePadSetAudioOutPath and scePadSetVolumeGain](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the console's answers are not measured; signatures, the 4-byte gain record (speaker, headset, padding, microphone) and the path values 0 to 3 (stereo headset, mono-left headset, mono-left headset and speaker, speaker only) come from [duaLib](https://github.com/WujekFoliarz/duaLib/blob/master/src/include/duaLib.h). A path above 4 is rejected, 4 included, as in duaLib's [check](https://github.com/WujekFoliarz/duaLib/blob/master/src/source/duaLib.cpp), and what 4 means is unknown; duaLib's gain range test can never fail, so none is applied. Path and gain are only kept per port in `PadOutputState`: no host backend drives the controller speaker or headset or scales the gain to the hardware range. A special handle accepts the setters without effect - [scePadIsValidHandle](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the `bool (int handle, int portType)` signature is recovered. Firmware 13.42's module, as BunnyFl ran it natively over a slot table he wrote ([comment](https://github.com/boykopovar/AnyPS5/pull/10880#issuecomment-6415854351); the driver emulated, the table not built by `scePadOpen`), answers 1 only when a slot holds the handle with exactly that port type, and 0 while not initialised. Here it is the same match on each open handle's type: 0 for a standard handle while its port is open, 2 for a special handle while its user's slot is open, 16 for the remote handle while the system user's remote port is open, and no match before `scePadInit`. `scePadOpen` opens no type-1 handle here, so no handle matches type 1. A closed standard handle is not valid, although the light bar, vibration and other setters keep accepting it - [scePadResetLightBarAll and scePadResetLightBarAllByPortType](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the `(void)` and `(uint32_t portType)` signatures are recovered, and both follow firmware 13.42's module as BunnyFl ran it natively ([comment](https://github.com/boykopovar/AnyPS5/pull/10880#issuecomment-6415854351); the driver emulated, port type 1 unknown, the table not built by `scePadOpen`). By port type, any type but 0, 1 and 2 returns `INVALID_ARG` before the initialisation check (3, 4, 16 and -1 measured). Otherwise each open handle of that type is reset as `scePadResetLightBar` resets it, and the call returns `PAD_OK`, also before `scePadInit`, when it does nothing; `scePadResetLightBarAll` is type 0. In this model a special handle's reset has no effect, as special handles have no light bar, and `scePadOpen` opens no type-1 handle - [scePadSetVibrationForce](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the `(int handle, const PadVibrationParam*)` signature is recovered. Firmware 13.42's module, as BunnyFl ran it natively ([comment](https://github.com/boykopovar/AnyPS5/pull/10880#issuecomment-6415854351); the driver emulated), returns `0x80920005` before `scePadInit`, even for a null `param`, then `INVALID_ARG` for a null `param`; otherwise it sends `scePadSetVibration`'s request with a force byte set, without looking the handle up, and returns the driver's answer. There is no pad driver here: a special handle returns `PAD_OK` without effect, as with `scePadSetVibration`, and a standard handle sets its pad port's motors whether or not it is still open. A handle with no pad port throws, as the driver's answer for it is not measured. In this model the force byte has no effect, as there is no system vibration-off setting to override (`scePadSetVibrationMode` only stores its mode) @@ -54,6 +57,10 @@ - [scePadGetAccessibilityControllerState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the value a console reports for a DualSense is not measured; the emulated pad, which is not an Access controller, reports 0. The name hashes to NID `zBsgF0q8DIM`. In the OS 10.01 system module the `(int handle, uint32_t* state)` function returns `0x80920005` before `scePadInit`, then `INVALID_ARG` for a NULL `state` and `INVALID_HANDLE` for a bad handle, and otherwise asks the pad driver for a per-device value of the device behind the handle: 0 when no device is behind it, 1 when the driver has no answer, else the driver's value. What the values mean is unknown - [scePadGetControllerType](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the value a console reports for a DualSense is not measured; the emulated pad reports `PAD_CONTROLLER_TYPE_DUAL_SENSE` (2), following [duaLib](https://github.com/WujekFoliarz/duaLib/blob/master/src/include/duaLib.h)'s `s_SceControllerType` enum where `UNKNOWN = 0` and `DUALSENSE = 2`; special handles report 0 (`UNKNOWN`). The signature `(int handle, int32_t* type)` follows duaLib, while the validation order (checking initialization, null pointer, then handle) follows the library's other pad queries; duaLib does not validate null pointers and returns `PAD_ERROR_DEVICE_NOT_CONNECTED` for an invalid pad. - [scePadDisableVibration, scePadIsLightBarBaseBrightnessControllable](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - `scePadDisableVibration(true)` stops active running motors on the port without disabling vibration for subsequent `scePadSetVibration` calls; `scePadIsLightBarBaseBrightnessControllable` reports 1 for standard DualSense handles and 0 for special handles while the modifier is a no-op. +- [scePadGetDeviceInfo, GetCapability, GetVersionInfo, GetIdleCount, GetControllerIdleCount, GetActiveTime, SetPS4BcVibrationMode, GetInfoByPortType, GetExtensionUnitInfo, ReadHistory and OpenExt2](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - checks and their order follow @BunnyFl's [13.42 disassembly](https://github.com/boykopovar/AnyPS5/issues/15087#issuecomment-6580925450) of `libScePad.sprx` (SHA-256 `57eab7054e10d95ba0028dbd4808f94eb34c7663333a2b7ca9f68dc90cf62c60`), not a console. Past them each asks the driver, so it throws; a closed device fd counts as uninitialised. `GetInfoByPortType` checks a null output, then initialisation, then the type (0 or 2). `GetIdleCount` returns 0 before `scePadInit`, and `GetActiveTime` clears its output first. `ReadHistory`'s third argument is a byte size (0x960), and `OpenExt2(int32 userId, int32, const void*)` requires 0x10 in its unexplained middle argument. `GetExtensionUnitInfo`'s second-flag check (0x80920101) isn't modelled. + +- [scePadReadStateExt and scePadGetDeviceId](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - per the same [reading](https://github.com/boykopovar/AnyPS5/issues/15087#issuecomment-6580925450), `ReadStateExt` is `scePadReadState` with an extension flag: it runs this library's `ReadState` checks (handle, then output), then throws, since what the flag changes isn't known. The module checks a null output first, zero-fills the record, then checks initialisation before the handle; neither `ReadState` follows that order yet. `GetDeviceId(handle, int32*)` writes -1, then refuses an unknown handle with 0x80920003; the console then copies an id from the handle's slot (+0x18) whose source isn't known, so a known handle throws. + - [libScePad internal data and device control](../../../core/libs/prx/libScePad/Export.cpp) - kernel-owned version, telemetry, Lotus-button and update payload fields remain unresolved. Lotus-button parameter units and the three blinking byte meanings are unknown. Port information occupies 152 bytes; history records occupy 72 bytes; Leonardo unique data occupies 24 bytes. Their complete field semantics remain unresolved. - [libScePad profile writes and virtual devices](../../../core/libs/prx/libScePad/Export.cpp) - profile writes take a handle, profile ID, profile pointer and a 32-bit result pointer; the profile payload direction and fields remain unresolved. Virtual-device creation takes an input record and an output ID pointer, whose width is not exposed by the wrapper. Component information copies 16 bytes; field meanings remain unknown. Virtual trigger state contains two 32-bit states and three control bytes, but the control semantics remain incomplete. diff --git a/docs/dev/debt/libScePlayGo.md b/docs/dev/debt/libScePlayGo.md index 2d42e4c1c5..5e5466b56d 100644 --- a/docs/dev/debt/libScePlayGo.md +++ b/docs/dev/debt/libScePlayGo.md @@ -6,5 +6,6 @@ - [scePlayGoRequestOptionalChunk, scePlayGoGetRequiredDisc](../../../core/libs/prx/libScePlayGo/Export.cpp) (libScePlayGo) - the package's scenario count is not read: scenario (type 1) bits 0 to 4 are accepted for every package and bits from 5 up return `SCE_PLAYGO_ERROR_BAD_SCENARIO_CONFIGURATION`, and `scePlayGoGetSupportedOptionalChunk` and `scePlayGoGetOptionalChunk` report bits 0 to 4 for type 1 too. `scePlayGoGetRequiredDisc` always reports disc 0. Only `scePlayGoSetInstallSpeed2` checks initialization, and unlike both sources `scePlayGoInitialize` accepts a null or small buffer and a second call, and `scePlayGoTerminate` succeeds without it. These checks follow [pgo_stub](https://github.com/drakmor/pgo_stub/blob/c95dec777563/library.cpp#L559-L596) and [rps5e](https://github.com/keklick1337/rps5e/blob/7028192c3246/crates/ps5hle/src/playgo.rs#L313-L344), which may not be independent; none is measured - [scePlayGoSetLanguageMask](../../../core/libs/prx/libScePlayGo/Export.cpp) (libScePlayGo) - stores the mask that `scePlayGoGetLanguageMask` then reports, and returns `SCE_PLAYGO_ERROR_BAD_HANDLE` for another handle, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/8d50471b7dc141dd700b51276b8edd1f293bce26/src/core/libraries/playgo/playgo.cpp#L346-L359) does on the PS4. shadPS4 also returns `SCE_PLAYGO_ERROR_NOT_INITIALIZED` before `scePlayGoInitialize` and starts from the system language's mask; here the mask starts with every language and initialization is not tracked. Not measured on a PS5 - [libScePlayGo](../../../core/libs/prx/libScePlayGo/Export.cpp) - the chunk set comes from `playgo-chunkdefs.xml` (every listed chunk plus chunks 0 through the default chunk), otherwise from `sce_sys/playgo-chunk.dat`, whose `plgx` header is read as holding the chunk count as a 16-bit value at offset 0x0A and ids 0 to count - 1 are taken as valid. The field was identified on four dumps, where exactly that many 0x20-byte entries follow from offset 0x100 (RIDE 4 PPSA01599: 35, Persona 5 Royal PPSA05110: 25, LEGO Horizon Adventures PPSA14632 and Kena PPSA01802: 1); the rest of the format is not decoded and ids are assumed to be contiguous. Every listed chunk is reported as installed (`LOCAL_FAST`). Without either file, a package with `sce_sys/playgo-scenario.json` (PPSA01748, whose dump lists no chunks) accepts ids 0 to 999 as installed, and `scePlayGoGetChunkId` lists the same ids. A title that builds its set of installed chunks from that list skips any chunk it does not see. The console's chunk limit is not known here, so an id from 1000 up is answered like an unlisted id of a package with no `playgo-*` file: it throws unless `APS5_PLAYGO_UNLISTED_CHUNKS` answers it (`=absent` ends PPSA02630's id walk, which otherwise asks for every id until one is rejected and wraps the 16-bit id). A package with no `playgo-*` file in `sce_sys` (PPSA03001, PPSA03490 and PPSA01285 here) accepts chunk 0; querying any other id throws with a message naming the missing `playgo-chunk.dat` and `playgo-scenario.json`, since the incomplete dump cannot establish whether that id exists; `APS5_PLAYGO_UNLISTED_CHUNKS=installed` answers every such id as installed (`LOCAL_FAST`, which PPSA15388 needs to load its saves) and `=absent` answers `SCE_PLAYGO_ERROR_BAD_CHUNK_ID` (which ends PPSA03001's id walk), as the user's choice for that dump. A `sce_sys` that holds other `playgo-*` files (`playgo-ficm.dat`, `playgo-hash-table.dat`) without `playgo-chunk.dat` or `playgo-scenario.json`, or a `playgo-chunk.dat` without a `plgx` header and a nonzero count, throws naming the file, because no such dump is known. These answers are unverified on a console, as is the console's answer for an id outside a package's chunk table. +- [libScePlayGo](../../../core/libs/prx/libScePlayGo/Export.cpp) (libScePlayGo) - when the dump has no `playgo-chunk.dat` or `playgo-scenario.json`, `APS5_PLAYGO_UNLISTED_CHUNKS` may list installed chunk ids (for example `0,1`); the listed ids plus chunk 0 become the chunk set and every other unlisted id is absent. The dump cannot say which ids the title expects; no console run verifies the user's choice. - [libScePlayGo](../../../core/libs/prx/libScePlayGo/Export.cpp) - `scePlayGoGetSupportedOptionalChunk` and `scePlayGoGetOptionalChunk` report every bit set for `type` 0 or 2 and bits 0 to 4 for `type` 1, i.e. every optional chunk (type 0 languages, type 1 scenarios) supported and installed, like `scePlayGoGetLanguageMask`. Native requests confirm types 0 through 2 and an eight-byte result; their meanings and bit layout are still determined by the external service, not this module; PPSA01748 calls both with type 0 and hides every text language listed in `playgo-scenario.json` `chunkSupportedLanguages` when the bitmask is 0. The console's answer for a package that ships no optional chunks is unverified. - [scePlayGoPrefetchOptionalChunk](../../../core/libs/prx/libScePlayGo/Export.cpp) (libScePlayGo) - checks only the handle and returns 0, ignoring `type` and the option. Unlike `scePlayGoGetOptionalChunk` and `scePlayGoGetSupportedOptionalChunk`, it rejects neither a null option nor a type above 2. With every optional chunk reported installed, there is nothing to prefetch. Borderlands 4 (PPSA01494), MXGP 26 (PPSA30839) and Super Meat Boy 3D (PPSA31162) each call it from two wrappers, one with type 0 and one with type 1, passing a stack copy of an eight-byte option and returning the result to their caller (read statically from each `eboot.bin`). pgo_stub and rps5e reject a null option, a type above 2 and scenario bits from 5 up here as in `scePlayGoRequestOptionalChunk`; that is not applied, and the console's answer is unverified. diff --git a/docs/dev/debt/libScePlayReady.md b/docs/dev/debt/libScePlayReady.md index ef11188820..bf49cd1027 100644 --- a/docs/dev/debt/libScePlayReady.md +++ b/docs/dev/debt/libScePlayReady.md @@ -5,4 +5,4 @@ - [sceBufferAlloc, sceBufferDup, sceBufferFree, sceBufferRealloc, scePlayReadyApiIsStarted, scePlayReadyApiStart, scePlayReadyApiStop, scePlayReadyBufferFree, scePlayReadyBufferMalloc, scePlayReadyBufferRealloc, scePlayReadyContentGetProperty, scePlayReadyContentSetProperty, scePlayReadyDomainCertEnumFini, scePlayReadyDomainCertEnumInit and scePlayReadyDomainCertEnumNext](../../../core/libs/prx/libScePlayReady/Export.cpp) (libScePlayReady) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libScePlayReady.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified - [scePlayReadyDomainCertFind, scePlayReadyDomainJoinGenerateChallenge, scePlayReadyDomainJoinProcessResponse, scePlayReadyDomainLeaveGenerateChallenge, scePlayReadyDomainLeaveProcessResponse, scePlayReadyEnvelopeClose, scePlayReadyEnvelopeGetSize, scePlayReadyEnvelopeInitializeRead, scePlayReadyEnvelopeOpen, scePlayReadyEnvelopeRead, scePlayReadyEnvelopeSeek, scePlayReadyFinalize, scePlayReadyFreeDecryptContext, scePlayReadyInitialize and scePlayReadyLicenseAcqGenerateAck](../../../core/libs/prx/libScePlayReady/Export.cpp) (libScePlayReady) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libScePlayReady.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified - [scePlayReadyLicenseAcqGenerateChallenge, scePlayReadyLicenseAcqGetAdditionalResponseData, scePlayReadyLicenseAcqProcessAckResponse, scePlayReadyLicenseAcqProcessResponse, scePlayReadyMemAlloc, scePlayReadyMemFree, scePlayReadyMemRealloc, scePlayReadyNewDecryptContext, scePlayReadyReaderBind, scePlayReadyReaderCommit, scePlayReadyReaderDecrypt, scePlayReadyReset and scePlayReadyStoreCleanup](../../../core/libs/prx/libScePlayReady/Export.cpp) (libScePlayReady) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libScePlayReady.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified -- [sceBufferInit, scePlayReadyBufferInit, scePlayReadyApiInitializeInitParams, scePlayReadyInitializeInitParams, scePlayReadyApiVersion and scePlayReadyApiVersionString](../../../core/libs/prx/libScePlayReady/Export.cpp) (libScePlayReady) - prototypes and behaviour follow firmware 13.42's libScePlayReady.sprx, read from disassembly and run natively, not measured on a console. The buffer calls clear 0x18 bytes and the init-params calls clear 0x138 bytes and then write 0x50524459 and 1 as the first two 32-bit words; what the fields mean is unknown, so the calls take `void*`. The version string is one static 32-byte buffer that every call rewrites, as in the firmware, so a string returned earlier changes and concurrent calls race +- [sceBufferInit, scePlayReadyBufferInit, scePlayReadyApiInitializeInitParams, scePlayReadyInitializeInitParams, scePlayReadyApiVersion and scePlayReadyApiVersionString](../../../core/libs/prx/libScePlayReady/Export.cpp) (libScePlayReady) - prototypes and behaviour follow firmware 13.42's libScePlayReady.sprx, read from disassembly and run natively, not measured on a console. The buffer calls clear 0x18 bytes and the init-params calls clear 0x138 bytes and then write 0x50524459 and 1 as the first two 32-bit words; what the fields mean is unknown, so the calls take `void*`. The version string is one static 32-byte buffer that every call rewrites, as in the firmware, so a string returned earlier changes and concurrent calls race; returning PlayReadyInvalidArgument for unwritable pointers in scePlayReadyApiVersion and scePlayReadyBufferInit is a defensive host-protection fallback and console behaviour is unmeasured diff --git a/docs/dev/debt/libScePlayerSelectionDialog.md b/docs/dev/debt/libScePlayerSelectionDialog.md index c72b6e3940..2409e74f90 100644 --- a/docs/dev/debt/libScePlayerSelectionDialog.md +++ b/docs/dev/debt/libScePlayerSelectionDialog.md @@ -2,8 +2,8 @@ ### Silent stubs -- [libScePlayerSelectionDialog](../../../core/libs/prx/libScePlayerSelectionDialog/Export.cpp) shows no selection UI and immediately reports cancellation. Account discovery, selection and successful-result list delivery are not implemented. +- [libScePlayerSelectionDialog, scePlayerSelectionDialogGetResult](../../../core/libs/prx/libScePlayerSelectionDialog/Export.cpp) shows no selection UI and immediately reports cancellation. Account discovery, selection and successful-result list delivery are not implemented; returning `0x80B8000D` for an unwritable result pointer is a host-safety fallback with console behavior unmeasured. ### Unknown function info -- [scePlayerSelectionDialogParamInitialize](../../../core/libs/prx/libScePlayerSelectionDialog/Export.cpp) (libScePlayerSelectionDialog) - zeroes the parameter, sets the common dialog base size and address-derived magic, and sets the public size. The console defaults for caller-owned fields and the exact behavior when reinitializing a populated parameter remain unverified. +- [scePlayerSelectionDialogParamInitialize](../../../core/libs/prx/libScePlayerSelectionDialog/Export.cpp) (libScePlayerSelectionDialog) - zeroes the parameter, sets the common dialog base size and address-derived magic, and sets the public size; returning `0x80B8000D` for an unwritable parameter pointer is a host-safety fallback with console behavior unmeasured. The console defaults for caller-owned fields and the exact behavior when reinitializing a populated parameter remain unverified. diff --git a/docs/dev/debt/libScePsm.md b/docs/dev/debt/libScePsm.md index f0c83d9eae..6efb59d35e 100644 --- a/docs/dev/debt/libScePsm.md +++ b/docs/dev/debt/libScePsm.md @@ -15,3 +15,63 @@ - [_ZN3sce3pss5orbis9framework21PsmMainThreadCallList10UnregisterEPFvPvES4_, _ZN3sce3pss5orbis9framework21PsmMainThreadCallList6UpdateEv, _ZN3sce3pss5orbis9framework21PsmMainThreadCallList8RegisterEPFvPvES4_, _ZN3sce3pss5orbis9framework23TerminatePsmEventCsharpEv, _ZN3sce3pss5orbis9framework24InitializePsmEventCsharpEv](../../../core/libs/prx/libScePsm/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libScePsm.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. - [_ZN3sce3pss5orbis9framework27TerminatePsmFrameworkCsharpEv, _ZN3sce3pss5orbis9framework28InitializePsmFrameworkCsharpEv, _ZN3sce3pss5orbis9framework8PsmEvent10UnregisterEiPFvRKNS2_12PsmEventDataEPvES7_, _ZN3sce3pss5orbis9framework8PsmEvent17RegisterDelegatorEPFvRKNS2_12PsmEventDataEPvES7_, _ZN3sce3pss5orbis9framework8PsmEvent19UnregisterDelegatorEPFvRKNS2_12PsmEventDataEPvES7_](../../../core/libs/prx/libScePsm/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libScePsm.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. - [_ZN3sce3pss5orbis9framework8PsmEvent4PostERKNS2_12PsmEventDataE, _ZN3sce3pss5orbis9framework8PsmEvent4SendERKNS2_12PsmEventDataE, _ZN3sce3pss5orbis9framework8PsmEvent8RegisterEiPFvRKNS2_12PsmEventDataEPvES7_, scePsmRegisterInternalCall, scePsmRegisterPInvokeCallTable](../../../core/libs/prx/libScePsm/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libScePsm.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. + +### Additional callable exports + +- [Additional callable exports](../../../core/libs/prx/libScePsm/Export.cpp) - 53 entries present in both version-labelled images are import-resolution declarations only. None is listed in the pinned public 3.20 catalogue for this library. Neither label independently authenticates retail firmware provenance. Signatures and behavior are unverified; all declarations throw. Unknown names use literal NID aliases. Three data exports in the 3.20 image are outside this callable inventory and require separate layout and ownership analysis. Identified candidate names match their NIDs in the [public symbol database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv) or [supplemental catalog](https://github.com/claimore22/ps5rs/blob/2304be5131221bae0dac24b9767cabffda33f747/data/nids.csv). + +| Name | NID | +| --- | --- | +| `Unknown` | `-ndbidpphoc` | +| `Unknown` | `0PIOM2UIUpQ` | +| `Unknown` | `0Tit25nFAP4` | +| `Unknown` | `12ZHf6Z7WLg` | +| `Unknown` | `1Z9IoxZ508g` | +| `Unknown` | `5DwG5treG0o` | +| `Unknown` | `5dDsI2vg6dc` | +| `Unknown` | `6FPQyIqTJi0` | +| `Unknown` | `74+2xQDzd-c` | +| `Unknown` | `8wWLk0PaYGQ` | +| `Unknown` | `BaQ59CqSB34` | +| `Unknown` | `C08zEeeiTBI` | +| `Unknown` | `CN90Qhgm32U` | +| `Unknown` | `Ia3-a5X02RA` | +| `Unknown` | `IkQNwXle96Q` | +| `Unknown` | `IyRVF5CAG48` | +| `Unknown` | `KR9fL93LcnU` | +| `Unknown` | `L4SntCPTllw` | +| `Unknown` | `N0+F2yO3hRc` | +| `Unknown` | `OJ+3wLrf7Yk` | +| `Unknown` | `OVqKOEWE-yc` | +| `Unknown` | `PGLOWFhZTtM` | +| `Unknown` | `PN1KQl3wnoU` | +| `Unknown` | `QXGdhj5nfpM` | +| `Unknown` | `SpdSLM7ONAk` | +| `Unknown` | `UDGJZZ9jrCE` | +| `Unknown` | `UFkg2lqxKPQ` | +| `Unknown` | `W8PzsJgkCGU` | +| `Unknown` | `WAKyoyoA8Dw` | +| `Unknown` | `XmlFvKZlm7k` | +| `Unknown` | `YTcYAXVAW+A` | +| `Unknown` | `YnZFBPDyB2A` | +| `Unknown` | `bGtKORAU2eE` | +| `Unknown` | `c-fS4SiII1Q` | +| `Unknown` | `cb-Q6dnF1Uk` | +| `Unknown` | `dsbtKMQDhjM` | +| `Unknown` | `e0WJestaOok` | +| `Unknown` | `gW4wqd1GV8E` | +| `Unknown` | `iKBWRnPccBs` | +| `Unknown` | `j0tmOKH875I` | +| `Unknown` | `jU+doPzvPiQ` | +| `Unknown` | `jcyBwXYskmc` | +| `Unknown` | `m7QCwDRxED4` | +| `Unknown` | `ni7QHsx30nw` | +| `Unknown` | `owrMSr1b45c` | +| `Unknown` | `t3a4oAqrvrI` | +| `Unknown` | `tFmZ8R28vsE` | +| `Unknown` | `th1-yYnLsw0` | +| `Unknown` | `udthK0YXar0` | +| `Unknown` | `vClC0x57Nqs` | +| `Unknown` | `wDEygcO128w` | +| `Unknown` | `xzd24XYJpb0` | +| `Unknown` | `yemgMfc2oW8` | diff --git a/docs/dev/debt/libSceRtc.md b/docs/dev/debt/libSceRtc.md index b1c219b6d9..7ab4a8315e 100644 --- a/docs/dev/debt/libSceRtc.md +++ b/docs/dev/debt/libSceRtc.md @@ -9,6 +9,7 @@ - [sceRtcParseDateTime](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - a blank zone after seconds is accepted. With fully allocated input storage on PS5 firmware 13.42 and again on 13.60, the same NUL-terminated text returned `BAD_PARSE` when the byte after its terminator was zero and succeeded when that byte was uppercase `T`. This dependence on storage after the terminator is not modeled; controlled backing-byte measurements in a title process are unavailable. - [sceRtcFormatRFC2822, sceRtcFormatRFC2822LocalTime, sceRtcFormatRFC3339LocalTime](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - measured on PS4 firmware 10.71, not on a PS5: the RFC 2822 text, `INVALID_YEAR` when the zone-shifted tick is past 9999 as a signed value, otherwise `INVALID_VALUE` when the source tick or the zone-shifted tick is negative as a signed value, and the local-time variants using the offset in effect at the given instant (+01:00 in January, +02:00 in July on a Central European console). A null tick formatting the current time and the ±1439 minute offset limit come from the PS5 measurement in the next entry and are not measured on the PS4 - [sceRtcFormatRFC3339, sceRtcGetDayOfWeek, sceRtcSetTime_t](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - measured on PS4 firmware 10.71 from processes compiled with SDK versions 0x01000051, 0x02ffffff, 0x03000000 and 0x05050001, not on a PS5. From 0x03000000 on: `FormatRFC3339` returns `INVALID_VALUE` when adding the zone shift to the tick overflows or underflows 64 bits (earlier SDKs wrap); `GetDayOfWeek` checks the month before the year and rejects a year past 9999 (earlier SDKs check the year first, with no upper bound); `SetTime_t` rejects negative seconds with `INVALID_VALUE` and converts all 64 bits (earlier SDKs take the low 32 bits). 0x02ffffff still behaves the earlier way and 0x03000000 the later way. The earlier rules apply only to a PS4 title (a CUSA title ID): a PS5 title has its own SDK numbering, so it always gets the later rules, which PS5 firmware 13.60 follows; there `SetTime_t` checks for negative seconds before a null pointer. +- [sceRtcFormatRFC3339, sceRtcFormatRFC2822, sceRtcFormatRFC3339Precise](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - the zone-shifted tick is converted to a date with a 16-bit year, as `SceRtcDateTime` stores it, and only that year is checked, so past year 65535 it wraps: a tick in year 65536 returns `0x80B50008` and one in year 65537 formats as year 0001, with the RFC 2822 weekday of the wrapped date. `sceRtcFormatRFC3339Precise` returns `0x80B50003` when an eastern zone overflows 64 bits. From firmware 13.42's module run natively: the year rule with SDK versions 0x01000051, 0x03000000 and 0x0A000000, the overflow result with 0x03000000 and 0x0A000000 (0x01000051 wraps instead); not measured on a console - [sceRtcGetDosTime, sceRtcSetDosTime](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - measured in a payload process on a PS5 (firmware 13.60), and all 33 cases match: the standard FAT layout with seconds halved; a year before 1980 writes 0 and one after 2107 writes `0xff9fbf7d`, both returning `INVALID_YEAR`; year 0 and an invalid month, day, hour, minute, second or microsecond return their own error without writing; a null argument returns `INVALID_POINTER`; `SetDosTime` decodes every field unchecked (`0xffffffff` gives 2107-15-31 31:63:62) and clears the microseconds. What a title process sees is assumed to be the same - [sceRtcFormatRFC2822LocalTime, sceRtcFormatRFC3339LocalTime](../../../core/libs/prx/libSceRtc/Export.cpp): the native wrappers obtain the zone from a kernel timezone query, while the host uses the offset at the supplied instant. The kernel timezone configuration and its failure paths are not modeled. Native formatting confirms that null tick pointers select current time, RFC3339 uses two fractional digits, and explicit offsets are limited to -1439 through 1439 minutes. - [Local time beyond the host's range](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - when the host cannot convert an instant to local time (on Windows, before 1970 or after 3000), the zone offset is taken from the same instant moved by whole 400-year Gregorian cycles into the host's range, which keeps a POSIX `TZ` rule's offset; a zone with historical transitions gets its current rule there, not the console's offset, which is not measured outside the probe's -03:00 zone @@ -16,6 +17,7 @@ - [sceRtcGetCurrentAdNetworkTick, sceRtcGetCurrentDebugNetworkTick](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - the console reads clocks 0x12 and 0x11 and falls back to network clock 0x10 when either clock is unsupported. These getters use their process-local clock when it was set, otherwise they follow the process-local network clock. The firmware's unsupported-clock result is not modelled. Read from the disassembly of libSceRtc.sprx on firmware 13.60, not measured on a console - [sceRtcSetCurrentTick, sceRtcSetCurrentNetworkTick, sceRtcSetCurrentAdNetworkTick, sceRtcSetCurrentDebugNetworkTick](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - a valid tick sets a process-local clock that advances from a monotonic host clock; the host system clock is not changed. The console sets clock 0, 0x10, 0x12 or 0x11. Null pointers return `0x80B50002` and ticks before the Unix epoch return `0x80B50003`, from disassembly of libSceRtc.sprx on firmware 13.60, not measured on a console. sceRtcSetConf passes its third and fourth arguments to the same clock call with clock 0 and throws; what the pair selects is unknown - [sceRtcSetConf](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - the only libSceRtc export still throwing. Known from disassembly of libSceRtc.sprx on firmware 13.60, not measured on a console: it takes four integer arguments, passes its third and fourth to the same kernel clock-setting call the Set*Tick functions above share, with clock 0, and contains a throw path. A public PS4 declaration, the one the shadPS4 emulator registers, names the third and fourth arguments `minuteswest` and `dsttime` and forwards them as a kernel timezone, which fits the disassembly; no Sony header or console measurement confirms it. Unknown: what the first and second arguments select, whether the pair really is a timezone, what a successful call changes, whether a title can return normally at all, and every return code. No honest success exists: local time reads the host zone, so the process has no local timezone this could set +- [sceRtcGetCurrentTick, sceRtcGetCurrentNetworkTick, sceRtcGetCurrentClock, sceRtcGetCurrentClockLocalTime, sceRtcGetCurrentRawNetworkTick, sceRtcGetCurrentAdNetworkTick, sceRtcGetCurrentDebugNetworkTick, sceRtcSetCurrentTick](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - returning `0x80B50002` (`SCE_RTC_ERROR_INVALID_POINTER`) for unwritable destination or unreadable source pointers is a host-safety fallback; console behaviour for non-null unmapped pointers is unmeasured. - [sceRtcGetCurrentClockLocalTime](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - behavior for a current tick after year 9999 is not measured on a console. The process-local clock rejects it with `0x80B50003`, as the current UTC clock already does. - [sceRtcFormatRFC3339Precise, sceRtcFormatRFC3339PreciseLocalTime](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - measured in a payload process on a PS5 (firmware 13.60): a null tick means the current tick, a precision outside 0 to 6 or a zone outside -1439 to 1439 minutes returns `0x80B50003`, the fraction is the microseconds truncated to the requested digits, a local tick before 0001 returns `0x80B50003` and one after 9999 `0x80B50008`, and nothing is written on an error. The local-time variant takes the offset at that tick and checks only the local result, so a tick just past 9999 formats in a zone west of UTC. Firmware 13.42's module, run in an emulator, returns `0x80B50008` instead before 0001 for titles built below SDK 3.00; not followed here. At most 33 bytes are written; the output buffer size is unknown - [sceRtcFormatRFC3339PreciseLocalTime](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - a tick more than 1439 minutes past 9999 returns `0x80B50008` before the host time zone is read, because no zone in range brings its local time back to 9999 or earlier. Derived from the measured local-result check above; such ticks were not measured on a console diff --git a/docs/dev/debt/libSceSaveData.md b/docs/dev/debt/libSceSaveData.md index 453b8375a9..f6b6ce6ff8 100644 --- a/docs/dev/debt/libSceSaveData.md +++ b/docs/dev/debt/libSceSaveData.md @@ -24,3 +24,10 @@ | `Unknown` | `g1roz3Ux+3I` | | `Unknown` | `q8KxzjSpPVw` | | `Unknown` | `yBM2vzabhEA` | + +### Unknown function info + +- [libSceSaveData](../../../core/libs/prx/libSceSaveData/CMakeLists.txt) keeps PSN and cloud exports pending in libSceSaveData.native.md: `sceSaveDataBindPsnAccount`, `sceSaveDataBindPsnAccountForSystemBackup`, `sceSaveDataCheckCloudData`, `sceSaveDataCheckPremiumMember`, `sceSaveDataCheckSyncStatus`, `sceSaveDataCreateUploadData`, `sceSaveDataDeleteCloudData`, `sceSaveDataDownload`, `sceSaveDataGetAllSizeProspero`, `sceSaveDataGetAutoSyncInfo`, `sceSaveDataGetAutoUploadConditions`, `sceSaveDataGetAutoUploadRequestInfo`, `sceSaveDataGetAutoUploadSetting`, `sceSaveDataGetCloudInfo`, `sceSaveDataGetCloudQuotaInfo`, `sceSaveDataManualDownload`, `sceSaveDataManualUpload`, `sceSaveDataNotifySaveDataDownload`, `sceSaveDataSetAutoSyncSettings`, `sceSaveDataSetAutoUploadSetting`, `sceSaveDataSyncCloudList`, `sceSaveDataUpload`. shadPS4 0614ce3 has return-success templates for the PS4 names only; fpPS4 04cefd4, KytyPS5 569ec04 none. PS5 signatures and no-PSN host behaviour unknown. +- [libSceSaveData](../../../core/libs/prx/libSceSaveData/CMakeLists.txt) keeps the system and database exports listed as pending in libSceSaveData.native.md: `sceSaveDataChangeDatabase`, `sceSaveDataChangeInternal`, `sceSaveDataDeleteAllUser`, `sceSaveDataDeleteBackupData`, `sceSaveDataDeleteProspero`, `sceSaveDataDeleteTitle`, `sceSaveDataDeleteUser`, `sceSaveDataDirNameSearchInternal`, `sceSaveDataIsDeletingUsbDb`, `sceSaveDataManualDelete`, `sceSaveDataMount5`, `sceSaveDataMountInternal`, `sceSaveDataMountSys`, `sceSaveDataRebuildDatabase`, `sceSaveDataRestoreLoadSaveDataMemory`, `sceSaveDataShutdownStart`, `sceSaveDataSupportedFakeBrokenStatus`, `sceSaveDataUmountSys`. shadPS4 0614ce3 has return-success templates only for the PS4 names and none for the rest, and fpPS4 04cefd4 and KytyPS5 569ec04 carry none; RPS5E d540c46f wires `sceSaveDataMount5` to its mount3 handler, unmeasured. PS5 signatures and behaviour are unknown. +- [libSceSaveData](../../../core/libs/prx/libSceSaveData/CMakeLists.txt) keeps the query exports listed as pending in libSceSaveData.native.md: `sceSaveDataGetAllSize`, `sceSaveDataGetAppLaunchedUser`, `sceSaveDataGetBoundPsnAccountCount`, `sceSaveDataGetEventInfo`, `sceSaveDataGetSaveDataCount`, `sceSaveDataGetSaveDataRootDir`, `sceSaveDataGetSaveDataRootPath`, `sceSaveDataGetSaveDataRootUsbPath`, `sceSaveDataGetUpdatedDataCount`, `sceSaveDataInitializeForCdlg`, `sceSaveDataIsMounted`, `sceSaveDataSetEventInfo`, `sceSaveDataSetSaveDataLibraryUser`. shadPS4 0614ce3 has only return-success templates, and fpPS4 04cefd4 and KytyPS5 569ec04 none. RPS5E d540c46 answers `sceSaveDataGetEventInfo` and `sceSaveDataSetEventInfo` with plain success and writes through a second argument for `sceSaveDataGetSaveDataCount`, `sceSaveDataGetAllSize` and `sceSaveDataIsMounted`; the other eight have no RPS5E registration — shapes #14150 rejected as invented success; none is measured on a console. +- [libSceSaveData](../../../core/libs/prx/libSceSaveData/CMakeLists.txt) keeps the backup-check and dialog-variant exports listed as pending in libSceSaveData.native.md: `sceSaveDataCheckBackupDataForCdlg`, `sceSaveDataCheckBackupDataInternal`, `sceSaveDataCheckBackupDataLight`, `sceSaveDataCheckSaveDataBroken`, `sceSaveDataCheckSaveDataVersion`, `sceSaveDataCheckSaveDataVersionLatest`, `sceSaveDataRestoreBackupDataForCdlg`. shadPS4 0614ce3 has return-success templates only for the PS4 names and none for the rest, and fpPS4 04cefd4 and KytyPS5 569ec04 carry none; #12748 implements the PS4-shaped `sceSaveDataCheckBackupData` and `sceSaveDataRestoreBackupData` from shadPS4, which does not bind the ForCdlg, Internal or Light NIDs, so whether the PS5 variants share that behaviour, and their signatures, are unknown. diff --git a/docs/dev/debt/libSceSaveData.native.md b/docs/dev/debt/libSceSaveData.native.md index de4425b9e3..8849798f0e 100644 --- a/docs/dev/debt/libSceSaveData.native.md +++ b/docs/dev/debt/libSceSaveData.native.md @@ -20,9 +20,10 @@ - [sceSaveDataDirNameSearch](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - the sort keys (0 directory name, 1 user parameter, 2 blocks, 3 modification time, 5 free blocks) and orders (0 ascending, 1 descending) follow shadPS4, not a hardware run; saves with equal keys keep directory-name order before a descending reverse. The search infos (blocks, free blocks and 32 reserved bytes per returned name) follow shadPS4's `OrbisSaveDataSearchInfo`. Keys other than 0, 1, 2, 3 and 5 (including 4) and orders above 1 return `SCE_SAVE_DATA_ERROR_PARAMETER`; shadPS4 and KytyPS5 only reject keys above 5, so key 4 is rejected here because no source names it. In the directory name pattern `%` matches any run of characters, none included, and `_` exactly one, as the comments of shadPS4's matcher describe; also not confirmed on hardware - [sceSaveDataDirNameSearch, sceSaveDataGetMountInfo, sceSaveDataMount3](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - the 64 KiB block of [SharpEmu](https://github.com/sharpemu/sharpemu/blob/c6d304cc3328bad62cb671327ace181110cf2ddb/src/SharpEmu.Libs/SaveData/SaveDataExports.cs#L82-L84) and the 48 to 16384-block range of SharpEmu and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/569ec04d1a77127c601eed522ad5bba187173f9d/src/libs/libSaveData.cpp#L30-L31) are not measured on a console, and file system overhead is not counted. What the console does with a `blocks` outside that range is unknown: KytyPS5 rejects the creating mount and SharpEmu clamps it; here a larger one is kept as 16384 and a smaller non-zero one as given. A save's size is the `blocks` of the mount that created it, kept in `.blocks`, else that of its next mount that names one, else 16384; its free blocks are that size less its files, each rounded up to 64 KiB blocks - [sceSaveDataInitialize3](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - a repeated initialize succeeds: PPSA12544's executable and its Unity SaveData plugin both initialize, and the plugin fails on any error. Initializations are counted and `sceSaveDataTerminate` ends the session at the last one; how the console pairs them is unknown. In PPSA12544 the executable calls it once (0x1721d8f) with 0 as its argument, only while a flag byte of its object is 0, and does not import `sceSaveDataTerminate`; the plugin calls it once (0x1fd2) with 0, skips the call while a flag byte of its own is set, and calls `sceSaveDataTerminate` from two functions (0x2059, 0x20d9) that clear that byte. Of 53 local titles, all import and call it and 28 import `sceSaveDataTerminate` (25 do not); counting tail jumps as calls and ignoring indirect calls, 17 of those 28 call both once, 10 call both twice and 1 calls it once and `sceSaveDataTerminate` twice. +- [sceSaveDataInitialize2](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - is `sceSaveDataInitialize3` with the same parameter, as `sceSaveDataInitialize` already is: [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0614ce324be092791b758d79f93d9f1ca5b10a81/src/core/libraries/save_data/savedata.cpp#L1228-L1238) runs the same initialization for both, and [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/src/ps4_libscesavedata.pas#L291-L304) takes one pointer for all three. Not measured on a console - [sceSaveDataCheckIpmiIfSize, sceSaveDataDebug, sceSaveDataDebugCheckBackupData, sceSaveDataDebugCleanMount, sceSaveDataDebugCompiledSdkVersion, sceSaveDataDebugCreateSaveDataRoot, sceSaveDataDebugProspero, sceSaveDataDebugRemoveSaveDataRoot and sceSaveDataDebugTarget](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSaveData.native.c). The seven no-argument prototypes follow the PS4 stubs in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/save_data/savedata.cpp#L653-L725), not measured on a console; no credible prototypes were found for the other two. All declarations have no parameters solely to resolve known imports and make no ABI claim. -- [sceSaveDataGetProgress](../../../core/libs/prx/libSceSaveData.native/Export.cpp) - throws through `NotImplemented_nid_no_patch`. The `float*` parameter follows the OpenOrbis headers and shadPS4's `sceSaveDataGetProgress(float*)`, which agree; it is not measured on a console and the PS5 module may differ. -- The following pending declarations have unverified signatures and behavior; they are parameterless import-resolution declarations and throw through `NotImplemented_nid_no_patch`: [`sceSaveDataBindPsnAccount`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataBindPsnAccountForSystemBackup`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataChangeDatabase`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataChangeInternal`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataCheckBackupData`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataCheckBackupDataForCdlg`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataCheckBackupDataInternal`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataCheckBackupDataLight`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataCheckCloudData`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataCheckSaveDataBroken`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataCheckSaveDataVersion`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataCheckSaveDataVersionLatest`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataCreateUploadData`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataDebugGetThreadId`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataDeleteAllUser`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataDeleteBackupData`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataDeleteCloudData`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataDeleteProspero`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataDeleteTitle`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataDeleteUser`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataDirNameSearchInternal`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataDownload`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetAllSize`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetAppLaunchedUser`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetAutoUploadConditions`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetAutoUploadRequestInfo`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetAutoUploadSetting`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetBoundPsnAccountCount`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetClientThreadPriority`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetCloudQuotaInfo`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetEventInfo`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetSaveDataCount`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetSaveDataRootDir`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetSaveDataRootPath`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetSaveDataRootUsbPath`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetSavePoint`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetUpdatedDataCount`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataInitialize2`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataInitializeForCdlg`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataIsDeletingUsbDb`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataIsMounted`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataManualDelete`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataManualDownload`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataManualUpload`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataMount5`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataMountInternal`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataMountSys`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataRebuildDatabase`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataRegisterEventCallback`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataRestoreBackupData`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataRestoreBackupDataForCdlg`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataRestoreLoadSaveDataMemory`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataSetAutoUploadSetting`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataSetEventInfo`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataSetSaveDataLibraryUser`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataShutdownStart`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataSupportedFakeBrokenStatus`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataSyncCloudList`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataUmountSys`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataUmountWithBackup`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataUnregisterEventCallback`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataUpload`](../../../core/libs/prx/libSceSaveData.native/Export.cpp). +- [sceSaveDataGetProgress](../../../core/libs/prx/libSceSaveData.native/Export.cpp) - throws through `NotImplemented_nid_no_patch`. The `float*` parameter follows the OpenOrbis headers and shadPS4's `sceSaveDataGetProgress(float*)`, which agree; it is not measured on a console and the PS5 module may differ. shadPS4 ([savedata.cpp L1124-L1136](https://github.com/shadps4-emu/shadPS4/blob/0614ce324be092791b758d79f93d9f1ca5b10a81/src/core/libraries/save_data/savedata.cpp#L1124-L1136)) checks initialization first (`NOT_INITIALIZED`), then rejects a null pointer (`PARAMETER`), else returns the last backup's progress, `0.0f` idle and `1.0f` after the copy, reset by ClearProgress; a faithful mapping needs one driver line in `sceSaveDataBackup`, which open PR #15042 rewrites (guest-pointer validation there, a `GuestCopyOut` write-out in `sceSaveDataGetConvertProgress`), so implementation waits for that PR. RPS5E (d540c46f) returns a constant `1.0f` instead. +- The following pending declarations have unverified signatures and behavior; they are parameterless import-resolution declarations and throw through `NotImplemented_nid_no_patch`: [`sceSaveDataBindPsnAccount`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataBindPsnAccountForSystemBackup`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataChangeDatabase`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataChangeInternal`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataCheckBackupDataForCdlg`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataCheckBackupDataInternal`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataCheckBackupDataLight`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataCheckCloudData`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataCheckSaveDataBroken`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataCheckSaveDataVersion`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataCheckSaveDataVersionLatest`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataCreateUploadData`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataDebugGetThreadId`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataDeleteAllUser`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataDeleteBackupData`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataDeleteCloudData`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataDeleteProspero`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataDeleteTitle`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataDeleteUser`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataDirNameSearchInternal`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataDownload`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetAllSize`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetAppLaunchedUser`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetAutoUploadConditions`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetAutoUploadRequestInfo`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetAutoUploadSetting`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetBoundPsnAccountCount`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetClientThreadPriority`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetCloudQuotaInfo`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetEventInfo`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetSaveDataCount`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetSaveDataRootDir`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetSaveDataRootPath`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetSaveDataRootUsbPath`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetSavePoint`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataGetUpdatedDataCount`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataInitializeForCdlg`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataIsDeletingUsbDb`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataIsMounted`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataManualDelete`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataManualDownload`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataManualUpload`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataMount5`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataMountInternal`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataMountSys`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataRebuildDatabase`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataRestoreBackupDataForCdlg`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataRestoreLoadSaveDataMemory`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataSetAutoUploadSetting`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataSetEventInfo`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataSetSaveDataLibraryUser`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataShutdownStart`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataSupportedFakeBrokenStatus`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataSyncCloudList`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataUmountSys`](../../../core/libs/prx/libSceSaveData.native/Export.cpp), [`sceSaveDataUpload`](../../../core/libs/prx/libSceSaveData.native/Export.cpp). ### Functional @@ -32,7 +33,15 @@ - [sceSaveDataMount3](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - a failed mount (BUSY, EXISTS, NOT_FOUND, MOUNT_FULL) leaves the result as it was, as shadPS4 does; not checked on hardware. Animal Well (PPSA22520) depends on it: it checks for its save with a mount it never unmounts, then saves with a second mount of the same directory, ignores its BUSY and keeps using the first mount point for `sceSaveDataPrepare`, its file and `sceSaveDataUmount2`. In PPSA05347 the function that calls it at 0x235fbb5 zeroes its 0x40-byte result before the call and, for a negative result other than 0x809f0008 and 0x809f000f (BUSY included), logs the code and returns it without recording the mount; no abort follows. - [sceSaveDataMount3](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - an unterminated or empty directory name, `.`, `..`, or a name containing `/`, `\` or `:` returns `SAVE_DATA_ERROR_PARAMETER`, which the [SDK 12.000 reference](https://github.com/peterpuppy/bm/blob/3f9d1ba6b896a53080460c903f5c15af8affb80d/features/ps5/document/output/psn_12/System/Save_Data/SaveData-Reference/sce-save-data-mount3.md#L196-L204) lists for an invalid parameter, and leaves the result as it was, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/569ec04d1a77127c601eed522ad5bba187173f9d/src/libs/libSaveData.cpp#L652-L658) does. The [SDK](https://github.com/peterpuppy/bm/blob/3f9d1ba6b896a53080460c903f5c15af8affb80d/features/ps5/document/output/psn_12/System/Save_Data/SaveData-Reference/sce-save-data-dir-name.md#L32-L36) lets an application use only letters, digits, `-`, `.` and `@` in a name, and KytyPS5 also accepts `_` ([`valid_path_component`](https://github.com/KytyPS5/KytyPS5/blob/569ec04d1a77127c601eed522ad5bba187173f9d/src/libs/libSaveData.cpp#L276-L287)); which other names the console's mount rejects is not checked on hardware - [sceSaveDataBackup](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - an unterminated directory name returns `SAVE_DATA_ERROR_PARAMETER` to prevent unbounded copy; which other directory names the console's backup rejects is unverified on hardware. -- [sceSaveDataBackup, sceSaveDataConvert, sceSaveDataDelete, sceSaveDataDirNameSearch, sceSaveDataDirNameSearchPs4, sceSaveDataGetEventResult, sceSaveDataGetMountInfo, sceSaveDataGetParam, sceSaveDataLoadIcon, sceSaveDataMount, sceSaveDataMount3, sceSaveDataSaveIcon, sceSaveDataSetParam, sceSaveDataUmount2](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - null pointer arguments return `SAVE_DATA_ERROR_PARAMETER` instead of throwing C++ exceptions, an implementation assumption matching guest error expectations not checked on hardware; unmounted targets return `SAVE_DATA_ERROR_NOT_MOUNTED`, and parameter type or buffer size violations return `SAVE_DATA_ERROR_PARAMETER` after slot lookup +- [sceSaveDataCheckBackupData, sceSaveDataRestoreBackupData, sceSaveDataUmountWithBackup](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - not measured on a PS5: the block layouts, error checks and behaviour follow shadPS4's PS4 binding ([check](https://github.com/shadps4-emu/shadPS4/blob/ce74386283f559f2279a7c31416bcdc9359be0b3/src/core/libraries/save_data/savedata.cpp#L615-L664), [restore](https://github.com/shadps4-emu/shadPS4/blob/ce74386283f559f2279a7c31416bcdc9359be0b3/src/core/libraries/save_data/savedata.cpp#L1360-L1398), [unmount with backup](https://github.com/shadps4-emu/shadPS4/blob/ce74386283f559f2279a7c31416bcdc9359be0b3/src/core/libraries/save_data/savedata.cpp#L1758-L1761)), and the tests check that binding, not the console. `sceSaveDataBackup` still copies nothing, so the check and the restore see only backups that `sceSaveDataUmountWithBackup` made. +- [sceSaveDataUmountWithBackup](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - an unknown mount point returns `SAVE_DATA_ERROR_NOT_MOUNTED`, as `sceSaveDataUmount2` does, where [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/ce74386283f559f2279a7c31416bcdc9359be0b3/src/core/libraries/save_data/savedata.cpp#L512-L543) returns `SAVE_DATA_ERROR_NOT_FOUND`. The backup is copied before the call returns, so `SAVE_DATA_ERROR_BACKUP_BUSY` never occurs, and the `UMOUNT_BACKUP` event is queued at once, as [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/src/ps4_libscesavedata.pas#L444-L462) does; a failed copy sets its error to `SAVE_DATA_ERROR_INTERNAL`, as shadPS4's [backup thread](https://github.com/shadps4-emu/shadPS4/blob/ce74386283f559f2279a7c31416bcdc9359be0b3/src/core/libraries/save_data/save_backup.cpp#L167-L173) does. +- [sceSaveDataCheckBackupData, sceSaveDataRestoreBackupData](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - saves keep no icons, so the check never fills `icon`, as shadPS4 does without `icon0.png`, and another title's `titleId` returns `SAVE_DATA_ERROR_NOT_FOUND` because only the running title's saves are kept. Backups live in `_sd_backup/`, outside the save, and `sceSaveDataDelete` removes one with its save, as shadPS4's [backup](https://github.com/shadps4-emu/shadPS4/blob/ce74386283f559f2279a7c31416bcdc9359be0b3/src/core/libraries/save_data/save_backup.cpp#L93-L134) inside the save directory goes with it. On a PS4 Pro (firmware 10.71) another title's `titleId` also returns `SAVE_DATA_ERROR_NOT_FOUND`, and `sceSaveDataDelete` drops the backup too: after it the check and the restore return `SAVE_DATA_ERROR_NOT_FOUND` ([run](https://github.com/boykopovar/AnyPS5/pull/12748#issuecomment-6499581390)). +- [sceSaveDataCheckBackupData](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - a `param` buffer that isn't zeroed is not refused here. On a PS4 Pro (firmware 10.71, the system `libSceSaveData.sprx`, SDK 0x05050001) a 1,328-byte `param` filled with 0xAA gave `SAVE_DATA_ERROR_PARAMETER` on every call, also with a backup present, while a zeroed or null one gave 0 ([run](https://github.com/boykopovar/AnyPS5/pull/12748#issuecomment-6499581390)). Which bytes the console checks wasn't measured, so the check fills `param` whatever it held. Not measured on a PS5. +- [sceSaveDataRestoreBackupData](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - a crash during a restore is not recovered at the next start, and what the console leaves is not measured. A failed restore leaves the live save, its `.param` and its blocks file as they were: the backup is copied to `_sd_restore_tmp/` and checked first, and a failed move puts back what was already moved; only if that fails too does the old save stay in `_sd_restore_old/`. A crash during the copy leaves the live save untouched. One during the moves can leave parts of the live save in `_sd_restore_old/` and the copy in `_sd_restore_tmp/` or `_sd`; while the save's directory is missing from `_sd`, its restore returns `SAVE_DATA_ERROR_INTERNAL` and a creating mount makes an empty save. The next restore of the save first removes both directories. +- [sceSaveDataRegisterEventCallback, sceSaveDataUnregisterEventCallback](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - not measured on a console: the behaviour follows [shadPS4 #5226](https://github.com/shadps4-emu/shadPS4/pull/5226), and PS5's signatures are assumed to be PS4's (`void (*)(const SaveDataEvent*, void*)` and a userdata pointer). Unknown: which thread delivers, its priority and affinity, when it runs relative to the call, whether unregistering drops completions not yet delivered (here it does), and what a terminate from inside the callback does (here `SAVE_DATA_ERROR_BUSY`, as in shadPS4). Here the first successful `sceSaveDataRegisterEventCallback` after `sceSaveDataInitialize3` starts the delivering thread with `scePthreadCreate`, the last `sceSaveDataTerminate` joins it if one was started, and each backup completion (type 1 or 2) is queued for `sceSaveDataGetEventResult` before it goes to the callback registered at that moment. +- [sceSaveDataUmountWithBackup](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - a crash during a backup is not recovered at the next start either. A failed backup keeps the previous one in `_sd_backup/`. A crash between its two moves leaves the previous backup in `_sd_backup_old/`, where the check and the restore do not find it (`SAVE_DATA_ERROR_NOT_FOUND`); the next backup of the save removes it, and any partial copy in `_sd_backup_tmp/`, before it copies. +- [sceSaveDataRestoreBackupData, sceSaveDataUmountWithBackup](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - on Windows nothing shows that a backup's or a restore's renames survive a crash or power loss. Every staged file is flushed before the first rename (`FlushFileBuffers` on Windows, `fsync` on POSIX, as libc does), and on POSIX the directories on both sides of each rename are flushed after it; Windows cannot flush directory entries, so there it rests on NTFS journaling. +- [sceSaveDataBackup, sceSaveDataConvert, sceSaveDataDelete, sceSaveDataDirNameSearch, sceSaveDataDirNameSearchPs4, sceSaveDataGetEventResult, sceSaveDataGetMountInfo, sceSaveDataGetParam, sceSaveDataLoadIcon, sceSaveDataMount, sceSaveDataMount3, sceSaveDataSaveIcon, sceSaveDataSetParam, sceSaveDataUmount2](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - null pointer arguments return `SAVE_DATA_ERROR_PARAMETER` instead of throwing C++ exceptions, an implementation assumption matching guest error expectations not checked on hardware; unmounted targets return `SAVE_DATA_ERROR_NOT_MOUNTED`, parameter type or buffer size violations return `SAVE_DATA_ERROR_PARAMETER` after slot lookup, and an unterminated mount point string returns `SAVE_DATA_ERROR_PARAMETER` to prevent buffer overruns during slot lookup - [sceSaveDataUmount2](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - a mount point that is not mounted returns `SAVE_DATA_ERROR_NOT_MOUNTED` (0x809F0004), which no console run has checked. KytyPS5's `sceSaveDataUmount2` (PS5, `src/libs/libSaveData.cpp`, commit 5e837a9beac82c031868eaad9348d6a48245fb99) returns `SAVE_DATA_ERROR_NOT_FOUND` (0x809F0008) there. For the PS4 `sceSaveDataUmount`, which none of the local titles import, shadPS4 (PS4, `src/core/libraries/save_data/savedata.cpp`, commit 0614ce324be092791b758d79f93d9f1ca5b10a81) returns `SAVE_DATA_ERROR_NOT_FOUND` and fpPS4 (PS4, `sys/sys_path.pas`, commit 04cefd43e6fddd1ab033e7980cd356d14c964905) `SAVE_DATA_ERROR_NOT_MOUNTED`. Forty of the 53 local titles import `sceSaveDataUmount2`. - [sceSaveDataSyncSaveDataMemory](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - validates library initialization and parameters; since in-memory saves flush synchronously on write, sync confirms existence and readiness of the memory blob @@ -40,3 +49,4 @@ - [sceSaveDataMount3](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - the console's `NO_SPACE_FS` code, the rounding of `required_blocks` and whether it checks the volume when a save is created are not measured; with `ANYPS5_FREE_SPACE=host` a save larger than the room of its volume fails with `0x809F000A` and sets `required_blocks` to the missing blocks rounded up to 64 KiB, as shadPS4 does at [savedata.cpp L468-L481](https://github.com/shadps4-emu/shadPS4/blob/1d8004a22feb0d5cc58828bac066018e80b6d73f/src/core/libraries/save_data/savedata.cpp#L468-L481), which rounds down and checks the full size - [sceSaveDataGetMountInfo, sceSaveDataDirNameSearch](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - whether the console bounds a save's free blocks by the room of the disk is not measured; with `ANYPS5_FREE_SPACE=host` they are the smaller of the save's own free blocks and the 64 KiB blocks its volume still has, and a full volume reports none - [sceSaveDataMount3, sceSaveDataGetMountInfo](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - the 64 KiB block that the room and free-block figures use is not measured on a console; [shadPS4 L50-L52](https://github.com/shadps4-emu/shadPS4/blob/1d8004a22feb0d5cc58828bac066018e80b6d73f/src/core/libraries/save_data/savedata.cpp#L50-L52) uses 32 KiB blocks and 96 to 32768 blocks, so the bytes that host mode requires for a save, and its free-block counts, would differ by a factor of two if the console uses 32 KiB +- [sceSaveDataGetSaveDataMemory, sceSaveDataSetSaveDataMemory, sceSaveDataGetSaveDataMemory2, sceSaveDataSetSaveDataMemory2](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - whether retail console firmware returns SAVE_DATA_ERROR_PARAMETER for non-zero out-of-bounds offsets, negative offsets, or null buffers in save data memory operations remains unmeasured against console diagnostics. Non-zero requests with null buffers, negative offsets cast to unsigned sizes, or offsets exceeding prepared memory size return SAVE_DATA_ERROR_PARAMETER, while zero-length requests succeed without I/O. diff --git a/docs/dev/debt/libSceSaveDataDialog.native.md b/docs/dev/debt/libSceSaveDataDialog.native.md index a601fa1e29..54c3cab998 100644 --- a/docs/dev/debt/libSceSaveDataDialog.native.md +++ b/docs/dev/debt/libSceSaveDataDialog.native.md @@ -4,7 +4,7 @@ - [libSceSaveDataDialog.native](../../../core/libs/prx/libSceSaveDataDialog.native/Export.cpp) shows no UI and ignores progress values. Progress-bar mode (5) stays running until the caller closes it, matching Skyrim PPSA03747's save sequence; other modes still finish immediately and return button 1 and the first nonempty directory name. Console dialog timing and selection behavior remain unverified. Lifecycle state error codes follow shadPS4: `sceSaveDataDialogClose` returns `SAVE_DATA_DIALOG_ERROR_NOT_RUNNING` whenever not running, `sceSaveDataDialogGetResult` returns `SAVE_DATA_DIALOG_ERROR_NOT_FINISHED` whenever not finished (checked before null pointer), and `sceSaveDataDialogTerminate` closes running dialogs and returns OK, rejecting uninitialized calls with `SAVE_DATA_DIALOG_ERROR_NOT_INITIALIZED`, and progress updates (`sceSaveDataDialogProgressBarInc`, `sceSaveDataDialogProgressBarSetValue`) return `SAVE_DATA_DIALOG_ERROR_NOT_RUNNING` whenever not running. Parameter size in `sceSaveDataDialogOpen` is unvalidated as console behaviour is unmeasured. -- [sceSaveDataDialogIsReadyToDisplay](../../../core/libs/prx/libSceSaveDataDialog.native/Export.cpp) returns 1 without checking dialog initialization or display readiness. +- [sceSaveDataDialogIsReadyToDisplay](../../../core/libs/prx/libSceSaveDataDialog.native/Export.cpp) returns 1 unconditionally without checking state, following shadPS4 (savedatadialog.cpp at ce74386283); native console readiness state is unmeasured. ### Unknown function info diff --git a/docs/dev/debt/libSceScreenShot.md b/docs/dev/debt/libSceScreenShot.md index af827d60e7..f790fc3bd1 100644 --- a/docs/dev/debt/libSceScreenShot.md +++ b/docs/dev/debt/libSceScreenShot.md @@ -4,7 +4,7 @@ - [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 +- [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; returning ErrorArgument for a non-null unmapped pointer is a host-safety fallback, and console system software behaviour is unmeasured ### Unknown function info diff --git a/docs/dev/debt/libSceShare.md b/docs/dev/debt/libSceShare.md index 217fe03b07..ae439db21a 100644 --- a/docs/dev/debt/libSceShare.md +++ b/docs/dev/debt/libSceShare.md @@ -6,8 +6,8 @@ ### Unknown function info -- [sceShareGetCurrentStatus](../../../core/libs/prx/libSceShare/Export.cpp) (libSceShare) - validation (feature flag non-zero, status non-null) and the all-zero 16-byte status taken from the [KytyPS5](https://github.com/KytyPS5/KytyPS5) reimplementation, not confirmed on a PS5 title; the meaning of the recording status values is unknown -- [sceShareGetRunningStatus](../../../core/libs/prx/libSceShare/Export.cpp) - the native wrapper writes only the first 32-bit word, populated from a kernel event flag. The host reports zero because capture and broadcast services are absent; native initialization checks and event-flag failures are not modeled. +- [sceShareGetCurrentStatus](../../../core/libs/prx/libSceShare/Export.cpp) (libSceShare) - validation (feature flag non-zero, status non-null) and the all-zero 16-byte status taken from the [KytyPS5](https://github.com/KytyPS5/KytyPS5) reimplementation, not confirmed on a PS5 title; the meaning of the recording status values is unknown; returning ERROR_INVALID_PARAM for an unwritable status pointer is a defensive host-protection fallback and console behaviour is unmeasured +- [sceShareGetRunningStatus](../../../core/libs/prx/libSceShare/Export.cpp) - the native wrapper writes only the first 32-bit word, populated from a kernel event flag. The host reports zero because capture and broadcast services are absent; native initialization checks and event-flag failures are not modeled; returning ERROR_INVALID_PARAM for an unwritable status pointer is a defensive host-protection fallback and console behaviour is unmeasured. - [sceShareSetContentParamForApplicationTitle](../../../core/libs/prx/libSceShare/Export.cpp) - the recovered string length limit is 1 through 5119 bytes. The title is validated but not sent to the capture service; native initialization and service failures are not modeled. ### Functional diff --git a/docs/dev/debt/libSceSisrSys.md b/docs/dev/debt/libSceSisrSys.md new file mode 100644 index 0000000000..f09806ef6f --- /dev/null +++ b/docs/dev/debt/libSceSisrSys.md @@ -0,0 +1,5 @@ +# libSceSisrSys technical debt + +### Unknown function info + +- [sceSisrSysProcessUmdEvent and sceSisrSysSetup](../../../core/libs/prx/libSceSisrSys/Export.cpp) - names and NIDs are present in the complete [PS5 13.60 export inventory](https://github.com/xdCloudy/PS5-13.60-Function-Stub-Library/blob/bfec40deb4c47a17d7603ee2bc79aa6862c0c56b/data/firmware-13.60/libscesisrsys-d8069f882d.jsonl), and the pinned [module map](https://github.com/boykopovar/ps5-nid-map/blob/cc1d586fddab576a520f226da6b2632b031e6d09/data/Modules/libSceSisrSys.inc) assigns both identities to the physical `libSceSisrSys.sprx` provider. Parameter lists, return types and behavior are unverified; the parameterless `int` declarations are import-resolution placeholders, not verified ABI contracts. Both entries throw through `NotImplemented_nid_no_patch`. No SISR setup or UMD-event behavior is implemented. diff --git a/docs/dev/debt/libSceSocialScreenDialog.md b/docs/dev/debt/libSceSocialScreenDialog.md index b1b16a216d..69c053996f 100644 --- a/docs/dev/debt/libSceSocialScreenDialog.md +++ b/docs/dev/debt/libSceSocialScreenDialog.md @@ -2,4 +2,4 @@ ### Silent stubs -- [libSceSocialScreenDialog](../../../core/libs/prx/libSceSocialScreenDialog/Export.cpp) - the social screen dialog runs the common-dialog state machine but shows nothing and reports the user as having cancelled, as there is no second-screen viewer. The signatures are from [Orbital's type database](https://github.com/AlexAltea/orbital/blob/b321fcdbd700cc02da1cb1ad25412781d5ec73c6/tools/ida/db_types.json); the result layout beyond the leading `int32_t` result code is not known, so nothing else is written. +- [libSceSocialScreenDialog](../../../core/libs/prx/libSceSocialScreenDialog/Export.cpp) - the social screen dialog runs the common-dialog state machine but shows nothing and reports the user as having cancelled, as there is no second-screen viewer. In `sceSocialScreenDialogGetResult`, PS5 firmware 13.42 module disassembly (offset 0x360) and native execution data confirm that a null result pointer is checked before NOT_FINISHED (returning COMMON_DIALOG_ERROR_ARG_NULL). The signatures are from [Orbital's type database](https://github.com/AlexAltea/orbital/blob/b321fcdbd700cc02da1cb1ad25412781d5ec73c6/tools/ida/db_types.json); the result layout beyond the leading `int32_t` result code is not known, so nothing else is written. diff --git a/docs/dev/debt/libSceSsl.md b/docs/dev/debt/libSceSsl.md index 977b487969..d1b3d7a3d3 100644 --- a/docs/dev/debt/libSceSsl.md +++ b/docs/dev/debt/libSceSsl.md @@ -2,8 +2,9 @@ ### Unknown function info -- [CA_MGMT_extractKeyBlobEx, CA_MGMT_extractPublicKeyInfo, CA_MGMT_freeKeyBlob, CRYPTO_initAsymmetricKey, RSA_verifySignature, 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. +- [CA_MGMT_extractPublicKeyInfo, CA_MGMT_freeKeyBlob, RSA_verifySignature, 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, 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. +- [sceSslGetNanoSSLModuleId, CA_MGMT_extractKeyBlobEx and CRYPTO_initAsymmetricKey](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - what the module id is for, the layout of the key that the two key calls fill, and the names and types of the arguments they never read are unknown. They follow firmware 13.42's libSceSsl.sprx, read from its disassembly to every return and run natively on x86-64 (not a console): `sceSslGetNanoSSLModuleId` stores its 32-bit first argument through its second unless that is null and always returns 0; `CA_MGMT_extractKeyBlobEx` returns `0x8095177A` when its second or fourth argument is null and otherwise stores the second (a pointer) at offset 8 of the fourth and returns 0, reading neither the first nor the third; `CRYPTO_initAsymmetricKey` stores the 32-bit value 1 at its argument without a null check and returns 0 - [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`. 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) on the console, while here an exhausted pool throws `std::bad_alloc` (see `CheckBackend`) and returns no SSL error code. 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. 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 diff --git a/docs/dev/debt/libSceSysUtil.md b/docs/dev/debt/libSceSysUtil.md index 614a09c03b..dd8080ae42 100644 --- a/docs/dev/debt/libSceSysUtil.md +++ b/docs/dev/debt/libSceSysUtil.md @@ -9,4 +9,4 @@ ### Functional -- [sceSysUtilSendSystemNotificationWithText](../../../core/libs/prx/libSceSysUtil/Export.cpp) (libSceSysUtil) - follows @BunnyFl's [firmware 13.42 run](https://github.com/boykopovar/AnyPS5/pull/9081#issuecomment-6418015178) of the export, under an emulator with `sceKernelSendNotificationRequest` stubbed, not on a console. `(int32_t type, const char* message)` checks nothing itself: it builds a 0xc30-byte request with kind 5 in the first dword, `type` at +0xc and the message at +0x2d, cut to 179 characters and left empty for a null message, and returns what `sceKernelSendNotificationRequest(0, request, 0xc30, 0)` returns. The run did not report the other bytes of the request; they are zero here. There is no system UI here, so the text goes to the host's standard error; what the console shows for each `type` is decided outside the library and is not known. +- [sceSysUtilSendSystemNotificationWithText](../../../core/libs/prx/libSceSysUtil/Export.cpp) (libSceSysUtil) - follows @BunnyFl's [firmware 13.42 run](https://github.com/boykopovar/AnyPS5/pull/9081#issuecomment-6418015178) under an emulator with `sceKernelSendNotificationRequest` stubbed, not on console. Builds a 0xc30-byte request with kind 5 in the first dword, `type` at +0xc and the message at +0x2d, cut to 179 characters and empty for null, returning what `sceKernelSendNotificationRequest(0, request, 0xc30, 0)` returns. An unreadable message pointer returns `SCE_KERNEL_ERROR_EFAULT` (`0x8002000e`) as a host-safety fallback; console invalid-pointer behaviour was not measured. Other request bytes were not reported and are zero. Lacking system UI, text goes to standard error; console display per `type` is unverified. diff --git a/docs/dev/debt/libSceSystemService.md b/docs/dev/debt/libSceSystemService.md index c512fa68fb..636f2afbdb 100644 --- a/docs/dev/debt/libSceSystemService.md +++ b/docs/dev/debt/libSceSystemService.md @@ -1,5 +1,46 @@ # libSceSystemService technical debt +### Additional callable exports + +- [Additional callable exports](../../../core/libs/prx/libSceSystemService/Export.cpp) - 32 entries present in both version-labelled images are import-resolution declarations only. None is listed in the pinned public 3.20 catalogue for this library. Neither label independently authenticates retail firmware provenance. Parameter lists, return types and behavior are unverified; parameterless `int` declarations are placeholders, not verified ABI contracts. Every entry throws through `NotImplemented_nid_no_patch`. Unidentified names use literal NID aliases. No runtime functionality is implemented. Identified candidate names match their NIDs in the [public symbol database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv) or [supplemental catalog](https://github.com/claimore22/ps5rs/blob/2304be5131221bae0dac24b9767cabffda33f747/data/nids.csv). + +[Version-pinned selected ELF export records](https://gist.github.com/kyon-eth/3fbb1a4a6f95a936b4cd58e9c3d8c50c/5e3af709224ed8235af11337309af0725e5685cb) list every NID below with its image label, decoded export module and library, raw qualified symbol, ELF function type, value and size for each inspected version-labelled image. The extract SHA-256 is `02ab27f12bcd0cc9fa03fa33f55fc201110f0d54a86bbbaa093d55d0a8cf262d`; its full-module sorted-function-NID digests and raw module-parameter words permit independent comparison. This is an analyst-produced metadata record, not independent authentication of the original images or proof of prototypes or behavior. + +| Name | NID | +| --- | --- | +| `Unknown` | `16waHD4tFBo` | +| `Unknown` | `1m-E1Xh8COo` | +| `Unknown` | `7eu2qjQGigw` | +| `Unknown` | `90unWbnI0qE` | +| `Unknown` | `E-bXVJqwGZw` | +| `Unknown` | `G3oPdw3lkps` | +| `Unknown` | `KgiwSrROsIQ` | +| `Unknown` | `LxGOxroprpg` | +| `Unknown` | `NXFtO8BfVeo` | +| `Unknown` | `Oxh0CSsTZ78` | +| `Unknown` | `OyTwhCKWXi4` | +| `Unknown` | `U0HutHWnMBM` | +| `Unknown` | `VDs9VSjSJh0` | +| `Unknown` | `ZWNP+-dWr8E` | +| `Unknown` | `axzblV8OvWU` | +| `Unknown` | `bJyWctnSXyU` | +| `Unknown` | `bUNkT3XDg0Y` | +| `Unknown` | `c5+4Scso9EU` | +| `Unknown` | `fBGD-HNimSk` | +| `Unknown` | `fPAZ5DBsDxo` | +| `Unknown` | `ifJiF5witJ4` | +| `Unknown` | `kDGjRMtUDxo` | +| `Unknown` | `lgpIdZ8T0xg` | +| `Unknown` | `pj1SzIAxPlk` | +| `Unknown` | `qdTPJWjquDc` | +| `Unknown` | `rIqPq0oWlrg` | +| `Unknown` | `sgYo-zXHQRE` | +| `Unknown` | `tPMOYbbviYs` | +| `Unknown` | `uElcdmhh-c8` | +| `Unknown` | `xn-HgPW29x4` | +| `Unknown` | `y16IXiJfafA` | +| `Unknown` | `yOiZq+9-ZMQ` | + ### Silent stubs - [libSceSystemService](../../../core/libs/prx/libSceSystemService/Export.cpp) - `sceSystemServicePowerTick`, `sceSystemServiceReportAbnormalTermination`, `sceSystemServiceDisableMusicPlayer`, `sceSystemServiceReenableMusicPlayer`, `sceSystemServiceDisableMediaPlay` and `sceSystemServiceReenableMediaPlay` return success and do nothing: the host has no auto power-off timer to reset, no system crash reporter to notify, no system music player to pause or resume and no system media playback to disable. PPSA01521 calls `sceSystemServiceDisableMediaPlay` during start-up, and its wrappers for both pass no arguments @@ -8,11 +49,14 @@ - [sceSystemServiceShowDisplaySafeAreaSettings](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - returns success without showing the display safe area settings dialog: AnyPS5 has no system UI to open. PPSA03332 imports it, and the no-argument signature is assumed: [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json) has `sceSystemServiceShowControllerSettings` but not this call - [sceSystemServiceOpenChallengeActivity](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - returns success without opening the challenge activity: AnyPS5 has no system UI to open. Cat Quest III (PPSA15065) imports it - [sceSystemServiceOpenTournamentOccurrence](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - returns success without opening the tournament occurrence: AnyPS5 has no system UI to open +- [sceSystemServiceSetGpuLoadEmulationMode, sceSystemServiceGetGpuLoadEmulationMode, sceShellCoreUtilSetGpuLoadEmulationMode, sceShellCoreUtilGetGpuLoadEmulationMode, sceShellCoreUtilSetGpuLoadEmulationModeByAppId, sceShellCoreUtilGetGpuLoadEmulationModeByAppId](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - the mode is stored and read back but the host GPU is not throttled. Only modes 0 and 1 are accepted, as [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/SystemService/SystemService.cs#L121-L124) names them; any other mode and any app ID other than the running title throw. The initial mode 1 and the one mode shared by all six exports are assumed. [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/master/include/orbis/SystemService.h) lists only the `sceSystemService` names, without arguments +- [sceSystemServiceEnableSuspendConfirmationDialog, sceSystemServiceDisableSuspendConfirmationDialog, sceShellCoreUtilGetSuspendConfirmationDialogFlag](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - the flag is stored and read back for the running title, but AnyPS5 has no shell dialog, so it changes nothing. It starts enabled, which is assumed. Another app ID throws. The names come from [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/master/include/orbis/SystemService.h) and [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/system/systemservice.cpp) as argument-less stubs; the getter's signature is the pending exports' recovery - [sceSystemServiceHideSplashScreen, sceSystemServiceLaunchPlayerDialog](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - AnyPS5 shows no splash screen to hide and has no system UI to open the player dialog in. `sceSystemServiceHideSplashScreen` returns success, and `sceSystemServiceLaunchPlayerDialog` returns `SYSTEM_SERVICE_ERROR_PARAMETER` for a null parameter and success otherwise, without reading the parameter. All ten titles checked call `sceSystemServiceHideSplashScreen`. Borderlands 4 (PPSA01494), Ghost of Tsushima (PPSA02225), Metal Gear Solid 4 (PPSA18194), MXGP 26 (PPSA30839), Super Meat Boy 3D (PPSA31162), Call of Duty: Black Ops II (PPSA34502) and Steins;Gate Re:Boot (PPSA36591) call `sceSystemServiceLaunchPlayerDialog` - [sceSystemServiceSetNoticeScreenSkipFlag, sceSystemServiceDisableNoticeScreenSkipFlagAutoSet](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - return success without storing anything. `sceSystemServiceGetNoticeScreenSkipFlag` returns `SYSTEM_SERVICE_ERROR_PARAMETER` for a null output and otherwise always reports `false`. Metal Gear Solid 4 (PPSA18194) imports both and calls `sceSystemServiceDisableNoticeScreenSkipFlagAutoSet` once ### Unknown function info +- [sceSystemServiceUnknown_M_KwxF0amiw](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - imported by PPSA24263 (discussion #11591). Its signature and behaviour on hardware are unverified; all calls throw through `NotImplemented_nid_no_patch`. - [sceShellCoreUtilAcquireRemotePlayCpuBudget, sceShellCoreUtilReleaseRemotePlayCpuBudget, sceShellCoreUtilIsRemotePlayCpuBudgetAcquired, sceShellCoreUtilAcquireSharePlayCpuBudget, sceShellCoreUtilReleaseSharePlayCpuBudget, sceShellCoreUtilIsSharePlayCpuBudgetAcquired](../../../core/libs/prx/libSceSystemService/PendingExports.cpp) (libSceSystemService) - the contract is unresolved: AnyPS5 has no Remote Play or Share Play CPU budget service, and the console's acquire and release results, a repeated acquire and a release without an acquire are unmeasured. Every call throws `NotImplemented_nid_no_patch`. - [sceSystemServiceAddLocalProcessForPsmKit, sceSystemServiceKillLocalProcessForPsmKit](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - AnyPS5 runs one process and starts or stops no other, so both throw without reading their arguments. The parameters follow Forever Launcher's own firmware 5.50 probes (`ps5/probes/localproc` in [bizkut/ps5-forever](https://github.com/bizkut/ps5-forever)): a path, a null-terminated argv and a 0x90-byte parameter block in, a handle out; the kill takes that handle. Their results on the console are unknown. Forever Launcher (PPSA99030) imports both and stops at load without them. @@ -24,6 +68,9 @@ - [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 and time format stay fixed +- [sceSystemServiceParamGetInt](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - `ANYPS5_LANGUAGE=auto` takes the console language from the host locale with the table in [LanguageLocale.hpp](../../../core/libs/prx/libSceSystemService/LanguageLocale.hpp). The table is a host-side convenience: nothing was measured on a console about which language a person of a given host locale would choose, and what a title does with the number is unmeasured too. The language numbers and names are those of the entry above ([OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/06fcf7fd108fa080035cdd3259ed22eb07e0508c/include/orbis/_types/sys_service.h#L54-L83) for 0 to 29). +- [sceSystemServiceParamGetInt](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - `auto` chooses where the console has no exact variant, and none of these choices is measured: a bare `pt` is Portuguese (Brazil) and a bare `es` Spanish (Spain); `en-GB`, `en-IE`, `en-AU` and `en-NZ` are English (United Kingdom) and every other English is English (United States); a bare `zh` is Chinese (Simplified) and `zh-TW`, `zh-HK`, `zh-MO` and the Hant script Traditional; `es-US` and the Latin American countries are Spanish (Latin America); the Portuguese-speaking territories outside Brazil are Portuguese (Portugal). A language the console has no entry for (Baltic and the other Slavic languages, Hebrew, Hindi, Catalan, Basque and Galician) gives English (United States) and is not mapped to a neighbouring language. +- [sceSystemServiceParamGetInt](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - `auto` reads the host language through `LC_ALL`, `LC_MESSAGES`, `LANG` and `LANGUAGE` on Linux, `GetUserDefaultLocaleName` on Windows (the user's regional-format locale, not the list of display languages) and `CFLocaleCopyPreferredLanguages` on macOS. Only the Linux source has run, with injected and real environments and no non-English host; the Windows and macOS sources are not built or run. The resolver is inline, so a library that carries its own copy (a separate DLL on Windows) resolves the host again and can print its own fallback line; its answer is the same. - [sceSystemServiceParamGetInt](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - parameters 1 to 5, 100 to 111, 200 to 213, 300 and 400 answer (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. A sweep of parameters 0 to 2048 in the same way on 2026-10-23 also found 101 to 111, 200 to 213 and 300 answering, each writing 4 bytes; every other parameter, and a null value pointer for 100 to 111, returned 0x80A10003 and left the value as it was. One title (PPSA13356, call at 0xAF350) asks for parameter 1 and does not read the result. - [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 diff --git a/docs/dev/debt/libSceTEEClient.md b/docs/dev/debt/libSceTEEClient.md index 9fb36e3b99..f8cee76a44 100644 --- a/docs/dev/debt/libSceTEEClient.md +++ b/docs/dev/debt/libSceTEEClient.md @@ -13,5 +13,5 @@ | `Unknown` | `H9IE7wXy3qY` | | `Unknown` | `VaBqpbmaWpc` | | `Unknown` | `Xn5kfzvPht4` | -| `Unknown` | `ZAJdq-IONZ4` | +| `AMDTEE_DLM_StartTADebug` | `ZAJdq-IONZ4` | | `Unknown` | `d-OLk3nvFIA` | diff --git a/docs/dev/debt/libSceUserService.md b/docs/dev/debt/libSceUserService.md index 354a092a73..353af8369a 100644 --- a/docs/dev/debt/libSceUserService.md +++ b/docs/dev/debt/libSceUserService.md @@ -46,7 +46,7 @@ ### Unknown function info - [sceUserServiceGetLoginUserIdList and sceUserServiceGetEvent for user 0](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - under ANYPS5_PAD_PORT0_STATE=controller user 0 logs out when its controller is removed and logs in when it returns, after a hold of 500 ms that AnyPS5 chooses. Whether the console logs the initial user out on controller removal, how long it takes and which event it sends are not measured. The default mode keeps user 0 logged in as before. -- [sceUserServiceGetNpAccountId](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - the account id for a logged-in user with no linked PSN account is assumed to be 0 with success; libSceNpManager's `sceNpGetAccountIdA` answers the same question with `SCE_NP_ERROR_SIGNED_OUT`. Console measurement on PS4 Pro firmware 10.71 confirms a user that is not logged in returns `0x80960105` (`USER_SERVICE_ERROR_UNKNOWN_USER`) and leaves the output untouched, differing from `sceUserServiceGetUserName`. +- [sceUserServiceGetNpAccountId](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - the account id for a logged-in user with no linked PSN account is assumed to be 0 with success; libSceNpManager's `sceNpGetAccountIdA` answers the same question with `SCE_NP_ERROR_SIGNED_OUT`. Console measurement on PS4 Pro firmware 10.71 confirms a user that is not logged in returns `0x80960105` (`USER_SERVICE_ERROR_UNKNOWN_USER`) and leaves the output untouched, differing from `sceUserServiceGetUserName`. Firmware 13.42's `libSceUserService.sprx` returns `0x80960005` for a null output before it looks at the user; while it is not initialized it returns `0x80960002` first, which is not modelled ([disassembly and a native run with stubbed imports](https://github.com/boykopovar/AnyPS5/pull/12165#issuecomment-6512375278), not measured on a console). - [Older settings exports](../../../core/libs/prx/libSceUserService/LegacyExports.cpp) - Forty callable NIDs are declared as throwing placeholders for older software. Their signatures and behavior are unverified; parameterless `int` declarations are placeholders, not verified ABI contracts. The corresponding NIDs are absent from the later version-labelled inventory. Representative later settings functions use a related dispatch pattern, which does not establish compatibility for these settings. No functionality is implemented. - [sceUserServiceInitialize and sceUserServiceInitialize2](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - event delivery has no background service thread, so the confirmed priority and Initialize2 CPU affinity mask do not affect host scheduling. Of 23 `sceUserServiceInitialize` calls in ten titles checked, 17 pass no param, four pass priority 700 (Borderlands 4 PPSA01494, Crimson Desert PPSA25286, MXGP 26 PPSA30839, Super Meat Boy 3D PPSA31162), one passes 767 (Ghost of Tsushima PPSA02225) and one in Metal Gear Solid 4's `mgs4.elf` passes a param from a register. None calls `sceUserServiceInitialize2`. @@ -556,6 +556,6 @@ - [sceUserServiceSetPartySettingFlags](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - unknown signature - [sceUserServiceGetGlsCameraEffect](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - unknown signature - [sceUserServiceGetPbtcSundayHoursEnd](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - unknown signature -- [sceUserServiceGetIsSpartacusEnabled](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - unknown signature +- [sceUserServiceIsGuestUser, sceUserServiceIsSharePlayClientUser and sceUserServiceGetIsSpartacusEnabled](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - which user ids the console gives guest and Share Play client users, and what Spartacus is, are unknown; no AnyPS5 user is either kind. They follow firmware 13.42's libSceUserService.sprx, read from its disassembly and run natively on x86-64 (not a console): the two checks test only the id's top three bits (`0x20000000`) and top four bits (`0x30000000`) and read no library state, and `sceUserServiceGetIsSpartacusEnabled` never reads its first argument, rejects a null output with `0x80960005` and otherwise writes true (one byte), without checking initialization - [sceUserServiceGetKeyboardType, sceUserServiceGetJapaneseInputType, sceUserServiceGetTraditionalChineseInputType](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - AnyPS5 stores no per-user keyboard setting, so the initial user reports the keyboard type of the configured system language (`ANYPS5_LANGUAGE`, English US by default) and 0 for the two input variants, and any other user id returns `USER_SERVICE_ERROR_UNKNOWN_USER`. The language-to-keyboard-type table is the repository owner's (languages 0 to 29); a valid language outside it, such as Ukrainian (30), reports 20 (US English). A value that is not a console language number is rejected by `sceSystemServiceParamGetInt` with an error naming `ANYPS5_LANGUAGE`; the call throws it and leaves the output untouched. The input variant is 0 because the owner's table gives 0 for Japanese and Traditional Chinese, the only languages needing one. `sceConvertKeycodeGetImeKeyboardType` consumes these values. - [sceUserServiceGetPlatformPrivacyWs1](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - the platform privacy setting for an offline user without a linked PSN account is assumed to be 0 (not permitted) with success; actual console privacy querying is not modelled. Return of `0x80960009` (`USER_SERVICE_ERROR_NOT_LOGGED_IN`) with or without an output for a user who is not logged in is borrowed from `sceUserServiceGetAgeLevel` and is not measured for this call. diff --git a/docs/dev/debt/libSceVdecCore.md b/docs/dev/debt/libSceVdecCore.md index 476c42e862..d87bce7d1a 100644 --- a/docs/dev/debt/libSceVdecCore.md +++ b/docs/dev/debt/libSceVdecCore.md @@ -8,8 +8,9 @@ | `sceVdecCoreQueryMemorySizeSvp` | `uAsJCITzX5k` | -- [sceVdecCoreAnalyzeStream, sceVdecCoreFinalizeComputeResource, sceVdecCoreFlushDecodeOutput, sceVdecCoreGetDecodeOutput, sceVdecCoreInitializeComputeResource, sceVdecCoreResetDecoder, sceVdecCoreSetDecodeInput, sceVdecCoreSetDecodeOutputSw, sceVdecCoreSyncDecode, sceVdecCoreSyncDecodeOutputSw, sceVdecCoreSyncDecodeWptr, sceVdecCoreTrySyncDecode, sceVdecCoreTrySyncDecodeOutputSw and sceVdecCoreTrySyncDecodeWptr](../../../core/libs/prx/libSceVdecCore/Export.cpp) (libSceVdecCore) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceVdecCore.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified +- [sceVdecCoreAnalyzeStream, sceVdecCoreFinalizeComputeResource, sceVdecCoreInitializeComputeResource, sceVdecCoreSetDecodeOutputSw, sceVdecCoreSyncDecodeOutputSw and sceVdecCoreTrySyncDecodeOutputSw](../../../core/libs/prx/libSceVdecCore/Export.cpp) (libSceVdecCore) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceVdecCore.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified ### Functional -- [sceVdecCoreQueryInstanceSize, sceVdecCoreQueryFrameBufferInfo, sceVdecCoreQueryComputeResourceInfo, sceVdecCoreCreateDecoder, sceVdecCoreDeleteDecoder and sceVdecCoreMapMemoryBlock](../../../core/libs/prx/libSceVdecCore/Export.cpp) (libSceVdecCore) - console behaviour is unmeasured. The signatures, error codes, 1920x1080 and 16-frame defaults and slot cutting follow [shadPS4's `vdeccore.cpp`](https://github.com/shadps4-emu/shadPS4/blob/0614ce324be092791b758d79f93d9f1ca5b10a81/src/core/libraries/videodec/vdeccore.cpp#L275-L706), and the sizes are shadPS4's constants, not console values: instance 0x40000, compute 0x10000, and a frame buffer of NV12 plus 0x1000 rounded to 64 KiB. CreateDecoder opens an FFmpeg H.264 or HEVC context but starts no decode thread; the memory info and compute queue are ignored, and a handle CreateDecoder did not return, or a codec that is unavailable or fails to open, throws +- [sceVdecCoreQueryInstanceSize, sceVdecCoreQueryFrameBufferInfo, sceVdecCoreQueryComputeResourceInfo, sceVdecCoreCreateDecoder, sceVdecCoreDeleteDecoder and sceVdecCoreMapMemoryBlock](../../../core/libs/prx/libSceVdecCore/Export.cpp) (libSceVdecCore) - console behaviour is unmeasured. The signatures, error codes, 1920x1080 and 16-frame defaults and slot cutting follow [shadPS4's `vdeccore.cpp`](https://github.com/shadps4-emu/shadPS4/blob/0614ce324be092791b758d79f93d9f1ca5b10a81/src/core/libraries/videodec/vdeccore.cpp#L275-L706), and the sizes are shadPS4's constants, not console values: instance 0x40000, compute 0x10000, and a frame buffer of NV12 plus 0x1000 rounded to 64 KiB. CreateDecoder opens an FFmpeg H.264 or HEVC context and starts the decoder's decode thread, which DeleteDecoder stops and joins; the memory info and compute queue are ignored, and a handle CreateDecoder did not return, or a codec that is unavailable or fails to open, throws +- [sceVdecCoreResetDecoder, sceVdecCoreSetDecodeInput, sceVdecCoreSyncDecode, sceVdecCoreGetDecodeOutput, sceVdecCoreFlushDecodeOutput, sceVdecCoreSyncDecodeWptr, sceVdecCoreTrySyncDecode and sceVdecCoreTrySyncDecodeWptr](../../../core/libs/prx/libSceVdecCore/Export.cpp) (libSceVdecCore) - console behaviour is unmeasured; they follow [shadPS4's `vdeccore.cpp`](https://github.com/shadps4-emu/shadPS4/blob/0614ce324be092791b758d79f93d9f1ca5b10a81/src/core/libraries/videodec/vdeccore.cpp#L118-L768). Decoding is FFmpeg software decoding on a host thread. GetDecodeOutput's pitch is the width rounded to 64 while CreateDecoder's frame buffer size uses the horizontal alignment (256 by default), as in shadPS4. Without a caller frame buffer or mapped slot a picture goes to host memory the decoder owns, and the empty-queue fields are shadPS4's. Unlike shadPS4, a picture larger than its frame buffer throws, an access unit too large for an FFmpeg packet is refused, and the NV12 copy goes row by row diff --git a/docs/dev/debt/libSceVideoOut.md b/docs/dev/debt/libSceVideoOut.md index c590db18c9..2b66880fb0 100644 --- a/docs/dev/debt/libSceVideoOut.md +++ b/docs/dev/debt/libSceVideoOut.md @@ -13,6 +13,7 @@ - [sceVideoOutSetFlipMaster, sceVideoOutClearFlipMaster](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - what flip-master ownership means is unknown, and the signatures `sceVideoOutSetFlipMaster(handle, masterHandle)` and `sceVideoOutClearFlipMaster(handle)` are unverified; PPSA29866 imports both. Both throw on a handle that has ever had buffers registered, and `SetFlipMaster` throws for a master handle other than 0 or 1; those states are unmeasured, as is a closed handle (answered like -1). Measured in a payload process on a PS5 (firmware 12.70) on a handle from `sceVideoOutOpen(0xff, 0, 0, NULL)` with no buffer registered: `SetFlipMaster` with (handle), (handle, 0) and (handle, 1) returned 0x8029000b, `ClearFlipMaster(handle)` returned 0x8029001c, and both returned 0x8029000b for handle -1 - [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 +- [Delete*Event on a port that is not open](../../../core/libs/prx/libSceVideoOut/src/Event.cpp) (libSceVideoOut) - what the console returns when `sceVideoOutDeleteFlipEvent`, `sceVideoOutDeleteVblankEvent`, `sceVideoOutDeletePreVblankStartEvent`, `sceVideoOutDeleteOutputModeEvent` or `sceVideoOutDeleteVrrActiveStatusEvent` gets a handle that was never opened or was closed is unmeasured, and no title is known to do it. They return `VIDEO_OUT_ERROR_INVALID_HANDLE` by analogy with the `Add*Event` calls (#4058), and check the handle before the event queue, as `registerVideoOutEvent` does, so an invalid handle with a null queue also returns `VIDEO_OUT_ERROR_INVALID_HANDLE` - [sceVideoOutCursorEnable, sceVideoOutCursorDisable, sceVideoOutCursorSetImageAddress, sceVideoOutCursorSetPosition](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) - no cursor is shown: the image format and size are unknown, so a non-null image address and a cursor index other than 0 report not implemented, and `CursorSetPosition` returns 0 for every position but only (100, 100) was measured. Measured in a payload process on a PS5 (firmware 12.70) on a handle from `sceVideoOutOpen(0xff, 0, 0, NULL)` with no buffer registered: `CursorEnable` and `CursorSetImageAddress` with (handle, 0, NULL) returned 0x80290002, `CursorSetPosition(handle, 0, 100, 100)` and `CursorDisable(handle, 0)` returned 0, and all four returned 0x8029000b for handle -1. A closed handle is unmeasured and answered like -1. Parameters follow Prospero Win's [declarations](https://github.com/mpereiraesaa/prospero-win/blob/644e32e400c555e340d507383398c5d21b39b0b0/wine/ps5/pw_vulkan_radv.c#L65-L68). - [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 over registered buffers](../../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut) - a call whose buffer slots are already registered returns `SLOT_OCCUPIED` (0x80290010) and changes nothing. EVO Player (PPSA99039) [measured](https://github.com/sainsaji/EVO-PLAYER-PS5/blob/2db44b5294657b12bf91666154a05f029ddb5aa2/projects/evoplayer/media/src/evo_agc_runtime.c#L2830-L2838) that answer for set 0 and buffers 0 and 1 while that set was still registered, with the [call](https://github.com/sainsaji/EVO-PLAYER-PS5/blob/2db44b5294657b12bf91666154a05f029ddb5aa2/projects/evoplayer/media/src/evo_agc_runtime.c#L2968-L2982) it makes. Not measured, and still thrown: an occupied set over free buffer slots (`INVALID_INDEX`), and occupied slots in `sceVideoOutRegisterBuffers`, the PS4 call. @@ -50,6 +51,7 @@ - [Mouse capture and delta scaling](../../../core/libs/prx/libSceVideoOut/src/MouseInput.cpp) (libSceVideoOut) - the console's mouse reports relative motion only, so a host cursor can never coincide with the title's cursor; while a title has a mouse open and the window is focused the host cursor is hidden and captured (SDL relative mode, released on focus loss). Motion deltas are scaled from the presented image's size in window units to the title's display resolution with sub-pixel carry; the console's deltas are counts, so a title that applies its own sensitivity moves its cursor that much faster than the host pointer would have. The scale takes SDL's relative motion in logical window units, the units of `SDL_GetWindowSize`; under Windows DPI scaling SDL may report it in physical pixels, which is unverified (no Windows run yet), and a drawable larger than the window does not change the scale. - [HDR output](../../../core/libs/prx/libSceVideoOut/src/HostHdrProbe.cpp) (libSceVideoOut, libSceSystemService) - HDR output is reported only on Windows; elsewhere it stays SDR: SDL2 and Vulkan expose no output HDR state, and a surface can offer HDR10 formats with HDR off (KDE Wayland with RADV). The console's events for an HDR change are unknown; here every open port's output mode event fires when HDR availability or luminance changes. Console HDR settings (calibrated peak, the SDR-in-HDR conversion block) are not modelled. Surface format changes that leave the swapchain valid are seen only when DXGI's report changes or the swapchain is recreated. `sceSystemServiceGetHdrToneMapLuminance` reports SDR before VideoOut is first used, and no HDR change event fires for the state found at start-up. HDR needs a 10-bit HDR10 swapchain and a monitor DXGI reports in `DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020`; without a usable peak, luminance is 1000 nits peak and full frame, 0 black - [Windows game controllers](../../../core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp) (libSceVideoOut) - SDL's DirectInput joystick backend is disabled at default hint priority before `SDL_InitSubSystem`, because its device enumeration blocks forever on some vendor HID devices (#1042). XInput and HIDAPI controllers are detected; controllers reachable only through DirectInput are not. Setting `SDL_DIRECTINPUT_ENABLED=1` restores them on hosts without such a device. SDL's Windows.Gaming.Input backend stays on by default. Setting `APS5_NO_WGI` turns it off at default hint priority, for hosts where it faults at `Windows.Gaming.Input.dll+0x10a84` a few seconds into every run: seen on Windows 10 Pro 22H2 (19045) with the Nefarius and Oculus virtual gamepad emulation buses installed and no physical controller connected. With it off, devices and features reachable only through Windows.Gaming.Input are lost: racing wheels, arcade sticks, flight sticks, HID pads without an HIDAPI driver, and Xbox One and Series trigger rumble. +- [ImeTextInput](../../../core/libs/prx/libSceVideoOut/src/ImeTextInput.cpp) (libSceVideoOut) forwards SDL_TEXTEDITING with its text cut to 32 bytes by SDL and `start` and `length` read as counts of characters. SDL_TEXTEDITING_EXT (longer text) is not requested, `SDL_SetTextInputRect` is not called so the system candidate window is not placed at the field, and SDL's own text input, started when video starts, is left on outside dialogs. How each platform reports `start` and `length` (characters or UTF-16 units, cursor or selection) is not measured. Closes when: measured on Linux (ibus and fcitx) - The following pending declarations have unverified signatures and behavior; they are parameterless import-resolution declarations and throw through `NotImplemented_nid_no_patch`: [`sceDbgVideoOutAddOutputModeVr60Privilege`](../../../core/libs/prx/libSceVideoOut/Unimplemented.cpp). @@ -249,3 +251,21 @@ - [sceVideoOutSetConfigurationMode](../../../core/libs/prx/libSceVideoOut/InventoryExports.cpp): the name hashes to NID `N1+j0RymMUQ`, a function export in both older and later version-labelled images. Its signature and behavior are unverified; this import-resolution declaration throws. - [Older callable NIDs](../../../core/libs/prx/libSceVideoOut/InventoryExports.cpp): 7 unidentified exports have unverified names, signatures and behavior. These are throwing import-resolution placeholders; no functionality is implemented. No matching NIDs were found in the later module inventory. Six use the primary library qualifier and one uses the nosubmission qualifier; qualified guest import resolution is unverified. + +- [Later callable exports](../../../core/libs/prx/libSceVideoOut/InventoryExports.cpp): 13 function exports of the primary logical library observed only in the 13.60-labelled image; no matching NIDs were found in the older inventory. Each name hashes to its NID and matches the [public symbol database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv). Parameter lists, return types and behavior are unverified; parameterless `int` declarations are placeholders, not verified ABI contracts. Every entry throws through `NotImplemented_nid_no_patch`. `sceVideoOutSysGetMonitorInfo` is a separate export from `sceVideoOutSysGetMonitorInfo_`. `sceVideoOutDebugLatencyMeasureGetLatestLatency` is also exported under the `libSceVideoOut_nosubmission` and `libSceVideoOutLatency_nosubmission` qualifiers; qualified guest import resolution is unverified. + +| Name | NID | +| --- | --- | +| `sceVideoOutDebugLatencyMeasureGetLatestLatency` | `BTawastvk7s` | +| `sceVideoOutGetHandleByBus` | `zPVZiUbvzkg` | +| `sceVideoOutPostProcessorAddCallback` | `-NhjL1kuqhk` | +| `sceVideoOutPostProcessorRemoveCallback` | `8hTZgKGL-58` | +| `sceVideoOutSetContentDynamicRange` | `WK-TrW+vuqM` | +| `sceVideoOutSysCursorSetSize` | `I7ms8YxoZio` | +| `sceVideoOutSysGetCursorInfo` | `3mh7IA9NrAI` | +| `sceVideoOutSysGetMonitorInfo` | `WAsgAw34e1Y` | +| `sceVideoOutSysGetOutputStatusEx` | `Z699gIW4sok` | +| `sceVideoOutSysOpenPostProcessor` | `yvH3H30ozqU` | +| `sceVideoOutSysSetDrrPeg` | `cFy4rRDKZjQ` | +| `sceVideoOutSysSetDrrPegRefreshRate` | `mVJZobHnyiQ` | +| `sceVideoOutSysSetVrrSetting` | `FL6WhOnaR4Y` | diff --git a/docs/dev/debt/libSceVideodec2.md b/docs/dev/debt/libSceVideodec2.md index 7c73d2f659..fcc4b75f6b 100644 --- a/docs/dev/debt/libSceVideodec2.md +++ b/docs/dev/debt/libSceVideodec2.md @@ -15,10 +15,12 @@ | `sceVideodec2SyncAllSlice` | `JjvhoNUnZT8` | - [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 -- [sceVideodec2TermLibrary and eleven other exports](../../../core/libs/prx/libSceVideodec2/Unimplemented.cpp) (libSceVideodec2) - report that they are not implemented; what they do on the console is not known. The others are `QueryLibraryInfo`, `QueryDecoderInfo`, `MapMemory`, `MapDirectMemory`, `PresetDecoderConfigInfo` (plain, `Avc`, `Hevc`, `Vp9`), `CreateDecoderBid`, `CreateHevcDecoder` and `QueryHevcDecoderMemoryInfo`. They are exported so that a title which imports them with `sceVideodec2InitLibrary` and calls none of them directly loads. Only `sceVideodec2MapDirectMemory` has a known parameter list, `(decoder, memory)`, from [EVO-PLAYER-PS5's `sce_videodec2.h`](https://github.com/sainsaji/EVO-PLAYER-PS5/blob/9715f6ee9bf9b53461d6fdf9a2e11b4c48357783/projects/evoplayer/media/include/sce/sce_videodec2.h#L173); the others take none here +- [sceVideodec2TermLibrary and nine 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`) and `CreateDecoderBid`. They are exported so that a title which imports them with `sceVideodec2InitLibrary` and calls none of them directly loads. Only `sceVideodec2MapDirectMemory` has a known parameter list, `(decoder, memory)`, from [EVO-PLAYER-PS5's `sce_videodec2.h`](https://github.com/sainsaji/EVO-PLAYER-PS5/blob/9715f6ee9bf9b53461d6fdf9a2e11b4c48357783/projects/evoplayer/media/include/sce/sce_videodec2.h#L173); the others take none here ### Functional +- [HEVC decoder aliases](../../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2): whether the PS5 aliases behave exactly like the generic functions is unverified. `sceVideodec2CreateHevcDecoder` and `sceVideodec2QueryHevcDecoderMemoryInfo` forward their arguments unchanged to the generic functions, following [shadPS4's PS4 implementation at 28ad5d998fae6e4f15fd708222da9408388693fb, lines 327–339](https://github.com/shadps4-emu/shadPS4/blob/28ad5d998fae6e4f15fd708222da9408388693fb/src/core/libraries/videodec/videodec2.cpp#L327-L339). + - [sceVideodec2Decode and sceVideodec2Flush](../../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2): the decoder core's discarded flag is not modelled: a returned picture always has `OutputInfo.isDiscardedFrame` (byte 0xb) at 0, and an unshown VP9 frame returns no picture. Per a #10913 review reading firmware 13.42's module (not reproduced here), the byte is written only with a returned picture and is left as the caller set it otherwise, as here. Name and offset: [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Video/Videodec2.cs#L150-L192) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/83f8bfd3c9c35fcb135ef6c33dfd88bbfe066ee2/src/libs/libVideoDec2.cpp#L91-L105); shadPS4 leaves it as padding. PPSA05110 sets only `thisSize` before its VP9 Decode (0x13728dd) and Flush (0x1371f7e) and takes a picture only when the byte is 0 - [sceVideodec2CreateDecoder](../../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2) - One title (PPSA02225) disassembled for this entry calls it once (arguments set up at 0x8b5095-0x8b50a6) with three pointers: a structure built on its stack, a structure inside its decoder object and the address of a field of the same object. A nonzero result makes it leave the decoder setup; on one path (a flag byte set and two 32-bit fields equal to values it computed before) it passes that field to the next Videodec2 call. The codec type it requests and the meaning of the structure fields are not shown. diff --git a/docs/dev/debt/libSceVoiceChat.md b/docs/dev/debt/libSceVoiceChat.md index 8b292707d9..baac982dd1 100644 --- a/docs/dev/debt/libSceVoiceChat.md +++ b/docs/dev/debt/libSceVoiceChat.md @@ -8,5 +8,14 @@ ### Unknown function info +- [libSceVoiceChat additional exports](../../../core/libs/prx/libSceVoiceChat/Export.cpp) - these name/NID pairs are present in the inspected export inventory. Parameter lists, return types and behavior are unverified. Parameterless `int` declarations are import-resolution placeholders, not verified ABI prototypes; every entry throws through `NotImplemented_nid_no_patch`. No runtime functionality is implemented. + +The inventory label is not authenticated retail-firmware provenance. The inspected image contains raw module-parameter words `0x13590001` at +0x10 and `0x13600007` at +0x14. Using the [published static ELF membership procedure](https://github.com/boykopovar/AnyPS5/pull/12367#issuecomment-6470603298), its 37 unique function NIDs have sorted-LF SHA-256 `6ae782ec9803eb89b69814a4687c2e7e25940b3f26e48dde34f0abd465cd2448`; all 2 entries below are present. This digest identifies the function-NID set, not the whole binary or its ABI. An independent review on [this PR](https://github.com/boykopovar/AnyPS5/pull/11935#pullrequestreview) corroborated these identities using a separately reported 13.42 reference; that does not establish matching binaries, prototypes or behavior. + +| Name | NID | +| --- | --- | +| `sceVoiceChatRegisterMicEventHandler2` | `hVf-dQZU4nE` | +| `sceVoiceChatUnregisterMicEventHandler2` | `Jra48Mpq8+Q` | + - [sceVoiceChatGetActiveChannel, sceVoiceChatGetChannelMemberActiveChannelMatchingState, sceVoiceChatGetChannelMemberVoiceConnectionState, sceVoiceChatGetChatChannelMemberInfoList, sceVoiceChatGetChatGroupMemberInfoList, sceVoiceChatGetMicState, sceVoiceChatGetUserVoiceVolumeGain, sceVoiceChatRegisterChannelEventHandler, sceVoiceChatRegisterDaemonEventHandler, sceVoiceChatRegisterMemberEventHandler, sceVoiceChatRegisterPartyUserStateChangedHandler, sceVoiceChatRegisterVoiceEventHandler, sceVoiceChatRequest0, sceVoiceChatRequest1, sceVoiceChatRequestAcsTts, sceVoiceChatRequestAction, sceVoiceChatRequestSetMemberVolume, sceVoiceChatRequestUpdateGameSessionVoiceChatChannelName, sceVoiceChatRequestUpdatePlayerSessionVoiceChatChannelName, sceVoiceChatSetUserVoiceVolumeGain, sceVoiceChatStartAccessibilityEvent, sceVoiceChatStartChannelEvent, sceVoiceChatStartDaemonEvent, sceVoiceChatStartMemberEvent, sceVoiceChatStartRangedMemberEvent, sceVoiceChatStartVoiceEvent, sceVoiceChatStopAccessibilityEvent, sceVoiceChatStopChannelEvent, sceVoiceChatStopDaemonEvent, sceVoiceChatStopMemberEvent, sceVoiceChatStopRangedMemberEvent, sceVoiceChatStopVoiceEvent, sceVoiceChatUnregisterAccessibilityEventHandler, sceVoiceChatUnregisterChannelEventHandler, sceVoiceChatUnregisterDaemonEventHandler, sceVoiceChatUnregisterMemberEventHandler, sceVoiceChatUnregisterMicEventHandler, sceVoiceChatUnregisterPartyUserStateChangedHandler, sceVoiceChatUnregisterRangedMemberEventHandler, sceVoiceChatUnregisterVoiceEventHandler](../../../core/libs/prx/libSceVoiceChat/Export.cpp) (libSceVoiceChat) - parameter layouts are inferred; offline, handlers and event controls return 0, query calls, volume gain controls, and requests return `SIGNED_OUT` (0x80550006) without modifying guest buffers - [sceVoiceChatRequestCreateVoiceChatGroup, sceVoiceChatRequestJoinVoiceChatGroup, sceVoiceChatRequestLeaveVoiceChatGroup, sceVoiceChatRequestDeleteVoiceChatGroup](../../../core/libs/prx/libSceVoiceChat/Export.cpp) (libSceVoiceChat) - assumed to take a request id and two pointers like the other `sceVoiceChatRequest*` calls; offline, every argument is ignored and `SIGNED_OUT` is returned, as for those diff --git a/docs/dev/debt/libSceXml.md b/docs/dev/debt/libSceXml.md index 34ee59a045..fc6b7d1228 100644 --- a/docs/dev/debt/libSceXml.md +++ b/docs/dev/debt/libSceXml.md @@ -7,3 +7,43 @@ ### Value types and handle constructors - [The `String`, `SimpleData`, `SerializeParameter`, `NodeId` and `Node` value operations and the handle constructors](../../../core/libs/prx/libSceXml/Export.cpp) (libSceXml) - follow `libSceXml.sprx` of firmware 13.60, read from disassembly, not measured on a console. `NodeId::operator void*` returns null for every `NodeId`, and the `NodeId` constructor and copies leave its padding bytes untouched. The handle constructors only clear the implementation pointer. Exports that read the objects `initialize` builds still throw, because nothing creates those objects yet: `AttributeList` `getLength` and `isAvailable`, `Document::resetStatus`, the `DocumentBuilder` and `Sax::Parser` setters, `Node(NodeId)`, and `Node` `getNodeType`, `hasChildNodes`, `hasAttributes` and `getOwnerDocument`. `Node::getAttributes` and `getChildNodes` are a bare return in the module, and the deleting `MemAllocator` destructor is `ud2`, so they still throw too. + +### Additional callable exports + +- [Additional callable exports](../../../core/libs/prx/libSceXml/Export.cpp) - 33 callable NIDs match the [public 3.20 catalogue](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceXml.c) and the [selected export metadata](https://gist.github.com/kyon-eth/d35bcd082a172a28ada2052647b0ff20/72a4f16fcb238e61dc8c6424dc26eee106e6c8ce) from two version-labelled images (SHA-256 `3d83607a2ed81110a37d677e5ba1936a01cb78c5847a2e9b582a4a981d313050`). The labels are not authenticated. The names match the pinned NID name database above and hash to the recorded NIDs. Prototypes and behavior are unverified; the declarations throw through `NotImplemented_nid_no_patch` and implement no runtime functionality. + +| Name | NID | +| --- | --- | +| `_ZNK3sce3Xml3Dom8Document11getNodeTypeENS1_6NodeIdE` | `1JAZNsOKua0` | +| `_ZNK3sce3Xml3Dom8Document12getLastChildENS1_6NodeIdE` | `5JyhV10GpQE` | +| `_ZNK3sce3Xml3Dom8Document7getTextENS1_6NodeIdE` | `7MsYv4D1Ang` | +| `_ZNK3sce3Xml3Dom8Document13getChildNodesENS1_6NodeIdEPNS1_8NodeListE` | `AI6mDpNxPYk` | +| `_ZNK3sce3Xml3Dom8Document11getNodeNameENS1_6NodeIdE` | `GceM6WE2WS4` | +| `_ZNK3sce3Xml3Dom8Document20getElementsByTagNameENS1_6NodeIdEPKNS0_6StringEPNS1_8NodeListE` | `HGZ0DTuCvuQ` | +| `_ZNK3sce3Xml3Dom8Document13getEntityTypeENS1_6NodeIdE` | `I3ootbOM-00` | +| `_ZNK3sce3Xml4Attr7getNameEv` | `KgzBbi3DUDA` | +| `_ZNK3sce3Xml3Dom8NodeList8findItemENS1_6NodeIdE` | `N-2qBJYv6lw` | +| `_ZNK3sce3Xml3Dom8Document13getAttributesENS1_6NodeIdEPNS1_8NodeListE` | `NZMWDTSdPXM` | +| `_ZNK3sce3Xml3Dom8Document13getFirstChildENS1_6NodeIdE` | `PXmLYh0zF14` | +| `_ZNK3sce3Xml3Dom8NodeList4itemEj` | `Q-ryXoj4ez0` | +| `_ZNK3sce3Xml4Attr8getValueEv` | `QOmIaoXFNig` | +| `_ZNK3sce3Xml3Dom8Document11getAttrNameENS1_6NodeIdE` | `REcO7rLo0fA` | +| `_ZNK3sce3Xml3Dom8NodeListixEj` | `VdYoc9kGeyA` | +| `_ZNK3sce3Xml3Dom8Document9getStatusEv` | `WPyn5rEy2DE` | +| `_ZNK3sce3Xml3Dom8Document14getSkippedTextENS1_6NodeIdE` | `XlzFT8nnAMU` | +| `_ZNK3sce3Xml3Dom8NodeList8findItemEPKNS0_6StringE` | `Zmk8Mb9qNQ0` | +| `_ZNK3sce3Xml3Dom8Document9getEntityENS1_6NodeIdE` | `aZH+29bSsEk` | +| `_ZNK3sce3Xml3Dom8Document11getNextAttrENS1_6NodeIdE` | `b9b6PA5TKc0` | +| `_ZNK3sce3Xml3Dom8Document12getAttrValueENS1_6NodeIdE` | `fw4+tIZ-4t8` | +| `_ZNK3sce3Xml3Dom8Document12getFirstAttrENS1_6NodeIdE` | `jMWTTO6X-kw` | +| `_ZNK3sce3Xml3Dom8NodeList11isAvailableEv` | `jc7L3HSltT0` | +| `_ZNK3sce3Xml3Dom8Document7getRootEv` | `mODyaCpyDnM` | +| `_ZNK3sce3Xml3Dom8Document12getAttributeENS1_6NodeIdEPKNS0_6StringE` | `miVYGbk3LHs` | +| `_ZNK3sce3Xml3Dom8Document10getXmlMetaEv` | `muE9OA3xYv4` | +| `_ZNK3sce3Xml3Dom8Document11isAvailableEv` | `nfoWUQ1eDl8` | +| `_ZNK3sce3Xml3Dom8Document9getParentENS1_6NodeIdE` | `p0IeMemvZKo` | +| `_ZNK3sce3Xml3Dom8NodeList9getLengthEv` | `tZioKsu5pxs` | +| `_ZNK3sce3Xml3Dom8Document10isReadOnlyEv` | `uiPVQSxXlsc` | +| `_ZNK3sce3Xml3Dom8Document13hasChildNodesENS1_6NodeIdE` | `ulLP+f5h0rg` | +| `_ZNK3sce3Xml3Dom8Document13hasAttributesENS1_6NodeIdE` | `vWA1r9CiPgI` | +| `_ZNK3sce3Xml4Attr11isAvailableEv` | `wlgM8cLl1mQ` | diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index c4f670be60..c8249d6572 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -8,12 +8,17 @@ - [frexpf](../../../core/libs/prx/libc/src/Math.cpp) (libc) - follows the bundled PPSA03766 implementation: exponent-bit decomposition, conditional single-precision subnormal scaling, zero exponent for zero/NaN/infinity, and unchanged signaling NaNs. PPSA07923 agrees on results/exponents but unconditionally scales finite inputs, additionally raising overflow/inexact on large values; that SDK variant is not reproduced. Masked-exception observations cover rounding, DAZ/FTZ and sticky flags; unmasked traps and other SDK versions are unverified. +- [std::__sce_v2 _Dscale, _FDscale, _LDscale, _Dunscale, _FDunscale and _LDunscale](../../../core/libs/prx/libc/src/MathExact.cpp) (libc) edit the exponent and fraction bits as the `libc.prx` bundled with PPSA03671 v01.001.005 (J03848509) does: subnormal inputs are normalized, results that become subnormal are rounded to nearest even, overflow gives an infinity and underflow a signed zero, without `errno` or floating-point exception flags. The unversioned `_Dscale` and `_Dunscale` here compute the same values with host arithmetic, which raises those flags. The `long double` forms take an 8-byte `double`, as in that module, where `_LDscale` and `_LDunscale` call the `double` forms. Run side by side with that module on a Windows host, all six return the same code, output bits, exponent, `errno`, MXCSR and x87 state; the console is not measured. + - [fesetenv](../../../core/libs/prx/libc/src/Math.cpp) (libSceLibcInternal, implemented in libc, NID `Q-bLp+b-RVY`) uses the 32-byte amd64 environment and x87/MXCSR restore sequence in the [public SDK fenv.h](https://github.com/ps5-payload-dev/sdk/blob/a7685ce8e97ce1f8a4f38253a41d54241c9124f1/include/freebsd/fenv.h). The console layout, behavior and module/library versions are unmeasured. Null pointers and MXCSR bits unsupported by the current host's `FXSAVE` mask throw before changing state; a zero mask uses the architectural `0xffbf` fallback. Console fault behavior for invalid environments is unverified. The separate `__fe_dfl_env` object remains missing and its console initializer is unknown. - [sigvec](../../../core/libs/prx/libc/src/LegacySignals.cpp) (libc and libSceLibcInternal) has not been measured on a physical PS5 or run on macOS. Its Windows module-lookup fixture pins modules until process exit; explicit DLL unloading remains unverified. The cause of the reported unload crash is unknown. A contributor executed the firmware 13.42 export `yhxKO9LYc8A` from libSceLibcInternal in an emulator with recording `_sigaction`/`sigemptyset` imports; layout, conversion, aliasing and forwarded results match this implementation. Its vector ABI, low-word mask conversion and restart/interrupt flag inversion follow FreeBSD 11.4 [`sigcompat.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/compat-43/sigcompat.c) and [`signal.h`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/sys/signal.h). Calls use registered guest `sigaction`; absent registration throws before mutation. It inherits the [guest signal layer's gaps](libkernel.md): action masks, restart/reset flags and `SA_SIGINFO` delivery are not applied. New vectors clear the upper mask words; queries return only the low word. The previous-vector output is guarded before the action changes; a rejected host-write scope returns `EFAULT` without output or disposition mutation. Other invalid caller memory is undefined, as upstream. - [strtonum](../../../core/libs/prx/libc/src/StringExtensions.cpp) (libc) is unverified on the console. Its signature, decimal parsing, inclusive bounds, error strings, error precedence and errno preservation follow [FreeBSD 11.4 strtonum.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdlib/strtonum.c). Parsing uses the host `strtoll` with base 10. It is not exported by `libSceLibcInternal.sprx` on a PS5 (firmware 13.60), so a payload process cannot call it there +- [__inet_aton, __inet_addr](../../../core/libs/prx/libc/src/StringExtensions.cpp) (libc) - a null text throws `std::invalid_argument`, matching `inet_aton` in `libSceLibcInternal`; on the console dereferencing a null pointer faults. - [a64l, l64a and l64a_r](../../../core/libs/prx/libc/src/Radix64.cpp) (libc, NIDs `XR3orje3XCg`, `lF2uY5zV6tg`, `Zv-2rBFdZV0`) are ported from FreeBSD 11.4 [a64l.c](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/libc/stdlib/a64l.c) and [l64a.c](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/libc/stdlib/l64a.c): `a64l` maps every byte of the first six arithmetically, including bytes outside `./0-9A-Za-z`, and sign-extends the low 32 bits; `l64a_r` writes the digits that fit before returning -1 on a short buffer. Deviations from FreeBSD: `a64l(NULL)` returns 0, `l64a_r` returns -1 without writing for a null buffer or `buflen <= 0`, and `l64a` uses a per-thread buffer instead of one shared `static` buffer. The PS5 `libc` storage, parsing of invalid characters and error behaviour are unverified; the tests check the behaviour chosen here, not equivalence with the console. +- [__std_find_last_trivial_1/2/4/8](../../../core/libs/prx/libc/src/VectorAlgorithms.cpp) (libc, NIDs `eaLVyLMql6I`, `-ciYO9tOTVQ`, `fPGFDSCERGM`, `akVoBLxgReY`) have unverified console execution and SDK-version coverage. Valid half-open ranges follow PPSA03671 v01.001.005's bundled `libc.prx` at RVAs `0x658D0`, `0x659B0`, `0x65A90`, `0x65B70`: return the last equal 1/2/4/8-byte element, or the supplied end pointer if absent, without changing the input. Unaligned element reads and empty ranges, including null or inaccessible equal endpoints, are supported. Reversed, partial-element or nonempty null-ended ranges throw instead of reproducing native faults or nontermination. + - [sceLibcBacktraceGetBufferSize, sceLibcBacktraceSelf](../../../core/libs/prx/libc/src/Backtrace.cpp) (libc) - signatures and frame record layout inferred from a title (next pointer, u32 module offset, module name at +0x10); the u32 at +0xC is unknown and written as 0. Module names are host file names without the `.guest.prx` suffix, so their case may differ from the PS5 module name. Linux and macOS use `backtrace` and `dladdr`; Windows uses `_Unwind_Backtrace` and module lookup, and stops at a frame without `.ehmeta` or `.ehfram` tables whose PE unwind data has an exception or termination handler, leaving the trace empty or cut short while still returning 0; other platforms throw - [ApplicationGuestHeapApi](../../../core/libs/prx/libc/src/ApplicationProcessParameters.cpp) (libc) - on Linux, HLE application allocation uses a loaded guest SDK's ten allocator exports when the title's replacement table has an allocator entry and the exports belong to one native module. With an empty table it keeps the default heap, since a bundled `libc.prx` may export the allocator NIDs while forwarding them to HLE. Its CRT owns application allocator initialization and finalization; HLE retains the module until process exit. Partial or mixed providers throw. This avoids reinitializing an allocator already used by SDK startup; console behavior and other host platforms remain unverified. - [fdopen](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) - a closed or unknown descriptor returns null with `EBADF`, where the system `libSceLibcInternal` of PS5 firmware 13.60 returns a stream whose later flush and close fail with `EBADF`; a null mode returns null with `EINVAL`, where the console returns a stream; on Linux a mode the descriptor's access does not allow returns null with `EINVAL`, as the host `fdopen` checks it. Otherwise it follows the console: a mode whose first letter is not `r`, `w` or `a` fails with `EINVAL` before the descriptor is looked at, a negative descriptor then returns null without setting `errno`, and the descriptor's access mode is not checked, so the first read or write it does not allow fails instead @@ -24,6 +29,8 @@ - [_Mtx_trylock](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `k6pGNMwJB08`) - returns busy for a non-recursive mutex already owned by the caller. The `libc.prx` that titles ship (read from disassembly of PPSA13934's copy, 0x5cf0, not measured on a console) passes its argument to `scePthreadMutexTrylock` and returns 0 for success, `_Thrd_busy` (3) for `SCE_KERNEL_ERROR_EBUSY` and `_Thrd_error` (4) for any other error, so busy for an owned non-recursive mutex comes from the kernel mutex there; this libc keeps its own owner and count instead, and its result for a kernel error other than busy is not compared. - [nearbyintf](../../../core/libs/prx/libc/src/MathExact.cpp) (libc, NID `c+4r-T-tEIc`) - whether the console raises `FE_INEXACT` for exact inputs remains unmeasured. A PS5 firmware 13.60 payload measured MXCSR-controlled rounding and `FE_INEXACT` for inexact inputs; this implementation uses host `rintf` for those measured cases. - [nearbyint](../../../core/libs/prx/libc/src/MathExact.cpp) (libc) - exception flags for exact inputs remain unmeasured. The bundled libc of PPSA07923 and PPSA05916 raises `FE_INEXACT` for inexact inputs, which this implementation reproduces with host `rint`. + +- [std::__sce_v2 _Dtest, _FDtest, _LDtest, _Dint, _FDint, _LDint, _Dnorm and _FDnorm](../../../core/libs/prx/libc/src/MathExact.cpp) (libc) forward to the unversioned helpers. Their `long double` forms take an 8-byte `double`, as in the `libc.prx` bundled with PPSA03671 v01.001.005 (J03848509), whose `_LDtest` calls the unversioned `_Dtest` and whose `_LDint` calls `std::__sce_v2::_Dint`. Run side by side with that module on a Windows host, all eight return the same code, output bits, `errno` and floating-point exception flags; the console is not measured. - [nearbyintl](../../../core/libs/prx/libc/src/Math.cpp) (libc) - uses host `nearbyint`, which suppresses `FE_INEXACT` for inexact inputs; console behavior remains unmeasured. - [__std_min_4u](../../../core/libs/prx/libc/src/VectorAlgorithms.cpp) (libc, NID `60rcqDrkUNY`) throws for null, empty, reversed or partial-element ranges; their native failure behavior is not reproduced. The nonempty-range signature, unsigned 32-bit value result and unaligned reads follow PPSA03671 v01.001.005's bundled `libc.prx` at RVA `0x62680`, size `0x1a2`, read from disassembly and run directly on the host, not measured on a console. - [__std_min_8u](../../../core/libs/prx/libc/src/VectorAlgorithms.cpp) (libc, NID `i8DeU1sI6I4`) throws for null, empty, reversed or partial-element ranges; their native failure behavior is not reproduced. The nonempty-range signature, unsigned 64-bit value result and unaligned reads follow PPSA03671 v01.001.005's bundled `libc.prx` at RVA `0x62a30`, size `0x2a4`, read from disassembly and run directly on the host, not measured on a console. @@ -52,7 +59,7 @@ - [malloc_usable_size](../../../core/libs/prx/libc/src/ApplicationHeap.cpp) (libc) - with the default allocator it returns the size the block was requested with (the range registered for it), not the rounded-up chunk size Sony's allocator reports; both satisfy "at least the requested size", and bytes past the request would not be write-watched. With an application allocator it calls entry 9 of the replacement table (`malloc_usable_size` in KytyPS5's `MallocReplace` order) and throws when the table leaves it null, like `malloc_stats_fast` - [malloc_stats_fast](../../../core/libs/prx/libc/src/GuestHeap.cpp) (libc) - with the default allocator it fills the 0x28-byte managed-size block as `sceLibcMspaceMallocStatsFast` does, from the default heap's own accounting, not Sony's allocator: the system sizes count the 1 MiB spans of its small-block classes and its committed large blocks, cached free ones included, and the in-use sizes count each handed-out block at its size-class or 64 KiB-granule size, header included. A block size below 0x28 or a null pointer returns 22, as in the mspace version; the version field is not checked. Ship of Harkinian (PPSA99320) calls it about 10 s after start-up. - [malloc, calloc, realloc, reallocarray, memalign, aligned_alloc, reallocalign and posix_memalign](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) - the console's own heap was measured in a payload process on firmware 13.60, not in a title. A heap a title replaces was not measured and keeps the FreeBSD jemalloc behaviour: `ENOMEM` on failure, `EINVAL` for an alignment that is zero or not a power of two, and `realloc(ptr, 0)` frees the block. With the console's heap, failures set `EINVAL`, and `errno` is left alone for a size above `SIZE_MAX - 0x1008f` (`SIZE_MAX - (A + 0x100af)` for an alignment over 32 rounded up to A), a `calloc` overflow or an alignment of 2^63 or more. Alignments round up to a power of two, and `realloc(ptr, 0)` keeps the block with 0 usable bytes. -- [reallocalign](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) - `reallocalign(ptr, 0, alignment)` for a block without that alignment throws, where the console's heap in a payload process on PS5 firmware 13.60 returned a different block with that alignment and 0 usable bytes, `errno` untouched (a 64-aligned block given 256); whether it frees the old block is not known. There, as here, `reallocalign(ptr, 0, 16)` on a 256-aligned block returned it unchanged, `reallocalign(NULL, 0, 256)` a 256-aligned block with 0 usable bytes, and alignment 3 a moved block that kept its bytes +- [reallocalign](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) - `reallocalign(ptr, 0, alignment)` for a block without that alignment leaves the old block allocated: whether the console's heap frees it is not known. In a payload process on PS5 firmware 13.60 that heap returned a different block there (a 64-aligned block given 256) - [reallocf](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libSceLibcInternal, implemented in libc) - follows FreeBSD 11 `lib/libc/stdlib/reallocf.c` on top of this libc's `realloc`, as a payload process on a PS5 (firmware 13.60) does with the default heap (null and 0, size 0, 2^40 with `errno` 22 and the block freed, `SIZE_MAX` with `errno` untouched): size 0 does what `realloc` does here, keeping the block with 0 usable bytes with the default heap and freeing it and returning null with an application allocator, so the block is not freed twice; when `realloc` fails, the block is freed through the same allocator and null is returned with the `errno` that `realloc` left. Other failures (a commit failure that is not out of memory, an unknown pointer) throw, as they do for `realloc` - [recallocarray](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) - adapted from OpenBSD `lib/libc/stdlib/recallocarray.c` at commit `2cb7cdfc21` (ISC licensed, Otto Moerbeek): console behavior is unmeasured. Delegates to `calloc` on null pointer, checks multiplication overflow on new and old counts setting `ENOMEM` (12) and `EINVAL` (22), zeroes small shrunken tails in place (under half the old size and modeled 16 KiB page size), taking old count on trust as in reference (a caller passing `oldnmemb` larger than the block zeroes past it), and otherwise allocates new storage, copies elements, zeroes new memory or clears the old allocation, and frees old pointer - [sceLibcMspaceReallocalign](../../../core/libs/prx/libc/src/Mspace.cpp) (libSceLibcInternal, implemented in libc) takes `(void* heap, void* block, size_t alignment, size_t size)`, the order of the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x13880, whose internal routine at 0x12390 treats the third argument as the alignment and, for a null block, passes it to the routine `sceLibcMspaceMemalign` uses). Measured on a PS5 (firmware 13.60) from a homebrew payload: a null block with alignment 4096 and size 100 returns a 4096-aligned block; an alignment of 0 or 3 succeeds and 100 gives a 128-aligned block, as the routine raises it to at least 32 and to the next power of two; an alignment of 2^63 or more returns null, keeps the block and leaves `errno` alone; and a size of 0 returns a block (the same one when it is already suitable) instead of freeing it. A block that is aligned and large enough is kept where it is, without giving back the bytes past the new size as the console's in-place resize does @@ -108,8 +115,12 @@ - [fwide](../../../core/libs/prx/libc/src/StandardStreams.cpp) (libc) - the `printf` and `wprintf` families, `perror` and `putw` orient a stream as C11 7.21.2p4 requires, where FreeBSD 11.4's `vfprintf.c`, `vfwprintf.c`, `perror.c` and `putw.c` have no `ORIENT`; nothing is measured on a console. Otherwise it follows [FreeBSD 11.4 fwide.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdio/fwide.c#L38-L51) and `ORIENT`: the first byte or wide read, write, unget or scan orients a stream, also when `fgets`, `fgetws` or `ungetwc` then fail on their arguments; a zero-length `fread` or `fwrite` does not, and a successful `freopen` clears it. Mixed byte and wide calls still run. The export is in the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibcInternal.c#L17666) +- [HostOpenName, fopen, opendir, rename](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) - on Windows, paths with DOS device names (CON, PRN, AUX, NUL, COM1-9, COM¹-³, LPT1-9, LPT¹-³, CONIN$, CONOUT$) or trailing dots/spaces are prefixed with the `\\?\` verbatim prefix to prevent Win32 normalization from rewriting or stripping them. Host NTFS/FAT filesystems still reject POSIX-valid characters disallowed by Windows (`\`, `/`, `:`, `*`, `?`, `"`, `<`, `>`, `|`), and relative paths are normalized against the process current directory before verbatim prefixing. + ### Functional +- [Call tracing](../../../core/libs/prx/libc/src/specifics/x86_64/CallTrace.cpp) (libc) - an unlabeled import whose function has no loaded host image, such as a generated code address, retains an empty library label; `all` still records it. Otherwise missing labels use the defining host module filename; supplied labels are retained. + - [Math.cpp](../../../core/libs/prx/libc/src/Math.cpp) (libc) and [SignalList.cpp](../../../core/libs/prx/libSceLibcInternal/SignalList.cpp) (libSceLibcInternal) start with comments that keep the upstream FreeBSD, Sun Microsystems and University of California licence notices of the code they are derived from. The licences require the notices to stay with the code, so they are comments although they are not technical debt, and the conventions check cannot tell them from other comments. Closes when: the conventions check accepts licence notices without a debt entry - [_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. - [std::thread::hardware_concurrency](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `CHrhwd8QSBs`) - the starting mask of a title's process, and so the value a title sees, are not measured, and neither is whether the console's count follows a process-wide `cpuset_setaffinity`. This returns 13, the number of CPUs in libkernel's default thread mask (`0x1FFF`), and a process-wide `cpuset_setaffinity` does not change it. In payload processes whose threads started with mask `0xea00` it returned 5 on PS5 firmware 13.60 and 12.70, while `hw.ncpu` read 16 and `sceKernelGetAvailableCpumask` `0x1fff` (both on 13.60). On 12.70 it still returned 5 when called first after `scePthreadSetaffinity` had narrowed the calling thread to one CPU, after the mask was restored, and in a thread created with a two-CPU mask. PPSA34015 imports it. @@ -147,12 +158,13 @@ - [ftrylockfile](../../../core/libs/prx/libc/src/RuntimeSupport.cpp) (libc) - tries the host CRT stream lock that `flockfile`, `funlockfile` and each locking host stdio call on the stream take, so it returns -1 while another thread holds the stream through `flockfile` or is inside such a call, and 0 when it acquires the lock or already owns it (recursively, released by as many `funlockfile` calls), as FreeBSD 11 [`_ftrylockfile`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdio/_flock_stub.c#L89-L104) does. A libc export holds the lock only for each host call it makes, not across several host calls or while it updates the guest `FILE` prefix. Linux uses glibc `ftrylockfile`. UCRT has no try variant of `_lock_file`, so Windows calls `TryEnterCriticalSection` on the `CRITICAL_SECTION` that `_lock_file` enters, found through the private UCRT stream layout (`__crt_stdio_stream_data` in `ucrt/inc/corecrt_internal_stdio.h`, unchanged from Windows SDK 10.0.19041 to 10.0.26100); another CRT or a change of that layout breaks it. A null or closed stream throws where FreeBSD dereferences it - [sinf, cosf, tanf and sincosf](../../../core/libs/prx/libc/src/Math.cpp) (libc) are FreeBSD msun's [`s_sinf.c`, `s_cosf.c` and `s_tanf.c`](https://github.com/freebsd/freebsd-src/tree/release/11.4.0/lib/msun/src) over its double-precision kernels and `e_rem_pio2f.c`, with `k_rem_pio2.c` (for `|x| >= 2^28 * pi/2`) ported only for one input word and `prec` 0; `sincosf` returns the two. As in the `libc.prx` that titles ship, only an infinite argument sets `errno` (`EDOM`); NaN leaves it unchanged. Floating-point exceptions are not compared. All four give the same bits and `errno` as the copies bundled with PPSA07923, PPSA05916 and PPSA13934 for all 2^32 arguments, run on a Windows host. - [fgetln](../../../core/libs/prx/libc/src/StandardStreams.cpp) (libSceLibcInternal, implemented in libc) throws: in the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x5FE80) the export is a `hlt` instruction, which faults in user mode and ends the process +- [Versioned std::__sce_v2::filesystem calls](../../../core/libs/prx/libc/src/VersionedFilesystemCalls.cpp) (libc) - console execution, firmware and SDK coverage are unverified. `_Lstat`, `_File_size`, `_Last_write_time`, `_Set_last_write_time`, `_Make_dir`, `_Remove_dir`, `_Unlink`, `_Rename`, `_Resize`, `_Chmod`, `_Copy_file`, `_Statvfs` and `_Current_set2` follow the `libc.prx` bundled with PPSA03671 v01.001.005. libkernel registers the kernel calls they make, as libc cannot link it, and they are made in the module's order: kernel errors in the errno range give their errno, others give 1, and `_File_size` stores the kernel error code itself. As in the module, `_Statvfs` returns `ENOSYS` with unknown space and `_Current_set2` accepts only "/". `_Copy_file` copies 64 KiB at a time through a host buffer instead of the module's heap. - [Wide-character conversions](../../../core/libs/prx/libc/src/Multibyte.cpp) (libc) - The wide `wcsto*` functions parse the ASCII prefix of the guest string, as the C locale accepts only ASCII white space and digits. The multibyte conversions follow the C locale of the console's `libSceLibcInternal.sprx`: UTF-8 of at most three bytes, so `_Getmbcurmax` returns 3, overlong forms and encoded surrogates decode, and four-byte forms are invalid. `__mb_cur_max` stays 1 like the console's variable; `___mb_cur_max` is not in that module and also stays 1, unverified. Other locales are not supported. With a null destination, `wcstombs` returns the byte count; the console's count is wrong when the capacity argument equals the running count, because it reads past the terminator - [realpath](../../../core/libs/prx/libc/src/RealPath.cpp) (libc) follows FreeBSD 11's [`realpath.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libc/stdlib/realpath.c) with two gaps. A host symbolic link with a relative target under the guest root is expanded as `realpath1` does (a dangling link fails with `ENOENT`, more than 32 links with `ELOOP`), but a link with an absolute target is not expanded, nor is anything below it, because the target is a host path with no guest path to map to. Only Windows symbolic links are expanded there, not junctions. Expansion works on the guest path, so a host link whose text climbs above a path alias root is resolved against the guest tree, not the host one. Otherwise `..` drops the previous component lexically, as `Resolve` does for every guest path. - [Paths below a Windows symbolic link](../../../core/libs/prx/libc/include/FilesystemError.hpp) (libc) chooses `ENOENT` or `ENOTDIR` for a path below a link by reading the link text on Windows, and three cases still differ from FreeBSD 11.4's `namei`: below a chain of 33 or more links the answer is `ENOTDIR`, not `ELOOP`; a link made with the directory flag that points at a regular file, or at a link that runs through one, answers `ENOENT`, not `ENOTDIR`, because Windows fails its open with `ERROR_DIRECTORY`; and below a link loop `lstat`, `chmod`, `fchmodat`, `linkat`, `readlink`, `access`, `rename` and `truncate` answer `ENOTDIR`, not `ELOOP`. - [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). `strtold` and `wcstold` return the default quiet NaN (exponent `0x7fff`, significand `0xc000000000000000`) whatever 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`, 50 of them also through `strtold` and `wcstod`, and 18 through `wcstold`, and, through `strtod`, for `nan(1 2)` and for `nan( ` followed by NUL and `)`; titles' own `libc.prx` copies give the same `strtod` results. 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 - [strtof and wcstof NaN payloads](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - the console's `libSceLibcInternal.sprx` (firmware 12.70) returns the default NaN for every `NAN(...)` sequence, and modules that import it reach this function through the `libc.prx` fallback, so they get the payload the titles' copy keeps. That copy, in eleven titles (64 inputs), reads the sequence as `strtod` does but into one 32-bit word: a blank after the first group of digits ends the payload, the first 8 digits are kept and cut to 24 bits under the quiet bit, and a zero payload becomes 1. -- [strtod](../../../core/libs/prx/libc/src/Strings.cpp) (libc) sets `ERANGE` for tested decimal inputs with a nonzero subnormal result, including the exact decimal expansion of 2^-1074, matching nine distinct title-bundled `libc.prx` builds measured on Windows. Two other `libc.prx` builds and the console's `libSceLibcInternal.sprx` leave `errno` alone for decimal subnormals. `wcstod` still parses through the host `std::strtod` path without this explicit rule; title and console `wcstod` behavior for decimal subnormals is unverified. Hexadecimal subnormal `strtod` and `wcstod` match one title's `libc.prx` only (PPSA08329, SHA-256 a485b34ef160, run on Linux, not on a console). Results of at least 2^-1074 come from the host `strtod`, and glibc 2.35 rounds some a unit low: `0x3ffffffffffff5p-1077` gives `0x0007fffffffffffe`, that `libc.prx` `0x0007ffffffffffff`. +- [strtod](../../../core/libs/prx/libc/src/Strings.cpp) (libc) sets `ERANGE` for tested decimal inputs with a nonzero subnormal result, including the exact decimal expansion of 2^-1074, matching nine distinct title-bundled `libc.prx` builds measured on Windows. Two other `libc.prx` builds and the console's `libSceLibcInternal.sprx` leave `errno` alone for decimal subnormals. `wcstod` parses through `strtod` and follows the same rule, as the `wcstod` of the `libc.prx` bundled with eleven titles does (PPSA04158 to PPSA35169, measured on Windows); the console's `libSceLibcInternal` `wcstod` is unmeasured. Hexadecimal subnormal `strtod` and `wcstod` match one title's `libc.prx` only (PPSA08329, SHA-256 a485b34ef160, run on Linux, not on a console). Results of at least 2^-1074 come from the host `strtod`, and glibc 2.35 rounds some a unit low: `0x3ffffffffffff5p-1077` gives `0x0007fffffffffffe`, that `libc.prx` `0x0007ffffffffffff`. - [remquol](../../../core/libs/prx/libc/src/MathExact.cpp) (libc) leaves `*quo` unchanged when the result is NaN; the `libc.prx` bundled with PPSA08427 writes a value that depends on earlier x87 state (`remquol(1, 0)` stored 0 on a first call and 4 on later ones) - [_Stod, _Stodx, _Stofx, _Stoldx, _Stof and _Stold](../../../core/libs/prx/libc/src/DinkumwareInternals.cpp) (libSceLibcInternal, implemented in libc) throw for hexadecimal text with a non-zero power-of-ten scale. The console's result there does not follow a simple rule: `0x3` with a scale of -1 gives 0.29999999999999999. A nonzero subnormal result sets neither `errno` nor the error argument, as measured in firmware 13.60's `libSceLibcInternal.sprx` run in a Linux host process, not on a console; there about a fifth of decimal inputs with a subnormal result come out one unit apart in the last place (`_Stodx("2.2250738585072014e-308", 0)` is the largest subnormal), which is not reproduced. Hexadecimal underflow and subnormal results with a zero scale are unverified for `libSceLibcInternal.sprx`; they follow the `strtod` rule of PPSA08329's `libc.prx`, so `_Stodx("0x1.00000000000001p-1075", 0)` gives 0 with an error of 1, not 2^-1074 - [Scaled floating NaN subjects](../../../core/libs/prx/libc/src/DinkumwareInternals.cpp) (libc) - non-C locales, other SDK variants and console execution are unverified; references are the `libc.prx` bundled with PPSA03766 and PPSA07923, run in Linux host processes. @@ -173,12 +185,13 @@ - [log1p](../../../core/libs/prx/libc/src/Math.cpp) (libc) does not compare floating-point exceptions, and its double inputs are checked by sampling, not exhaustively. It is FreeBSD msun's [`s_log1p.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/s_log1p.c). As the titles' `libc.prx` does, it sets `EDOM` at msun's NaN return (arguments below -1, `-inf` and negative NaNs) and leaves `errno` alone for `log1p(-1)`, where it returns `-inf`. It gives the same bits, NaN payloads included, and `errno` as the `log1p` bundled with PPSA07923 and PPSA05916 on 320 million random and 68,216 structured arguments on a Windows host. The FMA-contracted build bundled with PPSA13934 differs on 73,682 of the random ones. - [nextafter and nextafterf](../../../core/libs/prx/libc/src/Math.cpp) (libc) have no physical-console or unmasked-exception validation. A Linux CPU probe compares result bits, `errno` and masked floating-point flags with the unchanged math code in FAR: Changing Tides PPSA03766 bundled `libc.prx` (`nextafter` 0x51b30, `nextafterf` 0x51c80, `_Feraise` 0x11da0): 16,864 calls across both precisions, four rounding modes and all DAZ/FTZ combinations, not exhaustive inputs. Earlier Windows sampling of eleven bundled libraries compares only bits and `errno`; exception behavior outside the FAR module remains unverified. - [tanh](../../../core/libs/prx/libc/src/Math.cpp) (libc) does not compare floating-point exceptions, and its double inputs are checked by sampling, not exhaustively. It is FreeBSD msun's [`s_tanh.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/s_tanh.c) over this file's msun `Expm1`, with `tiny` read at run time; it sets no `errno`, as the titles' `libc.prx` does not. It gives the same bits as the `tanh` bundled with PPSA07923 and PPSA05916 on 16 million random and 68,211 structured arguments on a Windows host; the FMA-contracted build bundled with PPSA13934 differs on 1,054 of them. +- [exp](../../../core/libs/prx/libc/src/Math.cpp) (libc) does not compare floating-point exceptions, its double inputs are checked by sampling, not exhaustively, and tiny arguments return `1 + x`, which may not raise `FE_INEXACT` when the addition is exact. It is FreeBSD msun's [`e_exp.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/e_exp.c) with `huge` and `twom1000` read at run time, except that `exp(1)` returns the correctly rounded `e` (`0x4005bf0a8b145769`, where msun gives `0x4005bf0a8b14576a`), as the titles' copy does. It sets `ERANGE` only in msun's overflow and underflow branches, as the titles' `libc.prx` does. It gives the same bits and `errno` as the `exp` bundled with PPSA07923 and PPSA05916 on 320 million random arguments on a Windows host. The FMA-contracted build bundled with PPSA13934 differs on 44,911 of them. - [log10](../../../core/libs/prx/libc/src/Math.cpp) (libc) does not compare floating-point exceptions, and its double inputs are checked by sampling, not exhaustively. It is the fdlibm [`e_log10.c`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/e_log10.c) over its [`k_log.h`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/k_log.h), which FreeBSD 11.4 later replaced. As the titles' `libc.prx` does, it sets `EDOM` for every nonzero argument with the sign bit set (negatives, `-inf` and negative NaNs) and leaves `errno` alone for ±0, where it returns `-inf`, unlike `log`. It gives the same bits, NaN payloads included, and `errno` as the `log10` bundled with PPSA07923 and PPSA05916 on 320 million random and 31,165 structured arguments on a Windows host. The FMA-contracted build bundled with PPSA13934 differs on 229,944 of the random ones. - [log2](../../../core/libs/prx/libc/src/Math.cpp) (libc) does not compare floating-point exceptions, and its double inputs are checked by sampling, not exhaustively. It is the fdlibm [`e_log2.c`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/e_log2.c) over the same `k_log.h` kernel as `log10` (`KernelLog`), which FreeBSD 11.4 later replaced. As the titles' `libc.prx` does, it sets `EDOM` for every nonzero argument with the sign bit set and leaves `errno` alone for ±0. It gives the same bits, NaN payloads included, and `errno` as the `log2` bundled with PPSA07923 and PPSA05916 on 16 million random and 31,165 structured arguments on a Windows host; the FMA-contracted build bundled with PPSA13934 differs on 727 of the random ones. - [sinhf](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows the `sinhf` of the `libc.prx` bundled with PPSA28319 (disassembly): FreeBSD msun's [`e_sinhf.c`](https://github.com/freebsd/freebsd-src/blob/5aa554c7e5b6fd9042986df07f12ac5687e41392/lib/msun/src/e_sinhf.c) from before `__ldexp_expf`, which squares `expf(|x| / 2)` between `logf(FLT_MAX)` and the overflow threshold, calling msun's [`expm1f`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/s_expm1f.c) and [`expf`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/e_expf.c), whose paths for the arguments it passes are inlined here. Past the threshold Sony's error helper sets `errno` to `ERANGE` before returning `x * 1e37`; msun itself sets no `errno`. That code, run on a Windows host, gives the same bits as this implementation for every finite argument up to the threshold, of both signs; it was not run on Linux. That the system `libSceLibcInternal`, which Ruffle (PPSA68091) and RetroArch (PPSA99169) import while bundling a zero-return `libc.prx`, has the same code is assumed - [acoshf](../../../core/libs/prx/libc/src/Math.cpp) (libc) exact system `libSceLibcInternal` behavior and FMA-contracted copies remain unverified. Positive finite inputs use [msun](https://github.com/freebsd/freebsd-src/blob/5aa554c7e5b6fd9042986df07f12ac5687e41392/lib/msun/src/e_acoshf.c) range handling and this file's `logf`; the middle range divides -1 before addition under directed rounding. Bits, errno and SSE/x87 status and controls match the supplied PPSA03766/PPSA07923 copies on 236,592 positive finite calls (4,929 inputs, four rounding modes, DAZ/FTZ combinations and sticky flags); these samples are not exhaustive. Invalid-domain errno and SSE/x87 invalid flags are checked against two supplied bundled copies; those samples are not exhaustive either. - [acoshl, asinhl, atanhl, atan2l, cbrtl, erfl, erfcl, expm1l, hypotl, log1pl, log2l, lgammal, tgammal, j0f, j1f, jnf, y0f, y1f and ynf](../../../core/libs/prx/libc/src/Math.cpp) (libc) return the host C library's results (the float Bessel functions through its double ones), not those of the console's own code, so the last bits can differ (`y0f`, `y1f`, `ynf` follow the console at `+-0`). `tgammaf` delegates to the host's double `tgamma` as FreeBSD 11 does ([`s_tgammaf.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/msun/src/s_tgammaf.c)), then restores the sign of a zero result for negative non-integer inputs from the parity of `floor(-x)`; nine title-bundled `libc.prx` builds return that sign for underflowing inputs, where Windows UCRT can return the opposite sign ([#9707](https://github.com/boykopovar/AnyPS5/issues/9707)). As in the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x56410), `lgammal` does not store the sign in `signgam`; `hypotl` returns infinity for an infinite argument even when the other is a NaN, as C11 F.10.4.3 requires. [hypot3, hypot3f and hypot3l](../../../core/libs/prx/libc/src/Math.cpp) follow that module (0x51D30, 0x520D0, 0x52420): the first NaN of x, y and z is returned unchanged, any infinity gives positive infinity, a zero argument gives the two-argument hypot of the other two, and otherwise the largest magnitude is combined with the hypot of the other two -- [asinl](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows the `asinl` of the `libc.prx` bundled with PPSA28319 (disassembly), FreeBSD msun's [`e_asinl.c`](https://github.com/freebsd/freebsd-src/blob/9f0432041b8fb69832f10c32da50a67f34feeb61/lib/msun/src/e_asinl.c) with the ld80 [`invtrig.c`](https://github.com/freebsd/freebsd-src/blob/9f0432041b8fb69832f10c32da50a67f34feeb61/lib/msun/ld80/invtrig.c) coefficients, which the binary holds as the same 80-bit constants. For |x| > 1, the infinities and NaN, Sony's error helper raises `FE_INVALID` and sets `errno` to `EDOM` before returning `(x - x) / (x - x)`, so a NaN argument also sets `EDOM`; msun itself sets no `errno`. 20 million random arguments in (-1, 1) give the same bits as that code run on a Windows host. The result needs the x87 precision control at 64 bits, as FreeBSD amd64 starts threads (control word `0x37f`), but guest threads do not set it: on Windows a thread started by `_beginthreadex` runs at 53 bits (`0x27f`), where `asinl(0.75)` returns mantissa `0xd91a98ae3406e000` instead of `0xd91a98ae3406e041`. The system `libSceLibcInternal` that RetroArch (PPSA99169) imports is assumed to have the same code +- [asinl](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows the `asinl` of the `libc.prx` bundled with PPSA28319 (disassembly), FreeBSD msun's [`e_asinl.c`](https://github.com/freebsd/freebsd-src/blob/9f0432041b8fb69832f10c32da50a67f34feeb61/lib/msun/src/e_asinl.c) with the ld80 [`invtrig.c`](https://github.com/freebsd/freebsd-src/blob/9f0432041b8fb69832f10c32da50a67f34feeb61/lib/msun/ld80/invtrig.c) coefficients, which the binary holds as the same 80-bit constants. For |x| > 1, the infinities and NaN, Sony's error helper raises `FE_INVALID` and sets `errno` to `EDOM` before returning `(x - x) / (x - x)`, so a NaN argument also sets `EDOM`; msun itself sets no `errno`. 20 million random arguments in (-1, 1) give the same bits as that code run on a Windows host. On Windows the initial thread runs at 53 bits (`0x27f`), where `asinl(0.75)` has mantissa `…e000`, not `…e041`. The system `libSceLibcInternal` that RetroArch (PPSA99169) imports is assumed to have the same code - [acosf](../../../core/libs/prx/libc/src/Math.cpp) (libc) - for |x| > 1, the infinities and NaN, quiet NaN included, it stores `EDOM` and raises `FE_INVALID` and returns the same NaNs as titles' `libc.prx` (PPSA07923, PPSA13934). For |x| <= 1 it follows the `acosf` of the `libc.prx` bundled with PPSA28319 (disassembly): FreeBSD msun's [`e_acosf.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/e_acosf.c), with the same constants, `pio2_lo` read at run time and the `0xfffff000` mask on `sqrtf(z)`, which the binary holds. Titles ship different builds of that `libc.prx`: PPSA13934's is FMA-contracted and differs from this one by 1 ulp on 129,170 inputs (#9668), so no single port matches every title's last bit. - [asinf](../../../core/libs/prx/libc/src/Math.cpp) (libc) - for |x| > 1, the infinities and NaN, it stores `EDOM` before returning `(x - x) / (x - x)`, as the `libc.prx` bundled with eleven titles does, PPSA13934's included. Tiny arguments return `x` without raising `FE_INEXACT`. Otherwise this is FreeBSD msun's [`e_asinf.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/e_asinf.c), which gives the same bits as the `asinf` of the `libc.prx` bundled with PPSA07923 and PPSA05916 for all 2^32 arguments (run on a Windows host). The FMA-contracted builds bundled with PPSA13934 and PPSA09614 differ from it by 1 ulp on 391,714 arguments, so no single port matches every title's last bit. - [atanhf](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows the `atanhf` of the `libc.prx` bundled with PPSA28319 (disassembly): FreeBSD msun's [`e_atanhf.c`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/e_atanhf.c) calling msun's [`log1pf`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/s_log1pf.c), whose paths for the positive arguments it passes are inlined here; the exported `log1pf` is still the host's. For |x| > 1, the infinities and NaN, Sony's error helper raises `FE_INVALID` and sets `errno` to `EDOM` before returning `(x - x) / (x - x)`, and for |x| = 1 it raises `FE_DIVBYZERO` and sets `errno` to `ERANGE` before returning `x / 0`; msun itself sets no `errno`. That code, run on a Windows host, gives the same bits, `errno` and floating-point exceptions as this implementation for all 2^32 arguments; it was not run on Linux. That the system `libSceLibcInternal`, which Dusklight (PPSA99305) imports while bundling a zero-return `libc.prx`, has the same code is assumed @@ -195,6 +208,7 @@ - a write fault on a page whose two predecessors were written since the last collection unprotects the rest of its 64 KiB block (at most `APS5_WRITE_WATCH_FAULT_AHEAD` pages, 32 by default) where the pages keep the same protection, and records them all as written, so a collection after a sequential write can report pages nobody wrote. They stay inside a 64 KiB block one of whose pages was written, the unit libSceAgcDriver's write-back decides on. - 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` +- [memset_explicit](../../../core/libs/prx/libc/src/Strings.cpp) (libc, NID `+Z4g+Mo8D2c`) has not been measured on a console or checked against other SDK versions. Return values, low-byte conversion, exact write ranges, zero counts and unchanged errno follow CPU-only execution of PPSA03671 v01.001.005's bundled libc.prx on Windows; volatile byte stores retain unused writes. - [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) - in a payload process on a PS5 (firmware 13.60) a directory opens in a read mode (`r`, `rb`, `rw`) and `fread` returns its raw directory blocks (`/data`, `/system`) or fails with `EIO` and sets the error indicator (`/`); `freopen` onto a directory in a read mode succeeds too. Here reading a directory stream fails with `EISDIR` and sets the error indicator on every host, since the host's directory blocks are not the console's, and on Windows `freopen` of `stdin`, `stdout` or `stderr` onto a directory in a read mode throws, as the host descriptor it keeps cannot hold the directory descriptor. The other measured answers match: `EISDIR` for a directory in a write or update mode, `EEXIST` for `wx` on an existing file, `EINVAL` for an unknown or empty mode, and `ENOENT` for a missing file - [`freopen` of a closed stream](../../../core/libs/prx/libc/include/FileStream.hpp) (libc) opens the file on a new host descriptor, so `fileno` after `fclose(stdout)` and `freopen(path, mode, stdout)` is whatever the host hands out, where a PS5 takes the lowest free descriptor. The stream object is kept and reopened as there, and a missing path (`ENOENT`), an overlong one (`ENAMETOOLONG`) and a null filename (`EBADF`) answer as measured on a console. A write to the closed stream answers `EBADF` here, where the console's `fputs` and `fputc` return -1 leaving `errno` unchanged and its `fprintf` gives `EINVAL`. @@ -202,13 +216,16 @@ - [Writes on an update stream](../../../core/libs/prx/libc/include/FileStream.hpp) (libc) directly after a read differ from the console in where they land. Windows follows FreeBSD 11.4's [`__swsetup`](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libc/stdio/wsetup.c#L59-L74): the read buffer and `ungetc` data are dropped and the write goes to the descriptor's offset, but past the UCRT's read-ahead (4096 bytes by default, 2 unbuffered) where the console reads `st_blksize` bytes (1 unbuffered), so the two agree while the rest of the file fits in both. Linux glibc writes at the stream position: after `fgetc` on an `r+` stream over `0123456789`, `fputs("XY")` gives `0XY3456789` where FreeBSD gives `0123456789XY`; matching FreeBSD there needs the stream to track whether it was positioned since its last read. Reads directly after writes follow FreeBSD's `__srefill` - [`fopen` and `freopen` of /dev/random and /dev/urandom](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) in a read mode return 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. ProsperoTV (PPSA99003) reads its phone pairing code this way - [setvbuf and setbuf](../../../core/libs/prx/libc/src/StandardStreams.cpp) (libc) on Windows never hand the guest's buffer to the host C library: UCRT allocates the stream's buffer itself, so later refills, which go through `ReadFile`, cannot fail on write-watched pages of shared guest memory. FreeBSD's `__srefill` reads into the guest's buffer, which on Windows keeps its old contents; a title that reads stream data out of its own `setvbuf` buffer sees a difference. On Linux and macOS the guest's buffer is still handed to the host library; macOS's write watch protects watched pages, so a refill into one is expected to fail there (not tested) +- [Reads on a write-only stream](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) - on Linux `fscanf`, `vfscanf`, `scanf` and `vscanf` on a stream opened only for writing return `EOF` with `EBADF` but leave the error indicator clear, where FreeBSD 11.4's [`__srefill`](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libc/stdio/refill.c#L84-L88) sets `__SERR`: glibc's [`__vfscanf_internal`](https://github.com/bminor/glibc/blob/glibc-2.39/stdio-common/vfscanf-internal.c#L164-L179) rejects `_IO_NO_READS` before reading without setting `_IO_ERR_SEEN`, and the Linux `SwitchToReadingUnlocked` lets such a stream through to it. `fread`, `fgetc`, `getc_unlocked` and `fgets` reach glibc's `_IO_new_file_underflow`, which sets both, and Windows fails every read in `SwitchToReadingUnlocked`. macOS is expected to follow BSD stdio (not run). The `file_streams` test checks the error indicator after `fscanf` on Windows only - [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`. +- [fseeko and fsetpos](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) on Windows move a stream whose seek fails: the UCRT drops the read buffer before its `lseek` to a negative target fails with `EINVAL`, so the stream continues at the descriptor's read-ahead offset. After 4 of 8 buffered bytes, a rejected `fsetpos` to -1 leaves `fgetpos` at 8 and the next read at end of file. FreeBSD 11.4's [`_fseeko`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libc/stdio/fseek.c) keeps the buffer and position when `_sseek` fails. Linux is unverified - 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) 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. - A guest frame built with `-fno-asynchronous-unwind-tables`, and libsystem_pthread `thread_start`, still fail to unwind on macOS. Compact unwind records them as encoding 0, and the [unwinder](../../../core/libs/prx/libc/src/exception/Unwind.cpp) rejects that encoding instead of inventing a CFA. A DWARF-mode entry whose `__eh_frame` FDE is missing or rejected fails the same way, with no fallback frame. libc itself is built with `-femit-dwarf-unwind=always` and `-Wl,-no_compact_unwind` (#8311), so its own frames stay in DWARF. The unwinder walks `__TEXT,__unwind_info` when `__eh_frame` misses the frame. A personality index in that section is a signed 32-bit delta from the Mach-O image base. - 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 and macOS, `AF_LINK` maps non-Ethernet/loopback/PPP types to `IFT_OTHER` (1); Linux's `if_data` leaves `ifi_omcasts`, `ifi_noproto`, `ifi_hwassist`, `ifi_metric`, `ifi_epoch`, `ifi_lastchange`, unknown baud rates and address-entry `ifa_data` as 0/null, and macOS's 32-bit host counters wrap and `ifi_oqdrops`, `ifi_hwassist`, `ifi_epoch` and `ifi_lastchange` are 0; 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). +- [getifaddrs](../../../core/libs/prx/libc/src/Interfaces.cpp) (libc) lists host interfaces and IPv4/IPv6 addresses in FreeBSD's layouts. On Windows, `ifi_collisions` is 0, `ifi_baudrate` is the transmit link speed and the multicast counters include broadcasts; 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, macOS and Windows, `AF_LINK` maps non-Ethernet/loopback/PPP types to `IFT_OTHER` (1); Linux's `if_data` leaves `ifi_omcasts`, `ifi_noproto`, `ifi_hwassist`, `ifi_metric`, `ifi_epoch`, `ifi_lastchange`, unknown baud rates and address-entry `ifa_data` as 0/null, and macOS's 32-bit host counters wrap and `ifi_oqdrops`, `ifi_hwassist`, `ifi_epoch` and `ifi_lastchange` are 0; 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 an empty name without querying the host machine name, as a payload process read on a PS5 (firmware 13.60): 0 with only the terminator for a 256-byte and a 1-byte buffer. Buffer copying, null queries and `ENAMETOOLONG` follow [FreeBSD gethostname](https://github.com/freebsd/freebsd-src/blob/main/lib/libc/gen/gethostname.c); whether a title process or a console with a configured name reads another name, and the integration with `sysctl` and name resolution, are unverified and not implemented. +- [devname and devname_r](../../../core/libs/prx/libc/src/DeviceName.cpp) (libSceLibcInternal, NIDs `fqLrWSWcGHw` and `BIALMFTZ75I`) never query the `kern.devname` sysctl, so a character device always gets the formatted `#C:` name; neither function was measured on a PS5. Otherwise they follow FreeBSD 11.4 [devname.c](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/libc/gen/devname.c) with a 64-byte static buffer: `NODEV` gives `#NODEV`, the number prints as `%#jx` does, a short buffer truncates as `strlcpy` does, and a length above `INT_MAX` + 1 on the formatted path sets `EOVERFLOW` and writes an empty string. The source tests `S_ISBLK(dev)` rather than `S_ISBLK(type)`, so a block-device type gives `#NODEV` unless the device number has the block type bits; this is kept, and whether the console does the same is unknown. - [sinh](../../../core/libs/prx/libc/src/Math.cpp) (libc) is FreeBSD msun's [`e_sinh.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/e_sinh.c) over this file's msun `Expm1` for |x| < 22, where it gives msun's bits (0 of 2,000,000 random arguments in [-20, 20] differ from a locally built msun on a Windows host). From 22 on it calls this file's `exp`, which is still the host's, so those results can differ from msun in the last bit until `exp` follows msun; the last step before the overflow threshold scales that `exp` as msun's `__ldexp_exp` does. On overflow it sets `errno` to `ERANGE`, as the host's `sinh` did and `sinhf` does; msun itself sets none. It was compared with msun built on the host, not with a console: the titles' `libc.prx` and the system `libSceLibcInternal` are unmeasured. - [wctomb_s, wcrtomb_s, mbstowcs_s and mbsrtowcs_s](../../../core/libs/prx/libc/src/BoundsChecking.cpp) (libc) follow the console's `libc.prx`, but convert in the single-byte C locale of `wctomb`, `wcrtomb` and `mbrtowc` (`MB_CUR_MAX` 1): a character above 0xFF is `EILSEQ` in `wctomb_s` and `wcrtomb_s`, and each byte is one wide character in `mbstowcs_s` and `mbsrtowcs_s`. The locale of a title's `libc.prx` was not measured. The `wcrtomb_s` path where a character does not fit `smax` and the `EILSEQ` paths of `mbstowcs_s` and `mbsrtowcs_s` are not reached - [wcsrtombs_s](../../../core/libs/prx/libc/src/Multibyte.cpp) (libc) follows the console's `libc.prx`, but a runtime-constraint violation only returns its error: the default handler there writes the message and a newline to `stderr` before returning, which is not done here. Conversion uses the C locale of `wcrtomb` (`MB_CUR_MAX` 1), so the path where a multibyte character does not fit the remaining space is not reached @@ -220,19 +237,22 @@ - [%p and %lp](../../../core/libs/prx/libc/include/WindowsFormatting.hpp) (libc) follow `libSceLibcInternal.sprx` as measured from a homebrew payload, not a title, on firmware 12.70 ([#7549](https://github.com/boykopovar/AnyPS5/issues/7549)) and 13.60, only through `snprintf` and `swprintf`. No `_s` form was run with `%lp` on a console; the wide `_s` forms print it as the narrow ones here do. Measured: bare lowercase hex with no `0x` even under `#`, `0` for null, `+` and space ignored, the width counting digits only, `-` padding right, `0` zero-padding without a precision, the precision as the minimum digits, nothing for null under `%.0p` (`%5.0p` gives five spaces), `%05.3p` gives ` 01f`, `%.0p` of non-null prints its digits, and `l` accepted. Another length such as `%hp` follows the printf length modifiers entry. Linux routes `%p` and `%lp` here because glibc prints the FreeBSD form, so a format combining `%p` with a conversion this formatter refuses throws there - [collate and collate _Getcat](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) build the facet for the C locale only and throw for any other locale name, the wide entry point following the narrow one it sits next to. The wide facet carries its own vtable, whose `compare`, `transform` and `hash` take the guest's 16-bit characters, so the length of a range is a count of characters: `compare` orders by 16-bit value (the C locale, as Dinkumware's `char_traits::compare` does), `transform` copies the characters into the result and records size and capacity in 16-bit elements (inline capacity 7, heap capacity `length | 7`, `(capacity + 1) * 2` bytes including the terminator), following Dinkumware's [VC12 `basic_string`](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/xstring#L500-L531), and `hash` mixes one whole character per round, so wide text below 0x100 hashes like its narrow spelling and a character above 0xff hashes as one value instead of the two bytes it holds. The console's `libc.prx` exports both `_Getcat` (NIDs BSVJqITGCyI and 2wz4rthdiy8) with identical 0x19d-byte bodies, believed to build the same 0x20-byte facet; the console bodies themselves are unreadable (obfuscated dynsym, and the text of the relinked `libc.prx` is zeroed), so the wide entries follow the narrow algorithm and the C locale instead of the console's wide code, and no console run checks the two. The console's `libc.prx` stores pointers to its collation state tables at offsets 0x10 and 0x18 of the facet; ours leaves them null, as only the facet's own functions read them. `do_transform` throws for input with embedded null characters, which the console drops depending on the output buffer size. The narrow result capacity (`length | 15`) and the over-aligned storage of results of 4080 characters or more follow the console's `libc.prx` code and are not checked on hardware. No module of the PPSA59322, PPSA76677 or PPSA97358 dumps in this tree imports either `_Getcat`, so neither has been exercised by a title. - [codecvt, numpunct, num_put and time_put _Getcat](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) build facets matching a bundled console `libc.prx` (dynsym decoded by NID, vtables disassembled): categories 2, 4, 4 and 5, sizes 0x10, 0x30, 0x10 and 0xb8. Unlike `collate`, they ignore the locale argument and always build the C-locale facet; the console's behaviour for a non-C name is unverified. `time_put`'s `do_put` formats through this library's `strftime` instead of the facet's unpopulated `_Locinfo` table, so the `strftime` gaps apply. `codecvt` reproduces the console virtuals (`noconv` conversions, `length` the shorter range); `numpunct` copies its C-locale table (`""`, `.`, `,`, `false`, `true`). The facets share the no-op `destroy`/`deleteObject`/`retain`/`release` base used for `collate`, so `numpunct`'s static C-locale strings are never freed. +- [`numpunct` _Getcat](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) builds the same 0x30-byte C-locale facet as the narrow entry: `grouping` is a narrow string pointer at 0x10, `falsename` and `truename` are pointers to 16-bit strings at 0x20/0x28, `decimal_point` and `thousands_sep` are 16-bit at 0x18/0x1a, and it returns 4. The console `libc.prx` from PPSA33016 (`_Getcat` at 0xa2670, 0x17b bytes) fills in `""`, `.`, `,`, `false` and `true`; its locale handling for a non-C name is unverified. The `grouping` virtual writes the narrow string with the narrow string rules (inline capacity 15, `length | 15`), while `falsename` and `truename` write the guest 16-bit string with the wide collate rules (inline capacity 7, `length | 7`, two bytes per unit); its id is an 8-byte zero like the narrow one. - [num_put::do_put](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) follows the SDK 7.00 `libc.prx` that PPSA06416 ships, read from its disassembly and run, but formats through this library's `snprintf`. That converts floating point exactly and rounds ties to even, where the module keeps about 18 exact significant digits, prints later ones from an inexact scaling (1e10 at precision 22 is `9999999999.999999999999`) and gives `-3` for -2.5 at precision 0. A precision so negative that the module's buffer of precision + 50 bytes cannot hold the result throws, where the module reads stale bytes. A stream locale without a `ctype` facet leaves digits unwidened, as the console's C-locale facet does. -- [std::clog, std::wclog and the facet ids](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc): `std::clog` and `std::wclog` are 0x400-byte zeroed objects like the rest of the `NSTDSTR` stream set (the console's `libc.prx` sizes `std::clog` at 96 bytes, measured from its dynsym `st_size`), and the `numpunct::id` and `time_put::id` objects are 8-byte zeros like the other facet ids. +- [std::clog, std::wclog and the facet ids](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc): `std::clog` and `std::wclog` are 0x400-byte zeroed objects like the rest of the `NSTDSTR` stream set (the console's `libc.prx` sizes `std::clog` at 96 bytes, measured from its dynsym `st_size`), and the `numpunct::id`, `numpunct::id` and `time_put::id` objects are 8-byte zeros like the other facet ids. - [std::__throw_bad_variant_access](../../../core/libs/prx/libc/src/exception/StandardExceptions.cpp) (libc) throws `std::bad_variant_access` with the `what()` string `bad_variant_access`, the one [LLVM libc++](https://github.com/llvm/llvm-project/blob/main/libcxx/src/variant.cpp) returns, since the export carries libc++'s `__throw_bad_variant_access` name; the console's string is unverified. Onimusha: Way of the Sword (PPSA27836 01.000.001) imports it from `libc`, and its bundled `libc.prx` does not export it. - [std::bad_optional_access and std::_Throw_bad_optional_access](../../../core/libs/prx/libc/src/exception/StandardExceptions.cpp) (libc) provide the exception type, constructor, destructor, and `what()` exports alongside `_Throw_bad_optional_access`, returning the `what()` string `bad_optional_access` matching Dinkumware and libc++ conventions; the console libc.prx string is unverified. PPSA26578 (issue #7624) and PPSA34015 import `_ZSt26_Throw_bad_optional_accessv` (`KwzEip7m6TM`) from `libc`. - [std::_Throw(const std::bad_array_new_length&)](../../../core/libs/prx/libc/src/exception/StandardExceptions.cpp) (libc, NID `1cKlQv7Kvmg`) is a 52-byte function at `0xcb2d0` in the `libc.prx` bundled with PPSA02530: it ignores its argument, allocates the 8-byte exception object with the static `_ZTVSt20bad_array_new_length`, and throws it. PPSA28416 imports it from `libc`. -- [The scanf family](../../../core/libs/prx/libc/src/Formatting.cpp) (libc), the `_s` forms included, decodes the input of the 16-bit wide conversions (`%ls`, `%lc`, `%l[`, `%S`, `%C`) on Linux as UTF-8, mirroring the narrow printf family, where FreeBSD uses the locale's `mbrtowc` (one byte per character in the C locale); the console's locale encoding is unverified. Input that is not UTF-8, a character outside the BMP, a scanset member outside ASCII, positional arguments next to a wide conversion and wide conversions in `fscanf` throw. Each character is stored as one 16-bit unit, widths and `_s` sizes count characters, and a `%lc` field cut short by the end of the input keeps what it read, as FreeBSD's `convert_wchar` does. Windows hands `%ls`, `%lc` and `%l[` to the host, whose `wchar_t` is 16 bits +- [The scanf family](../../../core/libs/prx/libc/src/Formatting.cpp) (libc), the `_s` forms included: where a stream is left after a failed 16-bit wide field (`%ls`, `%lc`, `%l[`, `%S`) was not measured; the byte the field failed on stays in the stream. Positional arguments next to a wide conversion throw. The rest follows the `libc.prx` of PPSA07923, PPSA05916 and PPSA13934 on every platform: each byte goes to `mbrtowc` with the state kept for the field, so a byte from 0x80 up ends the conversion. A held UTF-8 lead byte fails the field with `EILSEQ` when the field takes another byte, and is an input failure when the field ends; any other refused byte is an input failure as the field's first byte and a matching failure after it. Units read before a failure stay stored without a terminator, widths count bytes, scanset members and `X-Y` ranges are bytes (a reversed range matches nothing; a `-` first, last or after a range is a member), and `%C` ends the scan - [The scanf family](../../../core/libs/prx/libc/include/WindowsScanning.hpp) (libc) on Windows returns 0, as C17 and FreeBSD do, when input fails after only suppressed conversions have completed (`sscanf("12", "%*d%d", &value)`). Linux hands the format to glibc, which returns `EOF` there. On Windows the host stops a `%x`, `%X`, `%i` or `%p` after the `x` of a `0x` that no hex digit follows (or that the width cuts off), and a float conversion (`%e`, `%f`, `%g`, `%a`) after the `e` of an exponent with no digits (`1e`), where C17 and FreeBSD stop before the `x` or the `e` +- [Narrow scansets, string lengths and unknown conversions](../../../core/libs/prx/libc/include/WindowsScanning.hpp) (libc): on Linux the plain narrow `scanf` family still hands these to glibc. In the measured Linux build, plain and secure `%lls` and `%Ls` store host 32-bit wide units where the bundled libraries store narrow bytes; plain `%s` at end of input can clear errno. The shared secure scanner stops unknown conversions before testing input EOF. A format byte from 0x80 up outside a scanset matches its input byte here, where the `libc.prx` of PPSA07923 and PPSA05916 fails the match; neither was checked further. On Windows, scansets are rewritten as explicit byte lists that follow those titles: `X-Y` is a range when a `-` follows a member and is not last, a reversed range matches nothing, a `-` first, last or after a range is a member, a `]` that opens the set can start a range, and an unterminated set ends the scan. `%s`, `%c` and `%[` read narrow with any length but `l`, and an unknown conversion such as `%y` ends the scan as a matching failure, also in the `_s` forms - [The scanf family](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) - a `q` length is not measured on a console for `vsscanf`, `fscanf`, `vfscanf`, `scanf`, `vscanf` or the `_s` forms. On Linux and macOS the first four read it as `ll`, as the host libc and FreeBSD 11.4's source do; on Windows they end the scan at it. The `_s` forms take `q` as the conversion and pass `%q` to the host libc. `sscanf` ends the scan at the `q` directive with nothing stored on every host, as it does on a PS5 (firmware 12.70) for `%qd`, `%qS` and `%qC`, and `vsscanf` follows it, as it already did on Windows. - [Guest path resolution](../../../core/libs/prx/libc/src/General.cpp) falls back to ASCII case-insensitive component lookup, following [shadPS4's host filesystem](https://github.com/shadps4-emu/shadPS4/blob/945dbc3cc3eee80ac3e053b438502ed936fa6bb2/src/core/file_sys/backends/host_fs.cpp). PPSA03747 requests `Skyrim.INI`, `Data/Video` and lowercase archive/plugin names that differ from the dump's filenames. Exact host names take precedence; multiple fallback matches throw instead of selecting an arbitrary file. Existing parent names are resolved when creating a new file, and guest mount aliases use the same ASCII comparison. Results are not cached, so renames and newly created files remain visible. Unicode case folding and PS5 behaviour for case-colliding host entries are unverified; host roots are used as supplied. Explicit native Windows drive and UNC paths bypass guest parent-component validation.. A relinked module's `.guest.prx` is matched the same way when the original module is not beside it: Borderlands 4 (PPSA01494) loads `/app0/prx/libRenoirCore.PS5.prx`, which it ships as `librenoircore.ps5.prx`. A relinked module request that misses both the exact and the case-insensitive `.guest.prx` takes the sibling whose stem (the name without its last extension, ASCII case ignored) equals the requested stem and that ends with `.guest.prx`, shortest name first: Unity IL2CPP titles load `Media/Modules/.prx` and `Media/Plugins/.prx`, and the relinker names the converted module after the ELF it was built from, `.elf.guest.prx`. The console's module namespace is assumed to ignore case and the extension; several stem matches pick the shortest name instead of throwing, which is not measured on a console -- [Guest file names on Windows](../../../core/libs/prx/libc/src/General.cpp) (libc) are not escaped or mapped before the host call, so names Windows treats specially differ from FreeBSD, where only `/` and NUL are special. `a*b`, `a?b`, `ac`, `a|b` and `a"b` fail with `EINVAL` (22) where FreeBSD creates them. `trail.` and `trail ` both create the host file `trail`, so three FreeBSD names are one file here. `NUL` and a reopen of it succeed, but no file is created. `con`, `aux.txt` and `COM1` create ordinary files. Colons are under Colons in guest names. Measured on Windows 11 (10.0.26200), MinGW-w64 GCC 15.2.0 r7, with `open` (`O_CREAT|O_EXCL|O_WRONLY`), a one-byte write and a listing of the host directory; not measured on Linux or a console. A fix needs a guest-to-host name mapping. +- [Guest file names on Windows](../../../core/libs/prx/libc/src/General.cpp) (libc) are not escaped or mapped before the host call, so names Windows treats specially differ from FreeBSD, where only `/` and NUL are special. `a*b`, `a?b`, `ac`, `a|b` and `a"b` fail with `EINVAL` (22) where FreeBSD creates them. Colons are under Colons in guest names. Measured on Windows 11 (10.0.26200), MinGW-w64 GCC 15.2.0 r7, with `open` (`O_CREAT|O_EXCL|O_WRONLY`), a one-byte write and a listing of the host directory; not measured on Linux or a console. A fix needs a guest-to-host name mapping. - [Colons in guest names](../../../core/libs/prx/libc/src/General.cpp) (libc) - on Windows (NTFS) a guest name containing `:` is not a plain file name: `name:stream` opens or creates the alternate data stream `stream` of the file `name`. Creating `/savedata0/C:file` makes an empty file `C` whose stream `file` holds the data, and `stat` of `/savedata0/C` then reports a regular file. `mkdir` of a name containing `:`, such as `C:` or `dir:x`, fails with EINVAL. `name:stream` already behaved this way before drive-letter components were kept under their mount - [tzname](../../../core/libs/prx/libc/src/Time.cpp) (libc) points at the host's zone names, read at load and again by `tzset`: the abbreviations a `TZ` string gives (`EST5EDT` is `EST` and `EDT` on both hosts), but on Windows without `TZ` the CRT's full names such as `GMT Standard Time`, which ICU's [uprv_tzname](https://github.com/unicode-org/icu/blob/457157a92aa053e632cc7fcfd0e12f8a943b2d11/icu4c/source/common/putil.cpp#L1244-L1264) (ES-DE in PPSA99169) cannot map to a zone ID. A payload process on a PS5 (firmware 13.60) read both names empty with `TZ` unset, and its `%Z` printed nothing whatever `tm_zone` held; the names a title process sees are not known - [localtime, mktime](../../../core/libs/prx/libc/src/Time.cpp) (libc) on Windows convert through the CRT's `localtime_s` and `mktime`, which accept about 1970 to January 3001 UTC and apply the CRT's daylight-saving rule. A time outside that range is converted a whole number of 400-year Gregorian cycles away, where the calendar repeats, so it gets the rule the CRT applies to that year and not the zone's historical one. On macOS `mktime` converts through the host's `mktime`, which refuses every year before 1900. A local time before 1900 is instead solved for directly: the offset `localtime_r` gives for a candidate time, which follows the zone's history (local mean time before its first transition), is subtracted from the local fields, and the step is repeated until the offset holds. `tm_isdst` is not consulted there, so a time with `tm_isdst` set where the zone had no daylight offset is taken as standard time. FreeBSD's [`time1`](https://github.com/freebsd/freebsd-src/blob/stable/11/contrib/tzcode/stdtime/localtime.c#L2036) would instead shift it by the difference between one of the zone's daylight types and a standard one. `gmtime` converts every time whose year fits an `int` on both hosts. +- [timelocal](../../../core/libs/prx/libc/src/Time.cpp) (libc) sets `tm_isdst = -1` before converting through `mktime`, following FreeBSD 11 [timelocal](https://github.com/freebsd/freebsd-src/blob/stable/11/contrib/tzcode/stdtime/localtime.c#L2126-L2132). - [strftime and asctime](../../../core/libs/prx/libc/src/Time.cpp) (libc) follow a payload process on a PS5 (firmware 13.60) in the C locale. Out-of-range fields in `%G`, `%g`, `%V`, `%U` and `%W` follow the SDK 7.00 `libc.prx` that PPSA06416 ships, read from its disassembly and run, and are measured on a console only for the cases in the test; it counts a year divisible by 100 as leap only when `tm_year / 400` is 100 or -300. `%k`, `%l`, `%P`, `%s`, `%+`, `%v`, flags and widths are copied as written, `E` or `O` before an unknown conversion is dropped, and a format ending in either throws, where the console crashed. Negative fields print 0 and fields wrap at 100 (`%j` at 1000, `%u` and `%w` at 10); `%Y` and `%G` are the year mod 10000, `%C` is `tm_year / 100 + 19`. A negative weekday or month gives `Sun` or `Jan`. `%z` and `%Z` ignore `tm_gmtoff` and `tm_zone` and use the guest zone and `tzname` - [wcsftime](../../../core/libs/prx/libc/src/Time.cpp) (libc) has the `strftime` gaps above. A null buffer returns 0, where the console writes through it, and a size at or above `SIZE_MAX / _Getmbcurmax()` returns 0 with `EINVAL`, where the console formats as usual. A null format or `tm` throws. Conversion-overflow continuation follows [reported native-PC measurements of firmware 13.42](https://github.com/boykopovar/AnyPS5/pull/10895#issuecomment-6494817352), not a physical-console run. `%Z`/`%z`, failures within a conversion and differences in title-bundled `libc.prx` remain unverified by that measurement - [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 @@ -269,11 +289,12 @@ - [printf length modifiers](../../../core/libs/prx/libc/include/WindowsFormatting.hpp) (libc) - the `_s` forms (`snprintf_s`, `vswprintf_s` and the others) still throw for a length modifier that does not apply to its conversion, unmeasured on the console. The plain forms ignore it as the `libc.prx` of PPSA07923 and PPSA13934 does: `L` on an integer conversion or `%n`, any length but `l` on `%c` and `%s`, any but `L` on a floating conversion, and any on `%p`. - [child process registry](../../../core/libs/prx/libc/src/Process.cpp) (libc) - `RegisterChildProcess_nid_no_patch` and `WaitChildProcess_nid_no_patch` track the native handles of children created by `posix_spawn` ([libSceLibcInternal](../../../core/libs/prx/libSceLibcInternal/Spawn.cpp)) so libkernel can reap them on Windows, where the host has no `waitpid`. The registry lives in libc because libSceLibcInternal imports libc but not libkernel. `RegisterChildProcess_nid_no_patch` stores the process ID and handle; only reaped handles are closed, and a process that exits without waiting leaks them. Registration without a live child is not validated, and a duplicate process ID is accepted. The decoder maps recognized structured-exception codes to guest signals, keeping the guest `exit` byte layout for anything else. These exports are internal glue for libkernel, not console ABI: their behavior is unverified against any PS5 library and a title importing their NIDs directly is not expected. - [fprintf_s, vfprintf_s, vprintf_s, fwprintf_s, wprintf_s, vfwprintf_s and vwprintf_s](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) follow C11 K.3.5.3.1, K.3.5.3.10 and K.3.9.1.1 with the constraints of `snprintf_s` and `swprintf_s`: a null stream, format or argument list, any `%n`, and a null `%s`, `%ls` or `%S` argument return -1 and write nothing, because the output is formatted completely before it is written. A violation only returns: the constraint handler is not invoked, `errno` is untouched, and the value is the one the narrow `snprintf_s` returns on this branch, not a result measured for a stream function on a PS5. A closed stream sets `EBADF` (9) as `fprintf` does. `vprintf_s` and `vwprintf_s` write to the guest standard output stream, and the wide forms count characters while the narrow ones count bytes, as their unchecked siblings do -- [scanf_s, fscanf_s, vscanf_s and vfscanf_s](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) run the scanner of `sscanf_s` over the stream, taking the size of `%s`, `%c` and `%[` as `unsigned int` after the pointer, so everything listed for `sscanf_s` applies, including the count of items converted before an undersized destination, whose first element is set to zero. Each directive is read from the stream with the host `fscanf`, with one character of look-ahead pushed back, so the stream stops after the last character a conversion used. A null stream, null format or null destination returns `EOF`, a closed stream also sets `EBADF`, and the constraint handler is not invoked. The 16-bit wide conversions (`%ls`, `%lc`, `%l[`, `%S`, `%C`) throw on Linux, as in `fscanf`. None of the stream forms were measured on a PS5, and the size type follows `sscanf_s` rather than `rsize_t` +- [scanf_s, fscanf_s, vscanf_s and vfscanf_s](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) run the scanner of `sscanf_s` over the stream, taking the size of `%s`, `%c` and `%[` as `unsigned int` after the pointer, so everything listed for `sscanf_s` applies, including the count of items converted before an undersized destination, whose first element is set to zero. Each directive is read from the stream with the host `fscanf`, with one character of look-ahead pushed back, so the stream stops after the last character a conversion used. A null stream, null format or null destination returns `EOF`, a closed stream also sets `EBADF`, and the constraint handler is not invoked. The 16-bit wide conversions (`%ls`, `%lc`, `%l[`, `%S`) are read by the same scanner as in `fscanf`. None of the stream forms were measured on a PS5, and the size type follows `sscanf_s` rather than `rsize_t` - [freopen_s](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) follows C11 K.3.5.2.2 through `freopen`: a null result, mode or stream returns `EINVAL` (22) with `*result` set to null when it exists, and calls the constraint handler with `EINVAL`, a null pointer and the titles' `fopen_s` text: "fopen_s: bad FILE ** argument" for a null result, "fopen_s: bad argument" for a null mode or stream. A null name is passed to `freopen`, which reports mode-only reopening as unsupported (`ENOTSUP`, 45) with `*result` null and no handler call. A successful reopen stores the stream in `*result` and returns 0, and a failed one stores null and returns the errno `freopen` left, closing the stream as `freopen` does. The console's return values are unverified - [_Fltrounds](../../../core/libs/prx/libc/src/Math.cpp) (libc) console execution and unmasked pending exception behavior remain unverified. The x87 rounding query matches PPSA03766 and PPSA07923 bundled libc.prx run on a Linux host. The shared SonyFltrounds helper used by libSceAmpr still follows the host CRT and is not covered by these measurements. - [cbrt](../../../core/libs/prx/libc/src/Math.cpp) (libc) does not compare floating-point exceptions, and its double inputs are checked by sampling, not exhaustively. It is FreeBSD msun's [`s_cbrt.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/s_cbrt.c), which sets no `errno`, as the titles' `libc.prx` does not. It gives the same bits as the `cbrt` bundled with PPSA07923 and PPSA05916 on 320 million random and 10,305 structured arguments on a Windows host; the FMA-contracted build bundled with PPSA13934 differs on 32 of the random ones. - [ANYPS5_LOG_FILE](../../../core/libs/prx/libc/src/LogFilePlatform.cpp) (libc) on Windows diverts the standard handles with `CreatePipe`, `_dup2` and `SetStdHandle`, reads the pipes by polling `PeekNamedPipe` every 2 ms and writes the crash report through `log_file::CrashWrite`. This was written from the documented behaviour of those calls and compiled for MinGW, but never run on Windows: console, redirected-file, pipe and absent standard handles, the 2 ms polling latency and the handles `LoadExec` hands to the replacement are unmeasured. Closes when: `log_file_tests` is ported to a Windows host and passes. +- [C23 first-bit positions, single-bit test, bit width, floor and ceil](../../../core/libs/prx/libc/src/IntegerBuiltins.cpp) have not been measured on a console or checked against other SDK revisions. The 40 exports follow PPSA03671 v01.001.005's bundled libc.prx, read and run directly on a Windows host; ul is 64-bit. An unrepresentable bit-ceil result is zero in that module. - [ANYPS5_LOG_FILE](../../../core/libs/prx/libc/src/LogFile.cpp) (libc) copies standard output and standard error through two pipes read by two threads, so the file keeps the bytes of each stream in the order its pipe delivered them but can place lines of one stream a few lines away from lines of the other written at nearly the same time, and what a process wrote in its last milliseconds before a kill the runtime cannot intercept (`SIGKILL`, `TerminateProcess`) is lost. Closes when: a single ordered channel for both streams replaces the two pipes. - [ANYPS5_LOG_FILE](../../../core/libs/prx/libc/src/LogFile.cpp) (libc) puts no limit on the size of the file and does not rotate it, and a child process the title starts with the standard streams inherited keeps writing into the copy pipes, so after the title exits its writes fail (`SIGPIPE` on Linux and macOS). The pipes also hold up to 1 MiB (64 KiB when the host refuses the larger size), after which a writer waits for the copy threads. Closes when: a size cap and a handover of the streams to children are designed. - [ANYPS5_LOG_FILE](../../../core/libs/prx/libc/src/LogFile.cpp) (libc) starts from the static initialization of libc, which on Windows runs inside `DllMain` under the loader lock: it opens the file, creates the pipes and two threads there, and none of this has run on Windows. A thread started under the loader lock waits for the lock to be released, which `DllMain` does not wait for, so no deadlock is expected, but no Windows run has confirmed it. Closes when: measured on a Windows host. @@ -284,3 +305,7 @@ - [atan and atan2](../../../core/libs/prx/libc/src/Math.cpp) (libc) do not compare floating-point exceptions, and the double inputs are checked by sampling, not exhaustively. They are FreeBSD msun's [`s_atan.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/s_atan.c) and [`e_atan2.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/e_atan2.c), with `tiny`, `pi_lo` and `atan(inf)`'s low part read at run time; neither sets `errno`, as the titles' `libc.prx` does not. They give the same bits as the `atan` and `atan2` bundled with PPSA07923 and PPSA05916 on 320 million random arguments and 2,704 pairs of special values on a Windows host. The FMA-contracted build bundled with PPSA13934 differs on 35,741 `atan` and 20,409 `atan2` of those random arguments. - [logb and logbf](../../../core/libs/prx/libc/src/Math.cpp) (libc) do not compare floating-point exceptions, and their double inputs are checked by sampling, not exhaustively. They are FreeBSD msun's [`s_logb.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/s_logb.c) and [`s_logbf.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/s_logbf.c): ±0 returns `-inf` without `errno`, and a signalling NaN is returned quieted. The `libc.prx` bundled with eleven titles (PPSA04158 to PPSA35169) gives the same bits and no `errno` on 4,032,768 inputs per function on a Windows host. - [Versioned std::__sce_v2::filesystem primitives](../../../core/libs/prx/libc/src/VersionedFilesystem.cpp) (libc) - `_Open_dir`, `_Read_dir`, `_Close_dir` and `_Current_get` reuse the unversioned Dinkumware implementation and libkernel's directory calls. `_Stat` copies the guest path and maps errors as `stat_nid_postfix` does, reports app0.zar entries through libc's archive reader with the 0444/0555 modes `sceKernelStat` gives, and reads disk entries with host `std::filesystem::status`, as libc cannot link libkernel. The console loops over `sceKernelGetdents` itself, as the `libc.prx` bundled with PPSA28416 shows, and maps the same errors. `_Read_dir` reports type 8 for every entry, as both do; the console's `_Current_get` writes "/" and returns true, reproduced here. The `_Xfilesystem_error` throwers call `NotImplemented_nid_no_patch`; `std::__sce_v2::filesystem_error` is not implemented. +- [Leases while a mutation waits for an unpinned range](../../../core/libs/prx/libc/src/GuestAllocations.cpp) (libc) - whether the console keeps memory visible to GPU work submitted while a release or reprotect of it is pending is unknown; not measured on hardware. A mutation that finds its range leased leaves that range out of the leases taken by `GuestAllocationsAcquire` and `GuestAllocationsAcquireAll` until the mutation ends, so GPU work that takes those leases in that time does not see the memory the guest is releasing or reprotecting; `GuestAllocationsAcquireRegistered`, which the registry queries and the unit shadow retire loops take, still lists it. +- [std::__sce_v2 locale objects and accessors](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) - measured from the `libc.prx` bundled with PPSA28416 (build J03848509). The five versioned facet ids (`MmytiDdoGBA`, `b61-ZHIQuns`, `5+FD4VpX+nw`, `3ZotGOwzi9k`, `OVV3+UimpiY`) and `locale::id::_Id_cnt` (`EIyErVBW9QI`) are BSS objects that start at zero; `_sceLibcClassicLocale` (`Mcrl2crhxu0`) points at the classic `Implementation`. `_Init(bool)` and `_Getgloballocale` return it, and `_Init` takes its reference when the flag is set; the console keeps the global null until `_Init` builds an embedded `_Locimp`, while here it is ready from the start. `_Facet_Register` appends to a host vector instead of prepending a `{next, facet}` node. `_New_Locimp` and `_Locimp_Addfac` reuse the unversioned 0x30-byte `Implementation` copy and insertion. + +- [std::__sce_v2 ios_base and streams](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) - measured from PPSA28416's `libc.prx` (J03848509). The versioned `ios_base` is `0x48` bytes: `_Addstd` writes the 1-based slot at `0x08`; `_Ios_base_dtor` decrements it, calls the `0x38` callbacks, frees the `0x30`/`0x38` node lists, compares the `0x40` locale slot with `&_ZNSt8__sce_v221_sceLibcClassicLocaleE` (`0x18e120` holds `0x198000`), calls `release` on `*0x40`, and `deleteObject` on a non-null return. The console frees the 8-byte slot; here it is not freed because `release` returns nothing and two streams can share one slot. Node layouts come from disassembly. Six streams are zeroed `0x400` storage; the console has `0x68`/`0x70`-byte streams, so copies fault. `_Getctype`/`_Getcvt` are not exported here. diff --git a/docs/dev/debt/libcairo.md b/docs/dev/debt/libcairo.md index 796dae4746..147a203fb2 100644 --- a/docs/dev/debt/libcairo.md +++ b/docs/dev/debt/libcairo.md @@ -1,8 +1,12 @@ # libcairo technical debt +### Functional + +- [Cairo affine matrices](../../../core/libs/prx/libcairo/Matrix.cpp) - Ten pure affine operations implement the documented [Cairo matrix API](https://www.cairographics.org/manual/cairo-cairo-matrix-t.html), including multiplication with aliased outputs, singular inversion status and point/distance transforms. The 48-byte `cairo_matrix_t` layout and C signatures are public; the PS5 library revision and native behavior have not been measured. These functions do not require the other 151 pending surface, font or rendering exports. + ### Callable exports -- [Callable exports](../../../core/libs/prx/libcairo/Export.cpp) - 161 callable NIDs match the [public 3.20 catalogue](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libcairo.c) and the [selected export metadata](https://gist.github.com/kyon-eth/a0bb48f2b3070a5eac8a638810f195f2/a3838ac54ebcc0bbfeb3cc46002a9a2dcce4dbdc) from two version-labelled images (SHA-256 `ec2158e0c5875316d200037d7f4bc48861626451752523f6930d687307c75ef8`). The labels are not authenticated. Prototypes and behavior are unverified; these declarations throw through `NotImplemented_nid_no_patch` and implement no runtime functionality. +- [Callable exports](../../../core/libs/prx/libcairo/Export.cpp) - 151 remaining callable NIDs match the [public 3.20 catalogue](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libcairo.c) and [selected export metadata](https://gist.github.com/kyon-eth/a0bb48f2b3070a5eac8a638810f195f2/a3838ac54ebcc0bbfeb3cc46002a9a2dcce4dbdc) from two version-labelled images (SHA-256 `ec2158e0c5875316d200037d7f4bc48861626451752523f6930d687307c75ef8`). The labels are not authenticated. Prototypes and behavior of these remaining declarations are unverified; they still throw through `NotImplemented_nid_no_patch`. | Name | NID | | --- | --- | @@ -19,7 +23,6 @@ | `cairo_user_to_device` | `3grQ1jCgta8` | | `cairo_reference` | `3pmBdB-bl8w` | | `cairo_set_antialias` | `3urTbaMTNyE` | -| `cairo_matrix_translate` | `4C5ObvulD3I` | | `cairo_rotate` | `4FH5hyIXo9k` | | `cairo_image_surface_create_from_png_stream` | `4dvdsqyLM8c` | | `cairo_get_fill_rule` | `4e7O-m-JWVM` | @@ -31,10 +34,7 @@ | `cairo_set_source_rgb` | `6DvK+OdIPW4` | | `cairo_scaled_font_destroy` | `6Y3HHedGzlw` | | `cairo_pattern_reference` | `6qRL52tLOII` | -| `cairo_matrix_scale` | `7DS-qtZoKjM` | -| `cairo_matrix_invert` | `8Aa+vkMrTB8` | | `cairo_scaled_font_get_font_options` | `8SOxQrQ7AfQ` | -| `cairo_matrix_init_translate` | `8cDupwx3X+Y` | | `cairo_surface_get_type` | `8viKdiZL7j4` | | `cairo_in_stroke` | `8zMm0QOWZAA` | | `cairo_scaled_font_get_ctm` | `A++pFuyxZPg` | @@ -51,7 +51,6 @@ | `cairo_surface_reference` | `Dv1FqxQxOkw` | | `cairo_copy_path` | `E2E+DDLhCRg` | | `cairo_rectangle` | `E5LkpnAY7po` | -| `cairo_matrix_multiply` | `E5rWAm1VaRM` | | `cairo_get_matrix` | `EItmXRLen7w` | | `cairo_user_to_device_distance` | `F2ZShEhLU8Y` | | `cairo_scaled_font_extents` | `FbWFbLUQOys` | @@ -71,14 +70,11 @@ | `cairo_has_current_point` | `JwQxC4dA5DY` | | `cairo_pattern_create_linear` | `K62jNcRGQdY` | | `cairo_destroy` | `K6aayzvQMlE` | -| `cairo_matrix_init` | `KuArfZk6xEU` | | `cairo_clip_extents` | `LJNqwiDWTm0` | | `cairo_set_scaled_font` | `LUh-6rZhniA` | -| `cairo_matrix_transform_point` | `LdSTmOZ-KDs` | | `cairo_image_surface_get_format` | `LrT15KUYBvc` | | `cairo_region_create` | `MGMwq9DNMyE` | | `cairo_fill` | `Ndvm8s28Gyc` | -| `cairo_matrix_transform_distance` | `NsFZqlmmZ9E` | | `cairo_transform` | `OHgp1uE8P2c` | | `cairo_set_line_join` | `Ori3CK8Sx6g` | | `cairo_pop_group_to_source` | `Oyzf6kDk6Ns` | @@ -107,7 +103,6 @@ | `cairo_append_path` | `XX9tocaUX+g` | | `cairo_region_union_rectangle` | `Ya+vgrJjBQE` | | `cairo_region_reference` | `Yarxt1LHaf0` | -| `cairo_matrix_init_identity` | `ZFmp0rD1F14` | | `cairo_scaled_font_status` | `a5WkqZXnG2w` | | `cairo_in_fill` | `akimK5gpdho` | | `cairo_get_line_join` | `azNGOQLubLc` | @@ -154,7 +149,6 @@ | `cairo_move_to` | `smpeN8iWrcc` | | `cairo_scale` | `suww6V0qqpU` | | `cairo_get_operator` | `tYTJIi4s6t0` | -| `cairo_matrix_rotate` | `uIoYWKOA9yo` | | `cairo_scaled_font_create` | `uKNPDudJTmo` | | `cairo_pattern_create_radial` | `uYFjM3ReDRQ` | | `cairo_surface_get_content` | `ut+atyIgpQw` | diff --git a/docs/dev/debt/libfreetype.md b/docs/dev/debt/libfreetype.md index e3723db9bf..6d20d38fb9 100644 --- a/docs/dev/debt/libfreetype.md +++ b/docs/dev/debt/libfreetype.md @@ -2,6 +2,6 @@ ### Callable exports -- [Callable exports](../../../core/libs/prx/libfreetype/Export.cpp) - five callable NIDs appear in a 3.20-labelled `libfreetype` image and a later 13.60-labelled `libSceVshFreeType` image. The images have different module and library identities; matching NIDs do not establish forwarding or equivalent behavior. Neither version label is authenticated. Names, signatures and behavior are unknown; these declarations throw and implement no runtime functionality. +- [Callable exports](../../../core/libs/prx/libfreetype/Export.cpp) - five callable NIDs appear in a 3.20-labelled `libfreetype` image and a later 13.60-labelled `libSceVshFreeType` image. The images have different module and library identities; matching NIDs do not establish forwarding or equivalent behavior. Neither version label is authenticated. The [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv) names them `FT_Get_Color_Glyph_Layer`, `FT_Get_Var_Axis_Flags`, `FT_Palette_Data_Get`, `FT_Palette_Select` and `FT_Set_Default_Properties`, and the NID of each name matches. Each helper is named after its symbol and keeps the explicit alias. Signatures and behavior are unknown; these declarations throw and implement no runtime functionality. -- [Older callable NIDs](../../../core/libs/prx/libfreetype/Export.cpp): 19 unidentified exports have unverified names, signatures and behavior. These are throwing import-resolution placeholders; no functionality is implemented. No matching NIDs were found in the later module inventory. +- [Older callable NIDs](../../../core/libs/prx/libfreetype/Export.cpp): The [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv) names these exports `FTC_Manager_New`, `FT_Bitmap_Blend`, `FT_Error_String`, `FT_Face_Properties`, `FT_Get_BDF_Charset_ID`, `FT_Get_BDF_Property`, `FT_Get_MM_Blend_Coordinates`, `FT_Get_MM_WeightVector`, `FT_Get_Sfnt_LangTag`, `FT_Get_WinFNT_Header`, `FT_Gzip_Uncompress`, `FT_Library_SetLcdGeometry`, `FT_New_Glyph`, `FT_Palette_Set_Foreground_Color`, `FT_Set_MM_WeightVector`, `FT_Set_Named_Instance`, `FT_Stream_OpenBzip2`, `FT_Stream_OpenGzip` and `FT_Stream_OpenLZW`, and the NID of each name matches. Each helper is named after its symbol and keeps the explicit alias. Signatures and behavior are unverified. These are throwing import-resolution placeholders; no functionality is implemented. No matching NIDs were found in the later module inventory. diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 44acf16554..7ee8d74fba 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -20,8 +20,10 @@ ### Unknown function info +- [Trailing-slash file creation](../../../core/libs/prx/libkernel/File/src/Open.cpp) (libkernel) - lookup and native open are separate host operations, so concurrent path changes can alter the reported error. For `O_CREAT` with a trailing separator, a host `EISDIR` or `EINVAL` is corrected from a metadata lookup; [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_lookup.c#L947-L951) rejects a missing final component with `ENOENT`. Not measured on a PS5. + - [sceKernelHasTrinityMode](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - returns 0, a base PS5's answer; a PS5 Pro is not modelled. Not measured on a console; the module's body was not read here. [Backport](https://github.com/danthespal/Backport/blob/5ba843c5ea51caad30ac05e830302bc1b8707f3c/FINDINGS.md?plain=1#L246) (11.00 to 12.70) says it reads `hw.sce_main_socid`; only [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/03484809c29fee4dc9fc3a4d61743749899ef5ca/src/driver_prospero.c#L333-L340) (firmware 11.60) gives the test, that id with the low five bits cleared compared with 0x840fc0, which the base PS5 SoC id 0x840f80 (`sceKernelGetMainSocId`) fails. The prototype `int (void)` comes from PPSA34114 01.000.005 (0x8837e3, 0x67db227, 0x78dfa9e): no argument is set and the result is stored as `setne` of `test eax, eax` -- [sceCoredumpRegisterCoredumpHandler, sceCoredumpUnregisterCoredumpHandler](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) retain registration metadata but do not create the native report thread or deliver callbacks. Native registration-state precedence, stack bounds and SDK capture are recovered; thread-creation failures and active-report synchronization remain unimplemented. +- [sceCoredumpRegisterCoredumpHandler, sceCoredumpUnregisterCoredumpHandler](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) retain registration metadata but do not create the native report thread or deliver callbacks. Native registration-state precedence, stack bounds and SDK capture are recovered; thread-creation failures and active-report synchronization remain unimplemented. A registered handler makes the next registration return `0x81180002`; otherwise a null handler or a stack size below 0x4000 or from 0x20000000 on returns `SCE_COREDUMP_ERROR_INVALID_ARGUMENT` (0x81180000). Every direct call in the titles checked passes a constant stack size inside that range; the handler of a call is not read, and the titles that reach the function through a tail jump are not known. - [scePthreadMutexattrSetkind, scePthreadMutexattrGetkind](../../../core/libs/prx/libkernel/Pthread/src/Mutex.cpp) (libkernel) - not measured on a console or in a title process, only by running firmware 13.42's `libkernel.sprx` and `libkernel_sys.sprx` in an emulator ([#9632](https://github.com/boykopovar/AnyPS5/pull/9632#issuecomment-6363402814), [#10725](https://github.com/boykopovar/AnyPS5/pull/10725#issuecomment-6397556344)). `scePthreadMutexattrGetkind` of a negative stored kind returns 0x80020000 plus the kind, which follows the measured rule for other kinds but was not measured. `scePthreadMutexattrSetkind` stores any kind through `pthread_mutexattr_setkind_np`; for a kind outside 1 to 4 only the refusal by `scePthreadMutexInit` and `pthread_mutex_init` was measured - [sceKernelError](../../../core/libs/prx/libkernel/System/src/Error.cpp) follows [shadPS4's kernel implementation](https://github.com/shadps4-emu/shadPS4/blob/23c41333822fe8eca4c609b43119180af63ae4ac/src/core/libraries/kernel/kernel.cpp): zero returns zero; other guest POSIX error values receive the `0x80020000` bias without modifying errno. Arithmetic is defined modulo 32 bits to avoid signed overflow for inputs outside the errno domain. Console behavior for those inputs and PS5 equivalence have not been measured. @@ -34,7 +36,7 @@ - [kern.rng_pseudo](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctlbyname` draws a new block of 64 bytes from the host's `std::random_device` for each read (no new value) that passes a buffer and a length, and sizes its answers as a payload process on a PS5 (firmware 12.70) sees it: a size query reports 64; a buffer of 64 bytes or more gets 64 bytes and a length of 64 (tried up to 65536); a smaller buffer gets as many bytes as its length, which is left as given, and a null length gets none, both with -1 and `ENOMEM`; a null buffer with a null length returns 0. `errno` is kept when the call succeeds. A write returns -1 with `EPERM`, which was not tried on the console. The only caller seen is mbedTLS's `mbedtls_hardware_poll` in the catalog's ProsperoLight boilerplate, which asks for 64-byte blocks and fails if fewer come back. The console's generator is unknown, and a title process was not measured. On the console `sysctlnametomib` gives MIB {1, 50} for the name, and `sysctl` with it answers reads of 0, 1, 63, 64, 65 and 128 bytes, a size query, a null length and a null buffer with a null length as the name does; here `sysctlnametomib` maps the name to {1, 50} and `sysctl` {1, 50} answers as the name does; a write through the MIB was not measured and throws - [sysconf](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `_SC_NPROCESSORS_CONF`, `_SC_NPROCESSORS_ONLN` and `_SC_PHYS_PAGES` report the host's processor count and memory in 16 KiB pages, where a payload process on a PS5 (firmware 12.70) read 16, 16 and 139888. `_SC_CHILD_MAX`, `_SC_OPEN_MAX` and `_SC_STREAM_MAX` follow the guest `RLIMIT_NPROC` and `RLIMIT_NOFILE`; following a changed `RLIMIT_NPROC` was not measured, and the 32767 bound on `_SC_STREAM_MAX` was not tried above 13952. `_SC_CLK_TCK` returns the 128 that process read, but no `times` reports ticks at that rate; CPython in PS5MC (PPSA99420) needs >= 1. `_SC_CPUSET_SIZE` (122) returns 8, as that process read, and whether it follows the processor count is unknown. A failed `getrlimit` or a host count of 0 gives -1 with `EIO`, which was not seen on the console. What a title process gets was not measured - [hw.realmem](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctl` {`CTL_HW`, 12} and `sysctlbyname` report the same 8-byte value as `hw.physmem` (the `_SC_PHYS_PAGES` pages of 16 KiB), read-only with `EPERM` for a write and `ENOMEM` for a short buffer, as FreeBSD's [`sysctl_hw_realmem`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_mib.c#L191-L204) does. FreeBSD reports there the memory the firmware describes, which is at least `hw.physmem`. A payload process on a PS5 (firmware 12.70) gets -1 with `ENOENT` for `hw.physmem` and `hw.realmem` (a read, a size query and a null length) and for {`CTL_HW`, 5} and {`CTL_HW`, 12} (a read and a size query); what a title process gets was not measured. Dolphin reads it as the machine's memory size in [`MemPhysical`](https://github.com/dolphin-emu/dolphin/blob/8d2ad1a662472955497e5bb79d7b9ec6999e1f0d/Source/Core/Common/MemoryUtil.cpp#L253-L273) (RetroArch PPSA99169's `dolphin_libretro` core) -- [vm.overcommit](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctlbyname` reports the 4-byte value 0, the default FreeBSD gives its [`vm.overcommit`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/swap_pager.c#L158-L161) `int`, with `ENOMEM` for a short buffer, and a write throws. A payload process on a PS5 (firmware 12.70) gets -1 with `ENOENT` for it, for a read, a size query and a null length; what a title process gets was not measured. mimalloc reads it in [`unix_detect_overcommit`](https://github.com/microsoft/mimalloc/blob/31d034d94cdb8e22f7d7ed55967f581a2d6e831d/src/prim/unix/prim.c#L129-L153) and takes 0 as no overcommit, as PS5MC (PPSA99420) does at start-up +- [vm.overcommit](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctlbyname` reports the 4-byte value 0, the default FreeBSD gives its [`vm.overcommit`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/swap_pager.c#L158-L161) `int`, with `ENOMEM` for a short buffer, and a write fails with `EPERM`: the node has no `CTLFLAG_PRISON`, so `sysctl_root` needs `PRIV_SYSCTL_WRITE`, which `prison_priv_check` denies a jailed root (model agreed on #11216, not measured). A payload process on a PS5 (firmware 12.70) gets -1 with `ENOENT` for it, for a read, a size query and a null length; what a title process gets was not measured. mimalloc reads it in [`unix_detect_overcommit`](https://github.com/microsoft/mimalloc/blob/31d034d94cdb8e22f7d7ed55967f581a2d6e831d/src/prim/unix/prim.c#L129-L153) and takes 0 as no overcommit, as PS5MC (PPSA99420) does at start-up - [kern.sdk_version](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctlbyname` reports the system software version as the 4-byte value `0x01000000` (1.00), the value `sceKernelGetSystemSwVersion` reports; a write returns -1 with `EPERM`. The major version is byte 3 and the minor version byte 2, in hex digits that read as decimal (13.60 is `0x1360xxxx`), as [Capy Mobile](https://github.com/khalifa007/CapyMobile/blob/2758c016/src/ps5/platform.c#L64-L71) (PPSA92280), [remote_lua_loader](https://github.com/n0llptr/remote_lua_loader/blob/7c2840e475/savedata/misc.lua#L195-L209) and the [lapse payload](https://github.com/NetflixNHack/Netflix-N-Hack/blob/605eb93ba5/PS4/payloads/lapse.js#L436-L450) read it. Capy Mobile only displays it. The console's firmware version is not modeled and the low two bytes are unknown - [kern.ostype, kern.osrelease, kern.version, kern.hostname, hw.machine, hw.model](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctl` (1.1, 1.2, 1.4, 1.10, 6.1, 6.2), `sysctlbyname` and `sysctlnametomib` serve read-only strings. A payload process on a PS5 (firmware 13.60) read "FreeBSD", "0.0-prototype", an empty host name, "amd64" and, for `hw.model`, "100-000001020" padded with spaces to 47 characters, which are returned here. Its `kern.version` was "r229358/releases/13.60 Jul 17 2026 02:16:29", which names the firmware, so the FreeBSD 11.4 text "FreeBSD 11.4-RELEASE\n" stays here. Whether another PS5 model reports another `hw.model` is not known. A short buffer gets as many bytes as fit and `ENOMEM`, as on the console, and a write `EPERM` - [kern.osreldate, kern.osrevision, kern.argmax, kern.ngroups](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctl` (1.24, 1.3, 1.8, 1.18) and `sysctlbyname` report 900000, 199506, 262144 and 1023, the 4-byte values a payload process on a PS5 (firmware 13.60) read; a short buffer returns `ENOMEM` and a write `EPERM` @@ -56,7 +58,7 @@ - [APR gather and scatter reads](../../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libSceAmpr) - signatures, argument checks and the read cursor taken from the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation, not confirmed on a title: each read leaves the file, the next file offset and the next destination; a gather reads that file at a new offset into the next destination, a scatter continues in the file into a new destination, a gather-scatter takes both. Recording one needs a read file since the last `ResetGatherScatterState`; like ampr_emu, `sceAmprCommandBufferReset` keeps that state, and a gather or scatter with no read before it in the submitted buffer throws instead of returning `SCE_AMPR_ERROR_APR_INVALIDGATHERSCATTERSTATE` - [AMM map, map direct and unmap](../../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libSceAmpr) - signatures and the protection mask (CPU, GPU, AMPR and ACP read and write) taken from the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation, which hands the remaining checks and the mapping to the console kernel; the mapper model taken from [PS5PCEM](https://github.com/iStark/PS5PCEM): `sceAmprAmmGiveDirectMemory` usage 1 gives the allocated direct memory to the pool plain maps take pages from, usage 0 only allocates it. Not confirmed on a title. The AMM virtual address ranges are a 256 GiB range (PPSA02015 places its heaps 64 GiB above the start of the range, so 32 GiB is too small, and reached 84 GiB above it in its first 159 s; KytyPS5 reports 512 GiB, but every 64 GiB-aligned 544 GiB window in the guest arena covers 448 to 608 GiB, and on Windows one host thread stack there is enough to block it) and a 32 GiB multimap range reserved on first use at the top of the guest arena with the start on a 64 GiB boundary (PPSA02015 tells its pool pointers from its heap pointers by address bits 28 to 35, which needs such a start for its layout), above the addresses that maps without `MAP_FIXED` are given lowest first and that titles derive their next hints from; the console's addresses are not known (ampr_emu takes them from `sceKernelMapperGetParam`), and a reservation that cannot be made there throws. Maps outside them throw. Addresses, sizes and direct offsets must be 16 KiB aligned. The memory type and the GPU mask are ignored, AMPR and ACP access maps as CPU access, unmapped pages go back to the pool and stay reserved. A buffer may run after submission returns, the submit result is always 0 and a failing command throws; the AMM measures return errors sign-extended, as ampr_emu does. A map returns its target to the reservation before releasing the old pages, so pool exhaustion leaves that target reserved; mapping failure also restores the reservation before returning newly taken pages to the pool. The console's map failure and rollback behavior is unverified. `sceAmprAmmGetVirtualAddressRanges` throws for a null output - [AMM remap, multimap and protection changes](../../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libSceAmpr) - signatures and the protection and mask checks taken from ampr_emu; the effects are inferred from the names, not confirmed on a title. A remap moves every page of a fully mapped range to the new address with the given protection and leaves the old range reserved; a multimap maps the same pages at a second address, anywhere in the AMM ranges, and a pool page goes back to the pool once its last mapping is unmapped; a multimap onto its own source range throws. A protection change sets the masked bits on every page of a fully mapped range; the memory type is recorded and ignored. A command on a range that is not fully mapped throws -- [AMM PRT ranges](../../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libSceAmpr) - signatures, checks and their order taken from ampr_emu, where `MapAsPrt` is a map with the PRT flag and protection 0 and `AllocatePaForPrt` a memory type and protection change with mask 1019, both checking the buffer memory after its size; the effects are inferred from the names, not confirmed on a title. Unbacked PRT pages are zero-filled pages with GPU read access, so CPU reads see zeros instead of faulting and GPU writes fault instead of being dropped. `AllocatePaForPrt` backs each unbacked page with a pool page and sets the type and protection of backed ones; `RemapIntoPrt` moves a fully mapped range into a PRT range, its opcode argument (1011 when 0, as in ampr_emu) is recorded and ignored; `UnmapToPrt` returns the pages to the pool and zero-fills them again. A plain map, unmap or remap over a PRT range ends it; `AllocatePaForPrt`, `RemapIntoPrt` or `UnmapToPrt` outside a PRT range throws +- [AMM PRT ranges](../../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libSceAmpr) - effects inferred from the names, not confirmed on a title; checks and their order from ampr_emu, where `MapAsPrt` is a map with the PRT flag and protection 0 and `AllocatePaForPrt` a type and protection change with mask 1019, both checking the buffer memory after its size. Unbacked pages read as zeros with GPU read access, so GPU writes fault. On Windows they are views of one read-only 16 KiB zero section, so a `sceKernelMprotect` making them writable throws; a view cannot outgrow its section, so each page costs a placeholder split and a view like the per-page commit before: a 4 GiB `MapAsPrt` takes 1.05 s (0.86 s before) and a toggle 3.2 s, for 21 MiB of commit instead of 4 GiB. `AllocatePaForPrt` backs pages from the pool; `RemapIntoPrt` moves a mapped range in and ignores its opcode; `UnmapToPrt` returns pages to the pool. A plain map, unmap or remap ends a PRT range; PRT commands outside one throw - [AMM kernel write commands](../../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libkernel) - signatures of `sceKernelWriteModifyMtypeProtectCommand` and `sceKernelWriteRemapIntoPrtCommand` taken from [ampr_emu's libkernel hook targets](https://github.com/drakmor/ampr_emu/blob/cfa85df379f6eeeb165d7badf9b648e266fe77b7/include/ampr_libkernel_hook_targets.h), which forward to the real kernel; the measure mode (`dst` null reports the size, `outSize` null skips the report) follows ampr_emu's measure-then-record calls, not confirmed on a title. The range, protection and mask checks mirror the `libSceAmpr` siblings, and the recorded bytes match them exactly so the submitted buffer executes on the existing paths - [sceKernelWriteModifyMtypeProtectCommand and sceKernelWriteRemapIntoPrtCommand checks](../../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libkernel) - they apply their libSceAmpr siblings' checks, not those of firmware 13.42's libkernel (compared under a CPU emulator, not measured on a console), so some arguments get a different return code. The console checks the range against the AMM virtual address ranges, accepts a size that wraps past the end of the address space and takes a different set of protection bits; for the memory type change it accepts only types 11 to 20 and a protection inside the mask, and for the PRT remap only a last argument of 0x3F3, which it checks but does not record - [sceCoredumpWriteUserString](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel, NID `32KQRUK13kI`) - `(uint32_t, const char* text, int)` follows the console's `libkernel.sprx` (SDK 4.5x build of `J03950001`) at 0x13050: a null `text` returns `0x81180000`; otherwise the three arguments go to syscall 601 (command 10), whose `EPERM` becomes `SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER` (0x81180003), `EINVAL` `0x81180000`, and a success returns the value the kernel wrote back. Measured on a PS5 (firmware 13.60) from a homebrew payload with no core dump handler: the call succeeds with 0, the kernel rejects a first argument above 15 or a third outside 0 to 256 with `0x81180000`, and a 2047-character text is accepted. Those checks are reproduced and anything else returns 0; nothing is recorded, as there is no dump, and what the two numbers mean is unknown. PPSA16064 imports it @@ -75,6 +77,7 @@ - [sceKernelGetExecutableModuleHandle](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - returns the main program's handle, the one `sceKernelGetModuleList` lists first and `sceKernelGetModuleInfo` accepts. The `int(void)` signature and that use follow reSOT's [`load_code_copy`](https://github.com/bystritskiy/rePOP-ps5/blob/4e6fe79ea83b5e3814e1be27c7d2b85c9e1ad22c/port/ps5/src/mmio_ps5.c#L123-L137), which passes the handle to `sceKernelGetModuleInfo` to find its own code segment. The [PS5 3.20 export lists](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libkernel.c#L3922) have it in libkernel as well as libkernel_sys, so it is exported from libkernel only. The number is AnyPS5's own: a payload process on PS5 firmware 13.60 got its main program as handle 0 from `sceKernelGetModuleList`, and what this call returns on a console was not measured - [sceKernelAvailableFlexibleMemorySize, sceKernelConfiguredFlexibleMemorySize, sceKernelGetPrtAperture](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) validate destination pointers with `GuestCopyOut`: a null pointer returns `SCE_KERNEL_ERROR_EINVAL`, while an unmapped or unwritable pointer returns `SCE_KERNEL_ERROR_EFAULT` (in `sceKernelGetPrtAperture`, sequential copyout leaves the address written if length faults). Console fault behaviour on bad memory is unmeasured. - [sceKernelQueryMemoryProtection and sceKernelIsStack](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) copy each optional output with `GuestCopyOut`: a null output is skipped, while an unmapped or unwritable one returns `SCE_KERNEL_ERROR_EFAULT` as a host-safety fallback. Outputs are written in order (`start`, `end`, then the protection), so when a later one faults the earlier ones are already written. Console fault behaviour on bad memory is unmeasured. +- [sceKernelAioPollRequest, sceKernelAioPollRequests, sceKernelAioCancelRequest, sceKernelAioCancelRequests, sceKernelAioDeleteRequest and sceKernelAioDeleteRequests](../../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) copy the ids in with `GuestCopyIn` and the states out with `GuestCopyOut`: a null output still returns `SCE_KERNEL_ERROR_EFAULT`, and an unreadable id array or an unmapped or unwritable output returns it too, as a host-safety fallback. The states are copied out after the registry changed, so when the copy faults a poll has already marked the request reported and a delete has already removed it; a cancel changes nothing. Console fault behaviour on bad memory is unmeasured. `sceKernelAioWaitRequest`, `sceKernelAioWaitRequests` and the submit calls still access the ids, states, requests and results directly. - [sceKernelReleaseFlexibleMemory](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - on a PS5 (firmware 13.60) the released range stays mapped, with its protection and contents (64 MiB checked after 0.5 s), and a fixed no-overwrite map over it fails with `ENOMEM`, as here. What it does to the flexible budget was not measured, because a payload process has none (`sceKernelAvailableFlexibleMemorySize` did not change on map or release); here the released bytes return to the budget while the pages stay mapped, and a later map over them charges them again. The host pages behind a released range stay committed until `sceKernelMunmap`, as `madvise` keeps them for every advice; freeing them would lose the contents the console still returns. - [sceKernelAvailableFlexibleMemorySize](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - the segments of the title's executable and of its `.guest.prx` modules count against the configured flexible-memory budget (448 MiB by default) while loaded, for this size and for flexible maps. A title may declare a larger budget through `kernel.flexibleMemorySize` in `param.json`; this does not reduce the 13.5 GiB direct-memory size. Rejection of zero or a value not aligned to 16 KiB has not been measured on a console. This segment-accounting model follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/module.cpp#L165), which maps every segment of a module outside its system module directory as flexible memory on the PS4. [Counted](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) are the segments the title's ELF gives permissions, each rounded to 16 KiB pages; not counted are segments without permissions, the relinker's own blocks (on Linux both are the read/write/execute `PT_LOAD`s, plus the header block at file offset 0; on Windows everything outside the `.elf` sections) and the host system libraries. The Linux image carries no marker for the relinker's blocks, so they are told apart by those flags and that offset only: a title segment that is writable and executable, or starts at file offset 0, is not counted. A module whose segments exceed what is left still loads, and the available size then reads 0; shadPS4 fails the segment map instead ([memory.cpp](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/memory.cpp#L519-L524) returns `EINVAL`, [module.cpp](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/module.cpp#L168-L169) asserts). Which segments the PS5 charges, its rounding, whether system modules use the title's budget and when an unload releases the charge are not measured. PPSA15508 subtracts the reported size and a 10 MiB module allowance from its own 448 MiB budget at start-up and stops when the result exceeds its 146 MiB code and data limit ([#2883](https://github.com/boykopovar/AnyPS5/issues/2883)) - [Anonymous mmap and the flexible budget](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) - whether a PS5 charges a plain `MAP_PRIVATE | MAP_ANON` map to the flexible-memory budget is not measured. Here it is not charged: it is placed through `DoMapAnon` and is not added to the flexible ranges, so `sceKernelAvailableFlexibleMemorySize` does not drop and a map is never refused for the flexible budget. [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/libraries/kernel/memory.cpp#L784-L790) maps an anonymous map without `MAP_SYSTEM` as flexible memory on the PS4, which [checks it against and charges it to](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/memory.cpp#L519-L524) the flexible budget ([#7120](https://github.com/boykopovar/AnyPS5/issues/7120)) @@ -93,7 +96,7 @@ - [sceKernelWaitEventFlag](../../../core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp) (libkernel) - once the event flag is resolved, a zero bit pattern, a wait mode without exactly one of AND (0x1) and OR (0x2), or one with both clear flags (0x10 and 0x20) returns `EINVAL`, as `sceKernelPollEventFlag` does; the console's answers for them are not measured. fpPS4 (PS4, commit 04cefd43e6fddd1ab033e7980cd356d14c964905), shadPS4 (PS4, commit 0614ce324be092791b758d79f93d9f1ca5b10a81) and KytyPS5 (PS5, commit 5e837a9beac82c031868eaad9348d6a48245fb99) return `EINVAL` for a zero pattern; for a wait mode without exactly one of AND and OR fpPS4 returns it too, while shadPS4 and KytyPS5 stop. Every direct call in the titles checked with a constant bit pattern passes a non-zero one and every call with a constant wait mode passes a valid one, so none reaches the check; calls not shown to be constants and the tail jumps are not known. - [sceKernelCreateEventFlag](../../../core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp) (libkernel) - event flag handles are host addresses here, while the console returns small IDs (0x22 for the first in a payload process on PS5 firmware 13.60). That console checks the attribute before the name length, as here: attributes 0x3, 0x30, 0x40 and 0x200 return `SCE_KERNEL_ERROR_EINVAL` with a 32-byte name too, and attributes 0 and 0x100 return `SCE_KERNEL_ERROR_ENAMETOOLONG` with it. Of 114 call sites read in 12 titles (PPSA02530 not included), single sites pass the attribute 0x10 (PPSA24779 0x943887) and 0x22 (PPSA30601 0x16157f1), and 0x21 in PPSA04354 (0xee7dc7) and PPSA35169 (0x5a539e), all with the initial value 0; how the attributes are distributed over the other sites is not shown. - [sceKernelAioSubmitReadCommandsMultiple / sceKernelAioSubmitWriteCommandsMultiple / sceKernelAioWaitRequests / sceKernelAioPollRequests / sceKernelAioCancelRequest / sceKernelAioCancelRequests / sceKernelAioDeleteRequests](../../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) - one id per request for the Multiple submits and wait mode 2 returning once one request completed follow shadPS4. Requests run synchronously on submit, so cancel never finds one processing. The console's ids are not the small numbers used here (it returned 33, 0x2021, 0x4021) -- [sceKernelWaitSema, sceKernelPollSema, sceKernelSignalSema, sceKernelCancelSema, sceKernelDeleteSema](../../../core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp) (libkernel) - `SCE_KERNEL_ERROR_ESRCH` for a null handle follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/60c42b925c3d1e00008621b8587819999260ec31/src/core/libraries/kernel/threads/semaphore.cpp#L329-L366) (PS4), unverified on PS5. Handles are pointers, so a non-null handle that is not a live semaphore is not detected; shadPS4 returns `ESRCH` for any unallocated handle. +- [sceKernelWaitSema, sceKernelPollSema, sceKernelSignalSema, sceKernelCancelSema, sceKernelDeleteSema](../../../core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp) (libkernel) - `SCE_KERNEL_ERROR_ESRCH` for a null handle follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/60c42b925c3d1e00008621b8587819999260ec31/src/core/libraries/kernel/threads/semaphore.cpp#L329-L366) (PS4), unverified on PS5. Handles are pointers, so a non-null handle that is not a live semaphore is not detected; shadPS4 returns `ESRCH` for any unallocated handle. `sceKernelWaitSema` with `need` above the semaphore's maximum returns `SCE_KERNEL_ERROR_EINVAL` by analogy with `sceKernelPollSema`; the console's result for that call was not measured. - [scePthreadAttr setters and getters](../../../core/libs/prx/libkernel/Pthread/src/Attr.cpp) (libkernel) - a new attribute holds a 1 MiB stack size here and 64 KiB on the console, because host code runs on guest threads; a null attribute pointer to `scePthreadAttrInit` and a null output of `scePthreadAttrGetaffinity` throw, where the console crashed. Measured on a PS5 (firmware 13.60): a null or uninitialized attribute, and a null output of the stack size, detach state, scheduling parameter, policy and inheritance getters, return `EINVAL`; a policy other than 1 and 3, an inheritance other than 0 and 4, or a priority outside 256 to 767 (or a null parameter) returns `ENOTSUP` (`0x8002002D`); a detach state other than 0 and 1 and a stack size below 16384 return `EINVAL`; any affinity mask is accepted; a new attribute holds policy 1 and inheritance 4 - [scePthreadAttrGet](../../../core/libs/prx/libkernel/Pthread/src/Attr.cpp) and [pthread_attr_get_np](../../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) (libkernel) - `scePthreadAttrGet` reports the inheritance a thread was created with and the policy and priority it runs with, as a payload process on a PS5 (firmware 13.60) does: 4 with the creator's policy and priority for an inheriting thread, 0 with the attribute's for an explicit one, and policy 1, inheritance 4 and priority 700 for a thread created with a freshly initialised attribute object; a null attribute was not measured. `pthread_attr_get_np` returned `EINVAL` for the payload's main thread, so its answer for other threads is not measured; it reports the recorded guest policy, following [FreeBSD 11.4 libthr](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_attr.c#L134-L164) - [pthread_getprio, scePthreadSetprio and pthread_setprio](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - measured in a payload process on a PS5 (firmware 13.60): `pthread_getprio` returns the priority, -1 with `ESRCH` for a joined thread and -1 with `EINVAL` for a null one; the setters accept a priority whose low 16 bits are 256 to 767 or 896 to 959 (so 65792 and -65280 too, stored as given), return `EINVAL` otherwise and set `errno` to it, and return `ESRCH` for a joined thread. As in firmware 13.42's `libkernel` run in an emulator, a null thread returns `EINVAL` and a finished or joined thread `ESRCH` before the priority is checked, both leaving `errno` alone; a finished thread that is not joined yet was not measured on a console. The guest priority is recorded and reported, not applied to the host thread @@ -109,9 +112,10 @@ - [_sceKernelRtldThreadAtexitIncrement, _sceKernelRtldThreadAtexitDecrement](../../../core/libs/prx/libkernel/System/src/Rtld.cpp) (libkernel) - the argument is the `dso` address a title's `libc.prx` passes to `__cxa_thread_atexit` (PPSA14632), which fails when Increment returns nonzero. As in a payload process on a PS5 (firmware 13.60), Increment returns -1 for an address outside every module (null, stack, heap) and 0 otherwise, and Decrement never fails on an address outside every module or after a matching Increment; the console's Decrement returns nothing, here 0. A Decrement on a module's address without a matching Increment was not measured and throws here. The module's `ref_count` did not change on the console. As in [fpPS4](https://github.com/red-prig/fpPS4), Increment here takes a reference on the image and Decrement releases one. The variants without the leading underscore are not exported by firmware 13.60's `libkernel_sys`; here they match - [pthread_cleanup_push, pthread_cleanup_pop](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel, NIDs `4ZeZWcMsAV0`, `RVxb0Ssa5t0`) share the caller-record cleanup list with `__pthread_cleanup_push_imp` and `__pthread_cleanup_pop_imp`, following [FreeBSD 11.4 thr_clean.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libthr/thread/thr_clean.c). The firmware body reading reported by [PR8428](https://github.com/boykopovar/AnyPS5/pull/8428) also sets the thread's unwind-disabled flag in the heap form; that flag is not modeled here, and runtime-created guest threads skip guest-frame unwinding in these exit paths. The firmware observation is attributed to that contributor, not independently reproduced. Heap records use C++ `new` here, whereas the FreeBSD reference leaves the cleanup list unchanged if `malloc` fails; console allocation-failure behavior and its interaction with unwinding remain unverified. - [tcgetattr, tcsetattr](../../../core/libs/prx/libkernel/File/src/Terminal.cpp) (libkernel) - every file and pipe descriptor is taken as not a terminal (`ENOTTY`), and a socket gets `EADDRNOTAVAIL` for `AF_INET` and `EOPNOTSUPP` otherwise, as FreeBSD's `in_control` and `pru_control_notsupp` answer a termios ioctl; what descriptors 0 to 2 of a title are on the console, and whether any of them is a terminal there, is unknown +- [tcgetsid](../../../core/libs/prx/libkernel/File/src/Terminal.cpp) (libkernel, also exported by libkernel_sys on the PS5) follows FreeBSD 11's [`termios.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libc/gen/termios.c), where it is `ioctl(fd, TIOCGSID, &sid)`, and fails the way the other terminal calls here do on a descriptor that is not a terminal: `EBADF` for a descriptor that is not open, `EOPNOTSUPP` for a local socket, `EADDRNOTAVAIL` for an internet socket and `ENOTTY` for anything else, so it never returns a session id, since terminals are not modelled. A pipe or FIFO gets `ENOTTY` and does not throw as `tcgetpgrp` does, because FreeBSD's `pipe_ioctl` answers `TIOCGPGRP` and `TIOCSPGRP` but not `TIOCGSID`. The PS5 `libkernel` is assumed to use this FreeBSD code. Console behaviour is unverified. - [cfsetspeed](../../../core/libs/prx/libkernel/File/src/Terminal.cpp) (libkernel, NID `C1MQ6OaXRjo`) - console behavior and module/library versions are unverified. The prototype, uint32 speed and 44-byte termios layout (input speed at 36, output speed at 40) follow the public PS5 payload SDK [termios.h](https://github.com/ps5-payload-dev/sdk/blob/a7685ce8e97ce1f8a4f38253a41d54241c9124f1/include/freebsd/termios.h) and [sys/_termios.h](https://github.com/ps5-payload-dev/sdk/blob/a7685ce8e97ce1f8a4f38253a41d54241c9124f1/include/freebsd/sys/_termios.h). The module association follows its libkernel stub table and the NID rehashes to the aerolib name. Both speed fields accept every uint32 value and no flags or control characters change, as [FreeBSD 11.4 termios.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libc/gen/termios.c) does. No terminal I/O is performed. Null pointers throw where the reference dereferences them. - [select](../../../core/libs/prx/libkernel/Socket/src/Socket.cpp) (libkernel) - console standard-stream behavior is unverified. Regular files, directories and archived files at descriptors 0 to 2 use vnode readiness, including host redirection (`< file`, `> log.txt` or `2> log.txt`): they are readable and writable even at EOF. Other standard descriptors reaching the fallback use the console model: descriptor 0 is never reported readable and descriptors 1 and 2 are writable; reads from descriptor 0 still go to host stdin. `/dev/random` and `/dev/urandom` follow `randomdev_poll` for a seeded device under [`selscan`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/sys_generic.c#L1178-L1236), which asks once with the flags of every set the descriptor is in: in the read set they are readable and reported in no other set, and otherwise they are reported in the write and exception sets they were asked in. A regular file, a directory or a file read from app0.zar is always readable and writable and never in the exception set, as `vop_stdpoll` answers, on every host. Guest sockets and libSceNet sockets are polled. A kqueue is readable while one of its events is triggered and is never writable or in the exception set, as FreeBSD 11.4's [`kqueue_poll`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/kern_event.c#L1900-L1924) answers, on every host. Other descriptors are polled through the host on Linux, while on Windows pipes are checked every millisecond with the readiness test `poll` uses and any other descriptor throws. Dungeons (PPSA16064) selects on descriptor 0, and RetroArch (PPSA99169) selects on its web server's wake-up pipe next to its listening socket -- [select](../../../core/libs/prx/libkernel/Socket/src/Socket.cpp) (libkernel) - a descriptor the socket poller reports as unknown 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 +- [select](../../../core/libs/prx/libkernel/Socket/src/Socket.cpp) (libkernel) - a descriptor the socket poller reports as unknown 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 fails with `EBADF` 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, 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 @@ -149,9 +153,11 @@ - [pthread_timedjoin_np](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel, NID `PkS44IGrDkM`) - the signature follows [ps5-payload-dev/sdk](https://github.com/ps5-payload-dev/sdk/blob/a7685ce8e97ce1f8a4f38253a41d54241c9124f1/include/freebsd/pthread_np.h#L71); deadline validation, absolute realtime deadlines, error precedence and join ownership follow [FreeBSD libthr](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libthr/thread/thr_join.c). The [public 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libkernel.c) lists this NID, but PS5 behavior and module/library versions have not been measured. Realtime is rechecked at most two seconds apart while waiting. After guest cleanup completes, final native thread teardown uses the existing unbounded join and may finish after the deadline; host teardown is not bounded by this implementation. - [sceKernelMapNamedSystemFlexibleMemory](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - the PS5 system budget and the `sceKernelVirtualQuery` record of a system map are unmeasured. It is not charged to the game flexible budget, as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/30f4170896ce974353a6f6486d0ecec179aec54b/src/core/libraries/kernel/memory.cpp#L354-L391), which charges only its flexible type; fpPS4 ignores `MAP_SYSTEM` and charges it. It reports as flexible, as in fpPS4; shadPS4 reports it as not flexible. Checks, alignment and placement are `sceKernelMapNamedFlexibleMemory`'s, so flag 0x8000 and protection 0x800 throw, unmeasured. A null name or one of 32 bytes or more throws, unmeasured: shadPS4 returns `EFAULT` or `ENAMETOOLONG` before mapping, [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/kernel/ps4_map_mm.pas#L1121-L1209) maps it unnamed. Closes when: measured on a PS5 +- [NativeMetadataPath, OpenNative, sceKernelUnlink, OpenPlaceholder](../../../core/libs/prx/libkernel/File/src/Open.cpp) (libkernel) - on Windows, paths with DOS device names (CON, PRN, AUX, NUL, COM1-9, COM¹-³, LPT1-9, LPT¹-³, CONIN$, CONOUT$) or trailing dots/spaces are prefixed with the `\\?\` verbatim prefix to prevent Win32 normalization from rewriting or stripping them. Host NTFS/FAT filesystems still reject POSIX-valid characters disallowed by Windows (`\`, `/`, `:`, `*`, `?`, `"`, `<`, `>`, `|`) and alternate data streams (stream names with colon) remain unhandled. ### Functional +- [Guest main stack](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - on Linux the title's entry point runs on a stack that ends at the address a payload's main thread stack ends at on a PS5 (firmware 13.60), `0x7eeffc000` (moved with the guest arena in ASan builds), with a 16 KiB guard below it. Its size is the host's `RLIMIT_STACK` (8 MiB when unlimited), not the console's: the `sceUserMainThreadStackSize` of the title's process parameters is not read, and the payload's stack was 2 MiB. A guest mapping already registered in that range stops the entry with an error that names the range instead of being replaced. Windows and macOS still enter the title on the host stack - [sceKernelGetModuleInfoForUnwind](../../../core/libs/prx/libkernel/Module/src/Module.cpp) (WebAssembly) throws because guest module unwind metadata is not implemented for linear memory; it does not query the host's process mappings - [scePthreadYield](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) on Windows calls Sleep(1) after 512 consecutive yields that find no other thread ready on the current processor and arrive less than ~10,000 TSC ticks apart (so the span varies with host CPU TSC rate); FreeBSD sched_yield never sleeps. - [__tls_get_addr](../../../core/libs/prx/libkernel/Module/src/Tls.cpp) (libkernel, NID `vNe1w4diLCs`) throws on macOS, where the relinker binds guest modules to `Aps5GuestTlsGetAddr_nid_no_patch` ([GuestTlsMacOs.cpp](../../../core/libs/prx/libkernel/Pthread/src/GuestTlsMacOs.cpp)) instead; it also throws for a null index, module 0 and a Windows thread without a block for the module. It reads the module word as the relinker writes it for `R_X86_64_DTPMOD64`: on Windows the address of the module's PE `_tls_index`, resolved through the thread's `ThreadLocalStoragePointer` as in [WindowsGuestStartup.cpp](../../../core/relinker/elfpatcher/src/windows/WindowsGuestStartup.cpp); on Linux the glibc module ID, passed to glibc's `__tls_get_addr`. Guest modules' imports are bound to the host resolver, so this export is reached only through a lookup by NID. @@ -167,7 +173,7 @@ - [sceKernelMprotect protection bits](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - bits outside `0x3F7` are dropped instead of refused, in the POSIX `mprotect` as well (FreeBSD 11.4 masks the argument, `sys/vm/vm_mmap.c:613`; the console was measured only through `sceKernelMprotect`), as a PS5 (firmware 13.60) drops them: Onimusha: Way of the Sword (PPSA27836 01.000.001) maps direct memory with `0xf2` and then protects it with `0xfffffff2`, which the console reports as `0xf3` and then `0x3f3`. In a payload process the console also drops the CPU execute bit (`0x4`) of flexible, direct and JIT memory, and POSIX `mprotect` with write and execute reports write only (`0x2`). A title process may keep execute, so it is kept here. The map calls and `sceKernelMtypeprotect` refuse undefined bits on the console with `EINVAL` (`0x80020016`); here the five flexible and direct map calls do too, before mapping, but `sceKernelMtypeprotect` and the batch map calls throw - [JIT shared memory on macOS](../../../core/libs/prx/libkernel/DirectMemory/Jit.cpp) (libkernel) - the object is an unlinked `mkstemp` file in the temporary directory, as `shm_open` makes its anonymous objects. macOS refuses an executable shared file mapping (`EPERM`), so an executable view is mapped without execute and then given it with `mprotect`. Under Rosetta 2 (Apple M2, macOS 26), code rewritten through one view after it ran from another keeps its old translation until the executable view's protection changes; first writes and a single read-write-execute view, as XPSemu PPSA97358 and VLC PPSA85300 map, are seen. `mmap` of a JIT descriptor is not supported, as file-backed guest `mmap` throws on macOS. - [JIT shared memory](../../../core/libs/prx/libkernel/DirectMemory/Jit.cpp) (libkernel) - `sceKernelJitCreateSharedMemory`, `sceKernelJitCreateAliasOfSharedMemory` and `sceKernelJitMapSharedMemory` take the signatures of [Orbital](https://github.com/AlexAltea/orbital)'s `tools/ida/db_types.json` (name, length, maximum protection, descriptor out; descriptor, maximum protection, descriptor out; descriptor, protection, address in and out), which ps4sdk, ps5-jar-loader and the call sites in RetroArch PPSA99169, XPSemu PPSA97358 and VLC PPSA85300 match; SharpProspero declares the alias as returning the descriptor. The object is a host anonymous file shared by its descriptors (a `memfd` on Linux, a delete-on-close temporary file mapped as an executable section on Windows), the alias is a duplicate descriptor, and every map maps the whole object, so writes through one view show through the others; a non-null address is a hint, as in `mmap` without `MAP_FIXED`. Not measured on the console: a length that is zero or not a multiple of 16 KiB and protection bits other than CPU read, write and execute are `EINVAL`; an alias or a map asking for more than the descriptor's maximum protection (write and execute counting as read) is `EACCES`, as FreeBSD 11's `vm_map_protect` treats a protection past `max_protection`; a descriptor that is not a JIT object is `EBADF`. The name is ignored, the objects do not count against the flexible memory budget, `mprotect` of a view is not limited to the maximum protection, and `mmap` of the descriptors maps the object as a [regular file](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp), so its views and the JIT views see each other's writes. On Linux the `memfd` is created without `MFD_EXEC`, which Linux 6.3 added and older kernels reject with `EINVAL`, and relies on the protection of the mapping: the flag only sets the file's execute permission bits, and the direct memory `memfd` is mapped executable the same way; a host with `vm.memfd_noexec` set to 2 refuses the creation and the call throws -- [kqueue and kevent](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) follow FreeBSD 11's [`kern_event.c`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/kern_event.c) (`kqueue_kevent`, `kqueue_register`, `kqueue_scan`, the timer and user filters) over the event queue; nothing here is taken from a console except what the kevent measurements entry lists. The descriptor is a host null device registered as the kqueue: `close` and `sceKernelClose` close the queue and `dup`/`dup2` of it throw, but `read`, `write` and `fstat` act on the null device instead of failing with `EOPNOTSUPP` or reporting `S_IFIFO`. Read and write events are supported on guest sockets only, plus `EVFILT_READ` on the read end and `EVFILT_WRITE` on the write end of a pipe (see the kevent on a pipe entry) and the timer, user and [vnode](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) filters and a read event on the host stream on descriptor 0, which never triggers, as `select` never reports descriptor 0 readable; PPSA34015 registers descriptor 0 for reading in a new kqueue at start-up and does not check the result; other filters except `EVFILT_AIO`, `EVFILT_LIO`, `EVFILT_SENDFILE` and `EVFILT_PROCDESC` (see the kevent measurements entry), `EV_CLEAR` on a socket or a pipe off Linux (see the kevent EV_CLEAR entry) and every change to an existing timer except `EV_DELETE` and the `EV_ADD` re-adds that the kevent measurements entry describes throw. A `NOTE_NSECONDS` period above one second with a fraction is kept to the nanosecond, where 11.0 converts the fraction with the microsecond macro. `EV_DISABLE`, `EV_DISPATCH` and `EV_FORCEONESHOT` follow kern_event.c: a disabled knote stays registered, stops polling or firing and is not reported, a trigger raised while it is disabled is kept and reported after `EV_ENABLE`, where `knote` would drop it, `EV_DISPATCH` reports once and then disables the knote, and `EV_FORCEONESHOT` is ignored on a new knote while on an existing one it sets `EV_ONESHOT` and fires the knote; a negative `nevents` fails with `EINVAL`. A new user event keeps its `fflags` unmasked and is not triggered by `NOTE_TRIGGER`, because `filt_userattach` reads the already cleared `kn_fflags`. The timer limit `kq_calloutmax` (`ENOMEM`) is not enforced, and closing a kqueue wakes a thread waiting on it in `kevent` with `EBADF`, where `kqueue_drain` makes the close wait for that thread. A level-triggered event is reported once per call and moved behind the other events, as in `kqueue_scan`; the `sceKernel*Equeue` calls keep their own collection loop. `NOTE_LOWAT` on a socket sets the low-water mark as FreeBSD 11's [`filt_soread` and `filt_sowrite`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/uipc_socket.c) do, and a later change of the knote replaces it as `kqueue_register` replaces `kn_sdata`: a read event fires once the buffered bytes reach the mark, or at end of file or on a listening socket. A read mark above 1 is taken on stream sockets only: on a datagram socket Linux's `FIONREAD` reports the next datagram where FreeBSD's `sbavail` counts every queued byte, so it throws. A write event fires once the send buffer's free space reaches the mark, as `filt_sowrite` does with `sbspace`; the host reports that space as `SO_SNDBUF` less `SIOCOUTQ` on Linux and as `SO_SNDBUF` alone elsewhere, so off Linux it does not shrink while data is queued. Marks below 1, which make FreeBSD's read filter always fire, throw +- [kqueue and kevent](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) follow FreeBSD 11's [`kern_event.c`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/kern_event.c) (`kqueue_kevent`, `kqueue_register`, `kqueue_scan`, the timer and user filters) over the event queue; nothing here is taken from a console except what the kevent measurements entry lists. The descriptor is a host null device registered as the kqueue: `close` and `sceKernelClose` close the queue and `dup`/`dup2` of it throw, but `read`, `write` and `fstat` act on the null device instead of failing with `EOPNOTSUPP` or reporting `S_IFIFO`. Read and write events are supported on guest sockets only, plus `EVFILT_READ` on the read end and `EVFILT_WRITE` on the write end of a pipe (see the kevent on a pipe entry) and the timer, user and [vnode](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) filters and a read event on the host stream on descriptor 0, which never triggers, as `select` never reports descriptor 0 readable; PPSA34015 registers descriptor 0 for reading in a new kqueue at start-up and does not check the result; other filters except `EVFILT_AIO`, `EVFILT_LIO`, `EVFILT_SENDFILE` and `EVFILT_PROCDESC` (see the kevent measurements entry), `EV_CLEAR` on a socket or a pipe on Windows (see the kevent EV_CLEAR entry) and every change to an existing timer except `EV_DELETE` and the `EV_ADD` re-adds that the kevent measurements entry describes throw. A `NOTE_NSECONDS` period above one second with a fraction is kept to the nanosecond, where 11.0 converts the fraction with the microsecond macro. `EV_DISABLE`, `EV_DISPATCH` and `EV_FORCEONESHOT` follow kern_event.c: a disabled knote stays registered, stops polling or firing and is not reported, a trigger raised while it is disabled is kept and reported after `EV_ENABLE`, where `knote` would drop it, `EV_DISPATCH` reports once and then disables the knote, and `EV_FORCEONESHOT` is ignored on a new knote while on an existing one it sets `EV_ONESHOT` and fires the knote; a negative `nevents` fails with `EINVAL`. A new user event keeps its `fflags` unmasked and is not triggered by `NOTE_TRIGGER`, because `filt_userattach` reads the already cleared `kn_fflags`. The timer limit `kq_calloutmax` (`ENOMEM`) is not enforced, and closing a kqueue wakes a thread waiting on it in `kevent` with `EBADF`, where `kqueue_drain` makes the close wait for that thread. A level-triggered event is reported once per call and moved behind the other events, as in `kqueue_scan`; the `sceKernel*Equeue` calls keep their own collection loop. `NOTE_LOWAT` on a socket sets the low-water mark as FreeBSD 11's [`filt_soread` and `filt_sowrite`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/uipc_socket.c) do, and a later change of the knote replaces it as `kqueue_register` replaces `kn_sdata`: a read event fires once the buffered bytes reach the mark, or at end of file or on a listening socket. A read mark above 1 is taken on stream sockets only: on a datagram socket Linux's `FIONREAD` reports the next datagram where FreeBSD's `sbavail` counts every queued byte, so it throws. A write event fires once the send buffer's free space reaches the mark, as `filt_sowrite` does with `sbspace`; the host reports that space as `SO_SNDBUF` less `SIOCOUTQ` on Linux and as `SO_SNDBUF` alone elsewhere, so off Linux it does not shrink while data is queued. Marks below 1, which make FreeBSD's read filter always fire, throw - [EVFILT_VNODE, sceKernelAddFileEvent and sceKernelDeleteFileEvent](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) (libkernel) watch the host file or directory behind the descriptor and turn host changes into FreeBSD 11 vnode hints, which [`filt_vfsvnode`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/vfs_subr.c#L5032) adds whole when they share a bit with the requested `fflags`: a write that grows a file is `NOTE_WRITE | NOTE_EXTEND`, another write `NOTE_WRITE`, a shrinking truncate or an attribute change `NOTE_ATTRIB`, a higher link count `NOTE_LINK`, a lower one or a pending delete `NOTE_DELETE`, a new path `NOTE_RENAME`; in a directory, an entry created, removed or renamed is `NOTE_WRITE`, with `NOTE_LINK` for a subdirectory, and on Linux an entry moved from or to another directory adds `NOTE_EXTEND`; inotify queues the two halves of a rename one after the other, so a watcher woken by the first can read it alone, and a half read without the other is classified only after a `getdents64` on the watched directory, which takes the directory lock that `rename` holds until both halves are queued ([`do_renameat2`](https://github.com/torvalds/linux/blob/v6.8/fs/namei.c#L4975) around `vfs_rename` and its `fsnotify_move`, [`iterate_dir`](https://github.com/torvalds/linux/blob/v6.8/fs/readdir.c#L103)), an ordering taken from the kernel source, not from inotify(7), which promises none; a directory registered with `NOTE_EXTEND` on Windows is accepted, as `vfs_kqfilter` does not check it, but never receives it, because the directory handle cannot tell a move between directories from a removal plus an addition. FreeBSD's hints depend on the filesystem and the console's filesystems are not known: a growing `ftruncate` reports `NOTE_WRITE | NOTE_EXTEND` where `vop_setattr_post` reports `NOTE_ATTRIB`, and on Windows `utimes` on a file reports `NOTE_WRITE`. The three Windows notifications are told apart in the error, which names the `parent`, `entries` or `subdirectories` watch, the path it was given and `GetLastError`, but a `FindFirstChangeNotification` that fails still throws rather than returning an error to the guest: the EVFILT_VNODE attach path is [`vfs_kqfilter`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/vfs_subr.c#L4936), which returns `EINVAL`, `ENOMEM` or 0 after `fget` has turned a descriptor that is not open into `EBADF`, and consults no directory permission, so no FreeBSD errno stands for a host that refuses to notify, and what the console does on such a host is not measured. Only the `parent` watch can fail this way in practice: `entries` and `subdirectories` need the `FILE_LIST_DIRECTORY` that opening the watched directory already took. The parent handle produces no hint of its own, as `RunHostWatch` only calls `FindNextChangeNotification` on it, so a later change may leave the watch on its 50 ms poll instead of throwing; that is not done here. Windows uses `FindFirstChangeNotification` on the parent directory (and on a watched directory itself) and compares `GetFileInformationByHandleEx` snapshots at least every 50 ms, because a write through an open handle is reported only when the handle is flushed or closed, so file hints can arrive 50 ms late, and a file deleted while watched, which the POSIX delete of Windows 10 1903 moves into `$Extend\$Deleted` until its last handle closes, reports `NOTE_DELETE` without `NOTE_RENAME` and keeps its handle without a parent directory watch, as that directory cannot be watched; a shrinking truncate of a watched file can also report `NOTE_WRITE` on Windows, because NTFS can show the truncate's new `LastWriteTime` before its new size (2 to 12 times per million back-to-back shrinking `SetEndOfFile` calls on a loaded machine, also through a single `NtQueryInformationFile(FileAllInformation)`), and a snapshot taken in between sees a write at the old size; Linux uses inotify on the file and is not run. `NOTE_REVOKE`, `NOTE_OPEN`, `NOTE_CLOSE`, `NOTE_CLOSE_WRITE`, `NOTE_READ`, unknown bits and descriptors other than files, directories, pipes and sockets throw. `sceKernelAddFileEvent(eq, fd, watch, udata)` and `sceKernelDeleteFileEvent(eq, fd)` take the [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/0a1aaf9d/include/orbis/libkernel.h#L35) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0/src/SharpProspero/Interop/Kernel/KernelEqueue.cs#L164) signatures; `watch` is taken as the `fflags` and the event is added with `EV_CLEAR`, neither confirmed on a title (feat(libkernel): implement kevent EV_DISABLE, EV_DISPATCH and EV_FORCEONESHOT) (fix(libkernel): follow kqueue_register for EV_FORCEONESHOT and modify flags) - [sem_open, sem_close, sem_unlink](../../../core/libs/prx/libkernel/Pthread/Posix/Sem.cpp) (libkernel) keep named semaphores in a table inside the process; FreeBSD 11.4's [`sem_new.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/sem_new.c#L138-L308) backs each with a file `/tmp/SEMD`. A semaphore lasts until `sem_unlink` and its last `sem_close` but is not shared with other processes, and the mode is ignored (no permission errors, as for a root caller). Whether a title can create files under `/tmp` on the console is unverified. A name with a further `/` throws, as it would resolve below `/tmp`, and so does an initial value above `SEM_VALUE_MAX`, which FreeBSD stores unchecked. - [sem_timedwait](../../../core/libs/prx/libkernel/Pthread/Posix/Sem.cpp) (libkernel) consumes an available count before checking the timeout and waits indefinitely for a null timeout, following FreeBSD 11's `sem_timedwait`. A payload process on a PS5 (firmware 13.60) gave the same results: with a count available, a null timeout, `tv_nsec` of -1 or 1000000000 and a past deadline all returned 0 and took the count; with none, an invalid `tv_nsec` failed with `EINVAL`, and a past deadline or a negative `tv_sec` with `ETIMEDOUT`. One title (PPSA29866) disassembled for this entry has one call site (0x130916), in a wait helper that reads the clock with id 0 into a zeroed 16-byte time structure, adds the milliseconds multiplied by 1,000,000 to the nanoseconds and normalises them below 1e9, so its deadline is never null. It repeats the wait with the same deadline when the error is 4, returns false for any other error and when reading the clock fails, and does not tell a timeout from other errors. This title therefore gives no evidence on null or invalid timeouts. @@ -184,7 +190,8 @@ - [sceKernelMapDirectMemory with execute](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - a protection with CPU execute (bit 0x4) returns `EINVAL` (0x80020016) and maps nothing, and `sceKernelMprotect` grants execute afterwards, as [PS5_PayloadSDK measured](https://github.com/mihawk-99/PS5_PayloadSDK/blob/3e31e0b1b68ada28beca68adc480cf0c3d17b9df/platform/docs/PROBE.md#L28-L34) in a title on firmware 12.09 for read-execute and read-write-execute. `sceKernelMapDirectMemory2`, `sceKernelMapNamedDirectMemory`, and `sceKernelBatchMap` and `sceKernelBatchMap2` with operation 0 share the check here, which is not measured for them, nor for a title with more privileges than a homebrew one. Of 40 `sceKernelMapDirectMemory` calls in ten titles checked, 35 pass a constant protection without execute and five read it from a register or field (one each in PPSA01494, PPSA21564 and PPSA30839, two in PPSA18194). - [sceKernelMapDirectMemory over unallocated pages](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - mapping a physical range that includes a page `sceKernelAllocateDirectMemory` has not allocated returns `ENOMEM` (0x8002000c) and maps nothing, as [shadPS4's `MemoryManager::MapMemory`](https://github.com/shadps4-emu/shadPS4/blob/82eb7cdf640990d2f5a2a3fa03b489f2b287b17e/src/core/memory.cpp#L543-L551) does; `sceKernelMapDirectMemory2`, `sceKernelMapNamedDirectMemory` and the map operation of `sceKernelBatchMap` go through the same check. The console's result is not measured. - [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 -- [pthread_getaffinity_np](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - answers a set of 8 or 16 bytes and returns `ERANGE` for 32 bytes or more, as [PS5_PayloadSDK measured](https://github.com/mihawk-99/PS5_PayloadSDK/blob/3e31e0b1b68ada28beca68adc480cf0c3d17b9df/platform/docs/PROBE.md#L287-L297) in a title on firmware 12.09; FreeBSD's 32-byte `cpuset_t` is therefore refused. Every other size throws, since none was measured. `pthread_setaffinity_np` and `cpuset_getaffinity` keep FreeBSD's 32-byte limit, which is not measured for them. A non-null mask pointer that cannot be read or written returns `EFAULT` (`-1` with errno 14 for `cpuset_*`, 14 for `pthread_*_np`) as a host-safety fallback; the console's answer for a non-null invalid pointer was not measured +- [pthread_getaffinity_np](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - a set of 17 to 23 bytes throws, since the console's answer is not measured. As [rooklz measured](https://github.com/boykopovar/AnyPS5/pull/9156#issuecomment-6391607043) on a PS5, matching [PS5_PayloadSDK](https://github.com/mihawk-99/PS5_PayloadSDK/blob/3e31e0b1b68ada28beca68adc480cf0c3d17b9df/platform/docs/PROBE.md#L287-L297), a set of 8 to 16 bytes is answered and zero-filled past byte 8, and one below 8 bytes or from 24 returns `ERANGE`. A non-null mask pointer that cannot be read or written returns `EFAULT` (`-1` with errno 14 for `cpuset_*`, 14 for `pthread_*_np`) as a host-safety fallback; the console's answer for a non-null invalid pointer was not measured +- [pthread_setaffinity_np, cpuset_getaffinity and cpuset_setaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - their set sizes still follow FreeBSD's 32-byte `cpuset_t`, not the console: `pthread_setaffinity_np` takes only 32 bytes, where [the console](https://github.com/boykopovar/AnyPS5/pull/7135#issuecomment-6390891591) took 8 and 16 and returned `ERANGE` for 32, and `cpuset_getaffinity` and `cpuset_setaffinity` accept up to 32 bytes, where [the console](https://github.com/boykopovar/AnyPS5/pull/9156#issuecomment-6391607043) returns `ERANGE` from 24 - 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; an unwritable or null status buffer pointer returns `EFAULT` (`0x8002000e`) as a host-safety fallback, and console behaviour for unwritable memory has not been measured. 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 @@ -195,13 +202,14 @@ - [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 total length above `SSIZE_MAX` fails with `EINVAL` before the descriptor is looked up, and the positioned forms, `pread` and `pwrite` included, report `EBADF` for a descriptor not open in that direction, then `ESPIPE` for a pipe, socket or kqueue, before `EINVAL` or `EFAULT`, as [`kern_preadv`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/sys_generic.c) does. FreeBSD passes a negative offset on a character device to the driver; here it still returns `EINVAL`. A Linux `process_vm_readv` failure other than `EFAULT` throws, including when the host denies that API. - [macOS directory entries](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) - `sceKernelGetdirentries` seeks to 0 and reads the whole directory again through the undocumented `__getdirentries64` on every call, then skips the entries already returned, so paging through a directory of n entries reads it O(n²) times. The descriptor offset and `*basep` are entry indexes, not host directory cookies, so a host `readdir` or `lseek` on the same descriptor lands on the wrong entry Because the offset is an entry index into a fresh listing, an entry removed between two calls shifts later entries down and one of them is skipped, and an entry created between two calls can be returned twice; FreeBSD uses the file system's directory cookie, which does not move. A title that reads a batch, deletes it and reads again can leave entries behind - [/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 +- [Guest thread x87 control word](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - not measured on a console, and the initial thread keeps the control word the host starts it with (`0x27f` on Windows): a new guest thread starts with `0x037f` (64-bit precision, every exception masked) whatever its creator uses. That is FreeBSD 11's start state, whose [`cpu_copy_thread`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/amd64/amd64/vm_machdep.c#L428-L446) leaves a new thread's FPU state uninitialized so that its first FPU instruction loads `fpu_initialstate` (`__INITIAL_FPUCW__`), and the control word of the libSceFiber option that starts fibers with the default FPU state - [pthread_attr_getinheritsched, pthread_attr_setinheritsched, pthread_attr_getscope, pthread_attr_setscope, pthread_attr_getstackaddr](../../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) (libkernel) - every guest thread is a host thread at system scope, so `PTHREAD_SCOPE_PROCESS` is stored and reported (`pthread_attr_get_np`, `scePthreadAttrGet`) but changes nothing. Measured in a payload process on a PS5 (firmware 13.60), as FreeBSD 11 libthr ([thr_attr.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libthr/thread/thr_attr.c)) does: the scope defaults to 2, only 0 and 2 are accepted (`EINVAL` otherwise), an inherit value other than 4 or 0 returns `ENOTSUP`, `pthread_attr_getstackaddr` returns the configured pointer (null by default), and a null output, a null, uninitialized or destroyed attribute return `EINVAL`. A thread keeps the scope it was created with, taken from its creator under `PTHREAD_INHERIT_SCHED` - [Shared memory](../../../core/libs/prx/libkernel/File/src/SharedMemory.cpp) (libkernel) on Linux implements anonymous and named objects, bounded descriptor writes and seeks, and `shm_unlink` with effective read/write permission checks from FreeBSD 11. Named paths up to 1023 bytes use `/anyps5-shm-` plus SHA-1; collisions are not detected, and the namespace is shared across processes in the host mount namespace. Named creation applies the guest umask without changing the host mask. Linux procfs and `/dev/shm` are required. Operations use an advisory directory lock; external host operations bypass it, unrelated objects serialize, and descriptor operations require two spare host descriptors and compare device/inode with the initial descriptor identity on both the pinned-object and ordinary-file fallback paths. [Guest mappings](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) support shared and private mappings of these descriptors through the allocation tracker and pin a duplicate descriptor during validation and mapping; mapping requires one spare host descriptor; file-backed mappings use the write-watch scan fallback because aliases and descriptor writes bypass per-mapping tracking. On Windows, anonymous objects are delete-on-close temporary files and `mmap` maps them as [regular files](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp); named objects are described in the next entry. Invalid non-null name pointers, console behaviour and title compatibility remain unverified. - [shm_open, shm_unlink](../../../core/libs/prx/libkernel/File/src/SharedMemory.cpp) (libkernel, Windows) - named objects are regular files in the current user's temporary directory under `AnyPS5/shm`, identified by SHA-1 of the exact guest name. The namespace is per host user and shared by every AnyPS5 process using that temporary root, so two titles using the same name share the object. Namespace operations use a cross-session named mutex, while file-size changes and writes use a mutex keyed by the host file identity; unlink renames the visible object to a unique internal name so already-open handles remain usable while the guest name can be recreated. Orphaned files are reaped when the last tracked guest descriptor closes or opportunistically during a later open. Closing through host stdio wrappers does not update this bookkeeping or reap immediately. The namespace directory has a protected ACL for the owner, SYSTEM and Administrators; newly created objects map the guest owner read/write bits after applying the guest creation mask. `fchmod` updates those owner permissions on the descriptor's file object. Guest group/other permission classes are not represented independently on Windows. The owner's read permission also grants execute, so a read-only descriptor can be mapped `PROT_EXEC` as FreeBSD 11.4's `shm_mmap` allows; objects created by earlier builds lack it until `fchmod`. Mappings take a regular file's checks and paths (`guest_named_shm_windows_mapping`). No behavior here has been measured on PS5. Lifecycle, cross-process resize/write, and mode behavior are exercised by Windows tests. See Microsoft's [file sharing modes](https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea), [DeleteFile](https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-deletefilew) and [mutex objects](https://learn.microsoft.com/en-us/windows/win32/sync/mutex-objects). - [sceKernelAddReadEvent and sceKernelAddWriteEvent](../../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel): a read event on a listening socket, here and in `kevent`, reports the number of completed connections waiting to be accepted in `data`, as FreeBSD 11.4's [`filt_solisten`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L3384-L3392) reports `so_qlen`: on Linux a TCP listener's accept queue from `TCP_INFO` (`tcpi_unacked`, which Linux fills with the accept backlog for a listening socket), and elsewhere, or for a listener that is not TCP, 1 while a connection is waiting, as no host call reports that count. A write event reports the free space of the socket's send buffer in `data`, as FreeBSD 11.4's [`filt_sowrite`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L3358-L3382) reports `sbspace`: on Linux the host's `SO_SNDBUF` less the bytes `SIOCOUTQ` counts as queued, which include the host's per-packet overhead, so the space shrinks by somewhat more than the bytes sent. Windows has no query for that space and macOS is not wired up, so both report the whole `SO_SNDBUF`, which does not shrink as data is queued. Only guest sockets are supported; other descriptors and a low watermark above 1 throw - [sceKernelAddReadEvent on pipes](../../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel) is an exception to the sockets-only rule above: it also takes the read end of a pipe, as `kevent` does through FreeBSD's `filt_piperead`, and ignores the size argument there. Host streams, other descriptors and a write event on a pipe throw - [sceKernelAddReadEvent and sceKernelAddWriteEvent on regular files](../../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel) are the other exception: a size argument above 1, directories and other vnode types throw, and whether the console's file systems answer as `vfs_kqfilter` does was not measured. A wait holding one of these events polls it every millisecond, as no host call reports every file change, so a write through another descriptor shows up to 1 ms late. Unlike FreeBSD, a seek can make a read event fire again: FreeBSD 11.4's [`kqueue_scan`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/kern_event.c) deactivates a level-triggered read knote once `filt_vfsread` reports `data` 0, and only a vnode `KNOTE` (a write, an extend or a truncate) activates it again; `lseek` posts none, so after a file read to its end is seeked back, the 1 ms poll here reports the new `data` (10 for a 10-byte file seeked to 0) where FreeBSD keeps waiting until the file changes. Otherwise they follow FreeBSD 11.4's [`filt_vfsread` and `filt_vfswrite`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_subr.c#L5122-L5168): a read event reports the size less the descriptor's offset in `data` and fires while that is not 0; a write event always fires with `data` 0 -- [close](../../../core/libs/prx/libkernel/File/src/Stdio.cpp), [setsockopt and getsockopt](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) reach a [libSceNet](../../../core/libs/prx/libSceNet/Export.cpp) socket through hooks libSceNet registers, as `select` and `poll` poll them and `fcntl` answers `F_GETFL` and `F_SETFL`, failing other commands with `EINVAL`; the other libkernel socket calls do not. libSceNet numbers its sockets from 32 in its own table, so a CRT file descriptor and a socket can share a number; `close` then closes the socket. On the console both are one kernel descriptor space +- [close](../../../core/libs/prx/libkernel/File/src/Stdio.cpp), [setsockopt, getsockopt, bind, getsockname, sendto and recvfrom](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) reach a [libSceNet](../../../core/libs/prx/libSceNet/Export.cpp) socket through hooks libSceNet registers, as `select` and `poll` poll them and `fcntl` answers `F_GETFL` and `F_SETFL`, failing other commands with `EINVAL`; the other libkernel socket calls do not. Those four validate as libkernel does, then run libSceNet's path: `sendto` to port 0 lacks libkernel's answer, a destination on a connected stream is not ignored, an over-long IPv4-mapped `bind` is refused, and stream `MSG_WAITALL` is a loop of receives. libSceNet numbers its sockets from 32 in its own table, so a CRT file descriptor and a socket can share a number; `close` then closes the socket. On the console both are one kernel descriptor space - [sceKernelPread, sceKernelPwrite, sceKernelPreadv, sceKernelPwritev](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) on Windows transfer with an overlapped `ReadFile`/`WriteFile` and restore the descriptor's file pointer afterwards. Guest scalar transfers, positioned transfers, vectored transfers and seeks share a position lock across duplicated descriptors; a vector holds it for the whole operation. Independently opened descriptors and other processes do not share that lock, so vector transfers over their overlapping file ranges are not coordinated. Handles that are not disk files return `ESPIPE`. A pipe or console `readv` can return a short read when no more input is available; each iovec is transferred separately, so a failure after the first transfer returns the bytes already done - [sceKernelPwrite, pwrite, sceKernelPwritev, pwritev](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) and the [AIO write commands](../../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) on macOS can leave the descriptor, or one duplicated from it, with the wrong file offset when another thread seeks, transfers or writes with an offset on it at the same time. On an `O_APPEND` regular file they append with `writev` and then seek back to the previous offset, because macOS `pwrite` writes at the given offset even under `O_APPEND`, where FreeBSD appends and leaves the offset unchanged. No lock covers the write and the seek back, so a concurrent transfer or seek can observe the end-of-file offset or have its own offset change undone, and two such writes at once can leave the offset at an earlier end of file - [sceKernelAioSubmitReadCommands, sceKernelAioSubmitWriteCommands and their Multiple variants](../../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) on Windows transfer through the same positioned path as `sceKernelPread`/`sceKernelPwrite` and restore the descriptor's file pointer afterwards; positioned transfers, plain transfers and seeks of one descriptor are serialized per descriptor (shared across duplicated descriptors), so a concurrent transfer observes a stable file pointer @@ -211,7 +219,7 @@ - [ktimer_create, ktimer_settime, ktimer_gettime, ktimer_getoverrun, ktimer_delete](../../../core/libs/prx/libkernel/Time/Ktimer.cpp) (libkernel) - a `SIGEV_SIGNAL` or `SIGEV_THREAD_ID` timer, and one created with a null `sigevent` (FreeBSD defaults it to `SIGEV_SIGNAL`), throws, because delivering a guest signal from a timer is not modelled. A non-zero value under one tick rounds up to 1 ms, as FreeBSD 11 does at hz 1000; the console's hz is not measured (`kern.clockrate` is unanswered). `SIGEV_NONE` timers on `CLOCK_REALTIME` and `CLOCK_MONOTONIC` follow `kern_time.c`: ids 3 to 31 (`EAGAIN` when full), `EINVAL` for another clock, a bad notify type or signal, an id that is not live or a bad `itimerspec` field, and `TIMER_ABSTIME` (1). Expiry is worked out from the guest clock when the timer is read, and `ktimer_getoverrun` returns 0, as FreeBSD only updates it when a signal is accepted - [The sandbox CA list](../../../core/libs/prx/libkernel/File/src/SandboxCertificates.cpp) (libkernel), `//common/cert/CA_LIST.cer`, `/system/common/cert/CA_LIST.cer` and `/common/cert/CA_LIST.cer`, is a PEM bundle of the host's trust anchors, written once per process to `cert_cache/CA_LIST.cer` in the run directory with the owner write bit cleared: on Windows the current user's `ROOT` store, filtered through Disallowed and the combined EKU extension/property for TLS server authentication (Windows fetches missing roots on demand, so a root that was never fetched is absent), elsewhere `SSL_CERT_FILE` or the first of `/etc/ssl/certs/ca-certificates.crt`, `/etc/pki/tls/certs/ca-bundle.crt`, `/etc/ssl/ca-bundle.pem` and `/etc/ssl/cert.pem` that exists. On macOS, `/etc/ssl/cert.pem` is a static snapshot and does not reflect roots added to or distrusted in the Keychain. Existing guest files, explicit aliases and blocked mounts take precedence over the provider. The PEM snapshot does not carry Windows CTL restrictions beyond EKU, native revocation policy or later trust-store changes. Without any certificate the path does not exist. Whether the console serves a file at `/system/common/cert/CA_LIST.cer` or `/common/cert/CA_LIST.cer` is unverified: the only evidence for the two is that PPSA99058 opens them for its certificate store and networking works once they resolve, which shows a call site and not a console listing, so a title that tries several locations may take a branch here that it never takes on the console (`/system/common/` is at least a prefix `sceKernelLoadStartModule` already maps, for `/system/common/lib/`; nothing shows that a sandboxed process can open `/common/cert/` at the sandbox root). The console file's format is unverified: only that bundled OpenSSL accepts a PEM bundle there is shown (ProsperoRadio PPSA99001 and SwanStation PPSA98510, #5001). The other entries of the console's `common/cert` directory, the directory itself (it does not exist here) and the error a write open gets (`EACCES` from the host here) are unverified - [pselect](../../../core/libs/prx/libkernel/Socket/src/Pselect.cpp) (libkernel) sets the signal mask, calls `select` and restores the mask, as FreeBSD 11 `kern_pselect` does. A pending signal that the new mask unblocks has its handler run during the swap, and `pselect` then returns `EINTR` unless a descriptor is already ready, the timeout is zero or an argument is invalid, as FreeBSD's `kern_select` decides before it sleeps. A signal raised during the wait, including one sent by another thread, does not interrupt it, because `select` has no `EINTR` path; and the mask is the process-wide one, so it applies to every thread for the duration of the call -- [getcontext and setcontext](../../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) and [makecontext](../../../core/libs/prx/libSceLibcInternal/UserContext.cpp) (libSceLibcInternal) use FreeBSD 11's amd64 `ucontext_t` (`mcontext` at 0x10, `mc_len` 800, `uc_link` at 0x330, `uc_stack` at 0x338, 0x370 bytes), the layout homebrew built against FreeBSD's headers passes: PPSA99039's OpenSSL `ASYNC` (`crypto/async/arch/async_posix.c`) captures into a 0x370-byte stack buffer. `getcontext` writes the 0x330 bytes before `uc_link`, as FreeBSD's `UC_COPY_SIZE`. The console's own layout for these functions is not verified; the exception handler's context is a different one (see `sceKernelRaiseException`) +- [getcontext and setcontext](../../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) and [makecontext](../../../core/libs/prx/libSceLibcInternal/UserContext.cpp) (libSceLibcInternal) use FreeBSD 11's amd64 `ucontext_t` (`mcontext` at 0x10, `mc_len` 800, `uc_link` at 0x330, `uc_stack` at 0x338, 0x370 bytes), as PPSA99039's OpenSSL `ASYNC` passes. `getcontext` writes and `setcontext` reads the 0x330 bytes before `uc_link` (`UC_COPY_SIZE`). Unwritable or unreadable pointers fail with `EFAULT` (14) via checked guest copies (`copyout`/`copyin` on FreeBSD); on Linux these make `process_vm_writev`/`process_vm_readv` syscalls instead of plain memory copies, a swap making two. The console's own layout is unverified; exception contexts differ (see `sceKernelRaiseException`). - [ksem_init, ksem_post, ksem_wait, ksem_trywait, ksem_timedwait, ksem_getvalue and ksem_destroy](../../../core/libs/prx/libkernel/Semaphore/src/Ksem.cpp) (libkernel) - `dup` of a ksem id, or `dup2` from one, throws, where FreeBSD shares the semaphore, and a guest signal does not interrupt a wait with `EINTR`. Otherwise they follow FreeBSD 11 `uipc_sem.c`: each semaphore is kept in a table keyed by a descriptor reserved on the host null device, as a ksem id is a descriptor there; a guest `close`, or `dup2` over the id, drops it through libkernel's close notification; `ksem_init` fails with `ENOSPC` when no descriptor is left or 30 semaphores (`SEM_MAX`) exist - [getcontext, setcontext and swapcontext](../../../core/libs/prx/libkernel/System/src/UserContext.cpp) (WebAssembly) throw because switching guest x86-64 register contexts on a WebAssembly host is not implemented - [Guest memory mappings](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (WebAssembly) throw for file-backed or shared mappings and for mapping one direct-memory range at two addresses; WebAssembly linear memory has no host mapping primitive for these aliases @@ -238,8 +246,8 @@ - [scePthreadCreate and scePthreadDetach, measured](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - as in a payload process on a PS5 (firmware 13.60): a name of 32 bytes or more returns `ENAMETOOLONG` (`0x8002003f`) without creating a thread and sets the handle to 0 (32 and 33 tried), 31 is kept whole, and a null name gives an empty one; detaching a joined thread returns `ESRCH`; detaching a thread another thread is joining, or a detached one, returns `EINVAL`, as does joining a detached one; a second concurrent join returns `EOPNOTSUPP`; a null thread returns `EINVAL` from both; a null handle pointer to `scePthreadCreate` faulted and throws here; `pthread_create_name_np` goes through `scePthreadCreate` and so also clears the handle on a long name, but only the sce form was measured - [Thread cancellation](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - PS5 kernel wakeup on mid-sleep cancellation remains unmeasured. [Firmware 13.42 disassembly and emulated checks](https://github.com/boykopovar/AnyPS5/pull/8916#issuecomment-6416760904) support the SCE cancellation checks before and after sleep. Long SCE waits still do not wake for cancellation here; six deferred tests pin that model until measured. POSIX `usleep`/`nanosleep`/`_nanosleep` wake on enabled cancellation only above 1 ms; shorter waits and short SCE waits retain the GPU wake path. POSIX `sleep` wakes on enabled cancellation. Requests act only in `scePthreadTestcancel`, condition-variable, semaphore, join and sleep waits and their POSIX names; file/socket calls are not cancellation points. States/types 0/1 and 0/2, canceled value 1 and cleanup layout follow FreeBSD libthr, not console measurements. Asynchronous cancellation acts like deferred except at the two set calls. - [Cancellation wait registration](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - cancellation of a wait registered before thread exit still waits for the target to unregister, including while cancellation is disabled. A blocked handler or self-cancellation inside the interrupted wait can prevent the requester from returning. Nested waits do not restore the outer registration. If Windows waiter allocation throws after registration, `cancelWait` remains set and a join retains its `_joining` claim. Runtime-created thread exit skips C++ stack unwinding, so destructors alone cannot clean up registrations. These host progress limitations are not console measurements. -- [cpuset_setaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - the calling thread, another live guest thread selected by its guest TID, or the calling process is handled; other levels (ROOT/CPUSET) and other scopes (CPUSET/JAIL) throw as unmodelled, invalid levels or scopes return `EINVAL` and unknown thread or process IDs return `ESRCH`, following [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L1281-L1286) thread selection. Bits outside `sceKernelGetAvailableCpumask` are dropped, and a mask with none of those bits fails with `EDEADLK`, as FreeBSD does when the new set has no CPU in common with the parent set. Guest-TID lookup and the update hold the thread registry lock so a join cannot release the target during access. Affinity is recorded for guest CPU reporting and child inheritance, not applied to host scheduling; PS5 behaviour has not been measured. A non-null mask pointer that cannot be read or written returns `EFAULT` (`-1` with errno 14 for `cpuset_*`, 14 for `pthread_*_np`) as a host-safety fallback; the console's answer for a non-null invalid pointer was not measured -- [cpuset_getaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - the calling thread, another live guest thread selected by its guest TID, or the calling process is handled; other levels (ROOT/CPUSET) and other scopes (CPUSET/JAIL) throw as unmodelled, invalid levels or scopes return `EINVAL` and unknown thread or process IDs return `ESRCH`, following [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L426-L437) guest-TID selection, not measured on PS5. For the process the mask is the union of the affinities of the live guest threads, as [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L1150-L1156) ORs the mask of every thread in the process; threads the guest did not create through libkernel are not included, and the console result is unverified. A non-null mask pointer that cannot be read or written returns `EFAULT` (`-1` with errno 14 for `cpuset_*`, 14 for `pthread_*_np`) as a host-safety fallback; the console's answer for a non-null invalid pointer was not measured +- [cpuset_setaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - Console behavior remains unverified; affinity is bookkeeping for guest CPU reporting and inheritance, not host scheduling. ROOT/CPUSET setters, numbered sets, jails, the WHICH base setter and interrupts throw. [`kern_cpuset_setaffinity`](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L1206-L1314) supplies level/scope error ordering. FreeBSD rejects any bit outside the parent set and the console returns `EPERM` for a CPU outside the process's set (rooklz, #7135); here the mask is clipped to `sceKernelGetAvailableCpumask` and 0 returned, with `EDEADLK` when none remain. Sizes 17 to 23 throw (unmeasured). A non-null mask pointer that cannot be read or written returns `EFAULT` (`-1` with errno 14 for `cpuset_*`, 14 for `pthread_*_np`) as a host-safety fallback; the console's answer for a non-null invalid pointer was not measured +- [cpuset_getaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - The title's base set at CPU_LEVEL_CPUSET and CPU_WHICH_CPUSET id -1 is assumed to be `sceKernelGetAvailableCpumask`, unverified on a console. CPU_LEVEL_ROOT answers `0xffff`, as a PS5 payload process read it (rooklz, #7135 comment 6390891591); a title's root set was not measured (PRWB00001 asks for that level). Numbered sets, jails and WHICH interrupt/domain masks throw. [`kern_cpuset_getaffinity`](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L1088-L1187) supplies target/level error ordering and process-mask union; threads not created through libkernel are missing from that union. A non-null mask pointer that cannot be read or written returns `EFAULT` (`-1` with errno 14 for `cpuset_*`, 14 for `pthread_*_np`) as a host-safety fallback; the console's answer for a non-null invalid pointer was not measured - [pthread_setaffinity_np](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - follows [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_affinity.c#L41-L62) libthr over `kern_cpuset_setaffinity` for the calling thread and any live guest thread: a size other than the 32-byte `cpuset_t` returns `ERANGE`, an unknown or finished thread `ESRCH`. Bits outside `sceKernelGetAvailableCpumask` are dropped and a mask with none of those bits returns `EDEADLK`, as `cpuset_setaffinity` does; the affinity is recorded on the guest thread and not applied to the host thread, and the console result is unverified. A non-null mask pointer that cannot be read or written returns `EFAULT` (`-1` with errno 14 for `cpuset_*`, 14 for `pthread_*_np`) as a host-safety fallback; the console's answer for a non-null invalid pointer was not measured - [dup and dup2](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) throw for the combinations they do not model: `dup2` of a guest socket onto descriptors 0 to 2, whose host streams libc writes without the socket table. Guest sockets share one socket object between the duplicates; the numbers come from the host descriptor table like those of files. The errno values for descriptor exhaustion are not verified against the console. On Windows a guest `dup2` onto descriptor 1 or 2 keeps host logging on an `_fdopen` of a duplicate, and the guest `FILE` stays on the descriptor that was replaced; opening that duplicate throws (#7791) - [waitpid and wait](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) reaps [posix_spawn](../../../core/libs/prx/libSceLibcInternal/Spawn.cpp) children, whose guest process IDs are the host's. On Linux the host call runs: the guest signal numbering applies in bits 8-15 for a stopped child and bits 0-6 for a killed one (7, 10, 12, 17, 18, 19, 20, 23, 29, 31 to 10, 30, 31, 20, 19, 17, 18, 16, 23, 12; Linux 16 and 30 are unmapped and throw), a continued status becomes 0x13, and guest `WCONTINUED` 4 becomes Linux 8. On Windows an unrecognized exit code keeps the guest `exit` byte layout, so `exit(-1)` reads as 255, and a structured-exception code maps to `SIGSEGV`, `SIGILL`, `SIGTRAP`, `SIGFPE` or `SIGABRT` (access violation/stack overflow, illegal instruction, breakpoint, division, fastfail). Windows has no stop/continue status; unknown `options` bits fail with `EINVAL`. Without an unreaped matching child, even after another thread reaps it, the call gives `ECHILD`. @@ -273,14 +281,15 @@ - [send, sendto](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - send and sendto without an address on an unconnected datagram socket map Windows `WSAENOTCONN` to FreeBSD's `EDESTADDRREQ` (39). - [bind, connect, sendto](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - a connected stream socket's `sendto` ignores the address once its length passes, as FreeBSD 11.4's `tcp_usr_send` does, which was not measured. Otherwise they check a socket address as a payload process on a PS5 (firmware 13.60) did on datagram and stream sockets: a length below 2 fails with `EINVAL` and above 255 with `ENAMETOOLONG`; then an `AF_INET` socket needs exactly 16 bytes and an `AF_INET6` socket 28, or `EINVAL`, before the family is looked at (an `AF_INET6` address of 28 or 10 bytes on an `AF_INET` socket fails with `EINVAL`); then another family, `AF_UNSPEC` with `INADDR_ANY` included, fails with `EAFNOSUPPORT`; and `connect` on a listening stream socket fails with `EOPNOTSUPP` - [bind, connect](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) on an `AF_INET6` datagram socket with `IPV6_V6ONLY` cleared accept an IPv4-mapped address longer than 28 bytes and use its first 28, as FreeBSD 11.4's `udp6_bind` and `udp6_connect` hand it to `in_pcbbind` and `in_pcbconnect` as a `sockaddr_in` without the length test of `in6_pcbbind`; `sendto` still fails it with `EINVAL`, as `udp6_send` tests the length first. The console was measured only with `::1` and `IPV6_V6ONLY` at its default, so this case is not measured -- [read, write and vector I/O](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) on a guest socket use `recv` and `send` without flags; `sceKernelReadv` and `sceKernelWritev` gather or scatter through one socket operation, preserving stream short reads and datagram boundaries. The positioned `pread`, `pwrite`, `preadv` and `pwritev` forms, and their `sceKernel` forms, return `ESPIPE` for an open guest socket and `EBADF` for a closed one. `EPIPE` from a write raises the host's `SIGPIPE` on Linux unless the socket has `SO_NOSIGPIPE` set, and the guest cannot ignore it: `signal(SIGPIPE, SIG_IGN)` and `sigaction` record the disposition, as they do for every number from 1 to 128, but only the six signals with host equivalents (2, 4, 6, 8, 11 and 15) take effect on the host or are delivered, so the host `SIGPIPE` is not stopped; nothing is raised on Windows; a `write` longer than `INT_MAX` bytes is `EMSGSIZE` where the console sends it. AIO on a guest socket still goes to the host, which has no such descriptor -- [write to a pipe without a reader](../../../core/libs/prx/libkernel/File/src/Open.cpp) (libkernel) - `write`, `sceKernelWrite`, `writev` and `sceKernelWritev` fail with `EPIPE` once the read end is closed, as FreeBSD 11.4's [`pipe_write`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/sys_pipe.c#L1062-L1069) does; on Windows the host reports `ERROR_NO_DATA`, which the C runtime would turn into `EINVAL`. [`dofilewrite`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/sys_generic.c#L597-L601) also sends `SIGPIPE` to the writing thread: nothing is raised on Windows, and on Linux the host raises its own `SIGPIPE`, which the guest cannot ignore, as for sockets above. Not measured on a console +- [bind, connect](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) on a stream socket fail with `EAFNOSUPPORT` for an IPv4 (`224.0.0.0/4`) or IPv6 (`ff00::/8`) multicast address, which was not measured on a PS5; this follows FreeBSD 11.4's [`tcp_usr_bind`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/netinet/tcp_usrreq.c#L308-L310) and [`tcp_usr_connect`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/netinet/tcp_usrreq.c#L524-L526) and their IPv6 forms. While another `connect` on the socket is in progress, blocking or non-blocking, a `connect` to a multicast address also gets `EAFNOSUPPORT`, as libkernel keeps no connecting state, where FreeBSD's [`kern_connectat`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_syscalls.c#L572-L575) returns `EALREADY` for every `connect` while `SS_ISCONNECTING`. Datagram sockets and IPv4-mapped multicast addresses still go to the host +- [read, write and vector I/O](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) on a guest socket use `recv` and `send` without flags; `sceKernelReadv` and `sceKernelWritev` gather or scatter through one socket operation, preserving stream short reads and datagram boundaries. The positioned `pread`, `pwrite`, `preadv` and `pwritev` forms, and their `sceKernel` forms, return `ESPIPE` for an open guest socket and `EBADF` for a closed one. `EPIPE` from a write raises the host's `SIGPIPE` on Linux unless the socket has `SO_NOSIGPIPE` set; guest signal 13 is that `SIGPIPE`, so a guest that ignores it gets `EPIPE`, a guest handler runs, and while 13 is blocked under a handler it stays pending, but blocking it under the default disposition does not stop the host's default action, which ends the process, as for the other signals with host equivalents; nothing is raised on Windows; a `write` longer than `INT_MAX` bytes is `EMSGSIZE` where the console sends it. AIO on a guest socket still goes to the host, which has no such descriptor +- [write to a pipe without a reader](../../../core/libs/prx/libkernel/File/src/Open.cpp) (libkernel) - `write`, `sceKernelWrite`, `writev` and `sceKernelWritev` fail with `EPIPE` once the read end is closed, as FreeBSD 11.4's [`pipe_write`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/sys_pipe.c#L1062-L1069) does; on Windows the host reports `ERROR_NO_DATA`, which the C runtime would turn into `EINVAL`. [`dofilewrite`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/sys_generic.c#L597-L601) also sends `SIGPIPE` to the writing thread: nothing is raised on Windows, and on Linux the host raises its own `SIGPIPE`, which is guest signal 13 as above. Not measured on a console - [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 `sceKernelClose` 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's st_ino comes from a counter this process starts at 1, where FreeBSD's uipc_sense uses one counter for every local socket on the system, so the numbers differ from the console's - [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 kqueue is ready for `POLLIN` and `POLLRDNORM` while one of its events is triggered and for no other event, as FreeBSD 11.4's [`kqueue_poll`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/kern_event.c#L1900-L1924) answers. A kqueue cannot be waited on either, so on every host `poll` checks it every millisecond and waits on the other descriptors in slices of 1 ms. A kqueue closed during the wait reports `POLLNVAL`, and one whose number was reused is polled again with the rest of the timeout, as FreeBSD 11.4's [`pollrescan`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/sys_generic.c#L1499-L1519) looks the descriptor up again. A close that has already removed the kqueue but not yet closed its host descriptor is seen as a descriptor of unknown kind, which throws. 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. At descriptors 0 to 2, regular files, directories and archived files use vnode readiness too, including host redirection (`< file`, `> log.txt` or `2> log.txt`). A standard descriptor that is not a socket, a kqueue, a random device or a vnode still takes the console fallback: descriptor 0 never reports an event and descriptors 1 and 2 report `POLLOUT`. 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) 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, pipe or kqueue to wait on. A closed socket descriptor reports `POLLNVAL`; a count above the guest `RLIMIT_NOFILE` hard limit (`rlim_max`) fails with `EINVAL`, a count of 1024 or less is never refused, a hard limit of infinity (macOS) refuses nothing, and an allocation failure for the descriptor copy fails with `ENOMEM`, which FreeBSD's waiting `malloc` never does. FreeBSD 11.4's [`kern_poll`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/sys_generic.c) refuses a count above both `maxfilesperproc` and `FD_SETSIZE` (1024), and the console's `maxfilesperproc` is unknown, so the hard limit stands in for it: its initial value depends on the host, and lowering the guest hard limit with `setrlimit` lowers the bound - [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. - [poll on a stream socket](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - on Linux a socket that reports `POLLHUP` reports it without `POLLOUT`, as FreeBSD 11.4's `pollscan` clears `POLLOUT` whenever it reports `POLLHUP`; Linux, like FreeBSD, reports `POLLHUP` only once both of the socket's directions are shut. Windows keeps the flags `WSAPoll` reports. 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 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 throws, as no edge-triggered wait over these descriptors is wired up there; macOS uses a private host kqueue with `EV_CLEAR` for socket reads and writes and pipe reads, retaining the host wakeup coalescing limitation and the existing socket write-space approximation; pipe writes still throw because their available write space is not implemented; `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` fires again only after the watched queue activates a note; one undone before the watching queue polls is missed. A cycle of watches throws; a note on its own queue is `EINVAL`. Not measured on a console - [Socket error numbers](../../../core/libs/prx/libkernel/Socket/include/SocketError.hpp) (libkernel) - a host socket error becomes the FreeBSD 11 `errno.h` number of the same name; the numbers are not checked on the console. Winsock `WSAESHUTDOWN` becomes `EPIPE`, as a send after `shutdown` does on FreeBSD, and a host error with no FreeBSD socket counterpart becomes `EIO`. On Windows a refused nonblocking `connect` is never reported (`SO_ERROR` is unsupported), so a repeated one keeps reporting `EALREADY` where FreeBSD reports `ECONNREFUSED`. @@ -295,6 +304,7 @@ - [SO_NOSIGPIPE](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - kept per socket and read back as its bit. On Linux a `send`, `sendto`, `sendmsg` or `write` on that socket passes `MSG_NOSIGNAL` to the host, as [`kern_sendit`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_syscalls.c#L890-L895) and [`soo_write`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_socket.c#L147-L151) leave out the signal, and an accepted socket takes it from its listener, as [`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L603) copies the options and as measured on PS5 firmware 13.60. Whether a write on it raises `SIGPIPE` there was not measured - [SO_SNDTIMEO, SO_RCVTIMEO](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - the value checks and the 32.32 fixed-point read-back follow [`sosetopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L2657-L2689), as a payload process on a PS5 (firmware 13.60) answered them. A receive or send on a blocking socket waits that long for the host to report the socket readable or writable and fails with `EWOULDBLOCK`, but only before the first transfer: once writable the host send has no limit, where FreeBSD returns the bytes sent so far, and a receive whose data another thread takes after the wait blocks again. `sendfile` ignores the send timeout, the host wait has millisecond steps, and an accepted socket takes both from its listener - [IPV6_V6ONLY](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - the `IPPROTO_IPV6` option 27 is set on the host socket and reads back as 0 or 1, after FreeBSD's [`ip6_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet6/ip6_output.c#L1716-L1732): a value that is not exactly an `int` fails with `EINVAL`, as does setting it on a bound socket, which the host refuses too. `socket` turns it on for every `AF_INET6` socket, as a payload process on a PS5 (firmware 13.60) reported, and Java turns it off on the sockets it opens: Java 25 Advanced Network (PPSA99871) stops at `java.net.SocketException: Protocol not available` without it. The value checks and the bound-socket answer were not measured +- [connect to the unspecified address](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - `sendto` and `sendmsg` to `::` on an `AF_INET6` socket, and `connect`, `sendto` and `sendmsg` to `INADDR_ANY` on an `AF_INET` socket, go to the host unchanged and fail with `EADDRNOTAVAIL` on Windows, where FreeBSD 11.4's `in_pcbconnect_setup` uses the first interface address for `INADDR_ANY` and `udp6_output` leaves `::` as it is. Only `connect` on an `AF_INET6` socket replaces `::` with `::1`, as FreeBSD 11.4's `in6_pcbladdr` does, also on Linux, whose `tcp_v6_connect` and `ip6_datagram_connect` would use `::ffff:127.0.0.1` for a socket bound to a v4-mapped address; none of this was measured on a console. [sceNetConnect](../../../core/libs/prx/libSceNet/Export.cpp) is not changed - [connect to AF_UNSPEC](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - on Linux and Windows the native socket can retain its explicit local address after disconnection. On Windows a `sendto` to a non-loopback address from such a socket then fails with `ENETUNREACH` (Winsock 10051, measured on Windows 10), where FreeBSD 11.4 sends it from the wildcard address. Reporting the wildcard address through `getsockname` does not change that native binding - [connect](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - `connect` to port 0 on an `AF_INET` or `AF_INET6` stream or datagram socket fails with `EADDRNOTAVAIL` (49), and on a connected datagram socket it disconnects first, as on a PS5 (firmware 13.60) and in FreeBSD 11.4's `soconnect()`. A connected stream socket keeps its host `EISCONN` answer. Known gap: `sendto` and `sendmsg` to port 0 on an `AF_INET` datagram socket fail with 49 (measured for an unbound socket) 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 the call returns the length without the host, as Linux's `udp_sendmsg` refuses port 0 with `EINVAL`; an `AF_INET6` socket still goes to the host. [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`: 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 fails with `EBUSY`, as it did in a payload process on a PS5 (firmware 13.60) for anonymous memory, locked or not, also with a hole unmapped inside it; a title process, reservations, direct memory and flexible memory were not measured @@ -305,21 +315,21 @@ - [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 - [mlockall, munlockall](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) - `mlockall` returns `EINVAL` for zero or unknown flags. Valid flags go through `NotImplemented_nid_no_patch`, as `MCL_CURRENT` would also lock host stacks, heaps, modules and driver mappings, and later mappings are not tracked for `MCL_FUTURE`. FreeBSD 11.4's `sys_munlockall` (`sys/vm/vm_mmap.c`:1133-1165) checks `priv_check(td, PRIV_VM_MUNLOCK)` first (returning `EPERM` without it) and unwires the user map (`vm_map_unwire` over `vm_map_min`...`vm_map_max` with `VM_MAP_WIRE_HOLESOK`). Returning 0 unconditionally assumes the calling process is privileged. `munlockall` unlocks memory locked by `mlock` (`VirtualUnlock` on Windows, tolerating `ERROR_NOT_LOCKED` for pages never locked, and `munlockall` on Linux; macOS is under Locked memory on macOS). XPSemu (PPSA97358) imports `mlockall` and `munlockall`; their calls in QEMU `os-posix.c` execute only when `-overcommit mem-lock=on` is enabled. The call has not been observed in a run with this implementation. - [socket](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) creates only `SOCK_STREAM` and `SOCK_DGRAM` sockets in `AF_INET` and `AF_INET6`. In a payload process on a PS5 (firmware 13.60) `SOCK_RDM`, `SOCK_SEQPACKET` and an unknown type failed with `EPROTOTYPE` when the protocol was 0 and `EPROTONOSUPPORT` otherwise, as here, and a family other than `AF_INET`, `AF_INET6` and `AF_UNIX` with `EAFNOSUPPORT`. The console also created `SOCK_RAW` (for that payload process, which may hold privileges a title does not), `SOCK_DGRAM` with `IPPROTO_UDPLITE`, and `AF_UNIX` stream, datagram and seqpacket sockets; these fail here with `EPROTONOSUPPORT` and `EAFNOSUPPORT` (`socketpair` supports `AF_UNIX`) -- [sigtimedwait](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) waits on a process-wide pending set instead of throwing: a signal raised while blocked stays pending and is taken by `sigwait`, `sigwaitinfo` or `sigtimedwait`, delivered to the handler when `_sigprocmask` unblocks it, or delivered by `pause`. The set covers all 128 signals and the lowest number that is pending and in the set is selected (FreeBSD specifies the lowest number only among real-time signals). Selection, `EAGAIN` on timeout, `EINVAL` for a `tv_nsec` outside 0 to 1000000000 while nothing is pending and `NULL` timeout blocking indefinitely follow FreeBSD's [sigtimedwait(2)](https://man.freebsd.org/cgi/man.cgi?query=sigtimedwait). `sigwait` returns 0 with the number in its output, as POSIX specifies, and `sigwaitinfo` and `sigtimedwait` return the number, as FreeBSD does. The guest `siginfo_t` follows FreeBSD 11's `sys/sys/siginfo.h`: 80 bytes, `si_signo` at 0, `si_code` at 8, `si_pid` at 12, `si_uid` at 16, `si_addr` at 24, `si_value` at 32 and `si_band` at 40; only `si_signo`, `si_code` (`SI_USER`) and `si_pid` (the guest pid) are written, `si_uid` stays 0 because no uid is modelled, and the console's layout is not verified. PPSA85300 imports `sigtimedwait` and fails to load without it; its only call site is VLC's `vlc_writev` ([filesystem.c](https://github.com/videolan/vlc/blob/6de05adcbaf2e8b85fe86aad4169393098628119/src/posix/filesystem.c#L243-L278)), which polls with a zero timeout to drain a `SIGPIPE` only after `writev` fails with `EPIPE`. The waits are not cancellation points and an unblocked caught signal does not interrupt them with `EINTR`, where FreeBSD's `sigwaitinfo` and `sigtimedwait` may return it. `SIGPIPE` has no host equivalent and is never raised by a host write here (see `read, write and vector I/O`), so VLC's drain finds nothing -- [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 +- [sigtimedwait](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) waits on a process-wide pending set instead of throwing: a signal raised while blocked stays pending and is taken by `sigwait`, `sigwaitinfo` or `sigtimedwait`, delivered to the handler when `_sigprocmask` unblocks it, or delivered by `pause`. The set covers all 128 signals and the lowest number that is pending and in the set is selected (FreeBSD specifies the lowest number only among real-time signals). Selection, `EAGAIN` on timeout, `EINVAL` for a `tv_nsec` outside 0 to 1000000000 while nothing is pending and `NULL` timeout blocking indefinitely follow FreeBSD's [sigtimedwait(2)](https://man.freebsd.org/cgi/man.cgi?query=sigtimedwait). `sigwait` returns 0 with the number in its output, as POSIX specifies, and `sigwaitinfo` and `sigtimedwait` return the number, as FreeBSD does. The guest `siginfo_t` follows FreeBSD 11's `sys/sys/siginfo.h`: 80 bytes, `si_signo` at 0, `si_code` at 8, `si_pid` at 12, `si_uid` at 16, `si_addr` at 24, `si_value` at 32 and `si_band` at 40; only `si_signo`, `si_code` (`SI_USER`) and `si_pid` (the guest pid) are written, `si_uid` stays 0 because no uid is modelled, and the console's layout is not verified. PPSA85300 imports `sigtimedwait` and fails to load without it; its only call site is VLC's `vlc_writev` ([filesystem.c](https://github.com/videolan/vlc/blob/6de05adcbaf2e8b85fe86aad4169393098628119/src/posix/filesystem.c#L243-L278)), which polls with a zero timeout to drain a `SIGPIPE` only after `writev` fails with `EPIPE`. The waits are not cancellation points and an unblocked caught signal does not interrupt them with `EINTR`, where FreeBSD's `sigwaitinfo` and `sigtimedwait` may return it. On Linux guest 13 is the host's `SIGPIPE` (see `read, write and vector I/O`): with a handler and 13 blocked, a write that fails with `EPIPE` leaves 13 pending for VLC's drain, and with 13 ignored nothing is pending, as FreeBSD 11.4's [`tdsendsignal`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/kern_sig.c#L2174-L2189) discards an ignored signal before it looks at the mask; under the default disposition the host's default action ends the process even while 13 is blocked, where FreeBSD keeps it pending. Nothing is raised on Windows, so the drain finds nothing there +- [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, and 13 outside Windows) 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 - [sigtimedwait, sigwaitinfo, sigwait memory validation](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) - unreadable `timeout` and unwritable `info` or `sig` follow FreeBSD 11.4's kernel `copyin` and `copyout` returning `EFAULT` (`sigwait` returning it as the result). An unreadable `set` returns `EFAULT` here where libthr faults in user space, and the console is not measured. As in the kernel, a signal already taken is not put back if writing `info` or `sig` fails. - [getsid, setpgid, setpgrp](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - getsid 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. setpgid and setpgrp follow FreeBSD 11.4's `sys_setpgid` for the only guest process: a negative group ID fails with `EINVAL`, a process ID other than 0 or the caller's PID (including a negative PID) fails with `ESRCH`, and because the single guest process is modeled as its own session leader, `SESS_LEADER(targp)` fails with `EPERM` before normalizing the process group or reaching the same-process-group no-op. PPSA74126 imports `getsid` 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. While a handler runs, its own signal (unless `SA_NODEFER`) and the signals of its mask other than `SIGKILL` and `SIGSTOP` are blocked, and `SA_RESETHAND` resets the disposition to the default before it runs, as FreeBSD's `postsig_done` does. Those blocks are held apart from the process-wide mask that `sigprocmask` sets, which reports them, and are released when the handler returns, so a block another thread adds meanwhile is kept, for the same signal too, and so is one the handler adds itself, which FreeBSD's `sigreturn` would undo. `SIG_UNBLOCK` of a held signal releases its hold and `SIG_SETMASK` releases them all. When the handler's own thread released one, that signal gets back its bit of the process mask from before the handler when the handler returns, as FreeBSD's `sigreturn` puts back the mask from before it, so a handler that saves and restores the mask with `SIG_SETMASK`, or calls `pselect`, leaves its signal unblocked. A `sigsuspend` inside a handler keeps the holds, where FreeBSD replaces the whole thread mask. A handler left with `longjmp`, or with `siglongjmp` to a buffer that saved the mask, gets back the mask that buffer saved, as on FreeBSD; a signal raised meanwhile stays pending and is delivered when the handler returns to `raise`, but one left pending by a handler that a host signal ran asynchronously waits for the next `raise` or `_sigprocmask`. `SA_ONSTACK` is 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 +- [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`, and `SIGPIPE` outside Windows) run, the same way `signal` handlers do. While a handler runs, its own signal (unless `SA_NODEFER`) and the signals of its mask other than `SIGKILL` and `SIGSTOP` are blocked, and `SA_RESETHAND` resets the disposition to the default before it runs, as FreeBSD's `postsig_done` does. Those blocks are held apart from the process-wide mask that `sigprocmask` sets, which reports them, and are released when the handler returns, so a block another thread adds meanwhile is kept, for the same signal too, and so is one the handler adds itself, which FreeBSD's `sigreturn` would undo. `SIG_UNBLOCK` of a held signal releases its hold and `SIG_SETMASK` releases them all. When the handler's own thread released one, that signal gets back its bit of the process mask from before the handler when the handler returns, as FreeBSD's `sigreturn` puts back the mask from before it, so a handler that saves and restores the mask with `SIG_SETMASK`, or calls `pselect`, leaves its signal unblocked. A `sigsuspend` inside a handler keeps the holds, where FreeBSD replaces the whole thread mask. A handler left with `longjmp`, or with `siglongjmp` to a buffer that saved the mask, gets back the mask that buffer saved, as on FreeBSD; a signal raised meanwhile stays pending and is delivered when the handler returns to `raise`, but one left pending by a handler that a host signal ran asynchronously waits for the next `raise` or `_sigprocmask`. `SA_ONSTACK` is 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 that libc serves through the `sigaction` and the set libkernel registers at load (see libc). - [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. On PS5 firmware 13.60 it returns what `fcntl` returns there, and `0x80020000` plus the errno, which it also sets, on failure; this was measured for `F_GETFD`, `F_SETFD`, `F_GETFL`, `F_DUPFD`, `F_DUPFD_CLOEXEC`, `F_GETLK`, an unknown command and bad descriptors, not for the other commands -- [Creating through a trailing slash](../../../core/libs/prx/libkernel/File/src/Open.cpp) (libkernel) - `open` and `sceKernelOpen` with `O_CREAT` on a missing name followed by a slash (`new/`) fail with `EINVAL` on Windows and with the host's `EISDIR` on Linux, where FreeBSD 11.4's `lookup()` (sys/kern/vfs_lookup.c) fails with `ENOENT` because only a directory may be created through a trailing slash - [sceKernelStat on archived paths](../../../core/libs/prx/libkernel/File/src/Open.cpp) (libkernel) returns EFAULT when the output buffer is unwritable, checked on the host only. +- [sceKernelAprGetFileSize and sceKernelAprGetFileStat](../../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libkernel) - the console's answer for an unwritable output is not known; they return EFAULT, checked on the host only. - [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_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_DUPFD` returns 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. `O_ASYNC` on a pipe or character device (`SIGIO` is not delivered), `F_GETOWN`, `F_SETOWN` and `F_SETLK_REMOTE` throw; `O_FSYNC` set by `open` or `F_SETFL` makes each `write`, `pwrite`, `writev` and `pwritev` on a file sync it before returning, as `vn_write` adds `IO_SYNC`; the `sceKernelAioSubmitWriteCommands` writes are not synced, as whether the console's AIO honours the flag is unknown; on Linux a file opened with it keeps the host `O_SYNC` after `F_SETFL` clears it. `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 - [fcntl on a guest socket](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) answers `F_DUPFD`, `F_DUP2FD`, `F_DUP2FD_CLOEXEC`, `F_GETFD`, `F_SETFD`, `F_GETFL` and `F_SETFL`, fails `F_DUPFD_CLOEXEC` with `EOPNOTSUPP` as a PS5 does, and fails every other command with `EINVAL`; FreeBSD 11.4's `kern_fcntl` also handles `F_GETOWN` and `F_SETOWN` on a socket. `FD_CLOEXEC` is only recorded (there is no `exec`): `dup`, `dup2`, `F_DUPFD`, `F_DUP2FD` onto another descriptor and an accepted socket start without it, `F_DUP2FD` onto the same descriptor keeps it, and `SOCK_CLOEXEC` and `F_SETFD` set it. None of this was measured on a console. - [F_DUPFD_CLOEXEC and F_DUP2FD_CLOEXEC](../../../core/libs/prx/libkernel/File/src/DescriptorControl.cpp) (libkernel) - `F_DUPFD_CLOEXEC` on a shared memory descriptor fails with `EOPNOTSUPP`, where PS5 firmware 13.60 returns 0 for any start without setting `errno`; whether it duplicates anything is not known, since descriptor 0 was already open. `F_DUP2FD_CLOEXEC` returns the lowest free descriptor, where the console picks scattered free numbers below 1024 (17 to 1012 measured). Otherwise both follow the console: `F_DUPFD_CLOEXEC` fails with `EOPNOTSUPP` for any start on files, sockets, pipes and kqueues, after `EBADF` for a bad descriptor; `F_DUP2FD_CLOEXEC` ignores its target, which it neither opens nor closes (-1, -2, `INT_MAX`, the source and an open descriptor alike), and returns a new descriptor for the same open file without `FD_CLOEXEC`, or `EBADF` for a bad source -- [ioctl, _ioctl](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) handle only `FIONBIO` and `FIONREAD` on a socket, and any other request there fails with `ENOTTY`. On the other descriptors they follow FreeBSD's [`kern_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_generic.c#L758), [`vn_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_vnops.c#L1488) and [`pipe_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_pipe.c#L1346): `FIONBIO` sets `O_NONBLOCK` as `fcntl` does, `FIOCLEX` and `FIONCLEX` set and clear the close-on-exec flag `F_GETFD` reports, and `FIONREAD` reports the size past the offset of a file, the queued bytes of a pipe's read end, 0 on its write end and `ENOTTY` on the random device, while it throws on a directory or another character device. Every termios request (group `t`, `TIOCGWINSZ` included) fails with `ENOTTY`, since those descriptors are taken as not a terminal, as in `tcgetattr`, and any other well-formed request throws. An input request with length 0 is not rejected as malformed: [`sys_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_generic.c#L703-L718) rejects it only in a kernel built without `COMPAT_FREEBSD4`, `COMPAT_FREEBSD5` and `COMPAT_43`, and which of these the PS5 kernel has is unknown. Stremio Plus (PPSA74126) imports `_ioctl` for its statically linked `tc*` functions, which send `TIOCGETA`, `TIOCSETA`, `TIOCSPGRP` and the other termios requests +- [ioctl, _ioctl](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) handle only `FIONBIO`, `FIONREAD`, `FIOCLEX` and `FIONCLEX` on a socket, and any other request there fails with `ENOTTY`. On the other descriptors they follow FreeBSD's [`kern_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_generic.c#L758), [`vn_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_vnops.c#L1488) and [`pipe_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_pipe.c#L1346): `FIONBIO` sets `O_NONBLOCK` as `fcntl` does, `FIOCLEX` and `FIONCLEX` set and clear the close-on-exec flag `F_GETFD` reports, and `FIONREAD` reports the size past the offset of a file, the queued bytes of a pipe's read end, 0 on its write end and `ENOTTY` on the random device, while it throws on a directory or another character device. Every termios request (group `t`, `TIOCGWINSZ` included) fails with `ENOTTY`, since those descriptors are taken as not a terminal, as in `tcgetattr`, and any other well-formed request throws. An input request with length 0 is not rejected as malformed: [`sys_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_generic.c#L703-L718) rejects it only in a kernel built without `COMPAT_FREEBSD4`, `COMPAT_FREEBSD5` and `COMPAT_43`, and which of these the PS5 kernel has is unknown. Stremio Plus (PPSA74126) imports `_ioctl` for its statically linked `tc*` functions, which send `TIOCGETA`, `TIOCSETA`, `TIOCSPGRP` and the other termios requests - [ioctl, _ioctl](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) do not pass a `sceNetSocket` socket to libSceNet as `fcntl` does. On Linux its number duplicates the host socket, so `FIONBIO` switches the host `O_NONBLOCK` but not the library's `SO_NBIO` state; on Windows it is a `NUL` placeholder, so `FIONREAD` and enabling `FIONBIO` throw as on a character device. `sceNetIoctl` handles both requests on such a socket. - [_accept, _bind, _connect, _getpeername, _getsockname, _getsockopt, _listen, _recvfrom, _recvmsg, _sendmsg, _sendto, _setsockopt](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp), [_getdirentries](../../../core/libs/prx/libkernel/File/src/Stdio.cpp), [_sigaction, _sigsuspend](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) are not measured on the console. They forward to the calls without the underscore, which have no libthr layer here, as FreeBSD 11.4 makes the underscore names the plain system calls. `_sigaction` and `sigaction` share one handler table, where FreeBSD's `__thr_sigaction` hands its own wrapper to the system call `_sigaction` reads back, and rejects `SIGCANCEL` (32). A null set throws in `_sigsuspend` as in `sigsuspend`, while the system call FreeBSD 11.4 exports as `_sigsuspend` ([`Symbol.map`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/sys/Symbol.map#L961)) returns `EFAULT` - [Regular file mappings](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) - on Linux `mmap` maps a regular file's descriptor shared or private like a shared memory object, with the same access checks; SQLite's WAL index (`-shm`) in Dusklight PPSA99305 needs it. FreeBSD 11 `mmap(2)` and shadPS4's `MemoryManager::MapFile` map regular files; the console's behaviour is not measured. Other descriptors (pipes, sockets, directories, devices) still fail with `EOPNOTSUPP`. On Windows a shared mapping is a view of a file section (`CreateFileMappingW` on the descriptor's handle, read-write when the descriptor is, otherwise read-only), so writes through the descriptor and the mapping see each other. This also maps anonymous `shm_open` objects and JIT shared memory descriptors, which are temporary files on Windows. A shared mapping may end inside the 16 KiB page that holds the end of the file, as POSIX [`mmap`](https://pubs.opengroup.org/onlinepubs/9799919799/functions/mmap.html) allows (llama.cpp in PS5LM Plus PPSA99581 maps a whole model file this way): the section keeps the file's size, so the file never grows, the view ends with the 4 KiB host page that holds the end of the file, which Windows fills with zeros past it, and the rest of the guest page is private zero pages. As POSIX requires, the bytes past the end read zero and are never written to the file; bytes written to those private pages are seen only through that mapping, not through other mappings of the file, and a file that grows later does not show its new bytes there. A shared mapping that reaches a whole page past the end of the file fails with `EOPNOTSUPP`, where POSIX maps it and delivers `SIGBUS` on access, as does a write-only descriptor; FreeBSD 11.4's `vn_mmap` maps a write-only descriptor without `PROT_READ`, and Linux refuses it with `EACCES`. A `PROT_EXEC` mapping is a view of a section created on a second handle that `ReOpenFile` opens with execute access, so a file whose ACL does not grant the user execute fails with `EACCES`, where FreeBSD maps it unless the file system is mounted `noexec`. A read-write alias (`GuestArenaMapAlias`) of a mapped page that holds the end of the file throws. `mprotect` above the view's protection (writing through a read-only descriptor, or adding `PROT_EXEC`) fails with `EACCES` before anything changes; FreeBSD allows `PROT_EXEC` on a readable descriptor. Shrinking the file with `ftruncate` fails with `EINVAL` while any shared mapping of it exists, even one that does not cover the removed part, because the section keeps the file's size at `mmap`; FreeBSD truncates. A read-write alias of a view (`GuestArenaMapAlias`, used when the GPU imports guest memory) of a read-only file section fails, as the alias is mapped `PAGE_READWRITE`. Views of the same file made by separate `mmap` calls are separate sections, so write tracking does not treat them as aliases of each other. A private mapping on Windows is a copy of the file read at `mmap`, which POSIX allows (later changes to the file need not show); pages wholly past the end of the file fail with `EOPNOTSUPP` instead of faulting on access, and a write-only descriptor fails with `EOPNOTSUPP`. @@ -357,6 +367,8 @@ - [flock on a directory descriptor](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) on Windows is seen only by this process. Windows `LockFileEx` rejects a directory handle, so the lock is kept in a table of this process keyed by the directory's volume serial number and file index; between descriptors of this process it conflicts, converts in place, is shared by `dup` and is released on the last close, as FreeBSD 11.4 does for any vnode locked with [`sys_flock`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/kern_descrip.c#L2889-L2929). Waiters are not served in arrival order: an unlock or downgrade may wake a request that waits behind an earlier conflicting one, which FreeBSD's `lf_setlock` serves first. Another host process neither sees it nor is seen. Linux and macOS use the host `flock`. Not measured on a console - [poll on macOS and urgent data](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - XNU's `poll` never reports `POLLPRI` for out-of-band data. On macOS it is taken from `SIOCATMARK`, which reports a pending urgent byte without consuming it, and a wait that asks for it uses the sliced path a kqueue wait uses, so it wakes on the next 1 ms slice; `POLLRDBAND` is set with it, as FreeBSD 11.4's `sopoll_generic` does. The other paths that ask the host for urgent readiness still miss it on macOS: guest `select` and `pselect` through `GuestSockets::Poll`, and libSceNet's `PollSockets`. `recv(MSG_OOB | MSG_PEEK)` is not the probe because it consumes the byte on macOS 27.0.1, and `kevent` with `NOTE_OOB` does not fire there. Every other flag and error comes from `poll`; every other host is unchanged. The guest `send` and `recv` refuse `MSG_OOB`, so urgent data can only come from an outside peer; the console's stop once the normal stream passes the mark was not reproduced - [sysctlnametomib](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) copies its name, size and mib safely and returns EFAULT on invalid pointers as a host-safety fallback where console behaviour has not been measured. FreeBSD 11.4's libc sysctlnametomib (which sysctlbyname calls) runs strlen on the name and reads and writes *size in user space before the syscall, so an unmapped name or unwritable size crashes a console process in user space; only the mib copy-out returns EFAULT from the kernel +- [sceKernelProtectDirectMemoryForPID](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel) throws. The only title known to import it from `libkernel` (PPSA99170) declares `int sceKernelProtectDirectMemoryForPID(int64_t offset, size_t length, int prot, int pid)` in [`native/pw_dmem_share.h`](https://github.com/mihawk-99/PS5_MihawkOS/blob/8a9c117/native/pw_dmem_share.h#L4-L18); its pinned `docs/PLAN.md`, "Direct memory shared between processes", adds a static reading of `libkernel_sys.sprx` 10.01 (one ioctl `0xc018800f`, `{offset, length, pid, prot}`, pid 0 refused) and one console run in which `(offset, 64 MiB, 0x33, pidB)` returned 0 and a second process then mapped the range. The error for pid 0 or a missing pid is unknown, and a guest runs as the only process here, with none to grant a range to. +- [sceKernelAllocateDirectMemoryForApp](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) forwards to `sceKernelAllocateDirectMemory`, sharing its validation and allocator. The same title's pinned [`native/pw_dmem_share.h`](https://github.com/mihawk-99/PS5_MihawkOS/blob/8a9c117cb265d0f54ead53b0aa37cdc8c0dc9c91/native/pw_dmem_share.h#L17) declares `int sceKernelAllocateDirectMemoryForApp(int64_t, int64_t, size_t, size_t, int, int64_t*)`, the six arguments of `sceKernelAllocateDirectMemory`, and its [`docs/PLAN.md`](https://github.com/mihawk-99/PS5_MihawkOS/blob/8a9c117cb265d0f54ead53b0aa37cdc8c0dc9c91/docs/PLAN.md) shows the same allocation reached through both calls. The forward is assumed from that shared signature: `ForApp`'s own argument checks and error codes were not measured, and a guest has one direct memory range, so an app-specific range restriction has nothing separate to observe. - [APS5_GAME_MODE](../../../core/libs/prx/libkernel/System/src/HostPerformanceProcess.cpp) (libkernel) is `off` when unset and `auto` is not the default, because no distribution or desktop was measured to say what `gamemoded` or an activity changes for a title (CPU governor, priority, frame pacing, power draw), and a default that changes scheduling for everyone must be measured first. Whether `on` helps or hurts a title is unmeasured. - [Host game mode on Linux](../../../core/libs/prx/libkernel/System/src/GameModeClient.cpp) (libkernel) - the request ends from the libc shutdown callbacks, which every exit path runs except an immediate `_Exit` and a crash, so those leave it to `gamemoded`. The daemon has a reaper thread for clients that disappear (FeralInteractive/gamemode at `a74b8106a2236d1f2696aa44c93bc4c8ef13b42e`, `daemon/gamemode-context.c`), but how soon it drops a client that never ended its request is unmeasured. - [Host game mode on Linux](../../../core/libs/prx/libkernel/System/src/GameModeClient.cpp) (libkernel) - the library names (`libgamemode.so.0`, then `libgamemode.so`) and the `real_gamemode_request_start`, `real_gamemode_request_end` and `real_gamemode_error_string` symbols are those of the client header in FeralInteractive/gamemode at `a74b8106a2236d1f2696aa44c93bc4c8ef13b42e` (`lib/gamemode_client.h`); a later ABI change was not checked, and no real `gamemoded` was run against the backend, only fixture libraries exporting those symbols. @@ -366,3 +378,4 @@ - [sceKernelAvailableDirectMemorySize, sceKernelGetDirectMemoryType, sceKernelDirectMemoryQuery](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) validate destination pointers via GuestCopyOut_nid_no_patch and return SCE_KERNEL_ERROR_EFAULT on bad memory as host-safety fallbacks. Console fault handling is unmeasured. - [send and recv](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) pass the guest buffer to the host call without a guest copy helper, so an unreadable or unwritable buffer is whatever the host does (a host fault signal is possible), where `sendto` and `recvfrom` fail with `EFAULT`. On Windows a stream receive whose writable prefix is shorter than the queued data fails with `EFAULT`, but data arriving after the check, or `MSG_WAITALL`, can return the prefix as a short success where FreeBSD fails. Linux and macOS return the bytes copied before a fault on a stream, where FreeBSD returns `EFAULT`. - [equeue pending-event bound](../../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel) - an undrained registration keeps the armed event plus at most 64 queued events: a trigger past the cap drops the oldest queued event and keeps the latest payload, and an `AddEvent` carrying more than 64 queued events keeps the latest 64. Drained bursts report as before. The console's queue depth and which completion it keeps are unknown +- [title sandbox app0 alias](../../../core/libs/prx/libkernel/AppMetadata/src/AppMetadata.cpp) (libkernel) - aliases `/mnt/sandbox/_000/app0` to the application directory `/app0` when metadata is loaded; console jail sandbox mounts for other virtual directories under `/mnt/sandbox` remain unmeasured. diff --git a/docs/dev/debt/libkernel_sys.md b/docs/dev/debt/libkernel_sys.md index bfc324c166..7d43123da7 100644 --- a/docs/dev/debt/libkernel_sys.md +++ b/docs/dev/debt/libkernel_sys.md @@ -7,7 +7,9 @@ ### Unknown function info -- [libkernel_sys additional exports](../../../core/libs/prx/libkernel_sys/AdditionalExports.cpp) - 325 callable exports have import-resolution declarations only. Parameter lists, return types and behavior remain unverified. Parameterless `int` declarations are placeholders, not verified ABI prototypes; every entry throws through `NotImplemented_nid_no_patch`. Unresolved names use literal NID aliases; no runtime functionality or data objects are fabricated. Existing providers and exports supplied by open pull requests are excluded. Kernel variant imports already resolve through `libkernel`, then `libc`; existing providers remain unchanged. Six native data exports (`errno`, `__stack_chk_guard`, `_sigintr`, `__progname`, `environ`, and NID `AxgW94umKqU`) require separate storage, layout, TLS and ownership validation and are not added as callable aliases or fabricated globals. +- [Nine unnamed additional exports](../../../core/libs/prx/libkernel_sys/AdditionalExports.cpp) (libkernel_sys, NIDs `pCbBddNl66Y`, `zAloppPJxy0`, `vq2+HyP5U5I`, `z+e0vV-tnH4`, `gJ5gpW45mU0`, `N94a0T4F4+U`, `C49jelxiaVE`, `1WXQlxMpnNA`, `FR0odl35yQY`) - return what firmware 13.42's `system/common/lib/libkernel_sys.sprx` returns, read from disassembly to the return, not measured on a console. The first five are `xor %eax,%eax; ret`, `N94a0T4F4+U` is `mov $0x8002004e,%eax; ret` and `C49jelxiaVE` is `mov $0x20018,%eax; ret`. `1WXQlxMpnNA` stores 1 to a zero-initialised module byte and returns 0, and `FR0odl35yQY` returns that byte zero-extended; no other code in the module touches the byte. None reads an argument, so the declarations are parameterless; the 32-bit loads clear the upper half of `rax`, so they return `std::uint64_t`. Names, intended parameters and callers are unknown. + +- [libkernel_sys additional exports](../../../core/libs/prx/libkernel_sys/AdditionalExports.cpp) - 316 callable exports have import-resolution declarations only. Parameter lists, return types and behavior remain unverified. Parameterless `int` declarations are placeholders, not verified ABI prototypes; every entry throws through `NotImplemented_nid_no_patch`. Unresolved names use literal NID aliases; no runtime functionality or data objects are fabricated. Existing providers and exports supplied by open pull requests are excluded. Kernel variant imports already resolve through `libkernel`, then `libc`; existing providers remain unchanged. Six native data exports (`errno`, `__stack_chk_guard`, `_sigintr`, `__progname`, `environ`, and NID `AxgW94umKqU`) require separate storage, layout, TLS and ownership validation and are not added as callable aliases or fabricated globals. | Name | NID | | --- | --- | @@ -148,7 +150,6 @@ | `sceKernelGetAppLibcData` | `-QipytgE4x0` | | `Unknown` | `-sYN8ucoAWw` | | `Unknown` | `0CV40L3tznI` | -| `Unknown` | `1WXQlxMpnNA` | | `Unknown` | `1v23w7Rnkic` | | `Unknown` | `1v6DjTPunjs` | | `Unknown` | `2+KZ4mZKp6E` | @@ -185,7 +186,6 @@ | `Unknown` | `BjmZn9pYSuk` | | `Unknown` | `C3ToW2CA+hM` | | `Unknown` | `C3xgqN95IoE` | -| `Unknown` | `C49jelxiaVE` | | `sceKernelSwitchToTrinityMode` | `Cz48pcLnLCY` | | `sceKernelIsGenuineCronosCex` | `DB0muDALi4Y` | | `Unknown` | `DWWduhQYA3I` | @@ -195,7 +195,6 @@ | `sceKernelTraceMarkerBookmark` | `EKMWpdzszVo` | | `Unknown` | `EaepepH-LmI` | | `Unknown` | `FIUPjhnoI+o` | -| `Unknown` | `FR0odl35yQY` | | `sceKernelIccGetExtBDDriveStatus` | `GADbvgNm4tY` | | `Unknown` | `GTyjVeZcdRM` | | `Unknown` | `Gtncco+mxio` | @@ -218,7 +217,6 @@ | `Unknown` | `LwGBaXgvFaY` | | `Unknown` | `MJ-err8l-XY` | | `Unknown` | `MKlp-bW1bz8` | -| `Unknown` | `N94a0T4F4+U` | | `Unknown` | `NhFlyw-77cY` | | `Unknown` | `OZBJnWxt7d4` | | `Unknown` | `PBq-OUg5A5s` | @@ -275,7 +273,6 @@ | `Unknown` | `ftHBlCfIEQo` | | `Unknown` | `g7MGwZ5UxBE` | | `Unknown` | `gBiaAGKEvHM` | -| `Unknown` | `gJ5gpW45mU0` | | `Unknown` | `gSIiye3ERqo` | | `Unknown` | `gpgi-GwE2Is` | | `Unknown` | `guq+otVpgj0` | @@ -302,7 +299,6 @@ | `Unknown` | `o141x2VDeVc` | | `sceKernelGetMainSocSubId` | `oSQQA8I595Y` | | `sceKernelInternalUnmapMapperSharedMemory` | `pAI+17im1o0` | -| `Unknown` | `pCbBddNl66Y` | | `Unknown` | `pOClGbxrx94` | | `sceKernelSetAppLibcData` | `pn68p6w2eK0` | | `Unknown` | `pp+G+UZhiqY` | @@ -324,7 +320,6 @@ | `Unknown` | `ulVjluL9TzM` | | `Unknown` | `v+qTlNjF6kE` | | `Unknown` | `vfYaxkxnJXQ` | -| `Unknown` | `vq2+HyP5U5I` | | `sceKernelSetCanvasHandle` | `wXnNof3LrrU` | | `Unknown` | `wn0XzOue4EM` | | `Unknown` | `wwWv+0MzhGM` | @@ -333,9 +328,7 @@ | `Unknown` | `yWOJkfd-DhA` | | `Unknown` | `yn0b42DApAI` | | `Unknown` | `yzvrk3YrPlE` | -| `Unknown` | `z+e0vV-tnH4` | | `Unknown` | `z0qKN5kkVj0` | -| `Unknown` | `zAloppPJxy0` | - [sceKernelGetHwModelName](../../../core/libs/prx/libkernel_sys/Export.Discovery.cpp) (libkernel_sys) - throws. On a CFI-2014 PS5 (firmware 13.60) a payload process got 0 from `sceKernelGetHwModelName(char*)`, which wrote "CFI-2014 A01X" and a terminator (14 bytes). The model string belongs to one console and the buffer size and null behaviour are not known, so it is not implemented - [sceKernelGetCompiledSdkVersion](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - which SDK version the console returns is unmeasured. The [PS5 payload SDK](https://github.com/ps5-payload-dev/sdk/blob/a7685ce8e97ce1f8a4f38253a41d54241c9124f1/crt/kernel.c#L257-L275) declares the process parameters with a 32-bit PS4 SDK version at offset `0x10` and a 32-bit PS5 SDK version at `0x14`; this export returns the field at `0x10`, which [libSceHmd2](../../../core/libs/prx/libSceHmd2/Export.cpp) reads and [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/615bcba5e21b82dc59588575f7ecbce8da09ca2c/src/emulator.cpp#L110-L138) reads from the eboot. The `int (int32_t*)` signature and `EINVAL` (`0x80020016`) for null follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/615bcba5e21b82dc59588575f7ecbce8da09ca2c/src/core/libraries/kernel/process.cpp#L56-L62); `SCE_KERNEL_ERROR_EFAULT` for an unwritable output pointer is a host-safety fallback so the process does not crash, not a measured PS5 result, and the console's behaviour for such a pointer is unmeasured. Closes when: measured on a PS5 @@ -345,8 +338,9 @@ - [sceKernelGetUpdVersion](../../../core/libs/prx/libkernel_sys/Export.Discovery.cpp) (libkernel_sys) - runs the same code as `sysKernelGetUpdVersion` on firmware 13.42 ([disassembly](https://github.com/boykopovar/AnyPS5/pull/10671#issuecomment-6398286973)), so it forwards there: a null output is refused with `SCE_KERNEL_ERROR_EINVAL`, and otherwise the 4 bytes come from `machdep.upd_version`, whose retail value is unmeasured (see libkernel's entry) - [get_vm_map_timestamp](../../../core/libs/prx/libkernel_sys/Export.Discovery.cpp) (libkernel_sys), firmware 13.60 `libkernel_sys.sprx`, syscall 624, returns the low 32 bits of the guest-allocation generation. That value advances whenever a guest-allocation registry scope ends, including lookups (`malloc_usable_size`) and every guest heap allocation or free, and whenever a tracked range is invalidated. FreeBSD advances a map's timestamp only when the map is write-locked, so the value changes far more often than the console's would. Host mappings made outside the tracker can be missed. Not hardware-validated. -- [kenv](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) follows FreeBSD 11.4's [`sys_kenv`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/kern_environment.c#L87-L204) over an in-memory table that starts empty, since the console's boot environment is unmeasured, so a variable the system sets at boot reads as `ENOENT`. `KENV_SET` and `KENV_UNSET` always pass the privilege check, as the guest is uid 0. A name or value is read with the guest path copy, which reads up to 1024 bytes, so an unterminated string followed by an unreadable page fails with `EFAULT` where FreeBSD's bounded `copyinstr` gives `ENAMETOOLONG`. A 513th variable is dropped silently, as in FreeBSD. Console behaviour is unverified +- [kenv](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) follows FreeBSD 11.4's [`sys_kenv`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/kern_environment.c#L87-L204) for a caller in a jail with the default parameters: `KENV_SET` and `KENV_UNSET` fail with `EPERM` before the name is read, as [`prison_priv_check`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/kern_jail.c#L3060) denies `PRIV_KENV_SET` and `PRIV_KENV_UNSET` there. That a program runs jailed as uid 0 is the model agreed for the credential checks, not measured on a console. The environment therefore stays empty, since the console's boot environment is unmeasured: `KENV_GET` reads `ENOENT` and `KENV_DUMP` returns 0. A name is read with the guest path copy, which reads up to 1024 bytes, so an unterminated string followed by an unreadable page fails with `EFAULT` where FreeBSD's bounded `copyinstr` gives `ENAMETOOLONG`. Console behaviour is unverified - [sched_rr_get_interval](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) accepts pid 0 or the host-backed guest pid and reports the FreeBSD 11 ULE default time slice for the usual AMD64 clock rates (94 ms); other pids return `ESRCH`, and a null interval returns `EFAULT`. AnyPS5 does not run a guest ULE scheduler or track a per-process configured quantum, so this reports the modeled default and does not reflect runtime scheduling. Console-specific clock rates and quantum configuration are unverified +- [is_in_sandbox and sceKernelIsInSandbox](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) return 1 as a title process runs in a sandbox container jail, matching shadPS4 (process.cpp:15-17) - [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, _openat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) pass `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. They return `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. As with `open`, `O_NONBLOCK` is ignored. PPSA99003 opens directory entries through `openat`. 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 @@ -357,7 +351,7 @@ - [aio_read, aio_write, aio_fsync, aio_error, aio_return, aio_suspend, aio_waitcomplete, aio_cancel](../../../core/libs/prx/libkernel_sys/Aio.cpp) (libkernel_sys) - a request with `SIGEV_SIGNAL`, `SIGEV_THREAD_ID` or `SIGEV_KEVENT` completion, or on a socket, throws, because delivering a guest signal or kevent and socket aio are not modelled. One worker thread runs the process's requests one at a time in submission order, so a request is cancellable only until that thread takes it. A queued request keeps the descriptor number, not the open file: closing or reusing the descriptor before it runs changes what it reaches. Pipes fail with `EOPNOTSUPP`, as FreeBSD 11 does with `vfs.aio.enable_unsafe` 0; the console's setting is unverified. Waits are not interrupted by signals (`EINTR`), and timeouts under 1 ms round up to 1 ms (hz 1000). Otherwise the rules follow FreeBSD 11 `vfs_aio.c` - [symlink](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) stores the target unchanged in a host symbolic link, which the host follows later, not the guest path resolver. It throws for an absolute target, which would need a host path in the link and a translation back in `readlink`. A target with a `..` component is created only when the host and the guest would reach the same place: every directory a `..` leaves must exist and not be a symbolic link, so the host's physical `..` matches the guest resolver's lexical one, and the target joined to the link's host directory must equal the guest path it names, resolved through the guest mounts. A `..` that climbs out of a mount such as `/savedata0`, or past a directory reached through a symbolic link, throws, where FreeBSD 11.4's `namei` follows it from the link's directory. Console behaviour is unverified - [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 +- [link](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) and `linkat` count the symbolic links met while resolving the source and the new name, and return `ELOOP` at the 33rd, as FreeBSD 11.4's [`namei`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_lookup.c#L484-L487) does with `MAXSYMLINKS` (32), where the hosts allow longer chains (Linux 40). The count walks the host path from its root, so symbolic links in the host directories a guest path maps to count too; only Windows symbolic links count there, not junctions. A guest path, and on Windows a descriptor-relative one, reaches the count with its `..` components already dropped lexically, so a link a dropped component would have followed is not counted - [pthread_attr_setcreatesuspend_np, pthread_resume_np, pthread_resume_all_np, pthread_multi_np, scePthreadAttrSetcreatesuspend, scePthreadResume, scePthreadResumeAll and scePthreadMulti](../../../core/libs/prx/libkernel_sys/Export.Discovery.cpp) (libkernel_sys) - when a thread created suspended stops relative to its signal mask is unmeasured on a console. Return values and errors follow FreeBSD 11.4 libthr ([`thr_attr.c`](https://github.com/freebsd/freebsd-src/blob/8c639945/lib/libthr/thread/thr_attr.c#L369-L381), [`thr_resume_np.c`](https://github.com/freebsd/freebsd-src/blob/8c639945/lib/libthr/thread/thr_resume_np.c#L47-L97), [`thr_multi_np.c`](https://github.com/freebsd/freebsd-src/blob/8c639945/lib/libthr/thread/thr_multi_np.c#L40-L51)) and the code of firmware 13.42's `libkernel_sys.sprx` ([#9573](https://github.com/boykopovar/AnyPS5/pull/9573#issuecomment-6393877843)), where the sce forms add `0x80020000` to a non-zero errno - [pthread_attr_setstackaddr and pthread_attr_getsolosched_np](../../../core/libs/prx/libkernel_sys/Export.Discovery.cpp) (libkernel_sys) - firmware 13.60 `libkernel_sys.sprx` (SHA-256 `60921322b155ba649cfa29ea5e6d358ca0229424b4f73fd5c30ae7f7ce7c304c`) places `pthread_attr_setstackaddr` (NID `suCrEbr0xIQ`) at function address `0x4ed0`: it returns `EINVAL` for a null attribute pointer, null attribute handle or null stack address, and stores a valid address at attribute offset `0x18`. `pthread_attr_getsolosched_np` (NID `ApMLfJ527Ag`) is at `0x51b0`: it returns `EINVAL` for a null attribute pointer, null handle or null output, otherwise writes the attribute field at `+0x10` masked with `0x10`. These details are from local firmware disassembly; console runtime behavior is not independently tested - [sceKernelGetCpumodeGame and sceKernelSetCpumodeGame](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - the setter takes the mode as its only argument and the getter writes it through an `int*`, as [ProsperoEden (PPSA99008) declares and calls them](https://github.com/mihawk-99/PS5_ProsperoEden/blob/a0b65fe50dd48d4064e2f58fa9c8d44665478878/headless/performance.cpp#L1030-L1062); its getter faulted when called without one, which throws here. The mode starts at 1, 0 and 1 are accepted, and 2 to 7 return `EINVAL` (0x80020016), as it [measured](https://github.com/mihawk-99/PS5_ProsperoEden/blob/a0b65fe50dd48d4064e2f58fa9c8d44665478878/docs/PERFORMANCE_FINDINGS.md#L113-L114) on a PS5; other modes were not tried and throw. That the getter reports the last accepted mode is inferred from the probe. The mode changes nothing else here: on the console 0 gave system CPU mode 1 and 1 mode 5, while `sceKernelGetCpumode` still returns 0. @@ -369,6 +363,7 @@ - [sched_setscheduler](../../../core/libs/prx/libkernel_sys/Export.Discovery.cpp) (libkernel_sys) follows FreeBSD 11.4's [`sys_sched_setscheduler`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/p1003_1b.c#L198-L225): an unreadable parameter returns `EFAULT`, a pid other than 0 and the guest's own returns `ESRCH`, and every other call returns `EPERM` whatever the policy and priority, because `kern_sched_setscheduler` needs `PRIV_SCHED_SET` before it checks them and `prison_priv_check` denies it to a jailed caller. A title is taken to run jailed; that is not measured on a console. The policy and priority are never validated or stored, so `EINVAL` for a bad policy is not modelled - [sigreturn](../../../core/libs/prx/libkernel_sys/Export.Discovery.cpp) (libkernel_sys) follows FreeBSD 11.4's amd64 [`sys_sigreturn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/amd64/amd64/machdep.c#L473-L576) on `setcontext`'s `ucontext_t`: a null context returns `EFAULT`, and `mc_flags` outside `_MC_FLAG_MASK` or an `rflags` differing from the current flags outside `PSL_USERCHANGE` return `EINVAL`; `mc_len` is not checked. As in [`set_fpcontext`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/amd64/amd64/machdep.c#L2349-L2370), `_MC_FPFMT_NODEV` keeps the FPU state, `_MC_FPOWNED_NONE` loads the initial one, the FPU and PCB owners load `mc_fpstate`, and other values return `EINVAL`. The rest is restored as by `setcontext`, which keeps the `%fs` and `%gs` bases FreeBSD would load. A `%cs` that is not a user selector (FreeBSD also raises `SIGBUS`) and `_MC_HASFPXSTATE` throw - [sceKernelGetModuleInfoInternal, sceKernelGetModuleListInternal and sceKernelLoadStartModuleForSysmodule](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) answer for every loaded module, system modules included. `sceKernelGetModuleInfoInternal` fills the 0x1A8-byte `SceKernelModuleInfoEx` of a module handle the way `sceKernelGetModuleInfoFromAddr` does for an address, with [shadPS4's checks](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/kernel/process.cpp#L243-L258) in their order: `EFAULT` for a null pointer, `EINVAL` when `st_size` is not 0x1A8, `ESRCH` for an unknown handle. `sceKernelGetModuleListInternal` returns the same list as `sceKernelGetModuleList`, as [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd4/kernel/ps4_libkernel.pas#L852-L858) does; fpPS4 marks that as a guess, and neither is checked on a PS5. `sceKernelLoadStartModuleForSysmodule` forwards to `sceKernelLoadStartModule` +- [scePthreadMutexIsowned, utrace and tcsetsid](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) follow FreeBSD 11.4 except where stated. `scePthreadMutexIsowned` returns 0 when libkernel's `pthread_mutex_isowned_np` does and `0x80020000` plus its result (`0x80020001`) for a mutex the calling thread holds, as firmware 13.42's `libkernel_sys.sprx` was measured to do in an emulator ([#9713](https://github.com/boykopovar/AnyPS5/pull/9713)); a console run is not measured. `utrace` returns 0 without reading its record, as [`sys_utrace`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/kern/kern_ktrace.c) does for an untraced process; that the PS5 kernel has `KTRACE` (else `ENOSYS`) is assumed. `tcsetsid` fails with `EINVAL` unless `pid` is `getsid(0)`, then issues `TIOCSCTTY`, which every guest descriptor refuses (`ENOTTY`, `EBADF` when closed), as libc's [termios.c](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libc/gen/termios.c) - [sceKernelIsModuleLoaded](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) follows firmware 13.42's libkernel_sys: it asks `sceKernelGetModuleListInternal` for up to 0x300 modules and returns its error, then returns 0 when the first 256 bytes of one module's `sceKernelGetModuleInfo` name equal the argument, else `SCE_KERNEL_ERROR_ESRCH`. It inherits the names `sceKernelGetModuleInfo` reports, so a system library is found under its host file name, such as `libSceAjm.prx`, and the console's `libSceAjm.sprx` returns `SCE_KERNEL_ERROR_ESRCH` here. A null name throws, where the console faults in `strncmp`. Closes when: `sceKernelGetModuleInfo` reports the console's module names - [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)) - [Console identity](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - `get_authinfo` errors and capabilities are unverified on PS5, returning `EFAULT` when passed an unwritable destination pointer and following [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/396c2a09a38d22eaa02aab32c1967263dceab95f/src/core/libraries/kernel/kernel.cpp) with `EPERM` for null, and zero authority and credential words. The host models a retail unit (`IsCEX` 1, `IsGenuineCEX` 1, `IsDevKit` 0, `IsGenuineDevKit` 0, `IsTestKit` 0, `IsGenuineTestKit` 0) and Prospero (`IsProspero` 1, `IsKratos` 0). On a CFI-2014 PS5 (firmware 13.60) a payload process got 0 from `sceKernelIsProspero()`; a payload process is not a title, so the value a title gets is unmeasured. @@ -384,7 +379,7 @@ - [pthread_mutexattr_getgen_np, pthread_mutexattr_setgen_np, scePthreadMutexattrGetgen and scePthreadMutexattrSetgen](../../../core/libs/prx/libkernel_sys/Export.Discovery.cpp) (libkernel_sys) were reconstructed from the firmware 13.60 exports: generation is a one-bit attribute, set accepts only 0 or 1 and otherwise returns `EOPNOTSUPP`, and the `sce` forms convert the POSIX error to `0x80020000 | errno`. The flag is retained in AnyPS5's attribute state; whether it changes mutex initialization is unmeasured. Runtime behaviour on a PS5 is not independently validated - [sceKernelTitleWorkaroundIsEnabled](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - the `{version, align, ids[2]}` layout, the bit test and the limit follow PS5 firmware 13.42 libkernel_sys and libkernel, read from disassembly and not measured on a console, where bits 0 to 0x7F are accepted, the bit is compared unsigned, and the misspelled export `sceKernelTitleWorkdaroundIsEnabled` runs the same code. Titles usually pass the `title_workaround` block of `sceKernelGetAppInfo`, which is written as zeros here; where `title_workaround` sits in the 0x58-byte structure is unmeasured, and with the shadPS4 offsets (0x30 to 0x48) it falls in the zeroed bytes, so its bits read 0; an unreadable workaround pointer or an unwritable output pointer returns `SCE_KERNEL_ERROR_EFAULT` (`0x8002000e`) as a host-safety fallback; console behaviour for unwritable memory has not been measured - [sceKernelGetProcessType](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - not measured on a console; as in shadPS4, the title's own pid, -1 and 0 are type 0 (big application) and any other pid is `ESRCH` -- [cpuset, cpuset_getid, cpuset_setid](../../../core/libs/prx/libkernel_sys/Cpuset.cpp) (libkernel_sys) - console root IDs and inheritance are unverified; non-process scopes (TID, CPUSET, JAIL) throw as unmodelled as per-thread cpusets and hierarchy visibility are not tracked. CPU set assignment and queries follow FreeBSD 11.4 [kern_cpuset.c](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/kern_cpuset.c): cpuset allocates sequential IDs from 2; cpuset_setid supports CPU_WHICH_PID for known IDs (or root 0, default 1); dynamic sets are single-process referenced and unlinked when vacated, returning ESRCH on lookup; cpuset_getid returns root 0 for CPU_LEVEL_ROOT and the assigned ID for CPU_LEVEL_CPUSET. Output writes use GuestCopyOut_nid_no_patch (EFAULT on bad pointers), with level/which/id errors returned before copyout; levels outside 1 to 3 return EINVAL where FreeBSD returns 0 with the base set ID; other which return EINVAL, unknown pids return ESRCH +- [cpuset, cpuset_getid, cpuset_setid](../../../core/libs/prx/libkernel_sys/Cpuset.cpp) (libkernel_sys) - console root IDs and inheritance are unverified; non-process scopes (TID, JAIL) throw as unmodelled as per-thread cpusets and hierarchy visibility are not tracked. CPU set assignment and queries follow FreeBSD 11.4 [kern_cpuset.c](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/kern_cpuset.c), and the console has not been checked: cpuset allocates sequential IDs from 2; cpuset_setid supports CPU_WHICH_PID for known IDs (or root 0, default 1); dynamic sets are single-process referenced and unlinked when vacated, returning ESRCH on lookup; cpuset_getid supports CPU_WHICH_PID and CPU_WHICH_CPUSET (semantics from 11.4 source, console unverified), returning root 0 for CPU_LEVEL_ROOT and the assigned or current process set ID for CPU_LEVEL_CPUSET and CPU_LEVEL_WHICH, with 64-bit IDs truncated to int and id == -1 resolving to the calling thread's base set. Output writes use GuestCopyOut_nid_no_patch (EFAULT on bad pointers), with level/which/id errors returned before copyout; levels outside 1 to 3 return EINVAL where FreeBSD returns 0 with the base set ID; other which return EINVAL, unknown pids return ESRCH - [sceKernelSettimeofday](../../../core/libs/prx/libkernel_sys/Export.Discovery.cpp) (libkernel_sys) - forwards to `settimeofday` and returns its result, or `0x80020000` plus `errno` for a failure, as shadPS4's [`sceKernelSettimeofday`](https://github.com/shadps4-emu/shadPS4/blob/7ea677e56d75bf3d90ace2b0a5144a665d1f38b6/src/core/libraries/kernel/time.cpp#L565-L571) maps its call through `ErrnoToSceKernelError`; that branch passes the call's return value, and shadPS4's own `settimeofday` never fails, so the errno form the other sce wrappers here use is taken. Nothing is measured on the console, and a valid time inherits `settimeofday`'s refusal to set the host clock ### Unknown function info diff --git a/docs/dev/debt/libwebrtc.md b/docs/dev/debt/libwebrtc.md index eceec3f6a8..3dedda269a 100644 --- a/docs/dev/debt/libwebrtc.md +++ b/docs/dev/debt/libwebrtc.md @@ -2,7 +2,7 @@ ### Unknown function info -- [385 exports (60 without a known name, bound by NID)](../../../core/libs/prx/libwebrtc/Export.cpp) (libwebrtc) - every function export of `libwebrtc.sprx` from the firmware 13.60 export table that the next section does not implement, names from aerolib.csv only where `tools/nid_names.py` `compute_nid` reproduces the NID. Signatures, parameter layouts and behaviour are unknown; every export throws through `NotImplemented_nid_no_patch` +- [351 exports (60 without a known name, bound by NID)](../../../core/libs/prx/libwebrtc/Export.cpp) (libwebrtc) - every function export of `libwebrtc.sprx` from the firmware 13.60 export table that the next section does not implement, names from aerolib.csv only where `tools/nid_names.py` `compute_nid` reproduces the NID. Signatures, parameter layouts and behaviour are unknown; every export throws through `NotImplemented_nid_no_patch` - [31 data exports](../../../core/libs/prx/libwebrtc/Export.cpp) (libwebrtc) - the module also exports 31 data objects (+qcl2IzxTzg, -ix-knPYqWA, 00mefEErs+Y, 5FM0YOa3NWc, 67nsSkXmJ7E, 7I2xDnqfDV4, 8Dzy4JayJSU, CVCDyAzGrhI, HpbtuVdvO-w, PGIpx6NnFLE, Q+xnDp4KI-s, QXDv9OpZNvM, aOOjLmup+u4, g5USRJNh3bU, hvpHGXjlpbk, iU6+qODNSjU, jpkYoOv34AM, lHLJlq3NEqE, lQJvoyxHov4, nUaORqH85DQ, o6LS+08kOUs, oh8srl3AcNE, oslHzys1vV0, sp5NqvqDdHw, t66Gw3OIpSo, tr8vseK8-ro, uSis0q+bv6k, uTz5pEKL+hM, uxkudCJIYJA, whHm70jlRXw, yW0sV3sCdPA); they are not defined, so imports of them stay unresolved until their layout is known ### Public source implementations @@ -14,3 +14,10 @@ - [webrtc::rtcp::CommonHeader::Parse](../../../core/libs/prx/libwebrtc/src/RtcpCommonHeader.cpp) (libwebrtc) - adapted from [common_header.cc](https://webrtc.googlesource.com/src/+/8d78f5de6c27b2c793039989ea381f1428fb0100/modules/rtp_rtcp/source/rtcp_packet/common_header.cc), with its WebRTC copyright notice and BSD licence at the top of the file, on the 16-byte layout of `common_header.h`: packet type, count or format and padding size at 0 to 2, payload size at 4 and payload pointer at 8. Its checks run in the same order, so a short buffer or a wrong version leaves the header untouched and the later failures leave the fields already parsed. The `RTC_LOG(LS_WARNING)` message each failure logs upstream is not emitted. A null header, or a null buffer of 4 bytes or more, which upstream reads, throws. Closes when: the warnings are logged through libwebrtc's logging and the parse is compared with the console. - [WebRtcOpus encoder functions](../../../core/libs/prx/libwebrtc/src/Opus.cpp) (libwebrtc) - the 16 `WebRtcOpus_*` encoder exports are adapted from [opus_interface.cc](https://webrtc.googlesource.com/src/+/8d78f5de6c27b2c793039989ea381f1428fb0100/modules/audio_coding/codecs/opus/opus_interface.cc), with its WebRTC copyright notice and BSD licence at the top of the file. They encode through the bundled libopus 1.6.1, not WebRTC's pinned copy, so packet bytes can differ. Signatures match at both revisions; M127 only adds a DTX change behind the `WebRTC-Audio-OpusAvoidNoisePumpingDuringDtx` field trial, which stays off because `InitFieldTrialsFromString` throws. Frames over 60 ms, up to 120 ms, throw: they depend on `WEBRTC_OPUS_SUPPORT_120MS_PTIME`. A null encoder or buffer in `Encode`, which upstream reads, throws. Closes when: the build flag is known and packets are compared with the console. - [WebRtcVad_Create, WebRtcVad_Free, WebRtcVad_Init, WebRtcVad_set_mode and WebRtcVad_Process](../../../core/libs/prx/libwebrtc/Vad.cpp) use the opaque-handle C API and validation rules from [upstream WebRTC](https://github.com/webrtc-mirror/webrtc/blob/3710345e71ad3a7c1f4ed4efef152fd4fc24869a/common_audio/vad/webrtc_vad.c), with the signal-processing core from [libfvad](https://github.com/dpirch/libfvad/tree/532ab666c20d3cfda38bca63abbb0f152706c369). PS5 signatures and classification parity are not measured; differences between the console core and this WebRTC-derived revision remain unverified. Handles are host allocations and expose no guest-visible object layout. +- [webrtc::TaskQueueBase::Current and CurrentTaskQueueSetter](../../../core/libs/prx/libwebrtc/src/TaskQueueBase.cpp) (libwebrtc) - adapted from [task_queue_base.cc](https://webrtc.googlesource.com/src/+/8d78f5de6c27b2c793039989ea381f1428fb0100/api/task_queue/task_queue_base.cc), with its WebRTC copyright notice and BSD licence at the top of the file. A thread-local pointer starts null on every thread; the 8-byte setter stores the current queue in `previous_`, makes its queue current, and its destructor makes `previous_` current again. The file's `thread_local` and pthread-key variants behave the same. A null setter, which upstream writes through, throws. Closes when: compared with the console. +- [rtc::IPAddress::ipv4_address, ipv6_address, v4AddressAsHostOrderInteger, operator== and rtc::CountIPMaskBits](../../../core/libs/prx/libwebrtc/src/IpAddress.cpp) (libwebrtc) - adapted from [ip_address.cc](https://webrtc.googlesource.com/src/+/8d78f5de6c27b2c793039989ea381f1428fb0100/rtc_base/ip_address.cc), with its WebRTC copyright notice and BSD licence at the top of the file, on the 32-byte layout of `ip_address.h`: the vtable pointer at 0, `family_` at 8 and the `in_addr` or `in6_addr` union at 12. `AF_UNSPEC`, `AF_INET` and `AF_INET6` are 0, 2 and 28, as in [FreeBSD's socket.h](https://github.com/freebsd/freebsd-src/blob/55ac3c32b9ddba18b78bb3c58325294ba2e0bf6b/sys/sys/socket.h#L221-L256) and libSceNet. `CountIPMaskBits` counts from the top bit to the lowest set bit of the first word that is not all ones, so a mask with gaps counts them, as upstream does. A null address, which upstream reads, throws. Closes when: compared with the console. +- [webrtc::AudioFrame::AudioFrame(), Mute, muted, data, mutable_data and AudioFrameOperations::Mute(AudioFrame*)](../../../core/libs/prx/libwebrtc/src/AudioFrame.cpp) (libwebrtc) - adapted from [audio_frame.cc](https://webrtc.googlesource.com/src/+/8d78f5de6c27b2c793039989ea381f1428fb0100/api/audio/audio_frame.cc) and [audio_frame_operations.cc](https://webrtc.googlesource.com/src/+/8d78f5de6c27b2c793039989ea381f1428fb0100/audio/utility/audio_frame_operations.cc), with their WebRTC copyright notices and BSD licence at the top of each file, on the 15456-byte layout of `audio_frame.h` with `muted_` at 15432 and the libc++ `std::optional` that abseil's `ABSL_OPTION_USE_STD_OPTIONAL 2` selects. The constructor leaves the samples uninitialized, as upstream does. A muted frame's `data()` points to a shared zeroed buffer. `AudioFrameOperations::Mute` on more than 7680 samples, which only an `RTC_DCHECK_GE` rejects, throws. A null frame throws. Closes when: compared with the console. +- [webrtc::ANAStats::ANAStats() and the AudioEncoder GetANAStats, GetDtx, SetTargetBitrate, DisableAudioNetworkAdaptor, OnReceivedRtt, OnReceivedOverhead and SetReceiverFrameLengthRange defaults](../../../core/libs/prx/libwebrtc/src/AudioEncoder.cpp) (libwebrtc) - adapted from [audio_encoder.cc](https://webrtc.googlesource.com/src/+/8d78f5de6c27b2c793039989ea381f1428fb0100/api/audio_codecs/audio_encoder.cc), with its WebRTC copyright notice and BSD licence at the top of the file. `ANAStats` is the 56-byte struct of seven libc++ `std::optional`s (value, then the engaged flag) that abseil's `ABSL_OPTION_USE_STD_OPTIONAL 2` selects; the constructor and `GetANAStats` leave all of them empty, and `GetANAStats` returns it through the caller's result pointer. `GetDtx` returns false and the other defaults do nothing, as upstream. A null `ANAStats` or result throws. Closes when: compared with the console. +- [webrtc::AudioDeviceGeneric BuiltInAECIsAvailable, BuiltInAGCIsAvailable, BuiltInNSIsAvailable, EnableBuiltInAEC, EnableBuiltInAGC, EnableBuiltInNS and GetPlayoutUnderrunCount](../../../core/libs/prx/libwebrtc/src/AudioDeviceGeneric.cpp) (libwebrtc) - adapted from [audio_device_generic.cc](https://webrtc.googlesource.com/src/+/8d78f5de6c27b2c793039989ea381f1428fb0100/modules/audio_device/audio_device_generic.cc), with its WebRTC copyright notice and BSD licence at the top of the file. As upstream, the availability checks return false and the others return -1 without reading the device. The `RTC_LOG_F(LS_ERROR)` "Not supported on this platform" message each call logs upstream is not emitted. Closes when: the message is logged through libwebrtc's logging and the defaults are compared with the console. +- [webrtc::AudioDecoder HasDecodePlc, DecodePlc, GeneratePlc and ErrorCode](../../../core/libs/prx/libwebrtc/src/AudioDecoder.cpp) (libwebrtc) - adapted from [audio_decoder.cc](https://webrtc.googlesource.com/src/+/8d78f5de6c27b2c793039989ea381f1428fb0100/api/audio_codecs/audio_decoder.cc), with its WebRTC copyright notice and BSD licence at the top of the file. As upstream, `HasDecodePlc` returns false, `DecodePlc` and `ErrorCode` return 0, and `GeneratePlc` leaves the concealment buffer untouched, none of them reading the decoder or the output. Closes when: compared with the console. +- [webrtc::GetAvailableCpuFeatures](../../../core/libs/prx/libwebrtc/src/CpuFeatures.cpp) (libwebrtc) - whether the module is built with `WEBRTC_ENABLE_AVX2` is unknown; this follows the default x86 build, where `avx2` needs AVX, XSAVE, OSXSAVE, the kernel saving the YMM state, AVX2 and BMI2. Without that flag the console reports `avx2` false on every CPU. Adapted from [cpu_features.cc](https://webrtc.googlesource.com/src/+/8d78f5de6c27b2c793039989ea381f1428fb0100/modules/audio_processing/agc2/cpu_features.cc) and `GetCPUInfo` in [system_wrappers/source/cpu_features.cc](https://webrtc.googlesource.com/src/+/8d78f5de6c27b2c793039989ea381f1428fb0100/system_wrappers/source/cpu_features.cc), with their WebRTC notices and BSD licence at the top of the file: `sse2` is CPUID leaf 1 EDX bit 26 and `neon` is false. The features are those of the host CPU running the title, not the console's Zen 2. Closes when: the build flag is known. diff --git a/docs/dev/debt/relinker.md b/docs/dev/debt/relinker.md index 21e210b90e..11ebff2c06 100644 --- a/docs/dev/debt/relinker.md +++ b/docs/dev/debt/relinker.md @@ -12,10 +12,11 @@ - [Guest exports at address 0 on Linux](../../../core/relinker/elfpatcher/src/linux/LinuxGuestModuleWriter.cpp) - the host loader skips a defined symbol whose value is 0 (`check_match` in glibc's `elf/dl-lookup.c`), and the [ps5-payload-dev SDK](https://github.com/ps5-payload-dev/sdk) links a shared object's code at address 0, so its first function is exported at 0, as in the SDK's `hello_so` sample. Such a function export is written as a `jmp` to it in the segment the module gains, so a pointer to it taken through the symbol differs from one the module computes itself, and two names for it get two `jmp`s at different addresses. A module whose `jmp` would lie 2 GiB or more above address 0 fails to convert. A data or untyped export at address 0 stays unresolvable on Linux, and an import of one is not reported at conversion: the host loader stops on it as an undefined symbol. - [Runtime-loaded modules](../../../core/relinker/relinker/src/guest/GuestModuleBuilder.cpp) (`--module-dir`) can be mapped early for bindings. Linux defers their initialization to an explicit load of each module; standard SDK folders keep their mapping initializer, including through the browser conversion API. Windows defers such a module too, unless a module outside these directories or in a standard SDK folder needs it, directly or through other modules; then it starts at startup without start arguments. On both, loading a module does not start the deferred modules it needs: each starts when the title loads it, and one the title never loads gets no `module_start` and no initializers. On macOS, a module another module needs is initialized at startup without start arguments; console dependency-start ordering is unmeasured. +- [Unavailable unused module dependencies](../../../core/relinker/relinker/src/guest/GuestModuleBuilder.cpp) (Linux) - with `--omit-unavailable-dependencies`, a guest module dependency that no converted module provides, from which the module imports no symbol, and that no `--rpath` directory holds (every run path directory existing at conversion time) is left out of the needed libraries with a warning. Without the option it is kept and a note names the option. A library found only through `LD_LIBRARY_PATH` or installed after conversion, including one a module needs only for its initialization, is lost when the option is used. That the console loader skips such a dependency is inferred, not measured: Evil Inside (PPSA02508) ships `Il2CppUserAssemblies.prx` naming `PS4sceSystemServiceParamGetInt_plugin.prx`, which its package does not contain. - [Runtime-loaded modules the executable needs](../../../core/relinker/relinker/src/guest/GuestModuleBuilder.cpp) (`--module-dir`) are mapped at startup on Windows and started when the title loads them with `sceKernelLoadStartModule`, with their start arguments, as a Unity player loads `mono-ps4.sprx`. A title that calls such a module before loading it finds its initializers not run; whether the console starts it earlier is not measured. A module the executable only imports from and the title never loads itself gets no `module_start` and no initializers at all under this deferral. The [entry stub](../../../core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp) prints `Deferred module ` at startup for each such module that an import of the executable names, whether or not the title loads it later; an import without a module name is not reported. macOS still starts it at startup. -- The executable file that [relinker](../../../core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp) generates opens a console when launched, unless it is relinked with `--windows-gui`, which selects the GUI subsystem. A GUI build started from Explorer has no standard output or error handle; the [entry stub](../../../core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp) gives each missing one `anyps5.log` beside the executable, replacing the previous run's file (#3282). Errors about the executable path or about waiting for the previous LoadExec process are written before the log is opened and are lost when the standard error handle is missing. If the log cannot be created, output to the missing handles is lost. +- The executable file that [relinker](../../../core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp) generates opens a console when launched, unless it is relinked with `--windows-gui`, which selects the GUI subsystem. A GUI build started from Explorer has no standard output or error handle; the [entry stub](../../../core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp) gives each missing one `anyps5.log` beside the executable, replacing the previous run's file (#3282). The stub waits for the previous LoadExec process after opening the log, so its errors reach it. Errors about the executable path are still lost when the standard error handle is missing: the log's path comes from that same executable path, so no log exists to write to yet. If the log cannot be created, output to the missing handles is lost. - [Guest import registries](../../../core/relinker/relinker/src/guest/GuestModuleBuilder.cpp) - declared host imports of bundled modules are recorded with their module name and relocation target, even when another guest module exports the same stripped NID. No-op relocations are omitted. Registries describe import bindings, not implementation correctness or runtime reachability. -- Registry [call-site analysis](../../../core/relinker/relinker/src/analysis/CallSiteResolver.cpp) does not classify instructions with `F3`, `66`, `67`, `F0`, `2E`, `26`, `36`, FS/GS prefixes, or `8B` loads without REX.W, so missing call sites do not establish that an import is unused. +- Registry [call-site analysis](../../../core/relinker/relinker/src/analysis/CallSiteResolver.cpp) does not classify instructions with `F3`, `66`, `F0`, FS/GS prefixes, or `8B` loads without REX.W, so missing call sites do not establish that an import is unused. - Converted Windows main executables [omit `DYNAMIC_BASE`](../../../core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp) and [prefer `0x400000`](../../../core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp) for their lowest ELF load page 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). 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). Relocations are kept, so if that range is taken Windows can relocate the image, possibly above that window; no run has observed this, and from this base an image cannot reach the shared user data page at `0x7ffe0000`, since Windows refuses a `SizeOfImage` above `0x77000000` (#9332). Verified on Windows 11 only. - [SDK memory-management syscall traps](../../../core/relinker/relinker/src/analysis/SyscallScanner.cpp) under `--trap-sdk-syscall-stubs` recognize three exact sequences from [SDK libc/mman.c](https://github.com/ps5-payload-dev/sdk/blob/12cf3dda40/libc/mman.c), seen in Nuvio Maxd 0.5.15 (PPSA99288): syscall 74 and the two syscall 477 paths, with their argument setup and carry-flag error handling. A match needs a decoded instruction boundary and the whole sequence inside the scanned range; other SDK/compiler output is rejected. The matched syscall becomes `UD2`; this does not implement mmap or mprotect and a call still faults. Three more cover the inlined `__syscall6` wrappers `mount`, `nmount` and `unmount` ([SDK libc/mount.c](https://github.com/ps5-payload-dev/sdk/blob/7116a650a0/libc/mount.c)) in ftpsrv v0.21.1 and websrv v0.35, with numbers 21, 378 and 22 pinned, so a stub carrying another number does not match. None is implemented; a trapped call 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. @@ -45,6 +46,7 @@ - In Windows output, the conversion fails when a code entry lies after the first byte of a guest GS instruction that the [TLS builder](../../../core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp) would replace with `UD2`: `Branch enters a guest GS instruction` for a relative branch target, `GS trap would overwrite an executable entry point` for any other entry the [code analysis](../../../core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp) follows (a function symbol, a relocated pointer, a RIP-relative `lea` target, an initializer array entry) or for a pointer target it takes as data. Both messages give the position of the entered byte, and no option skips either check. Linux output keeps the instruction, and libkernel's `sysarch` sets the GS base there. - A relocation against a dynamic symbol entry that is all zero, like the null symbol, takes the value 0 plus its addend, as the gABI defines for `STN_UNDEF` ([pipeline](../../../core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp)). The Linux output imports such an entry as an unnamed weak symbol, which the host loader resolves to 0; the Windows and macOS outputs write the value into the slot ([relocation builder](../../../core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp)). What the console's loader does with such an entry is not measured. OliSe Player (PPSA01153) has seven of them, and its code calls through those slots only when they are not 0 ([#4437](https://github.com/boykopovar/AnyPS5/issues/4437)). - In Windows output, an executable relocation against a named import whose 8 bytes overlap the `PT_TLS` initialization image fails the conversion ([PE patcher](../../../core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp)), as it does in a guest module. ld.so applies such a relocation before it copies the image into a thread's TLS block, so every thread reads the bound value. The `.gtls` template is copied from the image at conversion time, and the loader allocates the main thread's block from it before the entry stub binds imports; binding would require the stub to write the value into both the template and that block. No title with such a relocation is known. +- In macOS output, the conversion fails when a code entry lies after the first byte of a guest GS instruction that the [macOS image writer](../../../core/relinker/elfpatcher/src/macos/MacOsImage.cpp) would replace with `UD2`: `Branch enters a guest GS instruction` for a relative branch target, `GS trap would overwrite an executable entry point` for any other entry the [code analysis](../../../core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp) follows (a function symbol, a relocated pointer, a RIP-relative `lea` target, an initializer array entry) or for a pointer target it takes as data. Both messages give the position of the entered byte, and no option skips either check. - In macOS output, a guest instruction that addresses memory through GS becomes `UD2` and NOP padding of its own length ([macOS image writer](../../../core/relinker/elfpatcher/src/macos/MacOsImage.cpp)): the host owns GS, which holds the pthread TSD base the guest TLS stubs read, and the guest GS base that FreeBSD's `sysarch(AMD64_SET_GSBASE)` would set is not emulated. A converted module runs until such an instruction is reached, which then faults. Prospero Win (PPSA99995) has six in its Wine `ntdll`. - macOS output still writes pointer fixups into executable segments. `dyld_info -fixups` reports each as `pointer rebase is in executable segment`, next to `pointer rebase is in non-writable segment` for a fixup in a segment without write access, which the [macOS image writer](../../../core/relinker/elfpatcher/src/macos/MacOsImage.cpp) rejects. A segment is executable and writable when its `PT_LOAD` is `PF_R|PF_W|PF_X`, or when a writable `PT_LOAD` shares a 4 KiB page with an executable one, because each page takes the union of the flags of the `PT_LOAD`s that cover it. dyld on an M1 Pro running macOS 27.0.1 (x86-64 output under Rosetta 2, `LC_BUILD_VERSION` minos and sdk 11.0) loads such an executable, applies the fixup and runs it; a fixup into a `PF_R|PF_X` segment ended the process there with SIGBUS in `dyld4::Loader::applyFixupsGeneric` before `main`, not with a refusal. Not measured for a dylib or under another macOS or SDK version. - The Linux placement of the `--to-intel` stubs in the [ELF patcher](../../../core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp) is covered only by a synthetic test. @@ -52,7 +54,7 @@ - `--to-rosetta` output faults on an M2 running macOS 14.8.9 when it reaches AVX, AVX2, FMA, F16C, RDRAND or a BMI1/BMI2 instruction other than ANDN, BLSI, BLSMSK, BLSR, SHLX, SHRX, SARX, RORX, BZHI, BEXTR, MULX, TZCNT and LZCNT: Rosetta on that host raises SIGILL for them and for RDSEED, RDPID, CLWB, ADCX and ADOX ([matcher](../../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp)). Only TZCNT, LZCNT, POPCNT, MOVBE, CLFLUSHOPT, PREFETCHW and RDTSCP execute, and RDTSCP sets ECX to 0xAAAAAAAB. CPUID leaf 1 reports POPCNT and none of MOVBE, AVX, FMA, F16C or RDRAND, leaf 7 reads zero, and leaf 0x80000001 reports RDTSCP but neither LZCNT nor PREFETCHW. An M2 Max on macOS 27.2 and an M2 Pro on macOS 27.0.1 report the same CPUID but execute everything except RDSEED, RDPID, CLWB, ADCX and ADOX, so the faults follow the macOS version, not the chip. - `--to-intel` replaces the approximate legacy SSE and VEX.128 register forms of RSQRTPS/RCPPS and VRSQRTPS/VRCPPS with [correctly rounded](../../../core/relinker/codegen/src/x86/ReciprocalLowering.cpp) 1/sqrt(x) and 1/x, not with the AMD approximation: its tables are not public. Intel's approximation of 1/sqrt(1.0) is 0x3F7FF000, and two Newton-Raphson steps on it settle on 0.99999994 instead of 1.0; a renormalised identity quaternion then turns into a NaN axis in PPSA21564, which the console does not show. Refined results now reach the exact value; unrefined ones can still differ from the console in the low bits. Each site costs an out-of-line stub (two jumps, a spill below the red zone, SQRTPS/DIVPS). Legacy SSE forms preserve the destination's upper YMM bits; VEX forms clear them. The 256-bit, scalar and memory forms keep the native instruction, as does a site shorter than 5 bytes with no movable instruction after it for the jump (5 sites in PPSA21564, counted before a following RIP-relative instruction could move into the stub; not recounted). - XSAVEC raises an illegal instruction under Rosetta and is not converted, because it writes the compacted format. XSAVEOPT, which also faults there, is rewritten to XSAVE in place by the [matcher](../../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp) under `--to-rosetta` (`0F AE /6` becomes `/4`; prefixes and operand are kept). The two differ only in the init and modified optimizations, which let XSAVEOPT skip writing unchanged or initial state ([Intel SDM volume 1](https://cdrdv2-public.intel.com/812382/253665-sdm-vol-1.pdf), chapter 13), so XRSTOR restores the same state from either area. Measured on one M5 with macOS 27.0.1: XSAVE and XRSTOR handle x87, SSE and AVX state in the standard format, although CPUID leaf 1 reports neither XSAVE nor OSXSAVE and leaf 0xD sub-leaf 1 reports neither XSAVEOPT nor XSAVEC. XSAVE also executes on an M5 with macOS 26.4, where it writes an XSTATE_BV of 0x7 and XGETBV returns the same x87, SSE and AVX mask although leaf 1 still reports neither XSAVE nor OSXSAVE; XRSTOR was not exercised there. The converter tests' x86-64 build executes the replacement XSAVE under Rosetta on that host. -- `--trace-imports` ([Windows entry stub](../../../core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp), [Linux entry stub](../../../core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp), [macOS entry](../../../core/relinker/elfpatcher/src/macos/MacOsImage.cpp)) leaves every import that is not a PLT slot or a function GOT slot untraced, and the same for a function reached through any other relocation, `sceKernelDlsym`, a weak import, or a module loaded at run time. On Linux the entry stub makes the page of every traced slot readable, writable and executable with `mprotect` before it stores the thunk, so a slot inside `PT_GNU_RELRO` can be replaced; those pages stay writable, and data pages become executable, while tracing is on. On macOS the stub writes the thunk into the bound slot and does not change its protection, because that slot stays in a writable segment +- `--trace-imports` ([Windows entry stub](../../../core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp), [Linux entry stub](../../../core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp), [macOS entry](../../../core/relinker/elfpatcher/src/macos/MacOsImage.cpp)) leaves every import that is not a PLT slot or a function GOT slot untraced, and the same for a function reached through any other relocation, `sceKernelDlsym`, a weak import, or a module loaded at run time. On Linux the entry stub makes the page of every traced slot readable and writable with `mprotect` before it stores the thunk, so a slot inside `PT_GNU_RELRO` can be replaced; those pages stay writable while tracing is on. On macOS the stub writes the thunk into the bound slot and does not change its protection, because that slot stays in a writable segment - [ANDN lowering](../../../core/relinker/codegen/src/x86/AndnLowering.cpp) uses scalar NOT and AND for 32- and 64-bit register and memory operands under `--to-rosetta`, following the ANDN entry in [Intel SDM volume 2A](https://cdrdv2-public.intel.com/812383/253666-sdm-vol-2a.pdf). SF and ZF follow the result, CF and OF clear, and AF and PF are undefined. The scratch register is saved below the 128-byte red zone. A 64-bit stack-relative displacement that exceeds signed disp32 after reserving the spill frame throws. The M1 test covers register overlap, RSP operands, memory addressing, RIP relocation, flags and red-zone preservation; precise fault delivery inside a spill sequence has not been verified. - [BLSI, BLSMSK and BLSR lowering](../../../core/relinker/codegen/src/x86/BlsLowering.cpp) uses scalar LEA, AND, XOR, NEG and OR after a TEST on the source, with two scratch registers saved below the red zone, under `--to-rosetta`, following their SDM entries: SF follows the result, ZF follows the result for BLSI and BLSR and clears for BLSMSK, CF is set for a zero source (BLSR, BLSMSK) or a nonzero source (BLSI) through STC on the branch that needs it, OF clears, and AF and PF are undefined. A 64-bit stack-relative displacement that exceeds signed disp32 after reserving the spill frame throws, as for ANDN. PDEP and PEXT are not lowered. The execution test runs only on an x86-64 host and checks the SDM semantics, not the hardware instruction; precise fault delivery inside a spill sequence has not been verified. @@ -62,3 +64,5 @@ - Relative pointer-width unwind pointer decoding in the [exception frame reader](../../../core/relinker/relinker/src/analysis/UnusedNidFilter/EhFrameReader.cpp) (`DW_EH_PE_absptr` and `DW_EH_PE_udata8`) wraps modulo 2^64 to accommodate backward offsets, as standard DWARF unwinders add them. Console unwind table validation for out-of-range displacements is unmeasured. Unsigned offsets narrower than pointer width (`DW_EH_PE_udata2` and `DW_EH_PE_udata4`) cannot express negative displacements and throw when address addition overflows. - [SysV and GNU dynamic symbol hash table bounds](../../../core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp) - whether retail console dynamic linkers tolerate malformed bucket counts, out-of-range chain indices, or unterminated GNU hash chains remains unmeasured against console loader diagnostics. Dynamic symbol count resolution validates SysV and GNU hash headers against arithmetic overflow and segment bounds, throwing RelinkerException when bucket counts are zero, bucket indices exceed chain length, or chain traversal exceeds the mapped ELF image. + +- RIP-relative displacement calculation in the [control-flow graph](../../../core/relinker/relinker/src/analysis/UnusedNidFilter/ControlFlowGraph.cpp) and [strict reachability analysis](../../../core/relinker/relinker/src/analysis/UnusedNidFilter/StrictReachability.cpp) wraps modulo 2^64 for 64-bit operand sizes and modulo 2^32 under the address size override prefix (`0x67`), retaining conservative dependencies for out-of-range targets without discarding references. Console fault delivery and loader rejection for out-of-bounds RIP-relative displacements are unmeasured. diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 96c5feb31f..fc7d1ef656 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -28,6 +28,7 @@ - [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. A device without `shaderCullDistance` (MoltenVK) throws on a draw that enables a cull distance; none is emulated. - [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 v_cmpx](../../../core/shader/recompiler/RdnaDecoder/src/GcnInstructionDecoder.cpp) (shader, PS4) - writes its EXEC-masked compare mask to VCC (VOPC) or to SDST (VOP3, which leaves VCC alone) as well as to EXEC, as measured on a PS4 Pro (firmware 10.71) with the default `FLOAT_MODE`; the f32 denormal modes 1 and 2 (below) are not measured on GCN. The decoder keeps the RDNA opcode and carries the GCN destination as the instruction's second destination, which the [compare translation](../../../core/shader/recompiler/Translation/src/CompareInstructions.cpp) writes with the same masked result; [GcnCompareExecWrites](../../../core/libs/prx/libSceAgcDriver/tests/execution/GcnCompareExecWrites.cpp) runs the measured cases. A VOP3 form whose SDST is EXEC writes EXEC twice, in an order inferred from the cleanup rather than measured, and no case reads the SDST pair right after the compare. - [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. - [4_4_4_4 partial writes, refused draws](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp) (shader recompiler) - a merge throws when its shader exports depth, stencil or a sample mask, uses alpha-to-coverage or a quad sample mask, kills pixels under late tests in a draw that writes depth or stencil (a depth write, or a stencil test with a nonzero write mask and a non-keep operation), writes guest memory (stores, atomics, GDS writes) under late tests in a draw with a depth, stencil or depth-bounds test, as the forced early tests would drop the writes of rejected pixels, or kills pixels under late tests after the game has read an occlusion counter, as the early tests would count them. On an RTX 4070 SUPER (driver 610.88) a fragment killed after early tests still wrote depth, and `FragDepth` was ignored with `EarlyFragmentTests`, so the depth write cannot move after the shader there. @@ -35,7 +36,7 @@ ### Functional - [Buffer descriptor type](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) - a buffer V# whose TYPE (word 3 bits 30-31) is not 0 throws `buffer descriptor uses an unsupported type`, except for vector stores through TYPE 2 or 3. Measured on a PS5 (CFI-1115B, firmware 12.20): through TYPE 2 or 3 with FORMAT 0 or a valid format, `buffer_store_dword` and `buffer_store_format_x` wrote nothing and did not fault, also with an unmapped base; loads and GLC atomics left the destination VGPR unchanged, which a null V# can't reproduce, so they throw. TYPE 0 with FORMAT 0 returned 0 for loads and wrote nothing. TYPE 1 faulted the GPU for every access and throws. Unmeasured and still throwing for TYPE 2 and 3: `tbuffer_*`, scalar loads, FORMAT above 77, GCN shaders and V#s loaded at run time; multi-dword and multi-component stores through TYPE 2 and 3 are unmeasured and throw. Closes when: those cases are measured on a PS5. -- [Image format 131](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) (shader recompiler) - the name and bit layout of T# FORMAT 131 (`Format10_10_10_2Float` in `IrBufferFormat`) are not taken from AMD's RDNA 2 documentation, LLVM or Mesa and are not measured. A sampled or non-atomic storage access to it is refused with the `NumericClass` reason, since the [format table](../../../core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp) has no entry for 131; the driver's [texture format table](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp) has none either and refuses it as `unsupported guest texture format`. Closes when: the layout is taken from Mesa's GFX10 format table or measured with the [hardware oracle](../HW_ORACLE.md). +- [Image format 131](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) (shader recompiler) - T# FORMAT 131 (`Format10_10_10_2Float` in `IrBufferFormat`) is refused: a sampled or non-atomic storage access fails with the `NumericClass` reason, since the [format table](../../../core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp) has no entry for 131, and the driver's [texture format table](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp) refuses it as `unsupported guest texture format`. No Vulkan format has its layout, measured with `image_load` on RDNA2 (gfx1036, [hardware oracle](../HW_ORACLE.md)), not on the PS5's gfx1013: R, G and B are bits 0-9, 10-19 and 20-29, each an unsigned float with a 4-bit exponent (bias 7) over a 6-bit mantissa, denormal at exponent 0 and finite at exponent 15 (0x3ff reads 508); A is bits 30-31 as UNORM. - [Pixel interpolators past `SPI_PS_IN_CONTROL.NUM_INTERP`](../../../core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp) (shader recompiler) - when the IR reads an attribute at or past the guest interpolator count, the missing `SPI_PS_INPUT_CNTL` slots are filled as DEFAULT (`OFFSET` 0x20, constant 0/1) before info collection, and SPIR-V emission of `GetInterpolationParameter` for a missing binding returns the same defaults instead of throwing, matching Mesa's treatment of a PS input with no VS output. Component and mode bounds still throw. A retail title prepares a pixel shader with `NUM_INTERP` 0 while it still issues `v_interp` on attribute 0 (`undefined attribute` after the collector fill alone). Console behaviour for that pairing is not measured. - [Windows cache housekeeping](../../../core/shader/recompiler/ShaderDiskCache.cpp): eviction of old versions and temporary files is skipped for `\\?\` and `\\.\` cache roots because the supported WinLibs directory iterator can enumerate the process working directory for long prefixed paths. The active version's `last-used` stamp update still runs; it fails for long `\\.\` aliases with the supported toolchain. Ordinary Windows directory enumeration remains subject to MAX_PATH. @@ -58,7 +59,8 @@ - An [image table](../../../core/shader/recompiler/ImageTableAbi.hpp) entry that cannot be bound exactly reads zeros, and an active lane that selects it raises a BDA fault (reason 9) naming the record's address, words and reason; the dispatch's write-back reports the first one. - An [image table](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) snapshot holds at most 65536 keys per table, 65536 distinct images and 256 S# entries per shader; a key past a longer table's snapshot, or an entry over the budget, faults when selected. A selected column (a T# the SRT walker evaluates) holds its one record wherever it lies. - [Image tables](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) read by `s_load` through a 64-bit pointer hold the keys the key's known bits allow, at most 65536; a key past them, or an entry at a wrapping, unaligned or unmapped address, faults when selected. -- An [image table](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) whose key is neither a V#-relative scalar read nor a value the SRT walker evaluates (for example a shifted or masked runtime-offset V# read, a per-lane value made uniform by `v_readfirstlane`, or a loop-carried phi) binds every record of the snapshot domain instead of the selected ones. So does a table read at `(key << n) & mask` with such a key: a key domain would select records by the unmasked key, so that key is not narrowed. A key the walker evaluates selects its one record instead, unless the shader may write the memory the key is read from at run time (the same buffer, or any memory when the read or a write goes through a pointer or a GPU-only descriptor); then the column binds the whole domain too. A write to the key's memory through another V# over the same bytes is not detected. A shader whose selected records would pass 64 images or 64 sampled image/sampler pairs binds, for the heaps with the most such reads, the union of their keys in one table instead. +- An [image table](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) whose key is neither a V#-relative scalar read nor a value the SRT walker evaluates (for example a shifted or masked runtime-offset V# read, a per-lane value made uniform by `v_readfirstlane`, or a loop-carried phi) binds every record of the snapshot domain instead of the selected ones. So does a table read at `(key << n) & mask` with such a key: a key domain would select records by the unmasked key, so that key is not narrowed. A key the walker evaluates selects its one record instead, unless the shader may write the memory the key is read from at run time (the same buffer, or any memory when the read or a write goes through a pointer or a GPU-only descriptor); then the column binds the whole domain too. A write to the key's memory through another V# over the same bytes is not detected. A shader whose selected records would pass 64 images or 256 sampled image/sampler pairs binds, for the heaps with the most such reads, the union of their keys in one table instead. +- A shader plans at most 64 [samplers](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) (`ShaderInfo::MaxSamplers`, the size of the runtime ABI's `ShaderData` sampler metadata, which [`BindingAllocator`](../../../core/shader/recompiler/Optimization/src/BindingAllocator.cpp) holds every shader to), at most 32 of them direct (each takes two elements of the 64-element sampler heap), and at most 256 image/sampler pairs (`ShaderInfo::MaxSampledPairs`); past them it refuses with `sampler resource limit exceeded`, `shader sampler pairs exceed runtime heap capacity` or `sampled image/sampler pair limit exceeded`. The image-to-sampler masks are 64-bit, one bit per sampler heap element or per sampler, so more samplers need a larger `ShaderData` and wider masks. Ratchet & Clank: Rift Apart (PPSA01474) plans up to 63 samplers and 112 pairs in one shader. - [Image table pointer indexing](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) propagates 32-bit value bounds through `ReadFirstLane` and `SelectU32` so waterfall key masks keep their bit domain. The SRT walker validates runtime select conditions whose active lane mask implies the condition through `LogicalAnd` conjunctions, assuming unselected inactive lanes need not be uniform across the full wave. The same assumption lets the two `s_load`s of one T# record match when one offset is added before `v_readfirstlane` and the other after (`ReadFirstLane(Select(mask, (key << 5) + 16, old), mask)` against `ReadFirstLane(key, mask) << 5`): with no active lane the hardware reads lane 0 of the inactive arm, and the table still binds the whole record. - [Constant-stride image-table offsets](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) scaled before `v_readfirstlane` are recognized through selects only when the active mask implies their condition. Index expressions, including image-dimension queries, remain in GPU IR; variable scales are not constant-stride columns. Runtime SRT evaluation supports unsigned greater-than-or-equal comparisons, uniform selected components of constructed U32x4 values, and known active-mask conjuncts in nested selects. CPU regressions cover unrelated masks, disjunctions, varying/out-of-range components and variable shifts. Live GPU behavior of these additions remains unmeasured. @@ -68,10 +70,10 @@ - An [`image_sample_a` of a 2D MSAA T#](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) is served like the `image_sample` reads above, as sample 0: the `_a` form changes only how dword 3 of its S# is read, not its address. This is unmeasured: the gfx1031 runs have no `_a` read. - `image_store_mip`, `image_store_pck`, PCK2/PCK4 and MIMG BY2/BY4 through an [image table](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) T# fault (`unsupported operation`) when an active lane executes them; the store is dropped and the load reads zeros. 32-bit and 64-bit image atomics through a table lower to `OpAtomic*` on the table `AtomicView`/`Atomic64View` (`Device` scope, relaxed, `AcquireRelease`/`UniformMemory` barrier as the direct path) instead of faulting; the host needs `VK_FORMAT_FEATURE_STORAGE_IMAGE_ATOMIC_BIT` for the view format. Packed formats and BY element counts still fault. - An [image table](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) S# with a min or max reduction or FORCE_UNNORMALIZED faults when selected, as does an integer, converted or depth-bits table T# paired with a direct min or max S# that filters between texels. -- Uint and Sint [image table](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp) entries and direct T#s under a table S# sample through a point-filtered copy of the S#: Vulkan leaves linear filtering of integer formats undefined; the console is assumed to point-sample. +- Uint and Sint [image table](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp) entries and direct T#s under a table S# sample through a point-filtered copy of the S#: Vulkan leaves linear filtering of integer formats undefined. The PS5's gfx1013 is not measured. On RDNA2 (gfx1036, [hardware oracle](../HW_ORACLE.md)), `image_sample_lz` with a bilinear S# returns the same nearest texel as a point S# for 8-, 16- and 32-bit UINT and SINT T#s; mip filtering is not measured. - Comparison sampling of a color texture through an [image table](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) S# throws at the first dispatch or draw that binds it. - An [image table](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) key with no key domain whose `key * stride` wraps past 2^32 to an offset between records faults only when the build already expects a fault; otherwise the fault buffer is not read and it reads zeros. -- [Descriptor web paths](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) whose T# or S# dwords depend on the program counter, EXEC or a lane mask (registers left from `s_getpc_b64`, `s_mov_b64 exec` or a compare and its accumulation, typically on a loop's entry edge) select the first descriptor instead of throwing when the shader is compiled; the path is assumed not to reach the read, which is not proven, and the console would read through those values. A lane that takes such a path records an `InvalidDescriptor` BDA fault. Splits that substitute each incoming edge are not folded: a program-counter arm there stays a mandatory T#. PPSA23566 (UFC 6) needs this for compute shader 0x1163c84000. `APS5_STRICT_DESCRIPTOR_WEBS` makes such a read throw. +- [Descriptor web paths](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) whose T# or S# dwords depend on the program counter, EXEC, a lane mask, the lane index or an image read or sample result (registers left from `s_getpc_b64`, `s_mov_b64 exec`, a compare and its accumulation, `v_mbcnt`, an image load or an image sample, typically on a loop's entry edge) select the first descriptor instead of throwing when the shader is compiled; the path is assumed not to reach the read, which is not proven, and the console would read through those values. A lane that takes such a path records an `InvalidDescriptor` BDA fault. Splits that substitute each incoming edge are not folded: a program-counter arm there stays a mandatory T#. PPSA23566 (UFC 6) needs this for compute shader 0x1163c84000, PPSA26344 for 0x800037fe00 (an image load). A path through an image gather or an image atomic's returned value still throws. `APS5_STRICT_DESCRIPTOR_WEBS` makes such a read throw. - [MaskedSelectEliminator](../../../core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp) can discard inactive source lanes through `ReadFirstLane` only when its read mask implies the write mask and its result is unobserved outside the read mask. A loop-local read mask requires result uses in the same block without phi transport. The empty-EXEC lane-zero fallback follows AMD's [Vega ISA, section 12.8, V_READFIRSTLANE_B32, page 127](https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/instruction-set-architectures/vega-shader-instruction-set-architecture.pdf) and the existing SPIR-V emitter. The optimization is covered by synthetic IR tests but has not been compared with console hardware; a proof that needs a read mask of a kind that is already being solved is refused. - [DenormalFlushEliminator](../../../core/shader/recompiler/Optimization/src/DenormalFlushEliminator.cpp) drops the result flush of a `v_mul_f32` or `v_mul_legacy_f32` product behind its nearest-even tininess check (see f32 denormals) when both factors are denormal-free: a product the check keeps is then zero, infinite, NaN or at least 2^-126 in magnitude, provided the driver rounds it as it rounds the check's multiply. - [Shader modules built at registration](../../../core/shader/recompiler/Recompiler.cpp) (shader recompiler): on devices with graphics pipeline libraries a vertex or pixel module reads buffer V# words, image modes, swizzles and mip ranges, depth-compare state, export mappings and vertex component selectors at run time instead of specializing on them per draw, so its libraries can be built when the shader is registered. A module that would exceed `SpirvTarget::registrationWordLimit` (200000 SPIR-V words) is emitted specialized as before: on RADV (Radeon 660M, Mesa 26.2.4) a sample of 60 pixel shaders of a retail title compiled 1.9 to 2.2 times slower in registration form below 141k words and 3.8 to 5.7 times slower above 280k words (none in between), so the limit sits in that gap; it was not measured on other drivers. Texel-offset folding, the format word of a buffer accessed with the descriptor's format (`buffer_load_format_*`; as runtime values every format's decoder is compiled, which made CI's lavapipe JIT ten times slower) and the vertex attribute numeric class stay specialized, so shaders with the first two, and pixel shaders whose image needs a multisampled descriptor, get no registration result; these, and every draw whose fixed-function state differs from the prebuild's, still build their libraries at the draw. Compute and the other stages stay specialized: in registration form the `agc_driver_dword_pattern_fill` compute shader hung CI's lavapipe (Mesa 25.2.8) when built with `ANYPS5_ENABLE_SPIRV_TOOLS=ON`, not on RADV, and the cause was not found. @@ -88,6 +90,7 @@ - AMD image descriptors with `TYPE=0` are treated as null for [read-only image slots](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) even when the base and other words are nonzero: `image_load` on a gfx1035 with such a T# returns 0,0,0,0, and the same descriptors were seen in direct reads in PPSA01341 on an RTX 4070. Writable and atomic image slots and entries of an image table keep faulting because there is no hardware measurement for them, and the reserved types 2-7 also fault. `TYPE=1` is treated as null in the same read-only slots: gfx1035 reads it as zeros too, and gfx1010 returns zeros for `image_sample_lz`, `image_sample_b`, `image_load` and `image_get_resinfo` with format 22 (#5828); it is not measured on RDNA2, with other formats or with depth-compare reads (`image_sample_c*`). gfx1013 behavior is unverified - [2D accesses through a cube T#](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) (#4139) read the T#'s `BASE_ARRAY` face as a cube access with face coordinate 0 does, 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 on 8x8 and 32x32 32_FLOAT cubes (`DEPTH` 5 and 11, `BASE_ARRAY` 0 and 6): `image_sample_lz`, plain `image_sample` (one level, and six levels in 2x2 compute quads) and `image_sample_l` (the third address VGPR is the LOD, integer and fractional) with DIM 2D equal DIM CUBE with face 0 on every row, with point and bilinear filters, wrap, clamp and mirror addressing and `DISABLE_CUBE_WRAP` 0 and 1, and the VGPR after the two coordinates is not read; `image_load` with DIM 2D reads texel (x, y) of the `BASE_ARRAY` face. So the face coordinates are cube coordinates in [1, 2]: the texel is `floor((u - 1) * width)`. They are translated through the cube mode DIM CUBE accesses use, which samples the face of the 2D-array view at (u - 1, v - 1) with the S# addressing. That matches the hardware inside the face, except within half a texel of its edges with a bilinear filter and `DISABLE_CUBE_WRAP` 0, where the hardware blends texels of the neighbouring faces (also measured on RDNA2, gfx1035, 8x8 cubes with `BASE_ARRAY` 0 and 6). Outside it the hardware clamps a coordinate below 1 to the first texel under every filter, wrap mode and `DISABLE_CUBE_WRAP` setting measured, clamps a coordinate of 2 or more to the last texel when `DISABLE_CUBE_WRAP` is 0 (point filter) and applies the S# addressing to it when it is 1; the translation does none of this, as for DIM CUBE. Only `image_sample_lz`, plain `image_sample`, `image_sample_l`, mip-less `image_load`, and mip-less `image_store` to an image the program only stores to take this mode. Such a store writes texel (x, y) of the `BASE_ARRAY` face, measured on RDNA2 (gfx1036, [by simonbbby](https://github.com/boykopovar/AnyPS5/pull/6799#issuecomment-6217609589), not the PS5's gfx1013) with the [hardware oracle](../HW_ORACLE.md) on 8x8 mip-less cube arrays (`DEPTH` 11) of 32_UINT, 32_FLOAT and 16_16_16_16_FLOAT ([the last also by simonbbby](https://github.com/boykopovar/AnyPS5/pull/6799#issuecomment-6224023050), with exactly representable half values) with `BASE_ARRAY` 0 and 6, wave32 and wave64: the 64 stored texels land on that face and every other byte of the 64 KiB backing is unchanged. Other formats, and Crimson Desert's own 256x256 tiled descriptor, are not measured. Other DIM 2D accesses through a cube T# (gathers, bias, comparison, offsets, derivatives, atomics, stores to an image the program also reads, stores of formats written through a conversion such as 32_SINT, mip loads and stores) still throw. - [Unnormalized S#s](../../../core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp) are accepted only when direct and used only by `image_sample` without comparison or `_a`, on a single-layer 1D or 2D view that is one level at mip 0 (even when the image is also sampled through a normalized S#), or that only unnormalized S#s sample, each without a mip filter or only by `image_sample_lz` without minimum LOD or bias. +- [T# MIN_LOD without VK_EXT_image_view_min_lod](../../../core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp) - without the extension (MoltenVK), direct images take the clamp from a specialization constant. That raises λ where the view clamp raises only the level, so it throws where they differ: a paired S# with different magnification and minification filters (at λ 0 AMD RDNA 4's view clamp minified, spec and NVIDIA magnify, gfx1013 unmeasured), or with mips and a lower MAX_LOD or a bias on an explicit LOD; unnormalized or run-time S#s; `image_get_lod`; gathers at a clamp of a level or more; emulated depth-compare and 11:11:10. An S# holding the LOD at 0 gets MIN_LOD = MAX_LOD = the clamp unless it also samples a table or a mipmapped image of another clamp. An S# bias on an implicit or gradient LOD is unchecked (llvmpipe samples one level lower at a clamp of 2 and a bias of -1). Image tables and pipeline libraries throw; fetches below the clamp stay undefined. - [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. @@ -119,6 +122,7 @@ - On a host subgroup narrower than the wave, [`v_readlane_b32`](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) of a constant lane outside it builds only as lane 63 of a half-wave scan (DPP row_shr 1/2/4/8, `v_permlanex16_b32`, EXEC -1) or, in a program whose entry EXEC is every lane (vertex, local and tessellation stages), as 0 when the value is 0 in lanes 32-63; other such reads throw. A value is 0 there when it is built from 0 through selects under a lane index (lane id or `v_mbcnt` of -1) below at most 32, `v_cmp_ne_u32` against 0, OR, DPP and permlanes; the counts in `s3` give such a bound on a 32-lane host. On a 32-lane host a half-wave scan may also run under `s_mov_b64 exec, -1`, use bound_ctrl:1 steps for an OR, add or unsigned max, and take keys that are 0 in lanes 32-63; its lane 31 is then the subgroup reduction of the host's invocations (a shuffle of the scan on a device without subgroup arithmetic): idle invocations add the identity, where the console would add the keys of the lanes `s3` claims. - On a one-lane wave (vertex and local programs without vertex subgroups, merged hull programs), a wave64 [f32 max half-wave scan](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) whose lane 31 and 63 reads only elect, through `v_cmpx_eq_f32` against each lane's own key, the lanes that `buffer_atomic_umax` or `smax` that maximum at a wave-uniform address (result unused) takes each invocation's own key instead. Every active lane then writes its key, one atomic per lane instead of per wave, and memory ends with the wave's maximum because the keys are proven non-negative and not NaN (`FPSaturate`, constants, phis, f32 maxima with such an operand, selects whose condition the scan's lane mask implies within one loop iteration), where bit order matches float order. A vector register the elected lanes write may only feed that atomic. Other cross-lane reads of such a scan throw. - [NGG mesh entry](../../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) uses the guest wave size (32 or 64) for wave indices, per-wave vertex and primitive counts, and the workgroup wave count in `s3`. The four-bit wave fields follow Mesa's [get_wave_id_in_tg, get_tgsize and get_thread_id_in_tg](https://chromium.googlesource.com/chromiumos/third_party/mesa/+/refs/heads/stabilize-13982.70.B-chromeos-amd/src/gallium/drivers/radeonsi/gfx10_shader_ngg.c); partial waves and groups exceeding 15 waves throw. Indexed and non-indexed wave32 mesh rendering with 32, 64 and 128 threads was checked by pixel readback on Windows with an NVIDIA RTX 4070 Laptop GPU. The guest register values have not been compared with console hardware. +- [Alpha-to-coverage with a sample-mask export](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - not measured on hardware: with `DB_SHADER_CONTROL.MASK_EXPORT_ENABLE` set, `SampleMask` is the AND of the MRT0 alpha coverage and the MRTZ mask export (all ones if unwritten), as in Vulkan and [RADV](https://gitlab.freedesktop.org/mesa/mesa/-/blob/ae9e17fc684b2fe0ee55fe0d32584e0834a1759f/src/amd/vulkan/radv_shader.c#L2108-L2112). `DB_SHADER_CONTROL.COVERAGE_TO_MASK_ENABLE` (bit 7, which [RADV sets](https://gitlab.freedesktop.org/mesa/mesa/-/blob/ae9e17fc684b2fe0ee55fe0d32584e0834a1759f/src/amd/vulkan/radv_cmd_buffer.c#L13211-L13215) below GFX11 for alpha-to-one) is not decoded: the alpha always comes from MRT0. [AlphaToCoverage.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp) tests eight and two samples on the host GPU only. Closes when: measured on a PS5. - [Mesh shader grid size](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - every mesh shader loads `NumWorkgroups.x` and declares no vertices when it is 0, which no dispatch produces. The read is a workaround for RADV (Mesa 26.2.4, seen on an RX 9070 XT, GFX12): binding a mesh pipeline whose shader reads neither `NumWorkgroups` nor `DrawIndex` keeps the user SGPR count and draw-id flag of the vertex pipeline bound before it in the same command buffer (`radv_bind_pre_rast_shader` updates them only when the shader has the `AC_UD_VS_BASE_VERTEX_START_INSTANCE` slot), so `radv_emit_userdata_mesh` opens a 3-register `SET_SH_REG` and writes one value, the CP takes the following `DISPATCH_MESH_DIRECT` as register data and the GPU hangs (gfx ring timeout, MODE1 reset). Kena: Bridge of Spirits (PPSA01802) hits it on its layered volume draws. Reading `NumWorkgroups` gives every mesh shader the grid-size slot, so the count and the values match; drop the read once a fixed Mesa is the minimum - [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). @@ -128,7 +132,7 @@ - [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. 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 the driver may flush f64 subnormals, and `agc_driver_f64_fma_subnormals` skips if it does. - [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 with neg and abs modifiers over 16 values (13 of those, -1.5, 2.75 and 1 - 2^-53). 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. Shaders without a known float mode keep the previous lowering. -- [v_fma_f64 and v_div_fmas_f64 denormals](../../../core/shader/recompiler/Translation/src/Float64Instructions.cpp) follow the `FLOAT_MODE` f16/f64 denormal field as measured on RDNA3 (gfx1101, Radeon RX 7700 XT) with the hardware oracle; the PS5's gfx1013 is not measured. RDNA2 (gfx1035) matches this rule in denorm16 0 and 3 over 3,000 rows of each instruction at IEEE=0 and 1, including `0x3fefffffffffffff * 0x0010000000000000` (exactly 2^-1022 - 2^-1075), which flushes. Those rows do not tell tininess after rounding from before it, so `0x2000000000000001 * 0x1ffffffffffffffe` (exactly 2^-1022 - 2^-1126), which gfx1101 keeps as 2^-1022, and denorm16 1 and 2 are not measured on RDNA2. `v_fma_f64` flushes denormal operands to a zero of their sign when bit 0 is clear (0 and 2); `v_div_fmas_f64` keeps them. When bit 1 is clear (0 and 1), both flush a result whose value rounded to 53 significant bits with an unbounded exponent is below 2^-1022 to a zero of its sign, `v_div_fmas_f64` after its VCC scale by 2^±128. Measured at IEEE=0 and 1, denorm16 0..3, over 300 random triples biased to denormals. Shaders without a known float mode keep the previous lowering. +- [v_fma_f64 and v_div_fmas_f64 denormals](../../../core/shader/recompiler/Translation/src/Float64Instructions.cpp) follow the `FLOAT_MODE` f16/f64 denormal field (gfx1101); the PS5's gfx1013 is not measured. `v_fma_f64` flushes denormal operands to a zero of their sign when bit 0 is clear (0 and 2); `v_div_fmas_f64` keeps them. When bit 1 is clear (0 and 1) both flush a result that rounds to 53 significant bits below 2^-1022 with an unbounded exponent, to a zero of its sign, `v_div_fmas_f64` after its VCC scale by 2^±128. On RDNA2 (gfx1036), IEEE=0/1, denorm16 0..3, a product that rounds up to 2^-1022 is kept in every mode (`0x2000000000000001 * 0x1ffffffffffffffe`, 2^-1022 - 2^-1126), while the exact tie `0x3fefffffffffffff * 0x0010000000000000` (2^-1022 - 2^-1075) flushes in modes 0 and 1 and is kept in 2 and 3; `v_div_fmas_f64` matches. RDNA2 (gfx1035) matches in 0 and 3 (3,000 rows each). Also 300 random biased triples. Without a known float mode, lower as before. - [v_add_f64, v_mul_f64, v_cvt_f64_f32 and v_cvt_f32_f64](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp) are computed with 64-bit integers, rounded to nearest even with denormals kept, on a device without the Float64 capability when they are the only f64 instructions of the shader; any other f64 instruction still needs Float64 and throws. NaN results use the same bits as the native path. Checked bit for bit against the host's IEEE results by `agc_driver_f64_integer_arithmetic` (on Apple M5 Max through MoltenVK and an RTX 4090 per the author); not measured on AMD hardware or the PS5's gfx1013. - [SINT storage images](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) of 8- and 16-bit components saturate each stored 32-bit value to the component range, as adriadam10 measured for image stores on RDNA2 (gfx1035) in #944 (448 rows match); the PS5's gfx1013 is not measured. A D16 store sign-extends each 16-bit half before that saturation, as measured on RDNA2 (gfx1035) for `image_store d16` to all eight SINT formats with DMASK 0xf, 0x7, 0x5, 0x3, 0x2, 0xa and 0x1 (1792 rows match). - [D16 accesses of converted images](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (10_11_11 UNORM, and 11_11_10 or 10_11_11 FLOAT written through an R32_UINT storage view) are not measured on hardware. A D16 `image_store` widens each half to f32, which is exact, and encodes it as the 32-bit store does. A D16 `image_load` decodes the texel to f32 as the 32-bit load does and rounds each component to f16 to nearest even, so a UNORM component is rounded twice; the float formats fit f16 exactly, except for NaN payloads. `image_load` of the float formats reads the native B10G11R11 view and does not take this path. @@ -161,7 +165,7 @@ - [f32 compares in denormal modes 1 and 2](../../../core/shader/recompiler/Translation/src/VectorInstructions.cpp) (`v_cmp_X_f32` and `v_cmpx_X_f32` with X = eq, ge, gt, le, lg, lt, neq, nge, ngt, nle, nlg, nlt) flush their f32 denormal inputs by bit 0 of the `FLOAT_MODE` f32 denormal field: mode 1 keeps them as mode 3 does, and mode 2 flushes them as mode 0 does. Measured on RDNA2 (gfx1030, Radeon RX 6800, AMD Windows driver 32.0.21045.5002 through HIP) over 484 operand pairs of zeros, denormals, minimum normals, ones, the largest finite value, infinities and quiet and signaling NaNs of both signs, in VOPC form, at IEEE=0 and IEEE=1 with each f32 denormal mode 0 to 3, f32 round to nearest even, DX10_CLAMP=1, f16 denormals kept, f16 round to nearest even and FP16_OVFL=0. The PS5's gfx1013 is not measured. - [f32 conversions in denormal modes 1 and 2](../../../core/shader/recompiler/Translation/src/VectorInstructions.cpp) (`v_cvt_i32/u32/rpi_i32/flr_i32_f32`, `v_frexp_exp_i32_f32`, `v_cvt_f64_f32`, `v_cvt_pknorm_i16/u16_f32`, `v_cvt_pk_u8_f32`, `v_cvt_pkrtz_f16_f32` and `v_cvt_f16_f32`) flush their f32 denormal inputs by bit 0 of the `FLOAT_MODE` f32 denormal field: mode 1 keeps them as mode 3 does, and mode 2 flushes them as mode 0 does. Measured on RDNA2 (gfx1036, ROCm 7.1.1, Linux 7.2) over 148 sources and 484 operand pairs (zeros, denormals, minimum normals, f16 limits, the largest finite value, infinities, NaNs) at IEEE=0 and IEEE=1 in every f32 denormal and round mode, and for the f16 and f64 results in every f16/f64 denormal and round mode. `v_mad_mixlo/mixhi_f16`, which RDNA2 lacks, still throw in modes 1 and 2. The PS5's gfx1013 is not measured. - [v_div_scale_f32 and v_div_fixup_f32 in denormal modes 1 and 2](../../../core/shader/recompiler/Translation/src/VectorInstructions.cpp) throw with a directed f32 round mode, where an underflowing `v_div_fixup_f32` result rounds to the smallest denormal and bit 1 of the `FLOAT_MODE` f32 denormal field flushes it, and in mode 1 `v_div_scale_f32` throws when the module cannot declare `DenormPreserve 32`; the PS5's gfx1013 is not measured. At round to nearest even `v_div_fixup_f32` follows bit 0 (mode 1 as mode 3, mode 2 as mode 0), and `v_div_scale_f32` flushes its inputs in mode 2 as in mode 0 and in mode 1 keeps them and flushes a denormal result to a zero of its sign, with the VCC of mode 3. Measured on RDNA2 (gfx1036, AMD Windows driver 32.0.21030.31 through HIP) over 2,304 rows (24 x 24 operand pairs with src0 the denominator and the numerator, four quotients) at IEEE 0 and 1 in every f32 denormal and round mode. -- [v_div_scale_f64 in denormal modes 0, 1 and 2](../../../core/shader/recompiler/Translation/src/Float64Instructions.cpp) is not measured on the PS5's gfx1013 or on RDNA2, and directed round modes are not measured; gfx1101 returns an infinity where the recompiler scales the value operand by 2^-128 to a subnormal or zero, a scale-model gap kept separate from the denormal field and left unchanged. It flushes its three inputs to a zero of their sign when bit 0 of the `FLOAT_MODE` f16/f64 denormal field is clear (modes 0, 2) and keeps them when set (1, 3); a flushed denominator or numerator becomes zero, so the default NaN 0xfff8000000000000 results; a denormal result is flushed to a zero of its sign when bit 1 is clear (modes 0, 1) and kept otherwise (2, 3); the VCC is unchanged. IEEE 0 and 1 agree. Measured on RDNA3 (gfx1101, Radeon RX 7700 XT) with the hardware oracle at IEEE=0 and IEEE=1 over 91,136 rows. Shaders without a known float mode keep the previous lowering. +- [v_div_scale_f64 in denormal modes 0, 1 and 2](../../../core/shader/recompiler/Translation/src/Float64Instructions.cpp) is not measured on the PS5's gfx1013, nor are directed round modes; gfx1101 returns an infinity where the recompiler scales the value by 2^-128 to a subnormal or zero, a scale-model gap left unchanged. It flushes its three inputs to a zero of their sign when bit 0 of the `FLOAT_MODE` f16/f64 denormal field is clear (0, 2) and keeps them when set (1, 3); a flushed src1 or src2 gives the default NaN 0xfff8000000000000, while a flushed src0 alone gives a signed zero, measured on RDNA2 (gfx1036). A denormal result is flushed to a zero of its sign when bit 1 is clear (0, 1) and kept otherwise (2, 3); the VCC is unchanged. IEEE 0 and 1 agree. Measured on RDNA3 (gfx1101, RX 7700 XT) with the hardware oracle at IEEE=0 and 1 over 91,136 rows, and on RDNA2 (gfx1036) over 40 pairs, four arrangements, 2,560 values. Shaders without a known float mode keep the previous lowering. - [f16 compares](../../../core/shader/recompiler/Translation/src/CompareInstructions.cpp) (`v_cmp_X_f16` and `v_cmpx_X_f16` with X = eq, ge, gt, le, lg, lt, neq, nge, ngt, nle, nlg, nlt) flush their f16 denormal inputs to a zero of the same sign by bit 0 of the `FLOAT_MODE` f16 denormal field: modes 0 and 2 flush them, 1 and 3 keep them. Measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) over 256 operand pairs of zeros, denormals, minimum normals, ones, the largest finite value, infinities and quiet and signaling NaNs of both signs, in VOPC form, at IEEE=0 and IEEE=1 with each f16 denormal mode 0 to 3, f16 round to nearest even, DX10_CLAMP=1, f32 denormals kept and FP16_OVFL=0. Shaders without a known float mode keep the previous lowering. The PS5's gfx1013 is not measured. - [DB_SHADER_CONTROL.PRIMITIVE_ORDERED_PIXEL_SHADER](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - the host pixel interlock spans the whole shader, not only up to `s_sendmsg` ORDERED_PS_DONE. - [POPS collision SGPRs](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - the collision wave ID and intrawave collision mask read 0, so a title skips its POPS wait and runs its per-collision loop once. @@ -184,7 +188,7 @@ - [v_cvt_pkrtz_f16_f32 and v_pack_b32_f16 denormals](../../../core/shader/recompiler/Translation/src/ConvertInstructions.cpp) follow the `FLOAT_MODE` f16 denormal field: `v_cvt_pkrtz_f16_f32` flushes each denormal half result to its signed zero when bit 1 is clear (0 and 1), and `v_pack_b32_f16` flushes each denormal half input to its signed zero when bit 0 is clear (0 and 2). Measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) at IEEE=0 and IEEE=1 with denorm16 0..3 and f32 denormals kept, over 256 pairs of 16 f32 values around the f16 denormal range for `v_cvt_pkrtz_f16_f32` and 256 pairs of 16 halves (zeros, denormals, minimum normals, ±1, the largest finite value, infinities and quiet and signaling NaNs) for `v_pack_b32_f16`. Shaders without a known float mode keep the previous lowering. The PS5's gfx1013 is not measured. - [Color comparison sampling](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (`image_sample_c`/`image_sample_c_lz`, excluding native R32 float/R16 unorm) returns zero for every function on sampled floating-point GFX10 formats (codes 1–77 except 7 and 22) and corresponding GFX11 formats measured on gfx1100. Point and bilinear results match on gfx1100. The Vulkan shader still lowers the comparisons but overrides the result with zero, including NaNs and TRUNC_COORD. Scaled, integer and converted formats, image tables, min/max reduction, unnormalized coordinates and unsupported addressing throw; only 2D, 2D-array and cube views are accepted. PS5 gfx1013, cube behavior, arrays except codes 1 and 56, and anisotropic comparisons remain unverified. Comparison bilinear weights remain full float rather than hardware fixed point - [Shared sampling lowering](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) rounds array layers half to even and retains unordered NOT_EQUAL arithmetic, following adriadam10's RDNA2 gfx1035 measurements in [#907](https://github.com/boykopovar/AnyPS5/pull/907#pullrequestreview-5439697732), not measurements on a PS5. Constant-zero color comparison tests cannot verify layer selection or nonzero NaN comparisons. Converted unorm sampling shares the layer selection and tests it with distinct values in four layers -- [Negative color-texture comparison wrap indices](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (libSceAgcDriver) use explicit `OpSRem` normalization to implement modulo; the negative-offset point-wrap regression fails with direct `OpSMod` and passes with normalization on Mesa 22.3.6 llvmpipe (LLVM 15.0.6), but negative-index wrap has not been measured on PS5 hardware. +- [Negative color-texture comparison wrap indices](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (libSceAgcDriver) use explicit `OpSRem` normalization to implement modulo; the negative-offset point-wrap regression fails with direct `OpSMod` and passes with normalization on Mesa 22.3.6 llvmpipe (LLVM 15.0.6), but negative-index wrap was measured on RDNA2 (gfx1036) over coordinates four periods deep: wrap reads floor(u*w) mod w on both axes, mirror reflects every other period, mirror-once clamps after one, clamp-last pins to texel 0, and the comparison form reads the same texels as the plain form. The PS5's gfx1013 is not measured. - [Converted unorm images](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (10_11_11_UNORM, read and written through an R32_UINT view) are sampled in the shader, the way color comparisons are emulated: `image_sample` of a 2D or 2D array view fetches the one or four texels at LOD 0, decodes them and blends them, for point or bilinear filtering with equal minification and magnification filters and wrap or clamp-to-edge addressing. A shader gets these sample modes only once a converted descriptor reaches one of its sampled images: that capture throws `ConvertedSampleRequired`, the driver records the program and prepares it again with `GuestContext::convertedSamples`, so such a shader is prepared twice per run and its first capture is repeated. The record lives with the registered program and is not kept across runs. Bilinear weights are rounded to the hardware's 8-bit grid (`roundEven(fraction * 256) / 256`, ties to even), as measured with `image_sample_lz` on RDNA2 (gfx1035) for 10_11_11 UNORM texels by adriadam10 in #5958; point sampling, decoding, swizzles and wrap and clamp-to-edge addressing matched the same measurement. A sample that is not `_lz` needs an image with one mip level; gathers, LOD queries, clamp-to-border and mirrored addressing, other filters, min/max reduction, unnormalized coordinates, images selected at run time, 16-bit data and a DST_SEL of the missing W channel throw. - [64-bit buffer atomics without shaderBufferInt64Atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp) (`buffer_atomic_or_x2`, `buffer_atomic_and_x2`, `buffer_atomic_xor_x2`) whose result no instruction reads compile to one 32-bit atomic on each dword when the device has no `shaderBufferInt64Atomics`, for example on MoltenVK, if every 64-bit atomic of the program has that form and the same opcode. The pair is not atomic: a plain load that races it can see one half applied, and two different 64-bit bitwise operations on one word can end in a value that no serial order gives. Two programs that run together and apply different operations to one word are not covered. The address checks are those of the 64-bit form. Any other 64-bit buffer atomic, one whose result is read, or a program with two opcodes fails to compile on such a device. Not measured against a native 64-bit atomic on hardware; the compile cases are in `agc_shader_memory_tests`. - [64-bit LDS atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_*_u64`, `ds_*_b64`, `ds_*_f64`) read and write the two dwords under a workgroup lock stored after the guest LDS, as LDS is a 32-bit array without 64-bit atomics. On RDNA2 (gfx1036) the LDS serialises a 32-bit atomic on a slot's high dword and a 64-bit atomic on that slot: 128 lanes of `ds_add_u32` and 128 lanes of `ds_add_u64` on one slot sum exactly. Only that pair was measured. Every 32-bit LDS atomic of a program that contains a 64-bit LDS atomic therefore takes the same lock, including a 32-bit `flat_atomic_*` whose address lands in the LDS aperture (a 64-bit one takes it already); the flat forms were not measured against a 64-bit atomic either (the other 32-bit LDS atomics, `ds_min/max`, `ds_cmpst`, the float forms, `ds_inc/dec`, `ds_mskor` and `ds_wrxchg`, are extrapolated from `ds_add_u32` and were not measured against a 64-bit atomic); a program without a 64-bit LDS atomic keeps its native 32-bit atomics. `ds_condxchg32_rtn_b64` is two `ds_mskor` halves, each under the lock but not atomic as a pair, so a 64-bit atomic between the two halves is not excluded. Sub-dword writes (`ds_write_b8`, `ds_write_b16` and their `_d16_hi` forms) and plain `ds_read_*`, `ds_write_*`, `ds_append_u32` and `ds_consume_u32` are not taken under the lock, so a plain access racing a 64-bit atomic on the same dwords can see or overwrite one half of it; whether the hardware orders them was not measured. The lock serialises the workgroup's LDS atomics of such a program; the throughput cost was not measured. `VK_KHR_workgroup_memory_explicit_layout` with `shaderSharedInt64Atomics` would make both widths native and is not used. A 64-bit atomic at an address that is 4-byte but not 8-byte aligned faults the wave on gfx1036, as on gfx1035; with the workgroup lock, in device memory and in the invocation-private LDS of other stages alike, the recompiler records the `Unaligned` shader fault with the LDS byte address, leaves both dwords unchanged and returns 0, but the lane and the rest of its wave keep running. When the LDS lives in device memory (compute LDS beyond the device's shared-memory limit) and the device has `shaderBufferInt64Atomics`, no lock is used: a 64-bit atomic is a compare-exchange loop on a 64-bit view of the storage buffer and a 32-bit atomic is a native buffer atomic, so a mixed-width pair on one slot relies on the device ordering both widths on one buffer address; only `ds_add_u32` against `ds_add_u64` was measured, on gfx1036 (AMD Windows driver 32.0.21042.62) and an RTX 4070 SUPER. The lock is not used there because AMD's Windows driver compiles an acquire or release on a storage buffer, at workgroup scope or wider, to `s_barrier`, and those barriers inside the lock's divergent loop pair with the program's own `s_barrier`s. The fence after each LDS atomic compiles to `s_barrier` on that driver as well, so a wave that skips an LDS atomic that other waves of its workgroup execute misses a barrier they reach; its effect was not measured. @@ -219,6 +223,7 @@ - [BDA anchor halfword reads](../../../core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp) retain volatile byte loads even for noncoherent memory. On RX 6650 XT / RADV Mesa 26.2.4, the valid SPIR-V for two adjacent nonvolatile byte loads returned `0x80a0` for bytes `0xa0, 0xa1`, where lookup-based reads returned `0xa1a0`. Adding the volatile operand makes the same GPU exactness fixture pass, including mixed direct/fallback lanes. The driver transformation responsible has not been isolated; removing this restriction needs equivalent readback coverage on the affected driver. Single-byte reads keep their original access mask. - [BDA span reads](../../../core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp) (shader recompiler) - `read_bda_span` is one selection: one arm calls `load_bda_span`, a `DontInline` function that holds the in-place dword loads and calls nothing, and the other arm is the per-dword byte fallback. Two other shapes of the same reads each hang a group of NVIDIA drivers, where a dispatch never finishes and the device is lost. With the loads emitted in the arm itself, a dispatch whose lanes took both arms hung on an RTX 3060 and an RTX 4060 Laptop GPU (Windows, driver 591.86) and an RTX 5060 (Linux, driver 590.48.01) ([#3291](https://github.com/boykopovar/AnyPS5/issues/3291)). With the loads and the fallback as two selections, the first under the span's condition and the second under its negation, it hung on an RTX 4070 Laptop GPU (Windows, drivers 595.97 and 596.36), an RTX 5080 (Windows, driver 610.62) and an RTX 4090 (Linux, driver 610.57.04) ([#4975](https://github.com/boykopovar/AnyPS5/issues/4975)). The current shape is measured on the RTX 4090 with driver 610.57.04 only, where it does not hang with or without SPIRV-Tools; a shape with the loads in their own `DontInline` function was reported passing on driver 591.86 in #3291. The cause inside the driver is not known. - [GFX10 scratch address operands](../../../core/shader/recompiler/RdnaDecoder/src/RdnaMemoryOpDecoder.cpp) (shader recompiler) - `core/shader/recompiler/RdnaDecoder/src/RdnaMemoryOpDecoder.cpp` and `core/shader/recompiler/Translation/src/MemoryInstructions/MemoryResourceHelpers.cpp` use the SV/SS/ST scratch operand widths described by LLVM FLATInstructions.td at 3eb867f9b63ae2881bbe3c25d3c12842f2bdc55e (operand pseudos L557-L630, GFX10 encoding L2973-L3038). Decoding and IR tests cover the one-dword VGPR offset; PS5 hardware address-result validation remains outstanding. Unsupported scalar registers still throw. +- [f16 VOP2 SDWA clamp with byte selectors](../../../core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp) (`v_add_f16`, `v_sub_f16`, `v_subrev_f16`, `v_mul_f16`, `v_max_f16`, `v_min_f16`, `v_ldexp_f16`) (shader recompiler) - not measured on hardware: with SDWA `CLMP` and a byte `SRC0_SEL`, `SRC1_SEL` or `DST_SEL`, the f16 result is clamped as without SDWA, then written into `DST_SEL` as `DST_U` says. The [RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) does not state that order; ACO's [`apply_insert`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/01bab9302a6bf0a082c7ba8c49d74427cf10b0cf/src/amd/compiler/aco_optimizer.cpp#L3737-L3763) and LLVM's [SDWA peephole](https://github.com/llvm/llvm-project/blob/131f7ebb08369c2837665bab7061977e37c5f3ec/llvm/lib/Target/AMDGPU/SIPeepholeSDWA.cpp#L1252-L1256) assume it. The tests' clamped values come from it; no NaN result is checked. Closes when: measured with clamp on RDNA2 or a PS5. - [SDWA source selects on `v_cvt_f32_ubyte0` to `v_cvt_f32_ubyte3`](../../../core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp) (shader recompiler) - the PS5's gfx1013 is not measured. Byte N is taken from source 0 after `SRC0_SEL` and `SRC0_SEXT` select and extend it, as measured on RDNA2 (gfx1036, Ryzen 7 9800X3D integrated graphics, AMD Windows driver 32.0.21030.31) for all seven source selects with and without `SRC0_SEXT` over 16 sources (zero, all ones, each byte's and word's sign bit set alone, mixed patterns), wave32, IEEE 0, DX10_CLAMP 1, f32 denormals flushed, f16 denormals kept, round to nearest even, FP16_OVFL 0. - [Integer dot clamp](../../../core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp) (`v_dot2_i32_i16`, `v_dot2_u32_u16`, `v_dot4_i32_i8`, `v_dot4_u32_u8`, `v_dot8_i32_i4`, `v_dot8_u32_u4`) (shader recompiler) - with the VOP3P clamp bit set these throw. No source says the PS5's gfx1013 has these instructions: LLVM's [`FeatureISAVersion10_1_3`](https://github.com/llvm/llvm-project/blob/131f7ebb08369c2837665bab7061977e37c5f3ec/llvm/lib/Target/AMDGPU/GCNFeatures.td#L2282-L2285) has no dot feature and Mesa leaves `CHIP_GFX1013` out of `has_accelerated_dot_product`. LLVM selects clamp for a saturating add of the dot2 and dot4 products to src2, with no corresponding generic dot8 saturating-add pattern, and wraps a sum of the two dot2 products that does not fit in 32 bits, where Mesa's `sdot_2x16_iadd_sat` sums in 64 bits and `udot_2x16_uadd_sat` leaves it undefined. Closes when: a clamped integer dot is measured on a PS5. - [Flag structurization](../../../core/shader/recompiler/ControlFlow/src/Structurizer.cpp) (shader recompiler) - a graph that every structurizer attempt refuses, irreducible and non-terminating graphs included, is rebuilt with one reach flag per block and one again flag per loop (`structurizeWithFlags`): each block keeps one copy and runs behind a guard on its flag, and each strongly connected component becomes a loop whose single latch repeats it while a back edge set its flag. The output has about 8 times the input's blocks and one route variable per block, and ends in an empty Return that never runs. Only blocks that start the top-level order unconditionally run unguarded, so resource planning also sees the static paths that skip a guarded block and can plan extra descriptor candidates for them. The attempts still run first. Shared merge splitting refuses an attempt once the graph has grown past 16 times its block count at the start of splitting (at least 64 blocks); succeeding attempts grew at most 10.6 times on random graphs of up to 128 blocks, and an attempt that would succeed only beyond the budget leaves the graph to a later attempt or to this construction. Splitting that adds one block per round stays within that budget until its 4 rounds per block run out, so the attempts can still take more than 10 s (9 of 2,000 random graphs of up to 128 blocks, 63 s at most). `APS5_FORCE_STRUCTURIZER_FALLBACK=1` builds every graph this way, for tests @@ -227,10 +232,15 @@ - [Spanned wave64 compute](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvSubgroup.cpp) (shader recompiler) - a spanned wave exchanges ballots, shuffles and reductions between its two host subgroups through workgroup barriers, which hang the device when not every invocation reaches them. [SpannedBarrierInDivergentFlow](../../../core/shader/recompiler/SpirvBackend/src/SpirvSpannedUniformity.cpp) sends a program back to two lanes per invocation when a barrier sits in control flow that a per-invocation value decides. It counts a load at a uniform address as uniform only from memory the program never writes, or from workgroup memory a barrier fences on every path since the last write; writes by other dispatches or queues are not considered. An earlier one-barrier ballot also lost the device for a reason that is not understood. `APS5_NO_SPANNED_WAVE` turns spanning off. - [Guest memory load guards](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - a guest load whose lane is inactive or whose address is out of bounds is still performed and its result replaced by 0 with `OpSelect`, instead of a selection per guard, when the load can be pointed at a location nothing writes: scratch reads its own element 0; storage buffers (direct buffers, GDS) read element 0xfffffffe, which `robustBufferAccess2` turns into a zero load with no memory access, so the extra read cannot race with a write the guarded program would not have raced with ([Vulkan memory model](https://docs.vulkan.org/spec/latest/appendices/memorymodel.html#data-race)). Without `robustBufferAccess2` ([VulkanDevice.cpp](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) enables it when the device has it) those loads keep their selections; LDS loads, stores, atomics, formatted and physical-address (BDA) accesses always do. LDS keeps the branch because NVIDIA's compiler predicates a branched LDS load at no cost, while the select form costs about 2.5 instructions per dword and splits a `ds_read_b128` into four loads (RTX 4060 measurements by @karanvora2599 in #4048); Intel's static counts favour the select only for wide reads, and run time was not measured on either. The aligned high word of a misaligned-capable buffer read re-reads the low word's own element. A selection guard costs four blocks per lane half and the host compiler's time grows with the block count: PPSA11142's merged PSP vertex program (3176 ISA words, wave64) emitted 16,983 blocks and 1,710 conditional branches, 5,907 and 868 with the selects. `agc_guarded_memory_loads` traces the emitted loads in both device modes. When the device also reports `robustStorageBufferAccessSizeAlignment` 1, a direct buffer load leaves out the comparison with the descriptor's `OpArrayLength` (which reads the descriptor) and the device returns the zero past the descriptor range: the host rounds a guest buffer's or a draw snapshot's descriptor range of 4 bytes or more down to whole dwords, which no direct access reaches past, and the misaligned-base high word keeps its comparison with the length. The guest bound and the guards of stores, atomics, GDS, LDS and BDA are unchanged. The flag is part of the shader cache key and `APS5_NO_EXACT_ROBUST_LOADS` (any value) keeps the comparison. Hardware treatment of a 32-bit load that straddles the end of a 1 to 3 byte range is not measured. - [Merged hull programs](../../../core/shader/recompiler/Optimization/src/TessellationLowering.cpp) (registered binary type 3) run whole in the hull stage, one invocation per output control point. On the console hull lane i keeps local lane i's registers, which with fewer input than output control points stop matching the patch's control points from i = inputs on; here invocation i holds control point min(i, inputs - 1) of its own patch. A wave-mask branch (`s_cbranch_exec[n]z`, `s_cbranch_vcc[n]z`) tests the invocation's own lane, so it can skip a block, scalar instructions included, that the console runs for the wave; `v_readfirstlane_b32` returns the invocation's own value, not the first active lane's, and another lane's register throws. The register layout (`s3`, `v0`-`v5`, as for the separate halves) is not measured on hardware. s[0:1] hold the hull user-data address (SH 0x102/0x103) when a draw sets it and zero otherwise, so a program reading it unset faults at the null address. +- [Hull lane masks](../../../core/shader/recompiler/Optimization/src/TessellationLowering.cpp) (shader recompiler) - a hull program that uses a lane mask other than its own lane's bit throws (`the hull program uses a lane mask other than its own lane's bit`): one invocation runs one output control point of one patch and cannot read another patch's lanes. PPSA26344 02.002.000 has eight merged hull programs (0x8043d90400, 0x8043d92e00, 0x8043e25b00, 0x80798e8a00, 0x80798e9800, 0x80798f0e00, 0x80798f1b00, 0x807990d500) that apply `s_wqm_b64` to a ballot of a per-control-point bounds test and read their own bit of it. They draw triangle patches with 3 output control points and VGT_LS_HS_CONFIG.NUM_PATCHES 85, so on the console a lane quad spans two patches. - [SPIR-V instruction length and deferred phi validation](../../../core/shader/recompiler/SpirvBackend/src/SpirvModule.cpp) (shader recompiler) - SPIR-V instruction word count boundaries and deferred phi patch tokens validate bounds against logical layout rules; console GPU driver responses to oversized instructions or unlinked phi tokens remain unmeasured. Instruction emission enforces the 65,535 word limit (16-bit word count field), and deferred phi patching verifies token registration. - [Wave LDS ordering on AMD's proprietary driver](../../../core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp) (shader recompiler) - there ([SubgroupBarrier.hpp](../../../core/libs/prx/libSceAgcDriver/Execution/include/SubgroupBarrier.hpp)) one guest wave's LDS accesses are ordered by a subgroup-scope `OpMemoryBarrier` with no control barrier, relying on a 32-lane subgroup being one hardware wave whose lanes run in lockstep and issue LDS instructions in order, which Vulkan does not guarantee; for LDS kept in device memory the same is assumed of global loads and stores, unmeasured. Driver 32.0.21042.62 (Radeon 0x13c0, RDNA 2) compiled the subgroup-scope `OpControlBarrier` to `s_barrier`, which waves in a branch paired with other waves' barriers (PS5LM Plus). Measured on that driver only ([LdsTreeReduction.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/LdsTreeReduction.cpp)); a Radeon 860M on 32.0.13058.8001 reportedly did not show the fault. - [Wave LDS ordering on MoltenVK](../../../core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp) (shader recompiler) - there ([SubgroupBarrier.hpp](../../../core/libs/prx/libSceAgcDriver/Execution/include/SubgroupBarrier.hpp)) one guest wave's LDS accesses are ordered by a subgroup-scope `OpMemoryBarrier`, which becomes a simdgroup-scope `atomic_thread_fence` with no execution barrier, relying on a 32-lane simdgroup running in lockstep, which Metal does not guarantee. The subgroup-scope `OpControlBarrier` other drivers get cannot be used: SPIRV-Cross widens a barrier's execution scope to its memory scope, so it became a `threadgroup_barrier`, which waves in a branch paired with other waves' barriers. Measured on an M1 Pro (MoltenVK 1.4.2) only ([LdsTreeReduction.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/LdsTreeReduction.cpp), [LdsSubwordStoreBranch.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/LdsSubwordStoreBranch.cpp)). - [LDS atomic fences on AMD's proprietary driver](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp) (shader recompiler) - there ([SubgroupBarrier.hpp](../../../core/libs/prx/libSceAgcDriver/Execution/include/SubgroupBarrier.hpp)) the fence after an LDS atomic or a sub-dword or misaligned LDS store is a device-scope `OpMemoryBarrier`, which that driver compiles to GL0 and GL1 cache invalidations; the cost is unmeasured. Driver 32.0.21042.62 (Radeon 0x13c0, RDNA 2) compiled the workgroup-scope fence to `s_barrier`, which waves in a branch paired with other waves' barriers (PS5LM Plus with flash attention). Measured on that driver only ([LdsSubwordStoreBranch.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/LdsSubwordStoreBranch.cpp)); a Radeon 860M (RDNA 3.5, 32.0.13058.8001) reportedly gave correct flash-attention output, but the gate covers every version. - [Descriptor web arms without a valid T#](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp): an image arm of a read split through a descriptor web (`ImageResource::optionalDescriptor`) whose words are not a T# is bound as a null image; a lane that selects it reads zeros and records an `InvalidDescriptor` BDA fault. The console would sample through those words; this is undefined and not measured. Not a T#: not a null descriptor, and a `TYPE` from 2 to 7, or a `FORMAT` outside the GFX10 image format table ([Mesa](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/registers/gfx10-rsrc.json) `GFX10_FORMAT`: 1-77, 128-154, 156-182, 265-292). Other invalid arms, and a direct image with such words, are refused as invalid; an image table entry faults when selected (`InvalidFormat`). Such shaders bind the fault buffer, so their dispatches are not pipelined or replayed for stencil. PPSA23566 (UFC 6) has such an arm (formats 402-463). +- [Compute scratch in device memory](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvScratchPool.cpp) (shader recompiler) - the per-lane scratch of a compute program lives in a storage buffer, not Function arrays, which NVIDIA compiles slowly when indexed dynamically (downstream probe, PPSA26344 program 0x1994ed: about 141 s to about 100 s; not upstream). The buffer is `Coherent`: without it, on an RTX 4090 (Windows), an unaligned 32-bit store (four byte read-modify-writes) loses the first byte of each pair sharing a dword (`flat_store32` private and mixed at offset 2, and `FlatApertureSources`; aligned, 8-bit and 16-bit rows pass). The wrong value is the same every run and survives a wider lane stride, SPIR-V optimization off and `Aliased`; the cause is unknown. +- [Compute scratch runtime cost](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvScratchPool.cpp) (shader recompiler) - scratch accesses reach global memory: with 448 scratch dwords per lane on an RTX 4090 and the default 64 MiB pool, a dispatch takes about 2.4 times as long as with Function arrays (256 workgroups 3.8 against 1.56 ms, 32768 389 against 183 ms), with or without `Coherent`. The cost in a game frame is unmeasured, and no AMD or Intel GPU has been measured. +- [Scratch pool slot acquisition](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvScratchPool.cpp) (shader recompiler) - invocation 0 of a compute workgroup scans the slot locks from `linear workgroup id modulo slot count` and spins until it wins one, reading a lock before trying to take it; with about 9 slots of 114696 bytes (`APS5_SCRATCH_POOL_MIB=1`) a dispatch of 32768 workgroups takes 1308 ms against 395 ms with 64 MiB on an RTX 4090, so too few slots serialize the workgroups; every invocation decrements the slot counter at each exit and the last frees the lock. A workgroup that holds a slot must never wait for one that does not: a guest with such a dependency between the workgroups of one dispatch would hang once there are more workgroups than slots, which the hardware does not guarantee to run either. The emitter fails the compile when a compute `OpReturn` lacks the release, and the driver guarantees at least one slot. - [NGG host subgroup layout](../../../core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp) maps wave64 mesh programs to 32- or 64-lane host subgroups; narrower host subgroups have no verified wave64 mapping. The layered-volume mesh execution regression requires those supported widths. Vertex execution and mesh compilation remain separate checks; compile-only coverage does not establish execution on narrower hosts. +- [T#s with an unset format field](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) (shader recompiler) - a directly bound sampled read that is neither written nor atomic decodes like a null descriptor when its TYPE is an image type the decode implements and its format field names no format (0 or 0x1ff: the zero-format and 0xffffffff-filled unbound slots PPSA02721 samples), in the compile-time decode and the materialization; a reserved or non-image TYPE keeps its rejection however its format reads, as do storage and atomic images with an unset format, a TYPE the driver does not decode, and an image-table entry. What the console returns is not measured; the null decode follows the all-zero descriptor rule. The driver's [IsNullSampledTextureResource](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) applies the same rule to sampled consumers alone; storage bindings and written-buffer tracking keep the base-and-type null rule. diff --git a/docs/user/README.es.md b/docs/user/README.es.md index 21793e41f2..9b3bc49409 100644 --- a/docs/user/README.es.md +++ b/docs/user/README.es.md @@ -1,4 +1,4 @@ -[English](../../README.md) | [Português (Brasil)](README.pt-BR.md) | **Español** | [简体中文](README.zh-CN.md) | [Русский](README.ru.md) +[English](../../README.md) | [Português (Brasil)](README.pt-BR.md) | **Español** | [Italiano](README.it.md) | [简体中文](README.zh-CN.md) | [Русский](README.ru.md) # Acerca de @@ -16,7 +16,7 @@ Incluye un [relinker](../../core/relinker) que convierte el ejecutable al format ## Estado -[![libraries](https://boykopovar.github.io/AnyPS5/badge-libraries.svg)](https://boykopovar.github.io/AnyPS5/progress.html) [![shaders](https://boykopovar.github.io/AnyPS5/badge-shaders.svg)](https://boykopovar.github.io/AnyPS5/progress.html) [![implemented in 7 days](https://boykopovar.github.io/AnyPS5/badge-week.svg)](https://boykopovar.github.io/AnyPS5/history.html) +[![libraries](https://boykopovar.github.io/AnyPS5/badge-libraries.svg)](https://boykopovar.github.io/AnyPS5/progress.html) [![shader forms decoded, counted from the code](https://boykopovar.github.io/AnyPS5/badge-shaders.svg)](https://boykopovar.github.io/AnyPS5/progress.html) [![implemented in 7 days](https://boykopovar.github.io/AnyPS5/badge-week.svg)](https://boykopovar.github.io/AnyPS5/history.html) [![progress map](https://boykopovar.github.io/AnyPS5/progress.svg)](https://boykopovar.github.io/AnyPS5/progress.html) diff --git a/docs/user/README.it.md b/docs/user/README.it.md new file mode 100644 index 0000000000..baec48ae98 --- /dev/null +++ b/docs/user/README.it.md @@ -0,0 +1,47 @@ +[English](../../README.md) | [Português (Brasil)](README.pt-BR.md) | [Español](README.es.md) | **Italiano** | [简体中文](README.zh-CN.md) | [Русский](README.ru.md) + +# Informazioni + +Strumento per il porting automatico di eseguibili su Linux e Windows. + +La compilazione del solo relinker funziona su macOS, inclusi i chip Apple Silicon. + +[**Convertitore web**](https://boykopovar.github.io/AnyPS5/) - i file rimangono sul tuo dispositivo. + +![anyps5_collage1](https://github.com/user-attachments/assets/ed2f0b2d-33d3-46d7-ab88-767ca872fc99) + +Include un [relinker](../../core/relinker) che converte l'eseguibile nel formato nativo del sistema di destinazione e implementazioni delle [librerie prx di sistema](../../core/libs/prx) adatte al linking dinamico. Nessuna emulazione o processo di runtime separato. + +[**Discord**](https://discord.gg/BHFztBPUe), [Utilizzo](USAGE.md), [Istruzioni di compilazione](../dev/BUILD.md), [Architettura](../dev/ARCHITECTURE.md), [Debito tecnico del progetto](../dev/TechnicalDebt.md), [convenzioni sullo stile del codice](../dev/CONVENTIONS.md), [come contribuire](../../CONTRIBUTING.md) + +## Stato + +[![libraries](https://boykopovar.github.io/AnyPS5/badge-libraries.svg)](https://boykopovar.github.io/AnyPS5/progress.html) [![shaders](https://boykopovar.github.io/AnyPS5/badge-shaders.svg)](https://boykopovar.github.io/AnyPS5/progress.html) + +[![progress map](https://boykopovar.github.io/AnyPS5/progress.svg)](https://boykopovar.github.io/AnyPS5/progress.html) + +[Cronologia dei progressi](https://boykopovar.github.io/AnyPS5/history.html) + +* Librerie di sistema: percentuale delle funzioni note al progetto fino ad ora (dichiarate in [core/libs/prx](../../core/libs/prx)), non di tutte le funzioni di sistema di PS5. Il totale cresce man mano che vengono dichiarate più funzioni. + +Gli stati non supportati o imprevisti lanciano rigorosamente `std::runtime_error`. Il messaggio `what()` viene stampato su stderr e il processo termina. + +Il [recompilatore di shader](../../core/shader/recompiler/Recompiler.cpp) genera con successo codice SPIR-V (validato tramite [Spirv-Tools](../../3rdparty/SPIRV-Tools) se compilato con `ANYPS5_ENABLE_SPIRV_TOOLS`). + +## Compatibilità + +Lo stato di ciascun gioco e i report dei test si trovano nelle discussioni di [Game bring-up](https://github.com/boykopovar/AnyPS5/discussions/categories/game-bring-up), un thread per ciascun titolo. + +## Mappatura dei comandi + +I controller con mappatura SDL sono supportati, comprese levette analogiche e grilletti. I controlli con tastiera e mouse possono essere configurati tramite un file `anyps5-input.ini`. Consulta la [mappatura dei comandi](INPUT_MAPPING.md) per i dispositivi supportati e il formato di configurazione. + +## Clausola di esclusione della responsabilità + +Questo progetto è destinato a scopi di interoperabilità, ricerca, conservazione e compatibilità. Non include, distribuisce né richiede software protetto da copyright, firmware, chiavi crittografiche o librerie proprietarie. Gli utenti sono responsabili di garantire che tutti i file binari utilizzati con questo progetto siano ottenuti e utilizzati in conformità con le leggi applicabili e i rispettivi termini di licenza. + +AnyPS5 è un progetto indipendente e non è affiliato, autorizzato o approvato da Sony Interactive Entertainment. "PlayStation" e "PS5" sono marchi registrati di Sony Interactive Entertainment Inc. + +## Licenza + +Questo progetto è concesso in licenza esclusivamente sotto la GNU General Public License versione 2. diff --git a/docs/user/README.pt-BR.md b/docs/user/README.pt-BR.md index 582afca662..82f0795784 100644 --- a/docs/user/README.pt-BR.md +++ b/docs/user/README.pt-BR.md @@ -1,4 +1,4 @@ -[English](../../README.md) | **Português (Brasil)** | [Español](README.es.md) | [简体中文](README.zh-CN.md) | [Русский](README.ru.md) +[English](../../README.md) | **Português (Brasil)** | [Español](README.es.md) | [Italiano](README.it.md) | [简体中文](README.zh-CN.md) | [Русский](README.ru.md) # Sobre @@ -16,7 +16,7 @@ Inclui um [relinker](../../core/relinker) que converte o executável para o form ## Status -[![libraries](https://boykopovar.github.io/AnyPS5/badge-libraries.svg)](https://boykopovar.github.io/AnyPS5/progress.html) [![shaders](https://boykopovar.github.io/AnyPS5/badge-shaders.svg)](https://boykopovar.github.io/AnyPS5/progress.html) [![implemented in 7 days](https://boykopovar.github.io/AnyPS5/badge-week.svg)](https://boykopovar.github.io/AnyPS5/history.html) +[![libraries](https://boykopovar.github.io/AnyPS5/badge-libraries.svg)](https://boykopovar.github.io/AnyPS5/progress.html) [![shader forms decoded, counted from the code](https://boykopovar.github.io/AnyPS5/badge-shaders.svg)](https://boykopovar.github.io/AnyPS5/progress.html) [![implemented in 7 days](https://boykopovar.github.io/AnyPS5/badge-week.svg)](https://boykopovar.github.io/AnyPS5/history.html) [![progress map](https://boykopovar.github.io/AnyPS5/progress.svg)](https://boykopovar.github.io/AnyPS5/progress.html) diff --git a/docs/user/README.ru.md b/docs/user/README.ru.md index efef9a998f..4e545b08c7 100644 --- a/docs/user/README.ru.md +++ b/docs/user/README.ru.md @@ -1,4 +1,4 @@ -[English](../../README.md) | [Português (Brasil)](README.pt-BR.md) | [Español](README.es.md) | [简体中文](README.zh-CN.md) | **Русский** +[English](../../README.md) | [Português (Brasil)](README.pt-BR.md) | [Español](README.es.md) | [Italiano](README.it.md) | [简体中文](README.zh-CN.md) | **Русский** # О проекте @@ -16,7 +16,7 @@ ## Статус -[![libraries](https://boykopovar.github.io/AnyPS5/badge-libraries.svg)](https://boykopovar.github.io/AnyPS5/progress.html) [![shaders](https://boykopovar.github.io/AnyPS5/badge-shaders.svg)](https://boykopovar.github.io/AnyPS5/progress.html) [![implemented in 7 days](https://boykopovar.github.io/AnyPS5/badge-week.svg)](https://boykopovar.github.io/AnyPS5/history.html) +[![libraries](https://boykopovar.github.io/AnyPS5/badge-libraries.svg)](https://boykopovar.github.io/AnyPS5/progress.html) [![shader forms decoded, counted from the code](https://boykopovar.github.io/AnyPS5/badge-shaders.svg)](https://boykopovar.github.io/AnyPS5/progress.html) [![implemented in 7 days](https://boykopovar.github.io/AnyPS5/badge-week.svg)](https://boykopovar.github.io/AnyPS5/history.html) [![progress map](https://boykopovar.github.io/AnyPS5/progress.svg)](https://boykopovar.github.io/AnyPS5/progress.html) diff --git a/docs/user/README.zh-CN.md b/docs/user/README.zh-CN.md index c3f71aeccd..f1d0bd50db 100644 --- a/docs/user/README.zh-CN.md +++ b/docs/user/README.zh-CN.md @@ -1,4 +1,4 @@ -[English](../../README.md) | [Português (Brasil)](README.pt-BR.md) | [Español](README.es.md) | **简体中文** | [Русский](README.ru.md) +[English](../../README.md) | [Português (Brasil)](README.pt-BR.md) | [Español](README.es.md) | [Italiano](README.it.md) | **简体中文** | [Русский](README.ru.md) # 简介 @@ -16,7 +16,7 @@ ## 状态 -[![libraries](https://boykopovar.github.io/AnyPS5/badge-libraries.svg)](https://boykopovar.github.io/AnyPS5/progress.html) [![shaders](https://boykopovar.github.io/AnyPS5/badge-shaders.svg)](https://boykopovar.github.io/AnyPS5/progress.html) [![implemented in 7 days](https://boykopovar.github.io/AnyPS5/badge-week.svg)](https://boykopovar.github.io/AnyPS5/history.html) +[![libraries](https://boykopovar.github.io/AnyPS5/badge-libraries.svg)](https://boykopovar.github.io/AnyPS5/progress.html) [![shader forms decoded, counted from the code](https://boykopovar.github.io/AnyPS5/badge-shaders.svg)](https://boykopovar.github.io/AnyPS5/progress.html) [![implemented in 7 days](https://boykopovar.github.io/AnyPS5/badge-week.svg)](https://boykopovar.github.io/AnyPS5/history.html) [![progress map](https://boykopovar.github.io/AnyPS5/progress.svg)](https://boykopovar.github.io/AnyPS5/progress.html) diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 4a6ad9d323..99ec93356a 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -82,9 +82,10 @@ All switches are disabled by default. `unused-filter` defaults to `0`; `--rpath` | `unused-filter=2` | Apply strict unused-import analysis and compact the PLT. Unsupported analysis cases cause an error. | | `--registry` | Write `.registry.json` beside the output executable, and `..registry.json` for each converted bundled module. | | `--rpath ` | Set the system library search path. Quote `$ORIGIN` to prevent shell expansion, for example `--rpath '$ORIGIN/libs'` in Bash or PowerShell. On Linux every colon-separated entry must be nonempty, and with guest modules each must be absolute or begin with `$ORIGIN`. Windows requires a nonempty ASCII path and supports `$ORIGIN` as the executable directory. | +| `--omit-unavailable-dependencies` | Linux output: leave out a bundled module's dependency that no converted module provides, from which the module imports nothing, and that no `--rpath` directory holds when converting. Every run path directory must exist at that time. Without the switch such a dependency is kept and the conversion prints a note naming it. Use it only when the run path holds every system library: a library found later through `LD_LIBRARY_PATH` is no longer loaded. | | `--autorun` | Run the output after conversion, print its exit code, and wait for Enter. Requires the target OS and prepared runtime layout. | | `--sce-module-path ` | Parent of the module directories, not the module directory itself. Default: input ELF parent; relative input paths resolve from the process working directory. Explicit absolute path: used directly. Explicit relative path (including `.` and `..`): relative to the process working directory. Output path has no effect. Requires one nonempty value; repeats, nonexistent or nondirectory paths, access failures, and combination with `--skip-sce-module` are errors. | -| `--module-dir ` | Relink the ELF modules of a directory under the module parent directory, besides `sce_module`/`sce_modules`/`prx`; outputs keep the relative path. Needed modules are mapped at startup. On Linux, modules in these extra directories initialize only when explicitly loaded, with the arguments from `sceKernelLoadStartModule` (for example Unity plugins in `Media/Plugins`). On Windows, such a module initializes when explicitly loaded unless a module outside these directories needs it, directly or through other modules; then it initializes at startup. On macOS, a module another module or the executable needs initializes at startup. Other modules initialize when loaded. Repeatable; `.` is the parent itself, without the input ELF. Conflicts with `--skip-sce-module`. | +| `--module-dir ` | Relink the ELF modules of a directory under the module parent directory, besides `sce_module`/`sce_modules`/`prx`; outputs keep the relative path. Needed modules are mapped at startup. On Linux, modules in these extra directories initialize only when explicitly loaded, with the arguments from `sceKernelLoadStartModule` (for example Unity plugins in `Media/Plugins`). A module another module needs initializes at startup on Windows and macOS; one only the executable needs initializes when explicitly loaded on Windows and at startup on macOS. Other modules initialize when loaded. Repeatable; `.` is the parent itself, without the input ELF. Conflicts with `--skip-sce-module`. | | `--executable ` | Also convert another executable of the title, which it starts with `sceSystemServiceLoadExec`; the path is relative to the module parent directory, for example `picker/picker.bin`. It is written beside the output, named after the path with `/` replaced by `.` and given the output's extension (`picker.picker.bin.exe`), and uses the same `libs/`, `app0/` and guest modules. It is an error if a guest module converts differently for it. Repeatable. Conflicts with `--skip-sce-module`. | | `--patches ` | Apply the byte patches of this file during conversion, see [game patches](#game-patches). Overrides `relinker-patches.toml` discovery. Conflicts with `--no-patches`. | | `--no-patches` | Do not apply patches, and do not load `relinker-patches.toml` from the input directory. | @@ -364,7 +365,7 @@ Games that open the console's system font sets (`sceFontOpenFontSet`) need font ### Console language -Games ask for the console's language with `sceSystemServiceParamGetInt` and show their text in it when they have it. It is English (United States) unless `ANYPS5_LANGUAGE` is set to another console language number: 0 Japanese, 1 English (United States), 2 French, 3 Spanish, 4 German, 5 Italian, 6 Dutch, 7 Portuguese (Portugal), 8 Russian, 9 Korean, 10 Chinese (Traditional), 11 Chinese (Simplified), 12 Finnish, 13 Swedish, 14 Danish, 15 Norwegian, 16 Polish, 17 Portuguese (Brazil), 18 English (United Kingdom), 19 Turkish, 20 Spanish (Latin America), 21 Arabic, 22 French (Canada), 23 Czech, 24 Hungarian, 25 Greek, 26 Romanian, 27 Thai, 28 Vietnamese, 29 Indonesian, 30 Ukrainian. For example, `ANYPS5_LANGUAGE=21` selects Arabic. An empty value is the same as an unset one; any other value stops the game at its first language query with an error that names the variable. +Games ask for the console's language with `sceSystemServiceParamGetInt` and show their text in it when they have it. It is English (United States) unless `ANYPS5_LANGUAGE` is set to another console language number: 0 Japanese, 1 English (United States), 2 French, 3 Spanish, 4 German, 5 Italian, 6 Dutch, 7 Portuguese (Portugal), 8 Russian, 9 Korean, 10 Chinese (Traditional), 11 Chinese (Simplified), 12 Finnish, 13 Swedish, 14 Danish, 15 Norwegian, 16 Polish, 17 Portuguese (Brazil), 18 English (United Kingdom), 19 Turkish, 20 Spanish (Latin America), 21 Arabic, 22 French (Canada), 23 Czech, 24 Hungarian, 25 Greek, 26 Romanian, 27 Thai, 28 Vietnamese, 29 Indonesian, 30 Ukrainian. For example, `ANYPS5_LANGUAGE=21` selects Arabic. `ANYPS5_LANGUAGE=auto` (in any letter case) takes the language from the host operating system instead: the first of `LC_ALL`, `LC_MESSAGES` and `LANG` that is set and not empty on Linux, with the entries of `LANGUAGE` first when that locale is not `C` or `POSIX`, the user's locale name on Windows and the preferred language list on macOS. The regional variants the console has are kept (`en-GB` is English (United Kingdom), `fr-CA` French (Canada), `es-MX` Spanish (Latin America), `pt-PT` Portuguese (Portugal), `zh-TW`, `zh-HK` and `zh-Hant` Chinese (Traditional), `zh-CN` and `zh-Hans` Chinese (Simplified)), and a language without an entry for the region uses its main one. A host language the console has no entry for, and the `C` and `POSIX` locales, give English (United States) and print one line on stderr that names the locale. The host is read once per process, and every query then answers the same language. An empty value is the same as an unset one; any other value stops the game at its first language query with an error that names the variable. ### Save data free space @@ -378,6 +379,10 @@ On Linux the request goes to [Feral GameMode](https://github.com/FeralInteractiv `APS5_GAME_MODE=auto` makes the same request but says nothing when the host cannot take it (no GameMode library, no Foundation). With `on`, a missing library is logged once as `[host-performance]`. In both modes a request the daemon refuses, for example because `gamemoded` is not running, is logged once, and the title keeps running either way. Any other value stops the title at start with an error that names `APS5_GAME_MODE`. +### Add-ons + +Games ask whether an add-on (downloadable content) is installed with `sceNpEntitlementAccessGetAddcontEntitlementInfo` and list the installed ones with `sceNpEntitlementAccessGetAddcontEntitlementInfoList`. An add-on counts as installed only when its entitlement label is listed in `anyps5-entitlements.ini` beside the output executable, or in the file `ANYPS5_ENTITLEMENTS` names: one label per line, up to 16 characters, with `#` or `;` starting a comment. List the add-ons your copy includes. Every other label is reported as not installed, and the first query for each prints one line, such as `[entitlements] the title asked for the add-on EXAMPLEADDON0000, which anyps5-entitlements.ini does not list, so it is reported as not installed`. The line names the file as `anyps5-entitlements.ini`, or by the path `ANYPS5_ENTITLEMENTS` gives when it is set. Bytes of a label outside printable ASCII, and the backslash, are written as `\xNN`. After 64 such labels, one more line says that further ones are reported without a line. Some games wait at start-up for an add-on they look for instead of going on without it: a title (PPSA26344, version 02.002.000) stays on its loading spinner until the add-on label it asks for is listed. + ### Trophies A game unlocks a trophy by posting a `_UnlockTrophy` event with an int32 `_trophy_id` property to `libSceNpUniversalDataSystem`, or through the statistics rules described below. The unlock is stored in `_trophy/_.txt` in the directory the game runs in (the one that holds `app0/`), one ` ` line per trophy; the time is an RTC tick, microseconds since 0001-01-01 UTC. Delete the file before starting the game to lock its trophies again. Each new unlock prints one line, such as `[trophy] PPSA01556 unlocked trophy 12 for user 268435456`; unlocking a trophy again prints nothing. `sceNpTrophy2GetTrophyInfo`, `sceNpTrophy2GetTrophyInfoArray` and `sceNpTrophy2GetGameInfo` report the stored unlocks and their times to the game. When the game's trophy data is loaded (see below), a posted unlock of a trophy that the data does not have, of the platinum, or of a progressive trophy is not stored and prints the reason, such as `[trophy] ignored the posted unlock of trophy 0 for user 268435456: it is the platinum, which unlocks with the last trophy that names it`; the platinum unlocks with the last trophy that names it, and a progressive trophy through its statistic. Without trophy data every posted id of 0 or more is stored, and the platinum is not unlocked when the other trophies are. @@ -398,8 +403,9 @@ When a dump has neither `sce_sys/playgo-chunk.dat` nor `sce_sys/playgo-scenario. |-------|---------------------------------|------------------------------| | `installed` | Report it as installed (`LOCAL_FAST`). | High on Life (PPSA15388) needs this to load its saves. | | `absent` | Return `SCE_PLAYGO_ERROR_BAD_CHUNK_ID`. | Sifu (PPSA03001) and Demon Slayer: Kimetsu no Yaiba - The Hinokami Chronicles (PPSA03490) use this result to end their chunk-ID search. With `installed`, PPSA03490 keeps asking about higher chunk IDs forever, so its chunk list grows until startup fails with a `Ran out of memory allocating 5565054976 bytes` error. | +| A comma-separated list of IDs, e.g. `0,1` | Report those IDs as installed (`LOCAL_FAST`) and every other unlisted ID as absent (`SCE_PLAYGO_ERROR_BAD_CHUNK_ID`). | The Medium (PPSA03718) needs chunks 0 and 1; with `installed` it asks about every ID and UE4 runs out of memory. | -Leave the variable unset to keep the explicit error. Any other value also stops with an error. Dumps that include PlayGo metadata are unaffected. These title-specific results come from emulator runs and have not been verified against a console. +Leave the variable unset to keep the explicit error. A list holds decimal chunk IDs from 0 to 65535 and is added to chunk 0; it only applies when the dump has neither `playgo-chunk.dat` nor `playgo-scenario.json`. Any other value also stops with an error. Dumps that include `playgo-chunk.dat` or `playgo-scenario.json` are unaffected. These title-specific results come from emulator runs and have not been verified against a console. ### Screenshots @@ -484,7 +490,7 @@ To measure how a title runs on a machine, start it with `APS5_TRACE_SUMMARY_EVER ### Tracing system library calls -Windows, Linux and macOS. An executable converted with `--trace-imports` records the calls it makes to the libraries named in `APS5_TRACE_CALLS`, a comma-separated list such as `libkernel,libSceSaveData`, or `all`. Without the variable nothing is recorded. The crash and abort reports then end with the last 64 calls (on Linux and macOS, which have no crash report, only an abort prints them): sequence number, thread, library, import name as the executable imports it (a NID) and the caller's address. The last 32 of these calls made by the reporting thread follow, taken from the 65536 most recent calls, so a thread whose calls were pushed out of the last 64 by a busier one still shows them. Then come the 32 most called imports since start-up, with their number of calls. +Windows, Linux and macOS. An executable converted with `--trace-imports` records the calls it makes to the libraries named in `APS5_TRACE_CALLS`, a comma-separated list such as `libkernel,libSceSaveData`, or `all`. Without the variable nothing is recorded. If an import has no library label, the tracer uses the filename of the host module containing its function; select that name, such as `libSceAvPlayer.native`, rather than a guest alias. The crash and abort reports then end with the last 64 calls (on Linux and macOS, which have no crash report, only an abort prints them): sequence number, thread, library, import name as the executable imports it (a NID) and the caller's address. The last 32 of these calls made by the reporting thread follow, taken from the 65536 most recent calls, so a thread whose calls were pushed out of the last 64 by a busier one still shows them. Then come the 32 most called imports since start-up, with their number of calls. ``` last traced system library calls (APS5_TRACE_CALLS): @@ -620,7 +626,7 @@ python3 tools/import_audit.py app.registry.json app.libc.prx.guest.prx.registry. | `partial` | Exported, but the function throws for inputs whose behaviour is unknown. | | `stub` | Exported, but the whole function calls `NotImplemented_nid_no_patch`: the game loads and throws when it calls it. | | `absent` | No built library exports the NID: the loader fails. | -| `module` | The import names a file found in `--modules` (the title's own library), which is trusted to provide it. | +| `module` | The import names a title library found in `--modules` that exports the symbol or its NID. | An import that names a library requires the literal export name. If its implementation is found only by hashing the import name, it still counts as `implemented` or `stub`, but the report lists the name mismatch, sets `symbol_mismatch` on the JSON import record and counts it in the summary, and exits with code 1. A strong import with a name mismatch is a current stop. Imports without a library retain the audit's name-to-NID fallback. diff --git a/tools/ab_split.py b/tools/ab_split.py index ec10f212eb..40bc47062e 100644 --- a/tools/ab_split.py +++ b/tools/ab_split.py @@ -53,7 +53,7 @@ def parse_report(kind, text): return None programs_text, classes_text = text.split("by program:", 1)[-1].split("; by class:", 1) programs, classes = {}, {} - for key, ms in re.findall(r" (0x[0-9a-f]+) x\d+ ([\d.]+)ms", programs_text): + for key, ms in re.findall(r" (? len(self.data): + raise ModuleError("PT_DYNAMIC extends past the end of the file") + for position in range(offset, offset + size - 15, 16): tag, value = self.unpack("=*)\[.*?\](?P=equals)\]|[^\s()]+', re.S) LEXICAL = re.compile(r'\\.|"(?:\\.|[^"\\])*"|\[(?P=*)\[.*?\](?P=literal_equals)\]' r'|(?P\#\[(?P=*)\[.*?\](?P=comment_equals)\]|\#[^\n]*)', re.S) KNOWN_UNRUN = set() SCRIPT = re.compile(r"(? #include +#include +#include #include #include #include @@ -48,6 +50,18 @@ static hsa_status_t findCpu(hsa_agent_t agent, void* data) { return HSA_STATUS_SUCCESS; } +static int parseRange(const char* text, uint32_t minimum, uint32_t maximum, uint32_t* result) { + char* end; + const char* number = text; + while (isspace((unsigned char)*number)) ++number; + if (*number == '-') return 0; + errno = 0; + const unsigned long value = strtoul(text, &end, 10); + if (errno != 0 || end == text || *end != '\0' || value < minimum || value > maximum) return 0; + *result = (uint32_t)value; + return 1; +} + static void* readFile(const char* path, size_t* size) { FILE* f = fopen(path, "rb"); if (!f) { perror(path); exit(1); } @@ -69,8 +83,9 @@ int main(int argc, char** argv) { return 0; } if (argc != 6) { fprintf(stderr, "usage: oracle --target | oracle code.o threads in.bin out.bin out_dwords\n"); return 2; } - const uint32_t threads = (uint32_t)atoi(argv[2]); - const uint32_t outDwords = (uint32_t)atoi(argv[5]); + uint32_t threads, outDwords; + if (!parseRange(argv[2], 1, 1024, &threads)) { fprintf(stderr, "THREADS must be 1 to 1024\n"); return 2; } + if (!parseRange(argv[5], 1, 16, &outDwords)) { fprintf(stderr, "OUT_DWORDS must be 1 to 16\n"); return 2; } CHECK(hsa_init()); CHECK(hsa_iterate_agents(findGpu, NULL)); if (!haveGpu) { fprintf(stderr, "no GPU agent\n"); return 1; } diff --git a/tools/hw-oracle/test_hw_oracle.py b/tools/hw-oracle/test_hw_oracle.py index a057dab983..994aeaf8f9 100644 --- a/tools/hw-oracle/test_hw_oracle.py +++ b/tools/hw-oracle/test_hw_oracle.py @@ -83,6 +83,32 @@ class OracleCacheTests(unittest.TestCase): self.assertEqual([file for file in cache.rglob("*") if file.is_file()], [binary]) +class CacheDirectoryTests(unittest.TestCase): + def test_xdg_cache_home_is_used_only_when_absolute(self): + home = Path.home() / ".cache" / "anyps5-hw-oracle" + absolute = Path(tempfile.gettempdir()).resolve() + cases = ( + ({}, home), + ({"XDG_CACHE_HOME": ""}, home), + ({"XDG_CACHE_HOME": "relative/cache"}, home), + ({"XDG_CACHE_HOME": "."}, home), + ({"XDG_CACHE_HOME": str(absolute)}, absolute / "anyps5-hw-oracle"), + ({"XDG_CACHE_HOME": "relative/cache", "HW_ORACLE_CACHE": "chosen"}, Path("chosen")), + ({"XDG_CACHE_HOME": str(absolute), "HW_ORACLE_CACHE": ""}, absolute / "anyps5-hw-oracle"), + ) + for environment, expected in cases: + with self.subTest(environment=environment): + self.assertEqual(hw_oracle.cache_directory(environment), expected) + + def test_module_cache_follows_the_environment_at_import(self): + environment = {key: value for key, value in os.environ.items() if key not in ("HW_ORACLE_CACHE", "XDG_CACHE_HOME")} + environment["XDG_CACHE_HOME"] = "relative/cache" + result = subprocess.run([sys.executable, "-c", "import hw_oracle; print(hw_oracle.CACHE)"], + cwd=Path(hw_oracle.__file__).parent, env=environment, check=True, + stdout=subprocess.PIPE, text=True) + self.assertEqual(Path(result.stdout.strip()), Path.home() / ".cache" / "anyps5-hw-oracle") + + class InputRowsTests(unittest.TestCase): def test_cli_reports_invalid_input_before_tool_lookup(self): with tempfile.TemporaryDirectory() as tmp: @@ -484,6 +510,32 @@ class OracleConfigurationTests(unittest.TestCase): self.assertEqual(second.read_bytes(), b"second") self.assertNotEqual(first, second) + @unittest.skipIf(os.name == "nt", "POSIX executable compiler fixture") + def test_replacing_selected_compiler_invalidates_the_cache(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + compiler = root / "fixture-cc" + + def install(value, timestamp): + compiler.write_text( + "#!" + sys.executable + "\nimport sys\nfrom pathlib import Path\n" + "Path(sys.argv[sys.argv.index('-o') + 1]).write_bytes(" + repr(value) + ")\n", + encoding="utf-8") + compiler.chmod(0o755) + os.utime(compiler, ns=(timestamp, timestamp)) + + with patch.object(hw_oracle, "CACHE", root / "cache"), \ + patch.object(hw_oracle, "rocm_root", return_value=None), \ + patch.dict(os.environ, {"CC": "fixture-cc", "PATH": str(root)}): + install(b"first", 1_000_000_000) + first = hw_oracle.oracle() + self.assertEqual(first.read_bytes(), b"first") + self.assertEqual(hw_oracle.oracle(), first) + install(b"other", 2_000_000_000) + second = hw_oracle.oracle() + self.assertEqual(second.read_bytes(), b"other") + self.assertNotEqual(first, second) + @unittest.skipUnless(sys.platform.startswith("linux"), "Linux compiler and shared-library fixture") def test_cache_and_compiler_command_with_host_toolchain(self): @@ -672,6 +724,44 @@ class AssemblyTests(unittest.TestCase): self.assertTrue(code.is_file()) +class HsaRunnerTests(unittest.TestCase): + @unittest.skipUnless(sys.platform.startswith("linux"), "HSA runner is built on Linux") + def test_invalid_dispatch_sizes_fail_before_hsa_or_file_access(self): + with tempfile.TemporaryDirectory() as directory, patch.object(hw_oracle, "CACHE", Path(directory)): + try: + runner = hw_oracle.oracle() + except (OSError, subprocess.CalledProcessError, SystemExit) as error: + self.skipTest(f"HSA runner cannot be built: {error}") + cases = ( + (["missing.co", "not-an-int", "missing.in", "missing.out", "16"], "THREADS"), + (["missing.co", "1x", "missing.in", "missing.out", "16"], "THREADS"), + (["missing.co", "0", "missing.in", "missing.out", "16"], "THREADS"), + (["missing.co", "1025", "missing.in", "missing.out", "16"], "THREADS"), + (["missing.co", "-18446744073709551615", "missing.in", "missing.out", "16"], "THREADS"), + (["missing.co", " -18446744073709551615", "missing.in", "missing.out", "16"], "THREADS"), + (["missing.co", "1", "missing.in", "missing.out", "not-an-int"], "OUT_DWORDS"), + (["missing.co", "1", "missing.in", "missing.out", "1x"], "OUT_DWORDS"), + (["missing.co", "1", "missing.in", "missing.out", "0"], "OUT_DWORDS"), + (["missing.co", "1", "missing.in", "missing.out", "17"], "OUT_DWORDS"), + (["missing.co", "1", "missing.in", "missing.out", "-18446744073709551600"], "OUT_DWORDS"), + (["missing.co", "1", "missing.in", "missing.out", " -18446744073709551600"], "OUT_DWORDS"), + ) + for arguments, name in cases: + with self.subTest(arguments=arguments): + result = subprocess.run([runner, *arguments], capture_output=True, text=True, timeout=10) + self.assertEqual(result.returncode, 2, result.stdout + result.stderr) + self.assertIn(f"{name} must be", result.stderr) + self.assertNotIn("hsa_", result.stderr) + self.assertNotIn("missing.co", result.stderr) + for threads, out_dwords in (("1", "1"), ("1024", "16")): + with self.subTest(threads=threads, out_dwords=out_dwords): + result = subprocess.run( + [runner, "missing.co", threads, "missing.in", "missing.out", out_dwords], + capture_output=True, text=True, timeout=10) + self.assertNotEqual(result.returncode, 2, result.stdout + result.stderr) + self.assertNotIn("must be", result.stderr) + + class HipRuntimeTests(unittest.TestCase): def test_runtime_defaults_to_hip_on_windows_only(self): environment = {key: value for key, value in os.environ.items() if key != "HW_ORACLE_RUNTIME"} @@ -730,6 +820,167 @@ class HipRuntimeTests(unittest.TestCase): self.assertEqual(Path(hw_oracle.hip_runtime().__file__), hw_oracle.HERE / "oracle_hip.py") +class VulkanRuntimeTests(unittest.TestCase): + def setUp(self): + directory = tempfile.TemporaryDirectory() + self.addCleanup(directory.cleanup) + self.root = Path(directory.name) + self.runner = self.root / "agc_oracle_runner" + self.runner.write_bytes(b"runner") + environment = {key: value for key, value in os.environ.items() + if key not in ("HW_ORACLE_VULKAN_RUNNER", "HW_ORACLE_TARGET")} + environment["HW_ORACLE_RUNTIME"] = "vulkan" + patcher = patch.dict(os.environ, environment, clear=True) + patcher.start() + self.addCleanup(patcher.stop) + + def test_runtime_is_selected_only_explicitly(self): + self.assertIn("vulkan", hw_oracle.RUNTIMES) + self.assertEqual(hw_oracle.runtime(), "vulkan") + with patch.dict(os.environ, {"HW_ORACLE_RUNTIME": ""}): + self.assertNotEqual(hw_oracle.runtime(), "vulkan") + + def test_missing_runner_fails_before_assembly(self): + for value in (None, "", str(self.root / "missing")): + with self.subTest(value=value), patch.object(hw_oracle, "assemble", side_effect=AssertionError("assembler called")) as assemble, \ + patch.object(hw_oracle, "target", side_effect=AssertionError("target lookup")) as target: + if value is None: + os.environ.pop("HW_ORACLE_VULKAN_RUNNER", None) + else: + os.environ["HW_ORACLE_VULKAN_RUNNER"] = value + with self.assertRaises(SystemExit) as exited: + hw_oracle.run("s_nop 0", [(0, 0, 0, 0)], **MODES) + self.assertIn("HW_ORACLE_VULKAN_RUNNER", str(exited.exception)) + assemble.assert_not_called() + target.assert_not_called() + + def test_run_dispatches_through_the_runner_with_the_oracle_convention(self): + os.environ["HW_ORACLE_VULKAN_RUNNER"] = str(self.runner) + calls = [] + + def dispatch(command, **kwargs): + calls.append((command, kwargs)) + data = Path(command[3]).read_bytes() + self.assertEqual(data[:16], hw_oracle.struct.pack("<4I", 1, 2, 3, 4)) + self.assertEqual(data[-5:], b"extra") + Path(command[4]).write_bytes(bytes(int(command[2]) * 64)) + + with patch.object(hw_oracle, "assemble", return_value=self.root / "kernel.co"), \ + patch.object(hw_oracle, "oracle", side_effect=AssertionError("oracle built")), \ + patch.object(hw_oracle.subprocess, "run", side_effect=dispatch): + results = hw_oracle.run("s_nop 0", [(1, 2, 3, 4)], b"extra", **MODES) + self.assertEqual(results, [(0,) * 16]) + self.assertEqual(len(calls), 1) + command, kwargs = calls[0] + self.assertEqual([command[0], command[1], command[2], command[5]], + [str(self.runner), str(self.root / "kernel.co"), "32", "16"]) + self.assertEqual(Path(command[3]).name, "in.bin") + self.assertEqual(Path(command[4]).name, "out.bin") + self.assertTrue(kwargs["check"]) + self.assertIs(kwargs["stdout"], sys.stderr) + + def test_rows_are_padded_to_whole_waves_and_chunked(self): + os.environ["HW_ORACLE_VULKAN_RUNNER"] = str(self.runner) + for wave64, expected in ((False, [1024, 32]), (True, [512, 512, 64])): + sizes = [] + + def dispatch(command, **kwargs): + sizes.append(int(command[2])) + Path(command[4]).write_bytes(bytes(int(command[2]) * 64)) + + with self.subTest(wave64=wave64), patch.object(hw_oracle, "assemble", return_value=self.root / "kernel.co"), \ + patch.object(hw_oracle.subprocess, "run", side_effect=dispatch): + results = hw_oracle.run("s_nop 0", [(index, 0, 0, 0) for index in range(1025)], wave64=wave64, **MODES) + self.assertEqual(sizes, expected) + self.assertEqual(len(results), 1025) + + def test_output_size_is_checked(self): + os.environ["HW_ORACLE_VULKAN_RUNNER"] = str(self.runner) + + def dispatch(command, **kwargs): + Path(command[4]).write_bytes(bytes(int(command[2]) * 64 - 4)) + + with patch.object(hw_oracle, "assemble", return_value=self.root / "kernel.co"), \ + patch.object(hw_oracle.subprocess, "run", side_effect=dispatch), \ + self.assertRaisesRegex(ValueError, "Oracle output size"): + hw_oracle.run("s_nop 0", [(0, 0, 0, 0)], **MODES) + + def test_coarse_is_rejected(self): + os.environ["HW_ORACLE_VULKAN_RUNNER"] = str(self.runner) + with patch.object(hw_oracle, "assemble", side_effect=AssertionError("assembler called")), \ + self.assertRaisesRegex(ValueError, "coarse"): + hw_oracle.run("s_nop 0", [(0, 0, 0, 0)], coarse=True, **MODES) + + def test_target_needs_override_off_windows_and_probes_hip_on_windows(self): + with patch.object(hw_oracle.os, "name", "posix"), \ + patch.object(hw_oracle, "hip_runtime", side_effect=AssertionError("HIP probe")), \ + patch.object(hw_oracle, "oracle", side_effect=AssertionError("GPU probe")): + with self.assertRaises(SystemExit) as exited: + hw_oracle.target() + self.assertIn("HW_ORACLE_TARGET", str(exited.exception)) + with patch.dict(os.environ, {"HW_ORACLE_TARGET": "gfx1036"}): + self.assertEqual(hw_oracle.target(), "gfx1036") + + class Runtime: + @staticmethod + def target(): + return "gfx1010" + + with patch.object(hw_oracle.os, "name", "nt"), patch.object(hw_oracle, "hip_runtime", return_value=Runtime): + self.assertEqual(hw_oracle.target(), "gfx1010") + + def test_assembly_backend_follows_the_host(self): + class Runtime: + @staticmethod + def assemble(source, isa_target, wave64): + return b"comgr code" + + with tempfile.TemporaryDirectory() as tmp, patch.dict(os.environ, {"HW_ORACLE_TARGET": "gfx1036"}): + work = Path(tmp) + with patch.object(hw_oracle.os, "name", "nt"), patch.object(hw_oracle, "hip_runtime", return_value=Runtime), \ + patch.object(hw_oracle.subprocess, "run", side_effect=AssertionError("clang called")): + self.assertEqual(hw_oracle.assemble("s_nop 0", work, False, **MODES).read_bytes(), b"comgr code") + with patch.object(hw_oracle.os, "name", "posix"), patch.object(hw_oracle, "hip_runtime", side_effect=AssertionError("comgr called")), \ + patch.object(hw_oracle, "tool", side_effect=lambda name: name), \ + patch.object(hw_oracle.subprocess, "run") as run: + hw_oracle.assemble("s_nop 0", work, True, **MODES) + commands = [call.args[0] for call in run.call_args_list] + self.assertEqual([command[0] for command in commands], ["clang", "ld.lld"]) + self.assertIn("-mcpu=gfx1036", commands[0]) + self.assertIn("-mwavefrontsize64", commands[0]) + + def test_validate_only_does_not_touch_the_runner(self): + with tempfile.TemporaryDirectory() as directory: + body, rows = Path(directory) / "body.s", Path(directory) / "rows.txt" + body.write_text("s_nop 0", encoding="utf-8") + rows.write_text("1 2 3 4\n", encoding="utf-8") + argv = ["hw_oracle.py", str(body), str(rows), "--validate-only", "--json"] + argv += [item for name, value in MODES.items() for item in ["--" + name.replace("_", "-"), str(value)]] + with patch("sys.argv", argv), patch.object(hw_oracle, "vulkan_runner", side_effect=AssertionError("runner lookup")), \ + patch.object(hw_oracle, "run", side_effect=AssertionError("run")), \ + patch.object(hw_oracle, "target", side_effect=AssertionError("target lookup")), \ + contextlib.redirect_stdout(io.StringIO()) as output: + self.assertEqual(hw_oracle.main(), 0) + self.assertTrue(json.loads(output.getvalue())["validation_only"]) + + def test_json_report_names_the_runtime(self): + os.environ["HW_ORACLE_TARGET"] = "gfx1036" + with tempfile.TemporaryDirectory() as directory: + body, rows = Path(directory) / "body.s", Path(directory) / "rows.txt" + body.write_text("s_nop 0", encoding="utf-8") + rows.write_text("1 2 3 4\n", encoding="utf-8") + argv = ["hw_oracle.py", str(body), str(rows), "--json"] + argv += [item for name, value in MODES.items() for item in ["--" + name.replace("_", "-"), str(value)]] + for selected in ("vulkan", "hsa"): + with self.subTest(runtime=selected), patch.dict(os.environ, {"HW_ORACLE_RUNTIME": selected}), \ + patch("sys.argv", argv), patch.object(hw_oracle, "run", return_value=[(0,) * 16]), \ + contextlib.redirect_stdout(io.StringIO()) as output: + hw_oracle.main() + report = json.loads(output.getvalue()) + self.assertEqual(report["runtime"], selected) + self.assertEqual(report["assembly_target"], "gfx1036") + + class HipOracleTests(unittest.TestCase): def setUp(self): import oracle_hip diff --git a/tools/import_audit.py b/tools/import_audit.py index 28a327f8f4..de9e7e1b42 100644 --- a/tools/import_audit.py +++ b/tools/import_audit.py @@ -79,10 +79,16 @@ def elf_program_dynamic_symbols(path, data): def file_offset(address, size, what): for segment in loads: _, _, offset, virtual, _, file_size, _, _ = segment - if address >= virtual and address - virtual <= file_size and size <= file_size - (address - virtual): + if address >= virtual and address - virtual <= file_size: result = offset + address - virtual - take(data, result, size, path, what) - return result + covered = virtual + file_size + for following in sorted(loads, key=lambda item: item[3]): + next_offset, next_virtual, next_size = following[2], following[3], following[5] + if next_virtual <= covered and next_offset - next_virtual == offset - virtual: + covered = max(covered, next_virtual + next_size) + if address + size <= covered: + take(data, result, size, path, what) + return result raise AuditError(f"{path}: {what} address 0x{address:x} is outside file-backed load segments") dynamic_segments = [segment for segment in segments if segment[0] == 2] @@ -761,9 +767,9 @@ class ModuleNames(set): return filename in self.files or module_stem(library, self.fold) in self.stems -def classify(nid, library, providers, stubs, modules, partial=frozenset()): - if library and library in modules: - return "module" +def classify(nid, library, providers, stubs, guest_providers, partial=frozenset()): + if guest_providers: + return "module" if library else "implemented" if not providers: return "absent" if all((provider, nid) in stubs for provider in providers): @@ -798,6 +804,32 @@ def audit(imports, exports, stubs, libraries, modules, module_imports=None, modu def available(name): return name in modules or system_library(name) in libraries + guest_names = {} + for provider in modules: + names = {provider, provider.removesuffix(".guest.prx")} + guest_names[provider] = ModuleNames(names, fold=getattr(modules, "fold", False)) + + def guest_matches(name): + filename = name.lower() if getattr(modules, "fold", False) else name + exact = {provider for provider, names in guest_names.items() if filename in names.files} + return exact or {provider for provider, names in guest_names.items() if name in names} + + def resolve(symbol, record): + library = record["library"] + hosts = exports.get(symbol, []) + guests = (module_exports or {}).get(symbol, []) + if library: + selected = guest_matches(library) + guests = [provider for provider in guests if provider in selected] + if library in modules: + hosts = [] + elif hosts: + module = record.get("module") + needed = (module_imports or {})[module][1] if module else needed_libraries + selected = {provider for name in needed for provider in guest_matches(name)} + guests = [provider for provider in guests if provider == module or provider in selected] + return sorted(set(hosts) | set(guests)), guests + records = {} for nid, library, weak in imports: record = records.setdefault((nid, library), {"nid": nid, "library": library, "references": 0, "weak": weak}) @@ -810,18 +842,14 @@ def audit(imports, exports, stubs, libraries, modules, module_imports=None, modu record["weak"] = record["weak"] and weak for record in records.values(): nid, library = record["nid"], record["library"] - providers = exports.get(nid, []) - if not library: - providers = sorted(set(providers) | set((module_exports or {}).get(nid, []))) + providers, guest_providers = resolve(nid, record) resolved_nid = nid record["symbol_mismatch"] = False if not providers: resolved_nid = nid_names.compute_nid(nid) - providers = exports.get(resolved_nid, []) + providers, guest_providers = resolve(resolved_nid, record) record["symbol_mismatch"] = bool(library) and library not in modules and bool(providers) and resolved_nid != nid - if not library: - providers = sorted(set(providers) | set((module_exports or {}).get(resolved_nid, []))) - kind = classify(resolved_nid, library, providers, stubs, modules, partial) + kind = classify(resolved_nid, library, providers, stubs, guest_providers, partial) record["class"] = kind record["providers"] = providers record["library_mismatch"] = kind in ("implemented", "partial", "stub") and bool(library) and system_library(library) not in providers and "libc.prx" not in providers @@ -928,6 +956,13 @@ def label(record): return f"{text} (imported by {record['module']})" if record.get("module") else text +def locate_search_root(path): + resolved = Path(os.path.realpath(path)) + if resolved.is_symlink(): + raise AuditError(f"{path}: registry path is a symbolic link loop") + return resolved.parent + + def locate_missing(names, roots, depth=4): wanted = {name.lower(): name for name in names} found = {} @@ -1042,13 +1077,13 @@ def main(argv=None): "name": entry["stop"]["name"] if entry["stop"] else None} for entry in stops] result = {"source_commit": commit, "source_dirty": dirty, "registries": [str(path) for path in args.registries], "dependency_registries": [str(path) for path in dependency_registries], "needed_libraries": needed, **summary, "stops": json_stops, "imports": records} write_json(args.json, result) + located = locate_missing(missing, {locate_search_root(path) for path in args.registries}) if missing else None except AuditError as error: print(f"FAIL: {error}", file=sys.stderr) return 2 except OSError as error: print(f"FAIL: {error.strerror}: {error.filename}" if error.filename else f"FAIL: {error}", file=sys.stderr) return 2 - located = locate_missing(missing, {path.resolve().parent for path in args.registries}) if missing else None print(render(summary, records, missing, stops, missing_hosts, located)) return 1 if summary["unique_by_class"]["absent"] or summary["symbol_mismatch"] or missing or missing_hosts or (args.strict_providers and summary["library_mismatch"]) or (args.strict_stubs and summary["unique_by_class"]["stub"]) else 0 diff --git a/tools/input-mapper/i18n.js b/tools/input-mapper/i18n.js index 94308a2830..2c19eff53f 100644 --- a/tools/input-mapper/i18n.js +++ b/tools/input-mapper/i18n.js @@ -9,6 +9,11 @@ export const locales = [ "name": "English", "direction": "ltr" }, + { + "code": "sv", + "name": "Svenska", + "direction": "ltr" + }, { "code": "es", "name": "Español", @@ -192,6 +197,86 @@ export const messages = { "searchOptions": "Search options", "noOptions": "No matching options." }, + "sv": { + "controls": "Kontroller", + "configuration": "Konfiguration", + "help": "Hjälp", + "settings": "Inställningar", + "language": "Språk", + "close": "Stäng", + "eyebrow": "Tillbehör / Anpassade kontroller", + "intro": "Välj en kontroll och tilldela sedan tangentbords- eller musinmatning.", + "search": "Sök kontroller", + "import": "Importera INI", + "reset": "Återställ", + "group0": "Åtgärdsknappar", + "group1": "Axelknappar", + "group2": "Styrkors", + "group3": "Vänster spak", + "group4": "Höger spak", + "group5": "Pekskärm och verktyg", + "select": "Välj…", + "add": "+ Lägg till bindning", + "capture": "Fånga", + "remove": "Ta bort", + "builtin": "Inbyggt: {inputs}", + "custom": "Egna inmatningar ersätter inbyggda inmatningar.", + "noResults": "Inga kontroller matchar sökningen.", + "download": "Hämta INI", + "copy": "Kopiera konfiguration", + "previewEmpty": "Lägg till en bindning för att börja.", + "backControls": "Tillbaka till kontroller", + "switchGroup": "Byt grupp", + "moveFocus": "Flytta fokus", + "helpIntro": "Några svar som hjälper dig att komma igång.", + "configIntro": "Granska inmatningarna och kopiera eller hämta sedan filen.", + "captureTitle": "Tryck på en tangent för {action}", + "cancelCapture": "Avbryt inmatning", + "keepEditing": "Fortsätt redigera", + "discard": "Förkasta ändringar", + "faq0Question": "Hur tilldelar jag en tangent?", + "faq0Answer": "Välj en grupp, välj Lägg till bindning och välj sedan Fånga eller en tangent i listan. Flera bindningar fungerar som alternativ.", + "faq1Question": "Var ska min INI-fil ligga?", + "faq1Answer": "Hämta anyps5-input.ini och lägg den bredvid den skapade körbara spelfilen. För en annan plats anger du filsökvägen i ANYPS5_INPUT_CONFIG.", + "faq2Question": "Varför fångas inte tangenten?", + "faq2Answer": "Webbläsar- eller systemkortkommandon kan fånga den. Välj den manuellt i listan. Escape avbryter inmatningen. Välj Escape i listan om du vill tilldela den.", + "faq3Question": "Sparas ändringarna automatiskt?", + "faq3Answer": "Indataändringar finns bara i minnet. Kopiera eller hämta innan du läser in sidan på nytt. Bara språkvalet sparas i webbläsaren. Åtgärder utan inställning behåller sina standardvärden.", + "skip": "Hoppa till innehållet", + "mainNav": "Huvudnavigering", + "groups": "Kontrollgrupper", + "find": "Hitta en åtgärd", + "clock": "Lokal tid", + "output": "Konfigurationsutdata", + "captureHelp": "Inmatning använder fysiska tangentpositioner. Escape avbryter inmatningen. Välj Escape i listan för att tilldela den. En del webbläsar- och systemkortkommandon måste väljas manuellt.", + "replaceTitle": "Förkasta osparade ändringar?", + "replaceHelp": "Kopiera eller hämta konfigurationen först om du vill behålla den.", + "captureCancelled": "Inmatningen avbröts.", + "captureBlur": "Inmatningen avbröts eftersom fönstret tappade fokus.", + "captured": "{value} har fångats för {action}.", + "resetDone": "Konfigurationen har återställts.", + "importRejected": "Importen avvisades", + "importRead": "Importen lästes in", + "line": "Rad {line}: {message}", + "preserved": "Din aktuella konfiguration har bevarats.", + "imported": "{name} importerades.", + "importFailed": "Importen misslyckades: {message}", + "fileTooLarge": "INI-filen måste vara mindre än 1 MiB.", + "invalidUtf8": "Filen måste vara giltig UTF-8-text.", + "downloaded": "Hämtning av konfigurationen begärdes.", + "copied": "Konfigurationen har kopierats.", + "clipboardFailed": "Urklipp är inte tillgängligt. Kopiera den markerade förhandsvisningen med Ctrl+C.", + "noScript": "Aktivera JavaScript för att använda den här konfiguratorn.", + "noDefault": "ingen tangentbords- eller musinmatning", + "bindingLabel": "{action}-bindning {number}", + "typeLabel": "Inmatningstyp för {binding}", + "valueLabel": "Inmatningsvärde för {binding}", + "addLabel": "Lägg till bindning för {action}", + "captureLabel": "Fånga tangent för {binding}", + "removeLabel": "Ta bort {binding}", + "searchOptions": "Sök alternativ", + "noOptions": "Inga matchande alternativ." + }, "ar-EG": { "controls": "التحكم", "configuration": "ملف الإعدادات", @@ -1272,6 +1357,21 @@ export function translate(locale, key, values = {}) { } export function diagnostic(locale, message) { + if (locale === 'sv') return message + .replace('Configuration must be a list of bindings.', 'Konfigurationen måste vara en lista med bindningar.') + .replace('Unknown or empty action.', 'Okänd eller tom åtgärd.') + .replace('Input type must be KEY, MOUSE or WHEEL.', 'Indatatypen måste vara KEY, MOUSE eller WHEEL.') + .replace(/(\w+) cannot use (\w+)\./gu, '$1 kan inte använda $2.') + .replace(/Unknown or empty (\w+) value\./gu, 'Okänt eller tomt $1-värde.') + .replace('This SDL key name cannot be represented in the current INI comment syntax.', 'Det här SDL-tangentnamnet kan inte representeras med den aktuella INI-kommentarsyntaxen.') + .replace(/duplicate (.+) will be omitted\./gu, 'den dubbla bindningen $1 utelämnas.') + .replace(' controls ', ' styr ') + .replaceAll(' (built-in)', ' (inbyggd)') + .replace('Composition and dead keys are ambiguous. Select an SDL key from the list.', 'Döda tangenter och kombinationstangenter är tvetydiga. Välj en SDL-tangent i listan.') + .replaceAll(' and ', ' och ') + .replace('Expected Action = TYPE:VALUE.', 'Förväntat format: Action = TYPE:VALUE.') + .replace('Duplicate binding ignored.', 'Dubblettbindning ignorerades.') + .replace('This physical key cannot be captured in the current INI format. Select an SDL key from the list.', 'Den här fysiska tangenten kan inte fångas i det aktuella INI-formatet. Välj en SDL-tangent i listan.'); if (locale !== 'ar-EG') return message; return message .replace('Configuration must be a list of bindings.', 'الإعدادات لازم تكون قائمة روابط أزرار.') @@ -1283,11 +1383,11 @@ export function diagnostic(locale, message) { .replace(/duplicate (.+) will be omitted\./gu, 'الربط $1 متكرر وهيتحفظ مرة واحدة.') .replace(' controls ', ' بيشغّل ') .replaceAll(' (built-in)', ' (افتراضي)') + .replace('Composition and dead keys are ambiguous. Select an SDL key from the list.', 'زرار تركيب الحروف مش واضح في التسجيل. اختار زرار SDL من القائمة.') .replaceAll(' and ', ' و ') .replace('Expected Action = TYPE:VALUE.', 'الصيغة المطلوبة: Action = TYPE:VALUE.') .replace('Duplicate binding ignored.', 'الربط المتكرر تم تجاهله.') - .replace('This physical key cannot be captured in the current INI format. Select an SDL key from the list.', 'الزرار ده مينفعش يتسجل بالصيغة الحالية لملف INI. اختار زرار SDL من القائمة.') - .replace('Composition and dead keys are ambiguous. Select an SDL key from the list.', 'زرار تركيب الحروف مش واضح في التسجيل. اختار زرار SDL من القائمة.'); + .replace('This physical key cannot be captured in the current INI format. Select an SDL key from the list.', 'الزرار ده مينفعش يتسجل بالصيغة الحالية لملف INI. اختار زرار SDL من القائمة.'); } export function initialLocale(savedLocale) { diff --git a/tools/input-mapper/tests/i18n.test.js b/tools/input-mapper/tests/i18n.test.js index 65d98f0140..8bc9dc1bbd 100644 --- a/tools/input-mapper/tests/i18n.test.js +++ b/tools/input-mapper/tests/i18n.test.js @@ -1,6 +1,7 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import {diagnostic, initialLocale, locales, messages, translate} from '../i18n.js'; +import {captureKey} from '../capture.js'; import {parseIni, serialize} from '../validation.js'; test('every selectable language supplies the main interface and every FAQ', () => { @@ -32,8 +33,16 @@ test('language substitution preserves canonical inputs and exported INI bytes', assert.equal(translate('unsupported', 'help'), 'Help'); }); -test('Egyptian diagnostics retain action tokens and unknown technical details', () => { +test('translated diagnostics retain action tokens and unknown technical details', () => { assert.equal(diagnostic('ar-EG', 'Cross: Unknown or empty KEY value.'), 'Cross: قيمة KEY مش معروفة أو فاضية.'); + assert.equal(diagnostic('sv', 'Cross: Unknown or empty KEY value.'), 'Cross: Okänt eller tomt KEY-värde.'); assert.equal(diagnostic('ar-EG', 'Unexpected diagnostic'), 'Unexpected diagnostic'); + assert.equal(diagnostic('sv', 'Unexpected diagnostic'), 'Unexpected diagnostic'); assert.equal(diagnostic('fr', 'Cross: Unknown or empty KEY value.'), 'Cross: Unknown or empty KEY value.'); }); + +test('capture diagnostics are fully translated', () => { + const message = captureKey({key: 'Dead', code: 'BracketRight'}).error; + assert.equal(diagnostic('sv', message), 'Döda tangenter och kombinationstangenter är tvetydiga. Välj en SDL-tangent i listan.'); + assert.equal(diagnostic('ar-EG', message), 'زرار تركيب الحروف مش واضح في التسجيل. اختار زرار SDL من القائمة.'); +}); diff --git a/tools/nid_names.py b/tools/nid_names.py index a6d7feff12..3cf8909cd5 100644 --- a/tools/nid_names.py +++ b/tools/nid_names.py @@ -112,6 +112,123 @@ def source_assembly_definitions(text): yield match.group(1) +def data_declarations(tokens, c_linkage): + direct = tokens[:2] in (["extern", '"C"'], ["extern", '"C++"']) + if direct: + c_linkage = tokens[1] == '"C"' + tokens = tokens[2:] + if not c_linkage or not tokens or tokens[0] in {"using", "typedef"}: + return [] + header = tokens[:next((index for index, token in enumerate(tokens) if token in {"=", "{"}), len(tokens))] + if "static" in header: + return [] + external = direct or "extern" in header + names, groups = [], [] + name, initialized, local = None, False, False + base = header[:next((index for index, token in enumerate(header) if token in {"*", "&", ","}), len(header))] + base_const, constexpr, start = "const" in base, "constexpr" in base, 0 + pairs = {"(": ")", "[": "]", "{": "}", "<": ">"} + angle_ends, end = [None] * len(tokens), None + for index in range(len(tokens) - 1, -1, -1): + if tokens[index] in {">", "=", "{"}: + end = tokens[index] + angle_ends[index] = end + for index, token in enumerate(tokens): + if token == "," and not groups: + if name is not None and not local and (initialized or not external): + names.append(name) + name, initialized, local, start = None, False, False, index + 1 + elif token == "=" and not groups: + initialized = True + elif not initialized and re.fullmatch(r"[A-Za-z_]\w*_nid_postfix", token): + prefix = tokens[start:index] + unqualified = prefix[:] + while unqualified and unqualified[-1] in {"const", "volatile"}: + unqualified.pop() + pointer = len(groups) == 1 and unqualified[-2:] == ["(", "*"] + following = tokens[index + 1] if index + 1 < len(tokens) else None + previous = tokens[index - 1] if index else None + argument_index = index + 2 + while argument_index < len(tokens) and tokens[argument_index] in {"+", "-", "!", "~", "("}: + argument_index += 1 + argument = tokens[argument_index] if argument_index < len(tokens) else "" + direct_init = following == "(" and (re.match(r"[0-9.]|(?:u8|u|U|L)?R?[\"']", argument) or argument in {"true", "false", "nullptr"}) + if (not groups or pointer) and (following != "(" or direct_init) and previous not in {"struct", "class", "union", "enum"}: + name = token + pointers = [offset for offset, item in enumerate(prefix) if item in {"*", "&"}] + local = not external and (constexpr or ("const" in prefix[pointers[-1] + 1:] if pointers else base_const)) + if direct_init: + initialized = True + template = False + if token == "<" and index > 0 and re.fullmatch(r"[A-Za-z_]\w*|>", tokens[index - 1]) and \ + index + 1 < len(tokens) and tokens[index + 1] not in {"<", "="}: + template = angle_ends[index] == ">" + if token in pairs and (token != "<" or template): + if token == "{" and not groups: + initialized = True + groups.append(pairs[token]) + elif groups and token == groups[-1]: + groups.pop() + if name is not None and not local and (initialized or not external): + names.append(name) + return names + + +def source_data_definitions(text): + text = text.replace("\\\n", "").replace("\\\r\n", "") + scopes, groups, statement = [False], [], [] + initializer = False + pairs = {"(": ")", "[": "]", "{": "}"} + directive_end = 0 + for match in CPP_TOKEN.finditer(text): + if match.start() < directive_end or match.group("comment") is not None: + continue + token = match.group() + if token == "#" and not text[text.rfind("\n", 0, match.start()) + 1:match.start()].strip(): + end = text.find("\n", match.end()) + directive_end = len(text) if end < 0 else end + continue + if scopes[-1] is None: + if token == "{": + scopes.append(None) + elif token == "}": + scopes.pop() + continue + if token == "{" and not groups: + if statement in (["extern", '"C"'], ["extern", '"C++"']): + scopes.append(statement[1] == '"C"') + elif statement[:1] == ["namespace"] or statement[:2] == ["inline", "namespace"]: + scopes.append(scopes[-1] if statement[-1] != "namespace" else None) + elif initializer or (statement and ((re.fullmatch(r"[A-Za-z_]\w*", statement[-1]) and ")" not in statement) or statement[-1] == "]") and + statement[-1] not in {"noexcept", "override", "final"} and + statement[0] not in {"struct", "class", "union", "enum"}) or \ + data_declarations(statement + ["{", "}"], scopes[-1]): + statement.append(token) + groups.append("}") + continue + else: + scopes.append(None) + statement = [] + initializer = False + elif token == "}" and not groups: + if len(scopes) > 1: + scopes.pop() + statement = [] + initializer = False + elif token == ";" and not groups: + yield from data_declarations(statement, scopes[-1]) + statement = [] + initializer = False + else: + statement.append(token) + if token == "=" and not groups: + initializer = True + if token in pairs: + groups.append(pairs[token]) + elif groups and token == groups[-1]: + groups.pop() + + def collect_unknowns(): found = {} for path in source_files(PRX): @@ -133,6 +250,9 @@ def collect_real_names(): for name in source_assembly_definitions(text): names.add(name) names.add(strip_nid_postfix(name)) + for name in source_data_definitions(text): + names.add(name) + names.add(strip_nid_postfix(name)) return names @@ -143,6 +263,7 @@ def source_locations(nids): exports = set(source_aliases(text)) names = [match.group(1) for match in source_definitions(text)] names.extend(source_assembly_definitions(text)) + names.extend(source_data_definitions(text)) for name in names: canonical = strip_nid_postfix(name) if not canonical.endswith(("_nid_no_patch", "_nid_no_patch_cut")): diff --git a/tools/package_headers.py b/tools/package_headers.py index b22e6fa937..81679fe1ac 100644 --- a/tools/package_headers.py +++ b/tools/package_headers.py @@ -50,7 +50,9 @@ def public_headers(root): visited.add(header) text = header.read_text(encoding="utf-8").replace("\\\n", "").replace("\\\r\n", "") text = CPP_TOKEN.sub(lambda match: re.sub(r"[^\n]", " ", match.group()) if match.group("comment") or match.group("raw") else match.group(), text) - for line in re.findall(r"^[ \t]*#[ \t]*include\b([^\n]*)", text, re.M): + for keyword, line in re.findall(r"^[ \t]*#[ \t]*(include(?:_next)?)\b([^\n]*)", text, re.M): + if keyword == "include_next": + raise RuntimeError(f"Unsupported include_next in {header}: {line.strip()}") match = re.fullmatch(r'\s*(?:"([^"\n]+)"|<([^>\n]+)>)\s*', line) if not match: raise RuntimeError(f"Non-literal public include in {header}: {line.strip()}") diff --git a/tools/package_release.py b/tools/package_release.py index 7c2a0168d5..c143ee7d8d 100644 --- a/tools/package_release.py +++ b/tools/package_release.py @@ -40,6 +40,11 @@ def windows_runtime(build): if line.startswith("CMAKE_CXX_COMPILER:"): compiler = line.partition("=")[2] if compiler and not compiler.endswith("-NOTFOUND"): + if Path(compiler).parent == Path("."): + found = shutil.which(compiler) + if not found: + raise RuntimeError(f"CMAKE_CXX_COMPILER {compiler} in {cache} is not on PATH") + compiler = os.path.abspath(found) return Path(compiler).parent raise RuntimeError(f"CMAKE_CXX_COMPILER is not configured in {cache}") diff --git a/tools/pad_input.py b/tools/pad_input.py index a62f5f51d7..5700673e0c 100644 --- a/tools/pad_input.py +++ b/tools/pad_input.py @@ -17,31 +17,64 @@ BUTTONS = { } +def timeline_sequence(command, header): + if len(header.rstrip(b"\n")) > 127 or (os.name == "nt" and b"\x1a" in header): + return 0 + header = header.split(b"\0", 1)[0] + fields = re.fullmatch(rb"\s*APS5_PAD_TIMELINE\s+(\+?[0-9]+)(?:\s+|(?=\+))(\+?[0-9]+)(?:\s+|(?=\+))(\+?[0-9]+)\s*", header) + if fields is None: + return 0 + version, sequence, count = [int(token) for token in fields.groups()] + if version != 1 or not 1 <= sequence <= 2**64 - 1 or not 1 <= count <= 4096: + return 0 + previous_end = 0 + for _ in range(count): + line = command.readline(129) + if os.name == "nt": + if b"\x1a" in line: + line = line.split(b"\x1a", 1)[0] + command.seek(0, os.SEEK_END) + if line.endswith(b"\r\n"): + line = line[:-2] + b"\n" + if not line or len(line.rstrip(b"\n")) > 127: + return 0 + fields = re.fullmatch(rb"\s*(\+?[0-9]+)(?:\s+|(?=\+))(\+?[0-9]+)(?:\s+|(?=\+))(\+?[0-9]+)\s*", line.split(b"\0", 1)[0]) + if fields is None: + return 0 + start, duration, buttons = [int(token) for token in fields.groups()] + if buttons & ~0x70fffe or not 1 <= duration <= 5000 or start < previous_end or start + duration > 3600000: + return 0 + previous_end = start + duration + while tail := command.read(4096): + if os.name == "nt" and b"\x1a" in tail: + return sequence if not tail.split(b"\x1a", 1)[0].strip() else 0 + if tail.strip(): + return 0 + return sequence + + def previous_sequence(path): try: with path.open("rb") as command: line = command.readline(129) + if os.name == "nt" and line.endswith(b"\r\n"): + line = line[:-2] + b"\n" + if line.split()[:1] == [b"APS5_PAD_TIMELINE"]: + return timeline_sequence(command, line) except FileNotFoundError: return 0 - if os.name == "nt" and line.endswith(b"\r\n"): - line = line[:-2] + b"\n" tokens = line.split() - timeline = tokens[:1] == [b"APS5_PAD_TIMELINE"] - if not timeline and len(tokens) in (4, 8): + if len(tokens) in (4, 8): touch = re.fullmatch(rb"touch\((\+?[0-9]+),(\+?[0-9]+),(\+?[0-9]+),(\+?[0-9]+)\)", tokens.pop()) if touch is None or any(int(value) > limit for value, limit in zip(touch.groups(), (1919, 942, 127, 1))): return 0 - if timeline: - tokens = tokens[1:] - if len(line.rstrip(b"\n")) > 127 or len(tokens) not in ((3,) if timeline else (3, 7)): + if len(line.rstrip(b"\n")) > 127 or len(tokens) not in (3, 7): return 0 if any(re.fullmatch(rb"\+?[0-9]+", token) is None for token in tokens): return 0 values = [int(token) for token in tokens] if any(value > 2**64 - 1 for value in values): return 0 - if timeline: - return values[1] if values[0] == 1 and 1 <= values[2] <= 4096 else 0 if values[0] == 0 or values[1] & ~0x70fffe or not 1 <= values[2] <= 5000: return 0 if any(axis > 255 for axis in values[3:]): diff --git a/tools/pad_timeline.py b/tools/pad_timeline.py index 97197fd67c..746db3baa9 100644 --- a/tools/pad_timeline.py +++ b/tools/pad_timeline.py @@ -3,6 +3,7 @@ import json import os import stat from pathlib import Path +import sys import tempfile from pad_input import BUTTONS, next_sequence @@ -78,13 +79,16 @@ def queue_timeline(path, text): temporary.unlink(missing_ok=True) -def main(): +def main(argv=None): + for stream in (sys.stdout, sys.stderr): + if hasattr(stream, "reconfigure"): + stream.reconfigure(encoding="utf-8", errors="backslashreplace") parser = argparse.ArgumentParser(description="Queue a timed controller button sequence for APS5_PAD_INPUT_FILE.") parser.add_argument("timeline", type=Path) parser.add_argument("path", type=Path) parser.add_argument("--repeat", type=int, default=1) parser.add_argument("--gap-ms", type=int, default=200) - args = parser.parse_args() + args = parser.parse_args(argv) try: if os.path.realpath(args.timeline) == os.path.realpath(args.path) or (args.path.exists() and os.path.samefile(args.timeline, args.path)): raise ValueError("the command file must be separate from the timeline JSON") @@ -92,7 +96,7 @@ def main(): raise ValueError(f"{args.path} is a directory") if not args.path.parent.is_dir(): raise ValueError(f"directory does not exist: {args.path.parent}") - document = json.loads(args.timeline.read_text(encoding="utf-8")) + document = json.loads(args.timeline.read_text(encoding="utf-8-sig")) text = compile_timeline(document, next_sequence(args.path), args.repeat, args.gap_ms) queue_timeline(args.path, text) except (OSError, ValueError, RecursionError) as error: diff --git a/tools/pr_labels.py b/tools/pr_labels.py index b06dd68e15..6298b376c2 100644 --- a/tools/pr_labels.py +++ b/tools/pr_labels.py @@ -153,7 +153,10 @@ def main(argv=None): 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") + parser.add_argument("--json", action="store_true", help="emit a dry-run plan as JSON") args = parser.parse_args(argv) + if args.json and not args.dry_run: + parser.error("--json requires --dry-run") try: repo = repository() if args.pr is not None: @@ -164,19 +167,27 @@ def main(argv=None): parser.error((error.stderr or str(error)).strip()) except (ValueError, RecursionError) as error: parser.error(f"cannot read pull requests: {error}") - failed = [] + failed, plans = [], [] for pr in prs: try: add, remove, post = process(repo, pr, args.dry_run, comment=args.pr is not None) except subprocess.CalledProcessError as error: failed.append(pr["number"]) + plans.append({"number": pr["number"], "error": (error.stderr or str(error)).strip()}) print(f"cannot label #{pr['number']}: {(error.stderr or '').strip()}", file=sys.stderr) continue + plans.append({"number": pr["number"], "add": sorted(add), "remove": sorted(remove), + "comment_marker": MARKER if post else None}) + if args.json: + continue if add or remove or post: plan = [f"+{label}" for label in sorted(add)] + [f"-{label}" for label in sorted(remove)] + (["comment"] if post else []) print(f"{'would change' if args.dry_run else 'changed'} #{pr['number']}: {' '.join(plan)}") if args.all and not args.dry_run: time.sleep(WRITE_PAUSE) + if args.json: + print(json.dumps({"repository": repo, "plans": plans, "failed": failed}, sort_keys=True)) + return int(bool(failed)) if failed: sys.exit(f"{len(failed)} pull requests not labeled: {', '.join(f'#{number}' for number in failed)}") return 0 diff --git a/tools/prepare_release_notes.py b/tools/prepare_release_notes.py index 043b220f6e..e87cf84e8a 100644 --- a/tools/prepare_release_notes.py +++ b/tools/prepare_release_notes.py @@ -10,6 +10,21 @@ from pathlib import Path BODY_LIMIT = 125000 +def resolve_output_path(path): + path = Path(path) + try: + resolved = Path(os.path.realpath(path)) + except OSError as error: + raise ValueError(str(error)) from error + if resolved.is_symlink(): + try: + resolved.stat() + except OSError as error: + raise ValueError(f"{path}: release note output is a symbolic link loop: {error}") from error + raise ValueError(f"{path}: release note output is a symbolic link loop") + return resolved + + def release_body(notes): if len(notes) <= BODY_LIMIT: return notes @@ -22,7 +37,7 @@ def release_body(notes): ) -def prepare_notes(source, body, full): +def prepare_notes(source, body, full, check=False): paths = (source, body, full) for index, first in enumerate(paths): for second in paths[index + 1:]: @@ -40,19 +55,25 @@ def prepare_notes(source, body, full): body_text = release_body(notes) if len(body_text) > BODY_LIMIT: raise ValueError(f"Release notes body exceeds {BODY_LIMIT} characters after removing What's Changed") - for destination in (body, full): + if check: try: - destination.stat() + return body.read_bytes() == body_text.encode("utf-8") and full.read_bytes() == notes.encode("utf-8") + except FileNotFoundError: + return False + for destination in (body, full): + target = resolve_output_path(destination) + try: + target.stat() except FileNotFoundError: pass - if destination.is_dir(): + if target.is_dir(): raise IsADirectoryError(str(destination)) - if os.name == "nt" and destination.exists() and destination.stat().st_file_attributes & stat.FILE_ATTRIBUTE_READONLY: + if os.name == "nt" and target.exists() and target.stat().st_file_attributes & stat.FILE_ATTRIBUTE_READONLY: raise PermissionError(f"Release note destination is read-only: {destination}") staged = [] try: for destination, text in ((full, notes), (body, body_text)): - target = destination.resolve() + target = resolve_output_path(destination) target.parent.mkdir(parents=True, exist_ok=True) temporary = target.parent / (".release-notes-" + secrets.token_hex(16)) with temporary.open("xb") as output: @@ -76,13 +97,17 @@ def main(argv=None): parser.add_argument("--input", type=Path, required=True) parser.add_argument("--body", type=Path, required=True) parser.add_argument("--full", type=Path, required=True) + parser.add_argument("--check", action="store_true", help="fail with status 1 when outputs differ; never create or modify files") args = parser.parse_args(argv) try: - prepare_notes(args.input, args.body, args.full) + matched = prepare_notes(args.input, args.body, args.full, args.check) + if args.check and not matched: + print("Release note outputs are missing or stale", file=sys.stderr) + return 1 except (OSError, ValueError) as error: parser.error(str(error)) return 0 if __name__ == "__main__": - main() + sys.exit(main()) diff --git a/tools/progress.py b/tools/progress.py index cb06c2d5f9..c4aab2b7dd 100644 --- a/tools/progress.py +++ b/tools/progress.py @@ -97,7 +97,7 @@ def blank(match): def code(text): - return CPP_TOKEN.sub(blank, text.replace("\\\n", "").replace("\\\r\n", "")) + return re.sub(r"\n(?:[ \t]*\n)+", "\n", CPP_TOKEN.sub(blank, text.replace("\\\n", "").replace("\\\r\n", ""))) def body_end(text, start): depth = 0 @@ -691,6 +691,11 @@ def require_distinct_reports(output): for other in paths[index + 1:]: if other.exists() and path.samefile(other): raise ValueError(f"{path.name} and {other.name} in {output} are the same file") + targets = [os.path.normcase(os.path.realpath(path)) for path in paths] + for index, target in enumerate(targets): + for other in range(index + 1, len(targets)): + if targets[other] == target: + raise ValueError(f"{paths[index].name} and {paths[other].name} in {output} are the same file") def site_data(libraries, shaders, shader_source): diff --git a/tools/sanitizer_report.py b/tools/sanitizer_report.py index 05499a247c..3242caee22 100644 --- a/tools/sanitizer_report.py +++ b/tools/sanitizer_report.py @@ -1,4 +1,5 @@ import argparse +import json import re import sys import xml.etree.ElementTree as ET @@ -119,6 +120,7 @@ def main(argv=None): help="known failures, one per line: test name, comma-separated sanitizers and the reason") parser.add_argument("--sanitizer", action="append", choices=SANITIZERS, default=[], help="sanitizer the build used; repeat for several. Selects the --known entries that apply") + parser.add_argument("--json", action="store_true", help="write structured triage to stdout; --summary still appends Markdown") args = parser.parse_args(argv) if args.known and not args.sanitizer: parser.error("--known needs at least one --sanitizer") @@ -129,14 +131,22 @@ def main(argv=None): (source.exists() and args.summary.exists() and source.samefile(args.summary))): raise ValueError(f"summary output must differ from input: {source}") known = load_known(args.known, args.sanitizer) if args.known else {} - text, failing = summarize(args.title, results(args.junit), known, reports(args.junit)) + found, details = results(args.junit), reports(args.junit) + text, failing = summarize(args.title, found, known, details) + output = text + if args.json: + output = json.dumps({"title": args.title, "unexpected_failure": failing, + "tests": [{"name": name, "outcome": found[name], "known_reason": known.get(name), + "report": details.get(name), "unexpected": found[name] == "failed" and name not in known} + for name in sorted(found)], + "known_not_run": sorted(set(known) - set(found))}, ensure_ascii=False, indent=2) + "\n" if args.summary: with args.summary.open("a", encoding="utf-8") as summary: summary.write(text) except (OSError, ET.ParseError, ValueError, RuntimeError) as error: print(f"sanitizer_report: {error}", file=sys.stderr) return 2 - sys.stdout.write(text) + sys.stdout.write(output) return 1 if failing else 0 diff --git a/tools/select_tests.py b/tools/select_tests.py index be0cba6bac..1be7b12a95 100644 --- a/tools/select_tests.py +++ b/tools/select_tests.py @@ -110,7 +110,10 @@ class Build: def test_commands(self, names): found, visited, pending = collections.defaultdict(set), set(), [Path(self.path)] while pending: - directory = pending.pop().resolve() + try: + directory = pending.pop().resolve() + except RuntimeError: + continue file = directory / "CTestTestfile.cmake" if directory in visited or not file.is_file(): continue @@ -351,7 +354,10 @@ def main(argv=None): args = parser.parse_args(argv) if args.command == "ctest": selected = Path(args.build, "select.tests") - extra = ["-I", selected.read_text(encoding="utf-8-sig").strip()] if selected.exists() else [] + try: + extra = ["-I", selected.read_text(encoding="utf-8-sig").strip()] if selected.exists() else [] + except (OSError, UnicodeDecodeError) as error: + parser.error(f"{selected}: {error}") return subprocess.run(["ctest", "--test-dir", args.build, *args.arguments, *extra]).returncode if args.command == "explain": try: diff --git a/tools/shader_scan.py b/tools/shader_scan.py index a675d5d8c9..4c399f689e 100644 --- a/tools/shader_scan.py +++ b/tools/shader_scan.py @@ -153,6 +153,8 @@ TABLE_KEYS = ("vop3encodedvop1", "vop3p", "vop3", "vopc", "vop2", "vop1", "vintr TABLE_ENCODING = {"vop3encodedvop1": "VOP3:VOP1", "image": "MIMG"} BLOCK = re.compile(r"^(?![\s}#])(?:[\w:<>&*,]+[ \t]+)*?(\w+)\s*(?:\[\]\s*=|\()", re.M) PAIR = re.compile(r"(?:\{\s*|case\s+)0x([0-9a-fA-F]+)u?\s*(?:,|:\s*return)\s*RdnaOpcode::(\w+)") +CASE_RUN = re.compile(r"((?:\bcase\s+0x[0-9a-fA-F]+u?\s*:\s*)+)return\s+RdnaOpcode::(\w+)") +CASE_NUMBER = re.compile(r"0x([0-9a-fA-F]+)u?") REQUIRED_TABLES = ("SOP1", "SOP2", "SOPC", "SOPK", "SOPP", "VOP1", "VOP2", "VOPC", "VOP3", "VOP3:VOP1", "VOP3P", "SMEM", "MUBUF", "MTBUF", "FLAT", "DS", "MIMG") @@ -180,6 +182,9 @@ def scan_decoder(files): table = next((k for k in TABLE_KEYS if key.startswith(k)), None) if table: entries = d.tables.setdefault(TABLE_ENCODING.get(table, table.upper()), {}) + for m in CASE_RUN.finditer(block): + for number in CASE_NUMBER.finditer(m.group(1)): + entries[int(number.group(1), 16)] = m.group(2) for m in PAIR.finditer(block): entries[int(m.group(1), 16)] = m.group(2) else: diff --git a/tools/suggest_patches.py b/tools/suggest_patches.py index c601d0b4a9..f98e6614d0 100644 --- a/tools/suggest_patches.py +++ b/tools/suggest_patches.py @@ -1,4 +1,5 @@ import argparse +import errno import hashlib import json import os @@ -24,7 +25,7 @@ TEST_RESULTS = {"passed", "failed", "partial"} def relative_path(name): - if not isinstance(name, str) or not name or "\\" in name: + if not isinstance(name, str) or not name or "\\" in name or "\x00" in name: raise ValueError(f"invalid relative path: {name!r}") path = PurePosixPath(name) if path.is_absolute() or PureWindowsPath(name).drive or any(part in (".", "..") for part in name.split("/")): @@ -173,6 +174,29 @@ def validate_entry(directory, entry): return patch, {name: digest.lower() for name, digest in targets.items()}, metadata +def resolve_path(path): + path = Path(path) + try: + resolved = path.resolve() + except RuntimeError as error: + raise ValueError(str(error)) from error + except OSError as error: + raise path_error(path, error) from error + if resolved.is_symlink(): + try: + resolved.stat() + except OSError as error: + raise path_error(path, error) from error + raise ValueError(f"Symlink loop from {path!r}") + return resolved + + +def path_error(path, error): + if error.errno == getattr(errno, "ELOOP", None) or getattr(error, "winerror", None) == 1921: + return ValueError(f"Symlink loop from {path!r}: {error}") + return ValueError(str(error)) + + def main(argv=None): for stream in (sys.stdout, sys.stderr): if hasattr(stream, "reconfigure"): @@ -204,13 +228,14 @@ def main(argv=None): raise ValueError("--skip-sce-module conflicts with --sce-module-path") if args.timeout_seconds <= 0: raise ValueError("timeout-seconds must be positive") - root = (args.sce_module_path or args.input.parent).resolve() + root = resolve_path(args.sce_module_path or args.input.parent) if not root.is_dir(): raise ValueError("module parent must be a directory") for executable in args.executable: confined(root, executable) + catalog = resolve_path(args.catalog) matches = [] - entries = load_catalog(args.catalog.resolve(), title, version) + entries = load_catalog(catalog, title, version) if not entries: print(f"{prefix}: no catalog entry for {title} {version}.") return 1 @@ -218,14 +243,14 @@ def main(argv=None): for number, entry in entries: label = f"Catalog entry {number} ({entry.get('patch')!r})" try: - patch, targets, catalog_metadata = validate_entry(args.catalog.resolve(), entry) + patch, targets, catalog_metadata = validate_entry(catalog, entry) except (OSError, ValueError, tomllib.TOMLDecodeError) as error: print(f"{label}: rejected: {error}") continue errors = [] for name, expected in targets.items(): try: - source = args.input.resolve() if name == args.input.name else confined(root, name) + source = resolve_path(args.input) if name == args.input.name else confined(root, name) actual = sha256(source) if actual != expected: errors.append(f"{name}: SHA-256 mismatch ({actual})") @@ -234,7 +259,7 @@ def main(argv=None): if errors: print(f"{label}: rejected: {'; '.join(errors)}") continue - command = [str(args.relinker.resolve()), "--verify-patches", str(patch)] + command = [str(resolve_path(args.relinker)), "--verify-patches", str(patch)] if args.skip_sce_module: command.append("--skip-sce-module") if args.sce_module_path: diff --git a/tools/symbolize_samples.py b/tools/symbolize_samples.py index 9a1b8b6f3b..45938610f9 100644 --- a/tools/symbolize_samples.py +++ b/tools/symbolize_samples.py @@ -45,35 +45,86 @@ class PeModule: raise ValueError(f"{path}: not a PE file") _, sections, self.timestamp, symbol_table, symbol_count, optional_size, _ = struct.unpack_from(" len(data) - optional: + raise ValueError(f"{path}: PE32+ optional header is truncated") if struct.unpack_from(" (optional_size - 112) // 8: + raise ValueError(f"{path}: PE data directories exceed the optional header") + export_rva, export_size = struct.unpack_from(" 0x100000000: + raise ValueError("RVA exceeds address space") + result = bytearray() + while count > 0: + for start, size, raw_size, raw_pointer, _ in self.sections: + if start <= rva < start + size: + offset = rva - start + chunk = min(count, size - offset) + if offset < raw_size: + raw_chunk = min(chunk, raw_size - offset) + if raw_pointer + raw_size > len(data): + raise ValueError("Truncated section raw data") + result.extend(data[raw_pointer + offset:raw_pointer + offset + raw_chunk]) + rva += raw_chunk + count -= raw_chunk + else: + result.extend(b"\0" * chunk) + rva += chunk + count -= chunk + break + else: + raise ValueError(f"RVA {rva:#x} is in no section") + return bytes(result) + + def read_string(self, data, rva): + chunks = [] + while True: + if rva >= 0x100000000: + raise ValueError("RVA exceeds address space") + for start, size, raw_size, raw_pointer, _ in self.sections: + if start <= rva < start + size: + offset = rva - start + if offset < raw_size: + if raw_pointer + raw_size > len(data): + raise ValueError("Truncated section raw data") + raw_left = raw_size - offset + scan_len = min(raw_left, 4096, 0x100000000 - rva) + file_offset = raw_pointer + offset + chunk = data[file_offset:file_offset + scan_len] + nul = chunk.find(b"\0") + if nul >= 0: + chunks.append(chunk[:nul]) + return b"".join(chunks).decode("utf-8", "replace") + chunks.append(chunk) + rva += scan_len + else: + return b"".join(chunks).decode("utf-8", "replace") + break + else: + raise ValueError(f"RVA {rva:#x} is in no section") def coff_symbols(self, data, table, count): strings = table + count * 18 @@ -92,7 +143,7 @@ class PeModule: index += 1 + aux if storage not in (IMAGE_SYM_CLASS_EXTERNAL, IMAGE_SYM_CLASS_STATIC) or not 1 <= section <= len(self.sections): continue - start, _, _, characteristics = self.sections[section - 1] + start, _, _, _, characteristics = self.sections[section - 1] if not characteristics & IMAGE_SCN_CNT_CODE: continue if storage == IMAGE_SYM_CLASS_STATIC and (kind >> 4) != 2 and value == 0: @@ -112,22 +163,22 @@ class PeModule: return result def export_symbols(self, data, rva, size): - directory = self.offset(rva) - functions, names, function_rva, name_rva, ordinal_rva = struct.unpack_from(" 0x100000000 or name_rva + names * 4 > 0x100000000 or ordinal_rva + names * 2 > 0x100000000: + raise ValueError("PE export table exceeds address space") result = [] - if names: - name_table = self.offset(name_rva) - ordinal_table = self.offset(ordinal_rva) - for index in range(names): - ordinal = struct.unpack_from("= functions: - raise ValueError("PE export ordinal is outside the export address table") - address = struct.unpack_from("= functions: + raise ValueError("PE export ordinal is outside the export address table") + address = struct.unpack(" int { return 0; } +extern "C" { +void inner(int value = 0) {} +auto inner_trailing() -> int { return 0; } +int actual_nid_postfix = 1; +} +''') + self.assertEqual(nid_names.collect_real_names(), {"actual", "actual_nid_postfix"}) + + def test_data_initializers_preserve_following_declarators(self): + self.write_source('''extern "C" { +int first_nid_postfix = 1 < 2, second_nid_postfix = 0; +int shift_nid_postfix = 1 << 2, last_nid_postfix = 0; +bool low_nid_postfix = lower < upper, high_nid_postfix = upper > lower; +bool low_braced_nid_postfix = lower < upper, high_braced_nid_postfix{upper > lower}; +int templated_nid_postfix = make(), tail_nid_postfix = 0; +extern void (* const callback_nid_postfix)(int) = nullptr; +} +extern "C" int value_nid_postfix(42); +extern "C" int negative_nid_postfix(-1); +extern "C" int positive_nid_postfix(+1); +extern "C" int nested_nid_postfix((42)); +extern "C" const wchar_t *wide_nid_postfix(L"value"); +''') + names = {"first", "second", "shift", "last", "low", "high", "templated", "tail", "callback", "value", + "negative", "positive", "nested", "wide", "low_braced", "high_braced"} + self.assertEqual(nid_names.collect_real_names(), names | {name + "_nid_postfix" for name in names}) + + def test_local_constants_are_excluded_without_losing_pointers_to_const(self): + self.write_source('''extern "C" { +const int hidden_nid_postfix = 1, other_hidden_nid_postfix = 2; +constexpr int constant_nid_postfix = 3; +const int ordinary = 1, hidden_after_ordinary_nid_postfix = 2; +static int ordinary_static{1}, hidden_after_static_nid_postfix = 2; +const int ordinary_const{1}, hidden_after_const_nid_postfix = 2; +const int first_const_nid_postfix{1}, second_const_nid_postfix = 2; +int * const fixed_nid_postfix = nullptr; +void (* const local_callback_nid_postfix)(int) = nullptr; +const int *pointer_nid_postfix = nullptr, scalar_nid_postfix = 4; +extern const int exposed_nid_postfix = 5; +} +extern "C" const int direct_nid_postfix = 6; +''') + names = {"pointer", "exposed", "direct"} + self.assertEqual(nid_names.collect_real_names(), names | {name + "_nid_postfix" for name in names}) + def test_error_preserves_unicode_paths_with_legacy_stderr(self): database = self.root / "\u8cc7\u6599" database.mkdir() diff --git a/tools/tests/test_package_release.py b/tools/tests/test_package_release.py index 2394f28d59..ded9c3b83c 100644 --- a/tools/tests/test_package_release.py +++ b/tools/tests/test_package_release.py @@ -86,6 +86,12 @@ class PublicHeaderPackageTests(unittest.TestCase): self.api.write_text('/*\n#include "Missing.hpp"\n*/\nconst char* text = R"test(\n#include "AlsoMissing.hpp"\n)test";\n', encoding="utf-8") self.assertEqual(len(package_headers.public_headers(self.source)), 3) + def test_include_next_is_rejected(self): + self.api.write_text('#include_next \n', encoding="utf-8") + with self.assertRaisesRegex(RuntimeError, "Unsupported include_next"): + package_headers.package(self.source, self.output, "v1") + self.assertFalse(self.output.exists()) + def test_external_symlink_is_rejected(self): external = self.root / "external.hpp" external.write_text("", encoding="utf-8") @@ -307,6 +313,18 @@ class ReleasePackageTests(unittest.TestCase): (self.build / "CMakeCache.txt").write_bytes(b"\xef\xbb\xbfCMAKE_CXX_COMPILER:STRING=" + compiler.as_posix().encode("utf-8") + b"\n") self.assertEqual(package_release.windows_runtime(self.build), compiler.parent) + def test_windows_runtime_finds_a_bare_compiler_name_on_path(self): + compiler = self.root / "custom toolchain" / "bin" / "g++.exe" + (self.build / "CMakeCache.txt").write_text("CMAKE_CXX_COMPILER:UNINITIALIZED=g++\n", encoding="utf-8") + with patch.object(package_release.shutil, "which", return_value=str(compiler)) as which: + self.assertEqual(package_release.windows_runtime(self.build), compiler.parent) + which.assert_called_once_with("g++") + with patch.object(package_release.shutil, "which", return_value=os.path.join(".", "g++.exe")): + self.assertEqual(package_release.windows_runtime(self.build), Path.cwd()) + with patch.object(package_release.shutil, "which", return_value=None): + with self.assertRaisesRegex(RuntimeError, "not on PATH"): + package_release.windows_runtime(self.build) + def test_tracy_runtime_decodes_cmake_cache_with_utf8_bom(self): client = self.build / "core/libs/libs/libTracyClient.dll" client.write_bytes(b"client") @@ -765,7 +783,7 @@ class ReleaseNoteDestinationsTests(unittest.TestCase): previous = {path: path.read_bytes() for index, path in enumerate(paths) if index != position} result = subprocess.run([sys.executable, str(Path(prepare_release_notes.__file__)), "--input", str(paths[0]), "--body", str(paths[1]), "--full", str(paths[2])], - capture_output=True, text=True, timeout=30) + capture_output=True, text=True, encoding="utf-8", timeout=30) self.assertEqual(result.returncode, 2, result.stderr) self.assertNotIn("Traceback", result.stderr) self.assertTrue(paths[position].is_symlink()) @@ -870,6 +888,29 @@ class ReleaseNotesTests(unittest.TestCase): prepare_release_notes.prepare_notes(*paths) self.assertEqual([path.read_bytes() for path in paths], previous) + def test_symlink_loop_output_is_reported_without_traceback(self): + notes = json.dumps({"body": "release notes"}) + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + source = root / "notes.json" + source.write_text(notes, encoding="utf-8") + loop = root / "body.md" + try: + loop.symlink_to(loop.name) + except (OSError, NotImplementedError) as error: + self.skipTest(f"symbolic links unavailable: {error}") + full = root / "full.md" + with self.assertRaisesRegex(ValueError, r"symbolic link loop"): + prepare_release_notes.prepare_notes(source, loop, full) + result = subprocess.run( + [sys.executable, str(Path(prepare_release_notes.__file__)), + "--input", str(source), "--body", str(loop), "--full", str(full)], + capture_output=True, text=True, encoding="utf-8", timeout=20) + self.assertEqual(result.returncode, 2, result.stderr) + self.assertIn("symbolic link loop", result.stderr) + self.assertNotIn("Traceback", result.stderr) + self.assertEqual(result.stdout, "") + def test_short_notes_are_unchanged_including_unicode_at_limit(self): for notes in ("", "## What's Changed\n* Fix\n", "я" * 125000): @@ -1115,7 +1156,7 @@ class ReleaseMainTests(unittest.TestCase): self.skipTest(str(error)) result = subprocess.run([sys.executable, str(Path(package_release.__file__)), "--docs", str(docs), "--output", str(output)], - capture_output=True, text=True, timeout=30) + capture_output=True, text=True, encoding="utf-8", timeout=30) self.assertEqual(result.returncode, 2, result.stderr) self.assertNotIn("Traceback", result.stderr) self.assertIn("symbolic link loop", result.stderr) diff --git a/tools/tests/test_pad_input.py b/tools/tests/test_pad_input.py index 12700c78e6..52019fa5f7 100644 --- a/tools/tests/test_pad_input.py +++ b/tools/tests/test_pad_input.py @@ -66,6 +66,80 @@ class PadInputTests(unittest.TestCase): self.path.write_text("APS5_PAD_TIMELINE 1 2000 1\n0 100 16384\n", encoding="utf-8") self.assertEqual(self.queue(1000, "down"), [2001, 64, 300]) + def test_malformed_timeline_bodies_do_not_exhaust_sequence(self): + maximum = 2**64 - 1 + cases = [ + (1, b""), (2, b"0 100 16384\n"), (1, b"\n"), + (1, b"0 100\n"), (1, b"010016384\n"), (1, b"0 100 16384 extra\n"), + (1, b"0 0 16384\n"), (1, b"0 5001 16384\n"), + (1, b"0 100 1\n"), (1, b"0 100 18446744073709551616\n"), + (1, b"18446744073709551616 100 16384\n"), + (1, b"0 18446744073709551616 16384\n"), + (1, b"-0 100 16384\n"), (1, b"0 1_00 16384\n"), + (1, b"0 100 0x4000\n"), (1, b"3599999 2 16384\n"), + (1, b"3600000 1 16384\n"), (2, b"0 100 16384\n99 100 64\n"), + (2, b"100 100 16384\n0 100 64\n"), + (1, b" " * 117 + b"0 100 16384\n"), + (1, b" " * 117 + b"0 100 16384"), + (1, b"0 100 16384\0" + b" " * 117 + b"\n"), + (1, b"0 100 16384\n0 100 64\n"), + (1, b"0 100 16384\ntrailing\n"), + (1, b"0 100 16384\n" + b" " * 4096 + b"extra"), + ] + for count, body in cases: + with self.subTest(count=count, body=body): + self.path.write_bytes(f"APS5_PAD_TIMELINE 1 {maximum} {count}\n".encode() + body) + self.assertEqual(self.queue(1, "down"), [1, 64, 300]) + + def test_valid_timeline_boundaries_preserve_sequence(self): + bodies = [ + b"+0 +5000 +7405566\n5000 1 0\n3599999 1 16384\n \t\r\n\v\f", + b" " * 118 + b"0 1 16384\n1 1 0\n2 1 0", + b"0+100+16384\n100 100 64\n200 1 0\n", + b"0 100 16384\x00-1\n100 100 64\n200 1 0\n", + b"0 100 16384\n100 100 64\n" + b" " * 120 + b"200 1 0", + ] + for body in bodies: + with self.subTest(body=body): + self.path.write_bytes(b"APS5_PAD_TIMELINE +1 +2000 +3\n" + body) + self.assertEqual(self.queue(1000, "down"), [2001, 64, 300]) + + def test_timeline_header_numeric_adjacency_preserves_sequence(self): + self.path.write_bytes(b"APS5_PAD_TIMELINE 1+2000+1\n0 100 16384\n") + self.assertEqual(self.queue(1000, "down"), [2001, 64, 300]) + + def test_timeline_text_end_marker_matches_host_text_mode(self): + for body in [b"0 100 16384\n\x1aignored", b"0 100 16384\x1aignored"]: + with self.subTest(body=body): + self.path.write_bytes(b"APS5_PAD_TIMELINE 1 2000 1\n" + body) + expected = 2001 if os.name == "nt" else 1000 + self.assertEqual(self.queue(1000, "down"), [expected, 64, 300]) + + def test_timeline_header_text_end_marker_prevents_event_reads_on_windows(self): + self.path.write_bytes(b"APS5_PAD_TIMELINE 1 2000 1\0\x1a\n0 100 16384\n") + expected = 1000 if os.name == "nt" else 2001 + self.assertEqual(self.queue(1000, "down"), [expected, 64, 300]) + + def test_timeline_event_crlf_at_runtime_line_capacity_matches_host_text_mode(self): + self.path.write_bytes(b"APS5_PAD_TIMELINE 1 2000 1\r\n" + b" " * 116 + b"0 100 16384\r\n") + expected = 2001 if os.name == "nt" else 1000 + self.assertEqual(self.queue(1000, "down"), [expected, 64, 300]) + + def test_timeline_event_count_at_capacity_preserves_sequence(self): + body = b"".join(f"{index} 1 0\n".encode() for index in range(4096)) + self.path.write_bytes(b"APS5_PAD_TIMELINE 1 2000 4096\n" + body) + self.assertEqual(self.queue(1000, "down"), [2001, 64, 300]) + + def test_valid_maximum_timeline_sequence_is_exhausted(self): + original = f"APS5_PAD_TIMELINE 1 {2**64 - 1} 1\n3599999 1 16384\n" + self.path.write_text(original, encoding="utf-8") + errors = io.StringIO() + with contextlib.redirect_stderr(errors), self.assertRaises(SystemExit) as error: + self.queue(1, "down") + self.assertEqual(error.exception.code, 2) + self.assertIn("sequence is exhausted", errors.getvalue()) + self.assertEqual(self.path.read_text(encoding="utf-8"), original) + def test_equal_clock_still_produces_new_pulses(self): first = self.queue(1000, "cross") second = self.queue(1000, "down") diff --git a/tools/tests/test_pad_timeline.py b/tools/tests/test_pad_timeline.py index 66682cf5ce..41bb60c5c0 100644 --- a/tools/tests/test_pad_timeline.py +++ b/tools/tests/test_pad_timeline.py @@ -173,6 +173,34 @@ class TimelineTests(unittest.TestCase): self.assertEqual(self.run_cli(timeline, linked).returncode, 2) self.assertEqual(timeline.read_text(encoding="utf-8"), original) + def test_invalid_existing_timeline_can_be_replaced_after_sequence_exhaustion(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + timeline, output = root / "sequence.json", root / "command" + timeline.write_text(json.dumps(self.document), encoding="utf-8") + for body in ["", "0 100 16384\n", "0 100 16384\n99 100 64\n", + "0 100 16384\n100 100 64\nextra\n"]: + with self.subTest(body=body): + output.write_text(f"APS5_PAD_TIMELINE 1 {2**64 - 1} 2\n{body}", encoding="utf-8") + with mock.patch.object(sys, "argv", ["pad_timeline.py", str(timeline), str(output)]), \ + mock.patch.object(pad_input.time, "monotonic_ns", return_value=1), \ + contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()): + pad_timeline.main() + self.assertEqual(output.read_text(encoding="utf-8"), + "APS5_PAD_TIMELINE 1 1 2\n200 100 18432\n500 200 64\n") + + def test_valid_existing_timeline_sequence_exhaustion_preserves_command(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + timeline, output = root / "sequence.json", root / "command" + timeline.write_text(json.dumps(self.document), encoding="utf-8") + original = pad_timeline.compile_timeline(self.document, 2**64 - 1) + output.write_text(original, encoding="utf-8") + result = self.run_cli(timeline, output) + self.assertEqual(result.returncode, 2) + self.assertIn("sequence is exhausted", result.stderr) + self.assertEqual(output.read_text(encoding="utf-8"), original) + def test_symbolic_link_loops_are_usage_errors(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) @@ -247,6 +275,32 @@ class TimelineTests(unittest.TestCase): self.assertEqual(error.exception.code, 2) self.assertEqual(output.read_text(encoding="utf-8"), exhausted) + def test_timeline_accepts_utf8_bom(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + timeline, output = root / "sequence.json", root / "command" + timeline.write_text("\ufeff" + json.dumps(self.document), encoding="utf-8") + self.assertEqual(self.run_cli(timeline, output).returncode, 0) + self.assertTrue(output.read_text(encoding="utf-8").startswith("APS5_PAD_TIMELINE 1 ")) + self.assertIn("200 100 18432\n", output.read_text(encoding="utf-8")) + + def test_unencodable_command_path_still_reports_success(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + timeline, output = root / "sequence.json", root / "command\udcff" + timeline.write_text(json.dumps(self.document), encoding="utf-8") + try: + output.touch() + output.unlink() + except (OSError, UnicodeError): + self.skipTest("the file system cannot name a file with a lone surrogate") + env = dict(os.environ, PYTHONIOENCODING="utf-8:backslashreplace") + result = subprocess.run([sys.executable, str(Path(pad_timeline.__file__)), str(timeline), str(output)], + capture_output=True, text=True, timeout=5, env=env) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertTrue(output.read_text(encoding="utf-8").startswith("APS5_PAD_TIMELINE 1 ")) + self.assertIn("\\udcff", result.stdout) + if __name__ == "__main__": unittest.main() diff --git a/tools/tests/test_pr_labels.py b/tools/tests/test_pr_labels.py index 7881064b69..5984c6aef1 100644 --- a/tools/tests/test_pr_labels.py +++ b/tools/tests/test_pr_labels.py @@ -1,3 +1,4 @@ +import subprocess import contextlib import copy from functools import lru_cache @@ -47,6 +48,90 @@ def pr(title="fix: x", body="", labels=()): return {"number": 7, "title": title, "body": body, "labels": list(labels), "state": "open"} +class LabelPlanJsonTests(unittest.TestCase): + def test_batch_error_is_reported_and_later_items_continue(self): + import subprocess + records = [{"number": n, "title": "fix(core): example", "body": "", "labels": [], "state": "open"} for n in (1, 2)] + def process(repo, pr, dry_run, comment): + self.assertTrue(dry_run) + self.assertFalse(comment) + if pr["number"] == 1: + raise subprocess.CalledProcessError(1, ["gh"], stderr="read failed") + return set(), set(), False + output = io.StringIO() + with patch.object(pr_labels, "repository", return_value="test/repo"), patch.object(pr_labels, "gh", return_value=json.dumps(records)), patch.object(pr_labels, "process", side_effect=process) as run, contextlib.redirect_stdout(output), contextlib.redirect_stderr(io.StringIO()): + self.assertEqual(pr_labels.main(["--all", "--dry-run", "--json"]), 1) + data = json.loads(output.getvalue()) + self.assertEqual(data["failed"], [1]) + self.assertEqual(len(data["plans"]), 2) + self.assertEqual(data["plans"][0]["error"], "read failed") + self.assertEqual(run.call_count, 2) + + def test_json_requires_dry_run_before_any_access(self): + with patch.object(pr_labels, "gh", side_effect=AssertionError("unexpected access")), contextlib.redirect_stderr(io.StringIO()), self.assertRaises(SystemExit) as error: + pr_labels.main(["--pr", "42", "--json"]) + self.assertEqual(error.exception.code, 2) + + def test_plan_reads_only_and_records_comment(self): + data = {"number": 42, "title": "perf(core): reduce work", "body": "", "labels": [], "state": "open"} + calls = [] + def gh(*args): + calls.append(args) + if args == ("api", "repos/test/repo/pulls/42"): + return json.dumps(data) + if args[:2] == ("api", "--paginate"): + return "0" + raise AssertionError(args) + output = io.StringIO() + with patch.object(pr_labels, "repository", return_value="test/repo"), patch.object(pr_labels, "gh", side_effect=gh), contextlib.redirect_stdout(output): + self.assertEqual(pr_labels.main(["--pr", "42", "--dry-run", "--json"]), 0) + row = json.loads(output.getvalue())["plans"][0] + self.assertEqual(row["add"], sorted([pr_labels.PERF_LABEL, pr_labels.MISSING_LABEL])) + self.assertEqual(row["remove"], []) + self.assertEqual(row["comment_marker"], pr_labels.MARKER) + self.assertEqual(len(calls), 2) + + def test_plan_rows_match_the_text_plan(self): + body = "Unblocks: PPSA01325" + records = [ + {"number": 11, "title": "fix(core): x", "body": "", "labels": [{"name": n} for n in ("perf numbers missing", "unblocks title", "perf", "bug")], "state": "open"}, + {"number": 12, "title": "perf(core): x", "body": body, "labels": [], "state": "open"}, + {"number": 13, "title": "fix(core): x", "body": "", "labels": [], "state": "open"}, + ] + def gh(*args): + if args[:2] == ("api", "--paginate") and "pulls?" in args[2]: + return json.dumps(records) + if args[:2] == ("api", "--paginate") and args[2] == "repos/test/repo/issues/42/comments": + return "1" + if args == ("api", "repos/test/repo/pulls/42"): + return json.dumps(dict(records[1], number=42)) + if args[:2] == ("api", "--paginate"): + raise subprocess.CalledProcessError(1, ["gh"], stderr="read failed") + raise AssertionError(args) + class Descending(set): + def __iter__(self): + return iter(sorted(set.__iter__(self), reverse=True)) + original = pr_labels.process + def process(repo, pr, dry_run, comment): + if pr["number"] == 13: + raise subprocess.CalledProcessError(1, ["gh"], stderr="read failed") + add, remove, post = original(repo, pr, dry_run, comment) + return Descending(add), Descending(remove), post + for argv, expected, code in ( + (["--all"], {"repository": "test/repo", "failed": [13], "plans": [ + {"number": 11, "add": [], "remove": sorted(pr_labels.MANAGED), "comment_marker": None}, + {"number": 12, "add": sorted(pr_labels.MANAGED), "remove": [], "comment_marker": None}, + {"number": 13, "error": "read failed"}]}, 1), + (["--pr", "42"], {"repository": "test/repo", "failed": [], "plans": [ + {"number": 42, "add": sorted(pr_labels.MANAGED), "remove": [], "comment_marker": None}]}, 0), + ): + with self.subTest(argv=argv): + output = io.StringIO() + with patch.object(pr_labels, "repository", return_value="test/repo"), patch.object(pr_labels, "gh", side_effect=gh), patch.object(pr_labels, "process", side_effect=process), contextlib.redirect_stdout(output), contextlib.redirect_stderr(io.StringIO()): + self.assertEqual(pr_labels.main(argv + ["--dry-run", "--json"]), code) + self.assertEqual(json.loads(output.getvalue()), expected) + + class TitleIdTests(unittest.TestCase): def test_malformed_pr_records_are_rejected_before_any_writes(self): valid = {"number": 7, "title": "perf: x", "body": "", "labels": [], "state": "open"} @@ -300,5 +385,8 @@ class MainTests(unittest.TestCase): self.assertEqual(err.errors, "backslashreplace") + + + if __name__ == "__main__": unittest.main() diff --git a/tools/tests/test_progress.py b/tools/tests/test_progress.py index 562c1f6271..44268a4460 100644 --- a/tools/tests/test_progress.py +++ b/tools/tests/test_progress.py @@ -816,6 +816,42 @@ class ProgressTests(unittest.TestCase): for name, path in others.items(): self.assertEqual(path.read_text(encoding="utf-8"), name) + def test_adjacent_aliased_reports_are_refused_when_others_are_missing(self): + libraries, shaders = self.library_snapshot() + with tempfile.TemporaryDirectory() as directory: + output = Path(directory) / "site" + output.mkdir() + first, second = output / "badge-libraries.svg", output / "badge-shaders.svg" + first.write_text("badge", encoding="utf-8") + try: + os.link(first, second) + except OSError: + self.skipTest("hard links are not available") + stderr = io.StringIO() + with patch("sys.stderr", stderr), self.assertRaises(SystemExit) as raised: + self.run_main([str(output)], libraries, shaders) + self.assertEqual(raised.exception.code, 2) + self.assertIn("badge-libraries.svg and badge-shaders.svg", stderr.getvalue()) + self.assertEqual(first.read_text(encoding="utf-8"), "badge") + self.assertFalse((output / "progress.json").exists()) + + def test_reports_linked_to_one_missing_target_are_refused(self): + libraries, shaders = self.library_snapshot() + with tempfile.TemporaryDirectory() as directory: + output = Path(directory) / "site" + output.mkdir() + try: + os.symlink("shared", output / "progress.json") + os.symlink("shared", output / "progress.html") + except OSError: + self.skipTest("symbolic links are not available") + stderr = io.StringIO() + with patch("sys.stderr", stderr), self.assertRaises(SystemExit) as raised: + self.run_main([str(output)], libraries, shaders) + self.assertEqual(raised.exception.code, 2) + self.assertIn("progress.json and progress.html", stderr.getvalue()) + self.assertFalse((output / "shared").exists()) + def test_root_must_be_a_source_tree(self): with tempfile.TemporaryDirectory() as directory: empty = Path(directory) / "empty tree" diff --git a/tools/tests/test_progress_wrappers.py b/tools/tests/test_progress_wrappers.py index 3897bd125c..f75efa3598 100644 --- a/tools/tests/test_progress_wrappers.py +++ b/tools/tests/test_progress_wrappers.py @@ -1,3 +1,4 @@ +import re import sys import tempfile import unittest @@ -40,6 +41,13 @@ class WrapperTests(unittest.TestCase): def test_static_helper_still_works(self): self.assertTodo("static int Helper() " + STUB_BODY + CALLER) + def test_multi_line_return_type_helper_is_pending(self): + self.assertTodo("static const std::string\n&Helper() " + STUB_BODY + CALLER) + + def test_comment_only_lines_leave_no_blank_line_runs(self): + text = "int x;\n// first\n\n" + "/* second */\n" * 3 + "int y;\n" + self.assertEqual(re.findall(r"\n[ \t]*\n", progress.code(text)), []) + def test_nested_helpers_are_pending_in_either_source_order(self): for helpers in ( "int Last() { NotImplemented_nid_no_patch(__func__); return 0; }\n" diff --git a/tools/tests/test_release_notes_check.py b/tools/tests/test_release_notes_check.py new file mode 100644 index 0000000000..3249902399 --- /dev/null +++ b/tools/tests/test_release_notes_check.py @@ -0,0 +1,55 @@ +import json +import os +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + + +class FeatureTests(unittest.TestCase): + def setUp(self): + self.temporary = tempfile.TemporaryDirectory(prefix="tool feature 界 ") + self.addCleanup(self.temporary.cleanup) + self.root = Path(self.temporary.name) + + def cli(self, script, *args): + return subprocess.run([sys.executable, str(Path(__file__).resolve().parents[1] / script), *map(str, args)], + capture_output=True, encoding="utf-8", timeout=15) + + def test_real_outputs_are_checked_byte_for_byte_without_writes(self): + source, body, full = [self.root / name for name in ("response.json", "body.txt", "full.txt")] + notes = "Release 界\r\n## What's Changed\r\n- entry\r\n" + source.write_text(json.dumps({"body": notes}), encoding="utf-8") + args = ["--input", source, "--body", body, "--full", full] + self.assertEqual(self.cli("prepare_release_notes.py", *args).returncode, 0) + before = {p.name: (p.read_bytes(), p.stat().st_mtime_ns) for p in self.root.iterdir()} + self.assertEqual(self.cli("prepare_release_notes.py", *args, "--check").returncode, 0) + self.assertEqual(before, {p.name: (p.read_bytes(), p.stat().st_mtime_ns) for p in self.root.iterdir()}) + body.write_text("stale") + self.assertEqual(self.cli("prepare_release_notes.py", *args, "--check").returncode, 1) + self.assertEqual(body.read_text(), "stale") + body.write_bytes(before[body.name][0]) + full.write_bytes(b"stale full output") + stale = {p.name: (p.read_bytes(), p.stat().st_mtime_ns) for p in self.root.iterdir()} + self.assertEqual(self.cli("prepare_release_notes.py", *args, "--check").returncode, 1) + self.assertEqual(stale, {p.name: (p.read_bytes(), p.stat().st_mtime_ns) for p in self.root.iterdir()}) + full.unlink() + missing = {p.name: (p.read_bytes(), p.stat().st_mtime_ns) for p in self.root.iterdir()} + self.assertEqual(self.cli("prepare_release_notes.py", *args, "--check").returncode, 1) + self.assertEqual(missing, {p.name: (p.read_bytes(), p.stat().st_mtime_ns) for p in self.root.iterdir()}) + + def test_missing_outputs_are_not_created_and_bad_json_is_an_error(self): + source = self.root / "response.json" + source.write_text('{"body":"notes"}') + args = ["--input", source, "--body", self.root / "missing" / "body", "--full", self.root / "full", "--check"] + self.assertEqual(self.cli("prepare_release_notes.py", *args).returncode, 1) + self.assertEqual(list(self.root.iterdir()), [source]) + source.write_text("{") + self.assertEqual(self.cli("prepare_release_notes.py", *args).returncode, 2) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/tests/test_relinker_test_step.py b/tools/tests/test_relinker_test_step.py new file mode 100644 index 0000000000..bbe22f47d1 --- /dev/null +++ b/tools/tests/test_relinker_test_step.py @@ -0,0 +1,16 @@ +import unittest +from pathlib import Path + +WORKFLOW = Path(__file__).resolve().parents[2] / ".github/workflows/build.yml" + + +class RelinkerTestStepTests(unittest.TestCase): + def test_relinker_ctest_fails_when_no_test_is_registered(self): + steps = [line for line in WORKFLOW.read_text(encoding="utf-8").splitlines() + if "ctest --test-dir build-relinker " in line] + self.assertEqual(len(steps), 1) + self.assertIn("--no-tests=error", steps[0]) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/tests/test_sanitizer_json.py b/tools/tests/test_sanitizer_json.py new file mode 100644 index 0000000000..0510d795c2 --- /dev/null +++ b/tools/tests/test_sanitizer_json.py @@ -0,0 +1,57 @@ +import json +import os +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + + +class FeatureTests(unittest.TestCase): + def setUp(self): + self.temporary = tempfile.TemporaryDirectory(prefix="tool feature 界 ") + self.addCleanup(self.temporary.cleanup) + self.root = Path(self.temporary.name) + + def cli(self, script, *args): + return subprocess.run([sys.executable, str(Path(__file__).resolve().parents[1] / script), *map(str, args)], + capture_output=True, encoding="utf-8", timeout=15) + + def test_real_junit_preserves_policy_and_markdown_append(self): + source = self.root / "results.xml" + source.write_text('ERROR: AddressSanitizer: bad access', encoding="utf-8") + known = self.root / "known.txt" + known.write_text("界 address tracked issue\nmissing address unavailable\n", encoding="utf-8") + before = source.read_bytes() + result = self.cli("sanitizer_report.py", source, "--json") + self.assertEqual(result.returncode, 1, result.stderr) + data = json.loads(result.stdout) + self.assertTrue(data["unexpected_failure"]) + self.assertEqual([t["name"] for t in data["tests"]], ["okay", "skip", "界"]) + self.assertEqual([(t["outcome"], t["unexpected"]) for t in data["tests"]], + [("passed", False), ("skipped", False), ("failed", True)]) + self.assertIn("AddressSanitizer", data["tests"][2]["report"]) + summary = self.root / "summary.txt" + result = self.cli("sanitizer_report.py", source, "--json", "--known", known, "--sanitizer", "address", "--summary", summary) + self.assertEqual(result.returncode, 0, result.stderr) + data = json.loads(result.stdout) + self.assertFalse(data["unexpected_failure"]) + self.assertEqual([(t["name"], t["outcome"], t["unexpected"]) for t in data["tests"]], + [("okay", "passed", False), ("skip", "skipped", False), ("界", "failed", False)]) + self.assertEqual(data["known_not_run"], ["missing"]) + self.assertEqual(data["tests"][2]["known_reason"], "tracked issue") + self.assertIn("1 known", summary.read_text()) + self.assertEqual(source.read_bytes(), before) + + def test_invalid_report_never_emits_success_json(self): + source = self.root / "results.xml" + for text in ("", "", "<"): + source.write_text(text) + result = self.cli("sanitizer_report.py", source, "--json") + self.assertEqual((result.returncode, result.stdout), (2, ""), result.stderr) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/tests/test_select_tests.py b/tools/tests/test_select_tests.py index 18fefccad6..063722855b 100644 --- a/tools/tests/test_select_tests.py +++ b/tools/tests/test_select_tests.py @@ -1,3 +1,4 @@ +import io import json import os import re @@ -503,6 +504,25 @@ class BuildErrorTests(unittest.TestCase): self.assertIn("not a configured CMake build directory", result.stderr) self.assertNotIn("Traceback", result.stderr) + def test_ctest_reports_an_unreadable_selection_without_a_traceback(self): + with tempfile.TemporaryDirectory() as directory: + build = Path(directory) + selected = build / "select.tests" + for shape in ("non-UTF-8", "directory"): + with self.subTest(shape): + if shape == "directory": + selected.unlink() + selected.mkdir() + else: + selected.write_bytes(b"\xff\xfe0,0,0,1\n") + with mock.patch.object(select_tests.subprocess, "run") as run, \ + mock.patch("sys.stderr", new_callable=io.StringIO) as stderr: + with self.assertRaises(SystemExit) as raised: + select_tests.main(["ctest", str(build)]) + self.assertEqual(raised.exception.code, 2) + self.assertIn(str(selected), stderr.getvalue()) + run.assert_not_called() + def test_cache_without_the_build_locations_is_reported_without_a_traceback(self): with tempfile.TemporaryDirectory() as directory: (Path(directory) / "CMakeCache.txt").write_text("# empty\n", encoding="utf-8") @@ -717,6 +737,20 @@ class MissingCommandTests(unittest.TestCase): self.assertEqual(commands, {"absolute": [executable, helper], "relative": None, "path": None, "ambiguous": None, "existing": [helper], "stale": None}) + def test_looping_subdirectory_is_skipped_when_recovering_commands(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + (root / "nested").mkdir() + os.symlink("second", root / "first") + os.symlink("first", root / "second") + (root / "CTestTestfile.cmake").write_text('subdirs("first" "nested")\n', encoding="utf-8") + (root / "nested/CTestTestfile.cmake").write_text('add_test(kept "/bin/kept")\n', encoding="utf-8") + build = object.__new__(select_tests.Build) + build.path = str(root) + self.assertEqual(dict(build.test_commands({"kept"})), {"kept": {("/bin/kept",)}}) + build.path = str(root / "first") + self.assertEqual(dict(build.test_commands({"kept"})), {}) + def test_main_reconfigures_streams(self): class Stream: diff --git a/tools/tests/test_shader_cache_runtime_sources.py b/tools/tests/test_shader_cache_runtime_sources.py index eec57f2fb3..fa3dc35e40 100644 --- a/tools/tests/test_shader_cache_runtime_sources.py +++ b/tools/tests/test_shader_cache_runtime_sources.py @@ -11,7 +11,11 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[1])) import select_tests -EXPLICIT_ARGS = len(sys.argv) >= 3 and Path(sys.argv[1]).is_dir() +EXPLICIT_ARGS = ( + len(sys.argv) >= 3 + and __name__ == "__main__" + and Path(sys.argv[1]).is_dir() +) if EXPLICIT_ARGS: SOURCE = Path(sys.argv.pop(1)) CMAKE = sys.argv.pop(1) diff --git a/tools/tests/test_shader_scan.py b/tools/tests/test_shader_scan.py index d0935419ff..f038171896 100644 --- a/tools/tests/test_shader_scan.py +++ b/tools/tests/test_shader_scan.py @@ -65,8 +65,12 @@ ISA = [ def table_source(encoding): key = KEYS.get(encoding, encoding.capitalize()) - cases = "".join(f" case 0x{number:02x}: return RdnaOpcode::{name};\n" - for number, name in ENTRIES.get(encoding, {0: "Placeholder" + key}).items()) + cases = "" + for number, name in ENTRIES.get(encoding, {0: "Placeholder" + key}).items(): + numbers = number if isinstance(number, tuple) else (number,) + for member in numbers[:-1]: + cases += f" case 0x{member:02x}:\n" + cases += f" case 0x{numbers[-1]:02x}: return RdnaOpcode::{name};\n" return f"RdnaOpcode lookup{key}Opcode(uint32_t op)\n{{\n switch (op)\n {{\n{cases} }}\n}}\n\n" @@ -174,6 +178,18 @@ class ScanTests(unittest.TestCase): ("VOP1", "base"): (1, 1, 1), ("VOP1", "vop3"): (2, 2, 1), ("VOP1", "dpp16"): (1, 0, 0), ("VOPC", "vop3"): (2, 1, 1), ("EXP", "base"): (1, 1, 1)}) + def test_case_fallthrough_maps_every_label_to_the_result(self): + isa = ISA + [("S_CB_SYS", "SOPP", "base", 0x17), ("S_CB_USER", "SOPP", "base", 0x18), + ("S_CB_OR", "SOPP", "base", 0x19), ("S_CB_AND", "SOPP", "base", 0x1A), + ("S_END_SAVED", "SOPP", "base", 0x1B), ("S_END_ORDERED", "SOPP", "base", 0x1E)] + with tempfile.TemporaryDirectory() as directory: + with patch.dict(ENTRIES, {"SOPP": {(0x17, 0x18, 0x19, 0x1A): "SCbranchCdbg", (0x1B, 0x1E): "SEndpgm"}}): + write_tree(directory) + result = shader_scan.measure(directory, isa) + sopp = next(f for f in result["forms"] if f["encoding"] == "SOPP") + self.assertEqual((sopp["total"], sopp["decoded"], sopp["missing"]), + (6, 6, [])) + def test_unsupported_throws_skip_comments_rethrows_other_messages_and_tests(self): with tempfile.TemporaryDirectory() as directory: write_tree(directory) diff --git a/tools/tests/test_stale_conflicts_workflow.py b/tools/tests/test_stale_conflicts_workflow.py new file mode 100644 index 0000000000..b9fb2b6cd8 --- /dev/null +++ b/tools/tests/test_stale_conflicts_workflow.py @@ -0,0 +1,16 @@ +import unittest +from pathlib import Path + +WORKFLOW = Path(__file__).resolve().parents[2] / ".github/workflows/stale-conflicts.yml" + + +class StaleLabelStepTests(unittest.TestCase): + def test_label_cleanup_step_runs_with_pipefail(self): + text = WORKFLOW.read_text(encoding="utf-8") + step = text.split(" - env:", 1)[1].split(" - uses:", 1)[0] + self.assertIn("gh pr list", step) + self.assertIn("shell: bash", step) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/tests/test_suggest_patches.py b/tools/tests/test_suggest_patches.py index 1445cd71d1..20bdd49e9c 100644 --- a/tools/tests/test_suggest_patches.py +++ b/tools/tests/test_suggest_patches.py @@ -215,6 +215,51 @@ class PatchAdvisorTests(unittest.TestCase): self.assertIn("Catalog entry 1 ('loop-target.toml'): rejected", output.getvalue()) run.assert_called_once() + def test_resolve_path_reports_symlink_loops(self): + first, second = self.root / "loop-first", self.root / "loop-second" + try: + first.symlink_to(second) + second.symlink_to(first) + except (OSError, NotImplementedError) as error: + self.skipTest(str(error)) + with self.assertRaises(ValueError) as error: + suggest_patches.resolve_path(first) + self.assertRegex(str(error.exception), r"Symlink loop|Too many levels of symbolic links") + + def test_resolve_path_reports_windows_symlink_loops(self): + unresolvable = OSError(22, "The name of the file cannot be resolved by the system", "loop") + unresolvable.winerror = 1921 + missing = OSError(2, "No such file or directory", "missing") + for failure, expected in ((unresolvable, True), (missing, False)): + with self.subTest(winerror=getattr(failure, "winerror", None)), \ + patch.object(suggest_patches.Path, "resolve", side_effect=failure): + with self.assertRaises(ValueError) as error: + suggest_patches.resolve_path(self.root / "loop") + self.assertEqual("Symlink loop" in str(error.exception), expected, str(error.exception)) + + def test_cli_path_symlink_loops_are_reported_without_traceback(self): + first, second = self.root / "loop-first", self.root / "loop-second" + try: + first.symlink_to(second) + second.symlink_to(first) + except (OSError, NotImplementedError) as error: + self.skipTest(str(error)) + tool = Path(suggest_patches.__file__) + env = {**os.environ, "PYTHONPATH": str(Path(__file__).resolve().parents[1])} + cases = ( + ("catalog", ["--catalog", str(first), "--relinker", str(self.relinker)]), + ("module parent", ["--catalog", str(self.catalog), "--sce-module-path", str(first), "--relinker", str(self.relinker)]), + ) + for label, arguments in cases: + with self.subTest(label=label): + result = subprocess.run( + [sys.executable, str(tool), *arguments, "--param-json", str(self.param), "--input", str(self.input), + "--module-dir", "Media/Modules"], + capture_output=True, text=True, timeout=30, env=env) + self.assertEqual(result.returncode, 2, result.stderr) + self.assertNotIn("Traceback", result.stderr) + self.assertRegex(result.stderr, r"Symlink loop|Too many levels of symbolic links") + @patch.object(suggest_patches.subprocess, "run") def test_relinker_timeout_is_not_a_recommendation(self, run): run.side_effect = subprocess.TimeoutExpired([str(self.relinker)], 7) diff --git a/tools/tests/test_symbolize_samples.py b/tools/tests/test_symbolize_samples.py index a41d08c37a..1dc82a2010 100644 --- a/tools/tests/test_symbolize_samples.py +++ b/tools/tests/test_symbolize_samples.py @@ -42,6 +42,7 @@ def build_pe(path, with_symbols=True, with_exports=False, second_code=False, tim struct.pack_into("", "<x>")): + with self.subTest(name=name): + metadata = {"name": "thread_name", "ph": "M", "ts": 0, "pid": 7, "tid": 1, "args": {"name": name}} + result = trace_digest.digest(trace_digest.Trace(EVENTS + [metadata, self.marker()])) + self.assertEqual(result["event_loss"][0]["thread"], name) + markdown = trace_digest.markdown(result) + self.assertIn(f"- {escaped}: 8 events lost; buffer full at 1.5 ms", markdown) + self.assertNotIn(f"- {name}: 8 events lost", markdown) + + def test_thread_filter_keeps_only_relevant_loss_markers(self): + trace = trace_digest.Trace(EVENTS + [self.marker()]) + trace.select_threads(["presenter"]) + result = trace_digest.digest(trace) + self.assertNotIn("trace_incomplete", result) + self.assertNotIn("Incomplete trace", trace_digest.markdown(result)) + trace = trace_digest.Trace(EVENTS + [self.marker(tid=9)]) + trace.select_threads(["tid 9"]) + result = trace_digest.digest(trace) + self.assertEqual(result["event_loss"], [{"thread": "tid 9", "at_ms": 1.5, "events_lost": 8}]) + + def test_loss_counters_are_validated(self): + for lost in (None, True, -1, 1.5, "8"): + with self.subTest(lost=lost): + with self.assertRaisesRegex(ValueError, "args.events_lost must be a nonnegative integer"): + trace_digest.Trace(EVENTS + [self.marker(lost=lost)]) + + def test_zero_loss_does_not_change_the_digest(self): + expected = trace_digest.digest(trace_digest.Trace(EVENTS)) + result = trace_digest.digest(trace_digest.Trace(EVENTS + [self.marker(lost=0)])) + self.assertEqual(result, expected) + + class TraceJsonNestingTests(unittest.TestCase): def setUp(self): self.directory = tempfile.TemporaryDirectory() @@ -113,6 +245,33 @@ class TraceJsonNestingTests(unittest.TestCase): self.assertEqual(path.read_bytes(), original) class TraceDigestTests(unittest.TestCase): + + def test_summary_symlink_loop_is_reported_without_traceback(self): + loop = Path(self.directory.name) / "loop.jsonl" + try: + loop.symlink_to(loop.name) + except (OSError, NotImplementedError) as error: + self.skipTest(f"symbolic links unavailable: {error}") + with self.assertRaisesRegex(ValueError, r"symbolic link loop"): + trace_digest.load_summary(loop, self.trace) + default = self.trace.with_name("aps5-summary.jsonl") + if default.exists(): + default.unlink() + try: + default.symlink_to(default.name) + except (OSError, NotImplementedError) as error: + self.skipTest(f"symbolic links unavailable: {error}") + with self.assertRaisesRegex(ValueError, r"symbolic link loop"): + trace_digest.load_summary(None, self.trace) + for arguments in ([str(self.trace), "--summary", str(loop)], [str(self.trace)]): + with self.subTest(arguments=arguments): + result = subprocess.run([sys.executable, trace_digest.__file__, *arguments], + capture_output=True, text=True, encoding="utf-8", timeout=20) + self.assertEqual(result.returncode, 2, result.stderr) + self.assertIn("symbolic link loop", result.stderr) + self.assertNotIn("Traceback", result.stderr) + self.assertEqual(result.stdout, "") + def test_summary_jsonl_excessive_nesting_reports_line_without_traceback(self): path = Path(self.directory.name) / "deep.jsonl" path.write_text('{}\n\n{"extra":' + excessive_trace_json('[', ']') + '}\n', encoding="utf-8") @@ -151,6 +310,17 @@ class TraceDigestTests(unittest.TestCase): trace_digest.load_intervals(path) self.assertTrue(str(raised.exception).startswith(f"{path}:3: invalid JSON: "), str(raised.exception)) + def test_cli_reports_a_summary_line_without_the_window_duration(self): + summary = self.trace.with_name("aps5-summary.jsonl") + record = dict(SUMMARY, window={"spec": "frames 600:3"}) + summary.write_text(json.dumps(record) + "\n", encoding="utf-8") + result = subprocess.run([sys.executable, trace_digest.__file__, str(self.trace)], + capture_output=True, text=True, encoding="utf-8", timeout=20) + self.assertEqual(result.returncode, 2, result.stderr) + self.assertTrue(result.stderr.startswith("FAIL: the summary line has no 'ms' field"), result.stderr) + self.assertNotIn("Traceback", result.stderr) + self.assertEqual(result.stdout, "") + def test_cli_rejects_invalid_summary_records_without_partial_output(self): summary = self.trace.with_name("aps5-summary.jsonl") explicit = summary.with_name("explicit-\u03a9.jsonl") @@ -344,6 +514,14 @@ class TraceDigestTests(unittest.TestCase): self.assertEqual(thread["folded_ms_per_frame"], 1.5) self.assertEqual(thread["untraced_ms_per_frame"], 0) + def test_presents_sharing_a_timestamp_keep_their_order_even_without_a_present_number(self): + trace = trace_digest.Trace([ + {"name": "present", "ph": "i", "ts": 100, "pid": 7, "tid": 2, "s": "g", "args": {"present": 1}}, + {"name": "present", "ph": "i", "ts": 100, "pid": 7, "tid": 2, "s": "g", "args": {}}, + {"name": "present", "ph": "i", "ts": 50, "pid": 7, "tid": 2, "s": "g", "args": {"present": 3}}, + ]) + self.assertEqual(trace.presents, [(50, 3), (100, 1), (100, None)]) + def test_trace_accepts_event_array_and_empty_document(self): trace_from_array = trace_digest.Trace(EVENTS) self.assertEqual(len(trace_from_array.presents), 3) @@ -468,6 +646,29 @@ class TraceDigestTests(unittest.TestCase): self.assertAlmostEqual(largest[("Driver.Present", "presenter")], 16.0) self.assertAlmostEqual(largest[("Driver.Present.inflight_wait", "presenter")], 14.5) + def test_persistent_peaks_merge_a_plateau_into_one_stall(self): + peaks = trace_digest.persistent_peaks([10, 10, 40, 41, 40, 10, 30, 10, 10, 25, 10]) + self.assertEqual(peaks, [(3, 31), (6, 20), (9, 15)]) + + def test_persistent_peaks_measure_a_shoulder_against_the_saddle(self): + self.assertEqual(trace_digest.persistent_peaks([10, 50, 30, 45, 10]), [(1, 40), (3, 15)]) + + def test_persistent_peaks_ignore_flat_and_empty_series(self): + self.assertEqual(trace_digest.persistent_peaks([]), []) + self.assertEqual(trace_digest.persistent_peaks([5]), []) + self.assertEqual(trace_digest.persistent_peaks([5, 5, 5]), []) + self.assertEqual(trace_digest.persistent_peaks([1, 2, 3]), [(2, 2)]) + + def test_persistent_peaks_of_equal_peaks_are_both_reported(self): + self.assertEqual(trace_digest.persistent_peaks([9, 20, 9, 20, 9]), [(1, 11), (3, 11)]) + + def test_persistent_spikes_in_digest(self): + spikes = self.result()["persistent_spikes"] + self.assertEqual([spike["present"] for spike in spikes], [601]) + self.assertAlmostEqual(spikes[0]["ms"], 20.0) + self.assertAlmostEqual(spikes[0]["at_ms"], 10.0) + self.assertAlmostEqual(spikes[0]["persistence_ms"], 10.0) + def test_markdown_thread_names_cannot_split_table_cells_or_rows(self): name = "worker|\nrow & more" phase = "phase|\r\nnext" @@ -727,6 +928,7 @@ class TraceDigestTests(unittest.TestCase): self.assertEqual(result["slowest_frames"], []) self.assertEqual(result["top_slices_ms_per_frame"], []) self.assertEqual(result["longest_waits"], []) + self.assertEqual(result["persistent_spikes"], []) def test_command_line_markdown_and_json(self): @@ -734,7 +936,7 @@ class TraceDigestTests(unittest.TestCase): with contextlib.redirect_stdout(output): self.assertEqual(trace_digest.main([str(self.trace), "--stacks", str(self.stacks)]), 0) text = output.getvalue() - for heading in ("**Slowest frames**", "**Threads**", "**Largest slices**", "**Longest waits**", "**Worker stacks**"): + for heading in ("**Slowest frames**", "**Persistent spikes**", "**Threads**", "**Largest slices**", "**Longest waits**", "**Worker stacks**"): self.assertIn(heading, text) self.assertLess(len(text.encode("utf-8")), 5000) output = io.StringIO() diff --git a/tools/tests/test_unbuilt_exports.py b/tools/tests/test_unbuilt_exports.py new file mode 100644 index 0000000000..cc5c109e38 --- /dev/null +++ b/tools/tests/test_unbuilt_exports.py @@ -0,0 +1,121 @@ +from pathlib import Path +import contextlib +import io +import json +import subprocess +import sys +import tempfile +import unittest +from unittest.mock import patch + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import check_test_registration + + +class UnbuiltExportTests(unittest.TestCase): + def tree(self, files): + directory = tempfile.TemporaryDirectory() + self.addCleanup(directory.cleanup) + root = Path(directory.name) + for name, content in files.items(): + path = root / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content, encoding="utf-8") + return root + + def unbuilt(self, root, built): + return check_test_registration.unbuilt_exports(root, {(root / name).resolve() for name in built}) + + def test_a_source_no_target_compiles_is_reported(self): + root = self.tree({ + "core/libs/prx/libSceExample/Export.cpp": 'extern "C" {\nint APS5_VABI sceExampleOpen(void) {\n return 0;\n}\n}\n', + "core/libs/prx/libSceExample/src/Built.cpp": "int APS5_VABI sceExampleClose(int handle) { return handle; }\n", + }) + self.assertEqual(self.unbuilt(root, ["core/libs/prx/libSceExample/src/Built.cpp"]), [Path("core/libs/prx/libSceExample/Export.cpp")]) + + def test_multiline_parameters_and_qualifiers_still_count_as_definitions(self): + root = self.tree({ + "core/libs/prx/libSceExample/Built.cpp": "", + "core/libs/prx/libSceExample/Wide.cpp": "[[noreturn]] void APS5_VABI sceExampleAbort(int code,\n const char* reason) noexcept {\n}\n", + }) + self.assertEqual(self.unbuilt(root, ["core/libs/prx/libSceExample/Built.cpp"]), [Path("core/libs/prx/libSceExample/Wide.cpp")]) + + def test_declarations_tests_and_sources_without_exports_are_not_reported(self): + root = self.tree({ + "core/libs/prx/libSceExample/Built.cpp": "", + "core/libs/prx/libSceExample/Declared.cpp": 'extern "C" int APS5_VABI sceExampleOpen(void);\nusing Callback = int (APS5_VABI*)(int);\n', + "core/libs/prx/libSceExample/Plain.cpp": "static int helper() { return 1; }\n", + "core/libs/prx/libSceExample/tests/Guest.cpp": "int APS5_VABI sceExampleOpen(void) { return 0; }\n", + "core/libs/prx/libSceExample/src/test/Fixture.cpp": "int APS5_VABI sceExampleFixture(void) { return 0; }\n", + "core/libs/tests/GuestExample.cpp": "int APS5_VABI sceExampleOpen(void) { return 0; }\n", + }) + self.assertEqual(self.unbuilt(root, ["core/libs/prx/libSceExample/Built.cpp"]), []) + + def test_a_source_included_by_a_compiled_source_counts_as_built(self): + root = self.tree({ + "core/libs/prx/libc/src/exception/Exports.cpp": '#include "Unwind.cpp"\n # include "Runtime.cpp"\n', + "core/libs/prx/libc/src/exception/Unwind.cpp": '#include "Personality.cpp"\nint APS5_VABI _Unwind_Resume(void* exception) { return 0; }\n', + "core/libs/prx/libc/src/exception/Runtime.cpp": "void APS5_VABI __cxa_rethrow() {}\n", + "core/libs/prx/libc/src/exception/Personality.cpp": "int APS5_VABI __gxx_personality_v0(int version) { return version; }\n", + "core/libs/prx/libc/src/exception/Orphan.cpp": "int APS5_VABI __cxa_orphan(void) { return 0; }\n", + }) + self.assertEqual(self.unbuilt(root, ["core/libs/prx/libc/src/exception/Exports.cpp"]), [Path("core/libs/prx/libc/src/exception/Orphan.cpp")]) + + def test_function_try_blocks_still_count_as_definitions(self): + for suffix in (" try", " noexcept try", "\n noexcept\n try"): + with self.subTest(suffix=suffix): + root = self.tree({ + "core/libs/prx/libSceExample/Built.cpp": "", + "core/libs/prx/libSceExample/Try.cpp": ( + "int APS5_VABI sceExampleTry(int value)" + suffix + + " { return value; } catch (...) { return -1; }\n" + ), + }) + self.assertEqual(self.unbuilt(root, ["core/libs/prx/libSceExample/Built.cpp"]), + [Path("core/libs/prx/libSceExample/Try.cpp")]) + + def test_a_build_without_library_sources_is_not_checked(self): + root = self.tree({ + "core/libs/prx/libSceExample/Export.cpp": "int APS5_VABI sceExampleOpen(void) { return 0; }\n", + "core/relinker/Main.cpp": "int main() { return 0; }\n", + }) + self.assertIsNone(self.unbuilt(root, ["core/relinker/Main.cpp"])) + + def test_compiled_sources_come_from_the_compile_commands_of_ninja(self): + root = self.tree({"core/libs/prx/libSceExample/Export.cpp": "", "core/libs/prx/libSceExample/Other.cpp": ""}) + database = [ + {"directory": str(root / "build"), "file": "../core/libs/prx/libSceExample/Export.cpp", + "command": "/usr/bin/c++ -O2 -o Export.cpp.o -c ../core/libs/prx/libSceExample/Export.cpp"}, + {"directory": str(root / "build"), "file": str(root / "core/libs/prx/libSceExample/Other.cpp"), + "arguments": ["clang-cl", "/nologo", "/c", str(root / "core/libs/prx/libSceExample/Other.cpp")]}, + {"directory": str(root / "build"), "file": "libSceExample.prx", "command": "/usr/bin/c++ -shared -o libSceExample.prx Export.cpp.o"}, + {"directory": str(root / "build"), "file": "../core/libs/prx/libSceExample/Generator.cpp", + "command": "python3 generate.py ../core/libs/prx/libSceExample/Generator.cpp"}, + ] + + def run(command, *, capture_output, text, encoding, check): + self.assertEqual(command, ["ninja", "-C", "build", "-t", "compdb"]) + return subprocess.CompletedProcess(command, 0, json.dumps(database), "") + + with patch.object(check_test_registration, "ninja", return_value=("ninja", "utf-8")), \ + patch.object(check_test_registration.subprocess, "run", side_effect=run): + sources = check_test_registration.compiled("build") + self.assertEqual(sources, {(root / "core/libs/prx/libSceExample/Export.cpp").resolve(), + (root / "core/libs/prx/libSceExample/Other.cpp").resolve()}) + + def test_main_fails_and_names_each_unbuilt_source(self): + unbuilt = [Path("core/libs/prx/libSceExample/Export.cpp")] + for result, expected, line in ((unbuilt, 1, "error: core/libs/prx/libSceExample/Export.cpp defines APS5_VABI functions but no target compiles it"), + ([], 0, "every core/libs/prx source that defines an APS5_VABI function is compiled"), + (None, 0, "no core/libs/prx source is compiled in this build; library sources were not checked")): + with self.subTest(result=result), patch.object(check_test_registration, "built", return_value={"example_tests"}), \ + patch.object(check_test_registration, "registered", return_value={"example_tests"}), \ + patch.object(check_test_registration, "compiled", return_value=set()), \ + patch.object(check_test_registration, "unbuilt_exports", return_value=result), \ + contextlib.redirect_stdout(io.StringIO()) as output: + self.assertEqual(check_test_registration.main(["build"]), expected) + self.assertIn(line, output.getvalue()) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/tests/test_unwrap_self.py b/tools/tests/test_unwrap_self.py index 5184bf1894..da81aea2be 100644 --- a/tools/tests/test_unwrap_self.py +++ b/tools/tests/test_unwrap_self.py @@ -92,6 +92,18 @@ def read_program_headers(path): class UnwrapTest(unittest.TestCase): + def test_mapped_segments_at_virtual_address_zero_keep_their_template(self): + for kind in (PT_DYNAMIC, PT_TLS): + with self.subTest(kind=kind): + segments = default_segments() + segments[2] = (kind, 4, 0x1234, 0, 0x10, 8) + result, output = self.convert(build_container(segments, default_records(), default_payloads())) + self.assertEqual(result.returncode, 0, result.stderr) + data, headers = read_program_headers(output) + self.assertEqual(headers[2][2], headers[0][2]) + self.assertEqual(headers[2][5:7], (0x10, 0x10)) + self.assertEqual(data[headers[2][2]:headers[2][2] + 0x10], pattern(1, 0x10)) + def test_unsupported_inner_elf_machines_are_refused(self): for machine in (0, 3, 40, 183, 0x3E00, 0xFFFF): with self.subTest(machine=machine): @@ -1412,6 +1424,23 @@ class UnwrapTest(unittest.TestCase): _, _, headers = unwrap_self.parse_elf(container) self.assertEqual(headers[2]["align"], 3) + def test_null_segment_fields_do_not_affect_conversion(self): + segments = default_segments() + segments[2] = (PT_NULL, 0, 0, 0, 0, 0) + original = build_container(segments, default_records(), default_payloads()) + result, output = self.convert(original) + self.assertEqual(result.returncode, 0, result.stderr) + expected = output.read_bytes() + for field, format in ((4, " 0), key=lambda entry: (-entry[1], entry[0])) + + def digest(trace, summary=None, stacks=None, worst=5, top=10): window_ms = summary["window"]["ms"] if summary else None frames = trace.frames(window_ms * 1000.0 if window_ms is not None else None) span_us = frames[-1][2] - frames[0][1] if frames else 0.0 count = max(len(frames), 1) result = {"summary": summary, "frames": len(frames), "window_ms": span_us / 1000.0} + if trace.losses or (summary and summary.get("trace_dropped")): + result["trace_incomplete"] = True + result["event_loss"] = [{"thread": trace.name(tid), "at_ms": ts / 1000.0, "events_lost": lost} + for tid, ts, lost in trace.losses] slowest = [] for index, begin, end, present in sorted(frames, key=lambda frame: frame[1] - frame[2])[:worst]: @@ -210,6 +280,10 @@ def digest(trace, summary=None, stacks=None, worst=5, top=10): "slices": [{"name": name, "thread": thread, "ms": us / 1000.0} for (name, thread), us in inside.most_common(4)]}) result["slowest_frames"] = slowest + durations = [(end - begin) / 1000.0 for _, begin, end, _ in frames] + result["persistent_spikes"] = [{"present": frames[index][3], "ms": durations[index], "at_ms": frames[index][1] / 1000.0, "persistence_ms": persistence} + for index, persistence in persistent_peaks(durations)[:top]] + threads = [] totals = collections.Counter() waits = [] @@ -218,14 +292,20 @@ def digest(trace, summary=None, stacks=None, worst=5, top=10): traced = 0.0 waited = 0.0 leaders = collections.Counter() + gpu = trace.name(tid) == "gpu" + covered_until = -math.inf for ts, dur, name, depth, nested in slices: - overlap = max(min(ts + dur, frames[-1][2]) - max(ts, frames[0][1]), 0.0) if frames else dur + begin = max(ts, frames[0][1]) if frames else ts + end = min(ts + dur, frames[-1][2]) if frames else ts + dur + overlap = max(end - begin, 0.0) if overlap > 0: totals[name] += overlap - if depth == 0: - traced += overlap - if not is_wait(name): - leaders[name] += overlap + if depth == 0 or gpu: + contribution = max(end - max(begin, covered_until), 0.0) if gpu else overlap + covered_until = max(covered_until, end) + traced += contribution + if contribution > 0 and not is_wait(name): + leaders[name] += contribution if is_wait(name) and not nested: waited += overlap if is_wait(name) and not nested and (overlap > 0 or not frames): @@ -311,6 +391,10 @@ def markdown(result): lines.append("```") else: lines.append(f"{result['frames']} frames over {result['window_ms']:.1f} ms (no summary line found)") + if result.get("trace_incomplete"): + lines += ["", "**Incomplete trace:** recorded data was lost. Slice-based timings, waits and batch inventories cover retained events only; missing slices are not idle time. Summary statistics are reported separately."] + for loss in result["event_loss"]: + lines.append(f"- {table_cell(loss['thread'])}: {loss['events_lost']} events lost; buffer full at {loss['at_ms']:.1f} ms") lines.append("") lines.append("**Slowest frames** (largest slices inside each, inclusive)") lines.append("") @@ -318,6 +402,11 @@ def markdown(result): parts = ", ".join(f"{entry['name']} ({entry['thread']}) {entry['ms']:.1f}" for entry in frame["slices"]) lines.append(f"- present {frame['present']}: {frame['ms']:.1f} ms at {frame['at_ms']:.0f} ms: {parts}") lines.append("") + lines.append("**Persistent spikes** (frame time above the fastest frame that separates it from a longer frame; one entry per stall, not per frame)") + lines.append("") + for spike in result["persistent_spikes"]: + lines.append(f"- present {spike['present']}: {spike['ms']:.1f} ms at {spike['at_ms']:.0f} ms, persistence {spike['persistence_ms']:.1f} ms") + lines.append("") lines.append("**Threads** (ms per frame)") lines.append("") lines.append("| thread | busy | wait | untraced | in slices < 20 us | largest |") @@ -468,8 +557,13 @@ def main(arguments=None): parser.add_argument("--top", type=nonnegative_integer, default=10, help="entries per list (default 10)") parser.add_argument("--json", action="store_true", help="print the digest as JSON") options = parser.parse_args(arguments) - if options.summary and not Path(options.summary).is_file(): - parser.error(f"summary file not found: {options.summary}") + if options.summary: + try: + summary_path = resolve_summary_path(options.summary) + except ValueError as error: + parser.error(str(error)) + if not summary_path.is_file(): + parser.error(f"summary file not found: {options.summary}") if options.thread and options.trace.endswith(".jsonl"): parser.error("--thread requires an event trace, not interval summaries") if options.stacks and options.trace.endswith(".jsonl"): @@ -487,8 +581,11 @@ def main(arguments=None): trace = load_trace(options.trace) if options.thread: trace.select_threads(options.thread) - result = digest(trace, load_summary(options.summary, options.trace), - stacks, options.frames, options.top) + summary = load_summary(options.summary, options.trace) + try: + result = digest(trace, summary, stacks, options.frames, options.top) + except KeyError as error: + parser.exit(2, f"FAIL: the summary line has no {error} field\n") if options.thread: result["thread_filter"] = sorted(set(options.thread)) sys.stdout.write(json.dumps(result, indent=1) + "\n" if options.json else markdown(result)) diff --git a/tools/unwrap_self.py b/tools/unwrap_self.py index 00d8b6ac72..e439420275 100644 --- a/tools/unwrap_self.py +++ b/tools/unwrap_self.py @@ -53,6 +53,7 @@ MAX_OUTPUT_FACTOR = 4 MIN_OUTPUT_LIMIT = 256 * 1024 * 1024 PT_NULL = 0 PT_LOAD = 1 +PT_DYNAMIC = 2 PT_NOTE = 4 PT_PHDR = 6 ENTROPY_LIMIT = 7.5 @@ -159,6 +160,9 @@ def parse_elf(data): def place_segments(data, headers, records): + for header in headers: + if header["type"] == PT_NULL: + header.update(flags=0, offset=0, vaddr=0, size=0, memory=0, align=0, source=0) loads = [header for header in headers if header["type"] == PT_LOAD] used = set() placed = set() @@ -218,7 +222,7 @@ def place_segments(data, headers, records): if header["size"] and (tail is None or header["offset"] > tail[0]): tail = (header["offset"], header["source"] - header["offset"]) continue - if header["vaddr"] != 0 or header["type"] == PT_PHDR: + if header["vaddr"] != 0 or header["type"] in (PT_PHDR, PT_DYNAMIC, PT_TLS): host = load_for(header["vaddr"], header["size"]) if host is None: if header["size"] == 0: diff --git a/tools/vulkan_validation_gate.py b/tools/vulkan_validation_gate.py index 2b9a4e104c..fdcea43db7 100644 --- a/tools/vulkan_validation_gate.py +++ b/tools/vulkan_validation_gate.py @@ -75,12 +75,12 @@ def artifact_hashes(build, tests, runtime): paths = set() for test in tests: command = test.get("command") or [] - if not command or not Path(command[0]).is_file(): - raise GateError(f"{test['name']}: selected command is not an existing executable") working = next((Path(prop["value"]) for prop in test.get("properties", []) if prop["name"] == "WORKING_DIRECTORY"), Path(".")) if not working.is_absolute(): working = build / working + if not command or not (working / command[0]).is_file(): + raise GateError(f"{test['name']}: selected command is not an existing executable") for argument in command: value = argument.partition("=")[2] if argument.startswith("-D") else argument path = Path(value)